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 checkspull/54878/head
parent
e7fa3e727b
commit
faec82ae8f
|
@ -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
|
|
@ -7,6 +7,7 @@ CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
|
|||
DEFAULT_SCAN_INTERVAL = 300 # 5 minutes
|
||||
|
||||
PLATFORMS = [
|
||||
"binary_sensor",
|
||||
"sensor",
|
||||
]
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue