From faec82ae8f82bf5f9fb43a3c601a2e295b49dfc8 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 19 Aug 2021 09:27:43 +0200
Subject: [PATCH] Add binary sensor platform to Renault integration (#54750)

* Add binary sensor platform

* Add tests

* Simplify code

* Adjust descriptions

* Adjust tests

* Make "fuel" tests more explicit

* Updates for device registry checks
---
 .../components/renault/binary_sensor.py       |  58 +++++++
 homeassistant/components/renault/const.py     |   1 +
 tests/components/renault/const.py             |  50 ++++++
 .../components/renault/test_binary_sensor.py  | 155 ++++++++++++++++++
 4 files changed, 264 insertions(+)
 create mode 100644 homeassistant/components/renault/binary_sensor.py
 create mode 100644 tests/components/renault/test_binary_sensor.py

diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py
new file mode 100644
index 00000000000..dd3ccb036e0
--- /dev/null
+++ b/homeassistant/components/renault/binary_sensor.py
@@ -0,0 +1,58 @@
+"""Support for Renault binary sensors."""
+from __future__ import annotations
+
+from renault_api.kamereon.enums import ChargeState, PlugState
+
+from homeassistant.components.binary_sensor import (
+    DEVICE_CLASS_BATTERY_CHARGING,
+    DEVICE_CLASS_PLUG,
+    BinarySensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import DOMAIN
+from .renault_entities import RenaultBatteryDataEntity, RenaultDataEntity
+from .renault_hub import RenaultHub
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the Renault entities from config entry."""
+    proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id]
+    entities: list[RenaultDataEntity] = []
+    for vehicle in proxy.vehicles.values():
+        if "battery" in vehicle.coordinators:
+            entities.append(RenaultPluggedInSensor(vehicle, "Plugged In"))
+            entities.append(RenaultChargingSensor(vehicle, "Charging"))
+    async_add_entities(entities)
+
+
+class RenaultPluggedInSensor(RenaultBatteryDataEntity, BinarySensorEntity):
+    """Plugged In binary sensor."""
+
+    _attr_device_class = DEVICE_CLASS_PLUG
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if the binary sensor is on."""
+        if (not self.data) or (self.data.plugStatus is None):
+            return None
+        return self.data.get_plug_status() == PlugState.PLUGGED
+
+
+class RenaultChargingSensor(RenaultBatteryDataEntity, BinarySensorEntity):
+    """Charging binary sensor."""
+
+    _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if the binary sensor is on."""
+        if (not self.data) or (self.data.chargingStatus is None):
+            return None
+        return self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS
diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py
index 51f6c10c6f1..0987d1829ed 100644
--- a/homeassistant/components/renault/const.py
+++ b/homeassistant/components/renault/const.py
@@ -7,6 +7,7 @@ CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
 DEFAULT_SCAN_INTERVAL = 300  # 5 minutes
 
 PLATFORMS = [
+    "binary_sensor",
     "sensor",
 ]
 
diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py
index 8c3d6e9f98f..2c742aa07cd 100644
--- a/tests/components/renault/const.py
+++ b/tests/components/renault/const.py
@@ -1,4 +1,9 @@
 """Constants for the Renault integration tests."""
+from homeassistant.components.binary_sensor import (
+    DEVICE_CLASS_BATTERY_CHARGING,
+    DEVICE_CLASS_PLUG,
+    DOMAIN as BINARY_SENSOR_DOMAIN,
+)
 from homeassistant.components.renault.const import (
     CONF_KAMEREON_ACCOUNT_ID,
     CONF_LOCALE,
@@ -19,6 +24,8 @@ from homeassistant.const import (
     LENGTH_KILOMETERS,
     PERCENTAGE,
     POWER_KILO_WATT,
+    STATE_OFF,
+    STATE_ON,
     STATE_UNKNOWN,
     TEMP_CELSIUS,
     TIME_MINUTES,
@@ -54,6 +61,20 @@ MOCK_VEHICLES = {
             "cockpit": "cockpit_ev.json",
             "hvac_status": "hvac_status.json",
         },
+        BINARY_SENSOR_DOMAIN: [
+            {
+                "entity_id": "binary_sensor.plugged_in",
+                "unique_id": "vf1aaaaa555777999_plugged_in",
+                "result": STATE_ON,
+                "class": DEVICE_CLASS_PLUG,
+            },
+            {
+                "entity_id": "binary_sensor.charging",
+                "unique_id": "vf1aaaaa555777999_charging",
+                "result": STATE_ON,
+                "class": DEVICE_CLASS_BATTERY_CHARGING,
+            },
+        ],
         SENSOR_DOMAIN: [
             {
                 "entity_id": "sensor.battery_autonomy",
@@ -147,6 +168,20 @@ MOCK_VEHICLES = {
             "charge_mode": "charge_mode_schedule.json",
             "cockpit": "cockpit_ev.json",
         },
+        BINARY_SENSOR_DOMAIN: [
+            {
+                "entity_id": "binary_sensor.plugged_in",
+                "unique_id": "vf1aaaaa555777999_plugged_in",
+                "result": STATE_OFF,
+                "class": DEVICE_CLASS_PLUG,
+            },
+            {
+                "entity_id": "binary_sensor.charging",
+                "unique_id": "vf1aaaaa555777999_charging",
+                "result": STATE_OFF,
+                "class": DEVICE_CLASS_BATTERY_CHARGING,
+            },
+        ],
         SENSOR_DOMAIN: [
             {
                 "entity_id": "sensor.battery_autonomy",
@@ -233,6 +268,20 @@ MOCK_VEHICLES = {
             "charge_mode": "charge_mode_always.json",
             "cockpit": "cockpit_fuel.json",
         },
+        BINARY_SENSOR_DOMAIN: [
+            {
+                "entity_id": "binary_sensor.plugged_in",
+                "unique_id": "vf1aaaaa555777123_plugged_in",
+                "result": STATE_ON,
+                "class": DEVICE_CLASS_PLUG,
+            },
+            {
+                "entity_id": "binary_sensor.charging",
+                "unique_id": "vf1aaaaa555777123_charging",
+                "result": STATE_ON,
+                "class": DEVICE_CLASS_BATTERY_CHARGING,
+            },
+        ],
         SENSOR_DOMAIN: [
             {
                 "entity_id": "sensor.battery_autonomy",
@@ -327,6 +376,7 @@ MOCK_VEHICLES = {
             # Ignore,  # charge-mode
         ],
         "endpoints": {"cockpit": "cockpit_fuel.json"},
+        BINARY_SENSOR_DOMAIN: [],
         SENSOR_DOMAIN: [
             {
                 "entity_id": "sensor.fuel_autonomy",
diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py
new file mode 100644
index 00000000000..71bb90f16a6
--- /dev/null
+++ b/tests/components/renault/test_binary_sensor.py
@@ -0,0 +1,155 @@
+"""Tests for Renault binary sensors."""
+from unittest.mock import patch
+
+import pytest
+from renault_api.kamereon import exceptions
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
+from homeassistant.setup import async_setup_component
+
+from . import (
+    check_device_registry,
+    setup_renault_integration_vehicle,
+    setup_renault_integration_vehicle_with_no_data,
+    setup_renault_integration_vehicle_with_side_effect,
+)
+from .const import MOCK_VEHICLES
+
+from tests.common import mock_device_registry, mock_registry
+
+
+@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
+async def test_binary_sensors(hass, vehicle_type):
+    """Test for Renault binary sensors."""
+    await async_setup_component(hass, "persistent_notification", {})
+    entity_registry = mock_registry(hass)
+    device_registry = mock_device_registry(hass)
+
+    with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]):
+        await setup_renault_integration_vehicle(hass, vehicle_type)
+        await hass.async_block_till_done()
+
+    mock_vehicle = MOCK_VEHICLES[vehicle_type]
+    check_device_registry(device_registry, mock_vehicle["expected_device"])
+
+    expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN]
+    assert len(entity_registry.entities) == len(expected_entities)
+    for expected_entity in expected_entities:
+        entity_id = expected_entity["entity_id"]
+        registry_entry = entity_registry.entities.get(entity_id)
+        assert registry_entry is not None
+        assert registry_entry.unique_id == expected_entity["unique_id"]
+        assert registry_entry.unit_of_measurement == expected_entity.get("unit")
+        assert registry_entry.device_class == expected_entity.get("class")
+        state = hass.states.get(entity_id)
+        assert state.state == expected_entity["result"]
+
+
+@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
+async def test_binary_sensor_empty(hass, vehicle_type):
+    """Test for Renault binary sensors with empty data from Renault."""
+    await async_setup_component(hass, "persistent_notification", {})
+    entity_registry = mock_registry(hass)
+    device_registry = mock_device_registry(hass)
+
+    with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]):
+        await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type)
+        await hass.async_block_till_done()
+
+    mock_vehicle = MOCK_VEHICLES[vehicle_type]
+    check_device_registry(device_registry, mock_vehicle["expected_device"])
+
+    expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN]
+    assert len(entity_registry.entities) == len(expected_entities)
+    for expected_entity in expected_entities:
+        entity_id = expected_entity["entity_id"]
+        registry_entry = entity_registry.entities.get(entity_id)
+        assert registry_entry is not None
+        assert registry_entry.unique_id == expected_entity["unique_id"]
+        assert registry_entry.unit_of_measurement == expected_entity.get("unit")
+        assert registry_entry.device_class == expected_entity.get("class")
+        state = hass.states.get(entity_id)
+        assert state.state == STATE_OFF
+
+
+@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
+async def test_binary_sensor_errors(hass, vehicle_type):
+    """Test for Renault binary sensors with temporary failure."""
+    await async_setup_component(hass, "persistent_notification", {})
+    entity_registry = mock_registry(hass)
+    device_registry = mock_device_registry(hass)
+
+    invalid_upstream_exception = exceptions.InvalidUpstreamException(
+        "err.tech.500",
+        "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway",
+    )
+
+    with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]):
+        await setup_renault_integration_vehicle_with_side_effect(
+            hass, vehicle_type, invalid_upstream_exception
+        )
+        await hass.async_block_till_done()
+
+    mock_vehicle = MOCK_VEHICLES[vehicle_type]
+    check_device_registry(device_registry, mock_vehicle["expected_device"])
+
+    expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN]
+    assert len(entity_registry.entities) == len(expected_entities)
+    for expected_entity in expected_entities:
+        entity_id = expected_entity["entity_id"]
+        registry_entry = entity_registry.entities.get(entity_id)
+        assert registry_entry is not None
+        assert registry_entry.unique_id == expected_entity["unique_id"]
+        assert registry_entry.unit_of_measurement == expected_entity.get("unit")
+        assert registry_entry.device_class == expected_entity.get("class")
+        state = hass.states.get(entity_id)
+        assert state.state == STATE_UNAVAILABLE
+
+
+async def test_binary_sensor_access_denied(hass):
+    """Test for Renault binary sensors with access denied failure."""
+    await async_setup_component(hass, "persistent_notification", {})
+    entity_registry = mock_registry(hass)
+    device_registry = mock_device_registry(hass)
+
+    vehicle_type = "zoe_40"
+    access_denied_exception = exceptions.AccessDeniedException(
+        "err.func.403",
+        "Access is denied for this resource",
+    )
+
+    with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]):
+        await setup_renault_integration_vehicle_with_side_effect(
+            hass, vehicle_type, access_denied_exception
+        )
+        await hass.async_block_till_done()
+
+    mock_vehicle = MOCK_VEHICLES[vehicle_type]
+    check_device_registry(device_registry, mock_vehicle["expected_device"])
+
+    assert len(entity_registry.entities) == 0
+
+
+async def test_binary_sensor_not_supported(hass):
+    """Test for Renault binary sensors with not supported failure."""
+    await async_setup_component(hass, "persistent_notification", {})
+    entity_registry = mock_registry(hass)
+    device_registry = mock_device_registry(hass)
+
+    vehicle_type = "zoe_40"
+    not_supported_exception = exceptions.NotSupportedException(
+        "err.tech.501",
+        "This feature is not technically supported by this gateway",
+    )
+
+    with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]):
+        await setup_renault_integration_vehicle_with_side_effect(
+            hass, vehicle_type, not_supported_exception
+        )
+        await hass.async_block_till_done()
+
+    mock_vehicle = MOCK_VEHICLES[vehicle_type]
+    check_device_registry(device_registry, mock_vehicle["expected_device"])
+
+    assert len(entity_registry.entities) == 0
-- 
GitLab