diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index dd3ccb036e0..17a735f8bc7 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -1,22 +1,44 @@ """Support for Renault binary sensors.""" from __future__ import annotations +from dataclasses import dataclass + from renault_api.kamereon.enums import ChargeState, PlugState +from renault_api.kamereon.models import KamereonVehicleBatteryStatusData from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_PLUG, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import DOMAIN -from .renault_entities import RenaultBatteryDataEntity, RenaultDataEntity +from .renault_entities import RenaultDataEntity, RenaultEntityDescription, T from .renault_hub import RenaultHub +@dataclass +class RenaultBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + entity_class: type[RenaultBinarySensor] + on_value: StateType + + +@dataclass +class RenaultBinarySensorEntityDescription( + BinarySensorEntityDescription, + RenaultEntityDescription, + RenaultBinarySensorRequiredKeysMixin, +): + """Class describing Renault binary sensor entities.""" + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -24,35 +46,43 @@ async def async_setup_entry( ) -> 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")) + entities: list[RenaultBinarySensor] = [ + description.entity_class(vehicle, description) + for vehicle in proxy.vehicles.values() + for description in BINARY_SENSOR_TYPES + if description.coordinator in vehicle.coordinators + ] async_add_entities(entities) -class RenaultPluggedInSensor(RenaultBatteryDataEntity, BinarySensorEntity): - """Plugged In binary sensor.""" +class RenaultBinarySensor(RenaultDataEntity[T], BinarySensorEntity): + """Mixin for binary sensor specific attributes.""" - _attr_device_class = DEVICE_CLASS_PLUG + entity_description: RenaultBinarySensorEntityDescription @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 + return self.data == self.entity_description.on_value -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 +BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = ( + RenaultBinarySensorEntityDescription( + key="plugged_in", + coordinator="battery", + data_key="plugStatus", + device_class=DEVICE_CLASS_PLUG, + entity_class=RenaultBinarySensor[KamereonVehicleBatteryStatusData], + name="Plugged In", + on_value=PlugState.PLUGGED.value, + ), + RenaultBinarySensorEntityDescription( + key="charging", + coordinator="battery", + data_key="chargingStatus", + device_class=DEVICE_CLASS_BATTERY_CHARGING, + entity_class=RenaultBinarySensor[KamereonVehicleBatteryStatusData], + name="Charging", + on_value=ChargeState.CHARGE_IN_PROGRESS.value, + ), +) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 9188a1f0757..2a23d1de8f6 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -1,103 +1,73 @@ """Base classes for Renault entities.""" from __future__ import annotations -from typing import Any, Generic, Optional, TypeVar +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Optional, TypeVar, cast -from renault_api.kamereon.enums import ChargeState, PlugState -from renault_api.kamereon.models import ( - KamereonVehicleBatteryStatusData, - KamereonVehicleChargeModeData, - KamereonVehicleCockpitData, - KamereonVehicleHvacStatusData, -) +from renault_api.kamereon.models import KamereonVehicleDataAttributes -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import slugify from .renault_vehicle import RenaultVehicleProxy + +@dataclass +class RenaultRequiredKeysMixin: + """Mixin for required keys.""" + + coordinator: str + data_key: str + + +@dataclass +class RenaultEntityDescription(EntityDescription, RenaultRequiredKeysMixin): + """Class describing Renault entities.""" + + requires_fuel: bool | None = None + + ATTR_LAST_UPDATE = "last_update" -T = TypeVar("T") +T = TypeVar("T", bound=KamereonVehicleDataAttributes) -class RenaultDataEntity(Generic[T], CoordinatorEntity[Optional[T]], Entity): +class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): """Implementation of a Renault entity with a data coordinator.""" + entity_description: RenaultEntityDescription + def __init__( - self, vehicle: RenaultVehicleProxy, entity_type: str, coordinator_key: str + self, + vehicle: RenaultVehicleProxy, + description: RenaultEntityDescription, ) -> None: """Initialise entity.""" - super().__init__(vehicle.coordinators[coordinator_key]) + super().__init__(vehicle.coordinators[description.coordinator]) self.vehicle = vehicle - self._entity_type = entity_type + self.entity_description = description self._attr_device_info = self.vehicle.device_info - self._attr_name = entity_type - self._attr_unique_id = slugify( - f"{self.vehicle.details.vin}-{self._entity_type}" + self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower() + + @property + def data(self) -> StateType: + """Return the state of this entity.""" + if self.coordinator.data is None: + return None + return cast( + StateType, getattr(self.coordinator.data, self.entity_description.data_key) ) @property - def available(self) -> bool: - """Return if entity is available.""" - # Data can succeed, but be empty - return super().available and self.coordinator.data is not None - - @property - def data(self) -> T | None: - """Return collected data.""" - return self.coordinator.data - - -class RenaultBatteryDataEntity(RenaultDataEntity[KamereonVehicleBatteryStatusData]): - """Implementation of a Renault entity with battery coordinator.""" - - def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: - """Initialise entity.""" - super().__init__(vehicle, entity_type, "battery") - - @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of this entity.""" - last_update = self.data.timestamp if self.data else None - return {ATTR_LAST_UPDATE: last_update} - - @property - def is_charging(self) -> bool: - """Return charge state as boolean.""" - return ( - self.data is not None - and self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS - ) - - @property - def is_plugged_in(self) -> bool: - """Return plug state as boolean.""" - return ( - self.data is not None and self.data.get_plug_status() == PlugState.PLUGGED - ) - - -class RenaultChargeModeDataEntity(RenaultDataEntity[KamereonVehicleChargeModeData]): - """Implementation of a Renault entity with charge_mode coordinator.""" - - def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: - """Initialise entity.""" - super().__init__(vehicle, entity_type, "charge_mode") - - -class RenaultCockpitDataEntity(RenaultDataEntity[KamereonVehicleCockpitData]): - """Implementation of a Renault entity with cockpit coordinator.""" - - def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: - """Initialise entity.""" - super().__init__(vehicle, entity_type, "cockpit") - - -class RenaultHVACDataEntity(RenaultDataEntity[KamereonVehicleHvacStatusData]): - """Implementation of a Renault entity with hvac_status coordinator.""" - - def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None: - """Initialise entity.""" - super().__init__(vehicle, entity_type, "hvac_status") + if self.entity_description.coordinator == "battery": + last_update = ( + getattr(self.coordinator.data, "timestamp") + if self.coordinator.data + else None + ) + return {ATTR_LAST_UPDATE: last_update} + return None diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 7ef11fb2afc..56101cd378b 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -1,7 +1,23 @@ """Support for Renault sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from dataclasses import dataclass +from typing import Callable, cast + +from renault_api.kamereon.enums import ChargeState, PlugState +from renault_api.kamereon.models import ( + KamereonVehicleBatteryStatusData, + KamereonVehicleChargeModeData, + KamereonVehicleCockpitData, + KamereonVehicleHvacStatusData, +) + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -18,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import ( DEVICE_CLASS_CHARGE_MODE, @@ -25,17 +42,25 @@ from .const import ( DEVICE_CLASS_PLUG_STATE, DOMAIN, ) -from .renault_entities import ( - RenaultBatteryDataEntity, - RenaultChargeModeDataEntity, - RenaultCockpitDataEntity, - RenaultDataEntity, - RenaultHVACDataEntity, -) +from .renault_entities import RenaultDataEntity, RenaultEntityDescription, T from .renault_hub import RenaultHub -from .renault_vehicle import RenaultVehicleProxy -ATTR_BATTERY_AVAILABLE_ENERGY = "battery_available_energy" + +@dataclass +class RenaultSensorRequiredKeysMixin: + """Mixin for required keys.""" + + entity_class: type[RenaultSensor] + + +@dataclass +class RenaultSensorEntityDescription( + SensorEntityDescription, RenaultEntityDescription, RenaultSensorRequiredKeysMixin +): + """Class describing Renault sensor entities.""" + + icon_lambda: Callable[[RenaultDataEntity[T]], str] | None = None + value_lambda: Callable[[RenaultDataEntity[T]], StateType] | None = None async def async_setup_entry( @@ -45,224 +70,208 @@ async def async_setup_entry( ) -> None: """Set up the Renault entities from config entry.""" proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] - entities = get_entities(proxy) + entities: list[RenaultSensor] = [ + description.entity_class(vehicle, description) + for vehicle in proxy.vehicles.values() + for description in SENSOR_TYPES + if description.coordinator in vehicle.coordinators + and (not description.requires_fuel or vehicle.details.uses_fuel()) + ] async_add_entities(entities) -def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: - """Create Renault entities for all vehicles.""" - entities = [] - for vehicle in proxy.vehicles.values(): - entities.extend(get_vehicle_entities(vehicle)) - return entities +class RenaultSensor(RenaultDataEntity[T], SensorEntity): + """Mixin for sensor specific attributes.""" - -def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: - """Create Renault entities for single vehicle.""" - entities: list[RenaultDataEntity] = [] - if "cockpit" in vehicle.coordinators: - entities.append(RenaultMileageSensor(vehicle, "Mileage")) - if vehicle.details.uses_fuel(): - entities.append(RenaultFuelAutonomySensor(vehicle, "Fuel Autonomy")) - entities.append(RenaultFuelQuantitySensor(vehicle, "Fuel Quantity")) - if "hvac_status" in vehicle.coordinators: - entities.append(RenaultOutsideTemperatureSensor(vehicle, "Outside Temperature")) - if "battery" in vehicle.coordinators: - entities.append(RenaultBatteryLevelSensor(vehicle, "Battery Level")) - entities.append(RenaultChargeStateSensor(vehicle, "Charge State")) - entities.append( - RenaultChargingRemainingTimeSensor(vehicle, "Charging Remaining Time") - ) - entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power")) - entities.append(RenaultPlugStateSensor(vehicle, "Plug State")) - entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy")) - entities.append( - RenaultBatteryAvailableEnergySensor(vehicle, "Battery Available Energy") - ) - entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature")) - if "charge_mode" in vehicle.coordinators: - entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode")) - return entities - - -class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): - """Battery autonomy sensor.""" - - _attr_icon = "mdi:ev-station" - _attr_native_unit_of_measurement = LENGTH_KILOMETERS + entity_description: RenaultSensorEntityDescription @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - return self.data.batteryAutonomy if self.data else None - - -class RenaultBatteryAvailableEnergySensor(RenaultBatteryDataEntity, SensorEntity): - """Battery available energy sensor.""" - - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - - @property - def native_value(self) -> float | None: - """Return the state of this entity.""" - return self.data.batteryAvailableEnergy if self.data else None - - -class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): - """Battery Level sensor.""" - - _attr_device_class = DEVICE_CLASS_BATTERY - _attr_native_unit_of_measurement = PERCENTAGE - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - return self.data.batteryLevel if self.data else None - - -class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity): - """Battery Temperature sensor.""" - - _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - return self.data.batteryTemperature if self.data else None - - -class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity): - """Charge Mode sensor.""" - - _attr_device_class = DEVICE_CLASS_CHARGE_MODE - - @property - def native_value(self) -> str | None: - """Return the state of this entity.""" - return self.data.chargeMode if self.data else None - - @property - def icon(self) -> str: + def icon(self) -> str | None: """Icon handling.""" - if self.data and self.data.chargeMode == "schedule_mode": - return "mdi:calendar-clock" - return "mdi:calendar-remove" - - -class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): - """Charge State sensor.""" - - _attr_device_class = DEVICE_CLASS_CHARGE_STATE + if self.entity_description.icon_lambda is None: + return super().icon + return self.entity_description.icon_lambda(self) @property - def native_value(self) -> str | None: + def native_value(self) -> StateType: """Return the state of this entity.""" - charging_status = self.data.get_charging_status() if self.data else None - return charging_status.name.lower() if charging_status is not None else None - - @property - def icon(self) -> str: - """Icon handling.""" - return "mdi:flash" if self.is_charging else "mdi:flash-off" - - -class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity): - """Charging Remaining Time sensor.""" - - _attr_icon = "mdi:timer" - _attr_native_unit_of_measurement = TIME_MINUTES - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - return self.data.chargingRemainingTime if self.data else None - - -class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): - """Charging Power sensor.""" - - _attr_device_class = DEVICE_CLASS_POWER - _attr_native_unit_of_measurement = POWER_KILO_WATT - - @property - def native_value(self) -> float | None: - """Return the state of this entity.""" - if not self.data or self.data.chargingInstantaneousPower is None: + if self.data is None: return None - if self.vehicle.details.reports_charging_power_in_watts(): - # Need to convert to kilowatts - return self.data.chargingInstantaneousPower / 1000 - return self.data.chargingInstantaneousPower + if self.entity_description.value_lambda is None: + return self.data + return self.entity_description.value_lambda(self) -class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): - """Fuel autonomy sensor.""" - - _attr_icon = "mdi:gas-station" - _attr_native_unit_of_measurement = LENGTH_KILOMETERS - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - if not self.data or self.data.fuelAutonomy is None: - return None - return round(self.data.fuelAutonomy) +def _get_formatted_charging_status( + data: KamereonVehicleBatteryStatusData, +) -> str | None: + """Return the charging_status of this entity.""" + charging_status = data.get_charging_status() if data else None + return charging_status.name.lower() if charging_status else None -class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): - """Fuel quantity sensor.""" - - _attr_icon = "mdi:fuel" - _attr_native_unit_of_measurement = VOLUME_LITERS - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - if not self.data or self.data.fuelQuantity is None: - return None - return round(self.data.fuelQuantity) +def _get_formatted_plug_status(data: KamereonVehicleBatteryStatusData) -> str | None: + """Return the plug_status of this entity.""" + plug_status = data.get_plug_status() if data else None + return plug_status.name.lower() if plug_status else None -class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): - """Mileage sensor.""" - - _attr_icon = "mdi:sign-direction" - _attr_native_unit_of_measurement = LENGTH_KILOMETERS - - @property - def native_value(self) -> int | None: - """Return the state of this entity.""" - if not self.data or self.data.totalMileage is None: - return None - return round(self.data.totalMileage) - - -class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): - """HVAC Outside Temperature sensor.""" - - _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def native_value(self) -> float | None: - """Return the state of this entity.""" - return self.data.externalTemperature if self.data else None - - -class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): - """Plug State sensor.""" - - _attr_device_class = DEVICE_CLASS_PLUG_STATE - - @property - def native_value(self) -> str | None: - """Return the state of this entity.""" - plug_status = self.data.get_plug_status() if self.data else None - return plug_status.name.lower() if plug_status is not None else None - - @property - def icon(self) -> str: - """Icon handling.""" - return "mdi:power-plug" if self.is_plugged_in else "mdi:power-plug-off" +SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( + RenaultSensorEntityDescription( + key="battery_level", + coordinator="battery", + data_key="batteryLevel", + device_class=DEVICE_CLASS_BATTERY, + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + name="Battery Level", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="charge_state", + coordinator="battery", + data_key="chargingStatus", + device_class=DEVICE_CLASS_CHARGE_STATE, + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + icon_lambda=lambda x: ( + "mdi:flash" + if x.data == ChargeState.CHARGE_IN_PROGRESS.value + else "mdi:flash-off" + ), + name="Charge State", + value_lambda=lambda x: ( + _get_formatted_charging_status( + cast(KamereonVehicleBatteryStatusData, x.coordinator.data) + ) + ), + ), + RenaultSensorEntityDescription( + key="charging_remaining_time", + coordinator="battery", + data_key="chargingRemainingTime", + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + icon="mdi:timer", + name="Charging Remaining Time", + native_unit_of_measurement=TIME_MINUTES, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="charging_power", + coordinator="battery", + data_key="chargingInstantaneousPower", + device_class=DEVICE_CLASS_POWER, + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + name="Charging Power", + native_unit_of_measurement=POWER_KILO_WATT, + state_class=STATE_CLASS_MEASUREMENT, + value_lambda=lambda x: ( + cast(float, x.data) / 1000 + if x.vehicle.details.reports_charging_power_in_watts() + else x.data + ), + ), + RenaultSensorEntityDescription( + key="plug_state", + coordinator="battery", + data_key="plugStatus", + device_class=DEVICE_CLASS_PLUG_STATE, + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + icon_lambda=lambda x: ( + "mdi:power-plug" + if x.data == PlugState.PLUGGED.value + else "mdi:power-plug-off" + ), + name="Plug State", + value_lambda=lambda x: ( + _get_formatted_plug_status( + cast(KamereonVehicleBatteryStatusData, x.coordinator.data) + ) + ), + ), + RenaultSensorEntityDescription( + key="battery_autonomy", + coordinator="battery", + data_key="batteryAutonomy", + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + icon="mdi:ev-station", + name="Battery Autonomy", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="battery_available_energy", + coordinator="battery", + data_key="batteryAvailableEnergy", + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + device_class=DEVICE_CLASS_ENERGY, + name="Battery Available Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="battery_temperature", + coordinator="battery", + data_key="batteryTemperature", + device_class=DEVICE_CLASS_TEMPERATURE, + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + name="Battery Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="mileage", + coordinator="cockpit", + data_key="totalMileage", + entity_class=RenaultSensor[KamereonVehicleCockpitData], + icon="mdi:sign-direction", + name="Mileage", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=STATE_CLASS_TOTAL_INCREASING, + value_lambda=lambda x: round(cast(float, x.data)), + ), + RenaultSensorEntityDescription( + key="fuel_autonomy", + coordinator="cockpit", + data_key="fuelAutonomy", + entity_class=RenaultSensor[KamereonVehicleCockpitData], + icon="mdi:gas-station", + name="Fuel Autonomy", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=STATE_CLASS_MEASUREMENT, + requires_fuel=True, + value_lambda=lambda x: round(cast(float, x.data)), + ), + RenaultSensorEntityDescription( + key="fuel_quantity", + coordinator="cockpit", + data_key="fuelQuantity", + entity_class=RenaultSensor[KamereonVehicleCockpitData], + icon="mdi:fuel", + name="Fuel Quantity", + native_unit_of_measurement=VOLUME_LITERS, + state_class=STATE_CLASS_MEASUREMENT, + requires_fuel=True, + value_lambda=lambda x: round(cast(float, x.data)), + ), + RenaultSensorEntityDescription( + key="outside_temperature", + coordinator="hvac_status", + device_class=DEVICE_CLASS_TEMPERATURE, + data_key="externalTemperature", + entity_class=RenaultSensor[KamereonVehicleHvacStatusData], + name="Outside Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + RenaultSensorEntityDescription( + key="charge_mode", + coordinator="charge_mode", + data_key="chargeMode", + device_class=DEVICE_CLASS_CHARGE_MODE, + entity_class=RenaultSensor[KamereonVehicleChargeModeData], + icon_lambda=lambda x: ( + "mdi:calendar-clock" if x.data == "schedule_mode" else "mdi:calendar-remove" + ), + name="Charge Mode", + ), +) diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 2c742aa07cd..c100f85c498 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -12,8 +12,17 @@ from homeassistant.components.renault.const import ( DEVICE_CLASS_PLUG_STATE, DOMAIN, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, DEVICE_CLASS_BATTERY, @@ -32,6 +41,14 @@ from homeassistant.const import ( VOLUME_LITERS, ) +CHECK_ATTRIBUTES = ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_LAST_UPDATE, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, +) + # Mock config data to be used across multiple tests MOCK_CONFIG = { CONF_USERNAME: "email@test.com", @@ -66,13 +83,15 @@ MOCK_VEHICLES = { "entity_id": "binary_sensor.plugged_in", "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_ON, - "class": DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_ON, - "class": DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, ], SENSOR_DOMAIN: [ @@ -80,72 +99,94 @@ MOCK_VEHICLES = { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "141", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:ev-station", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.battery_available_energy", "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "31", - "unit": ENERGY_KILO_WATT_HOUR, - "class": DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", "result": "60", - "unit": PERCENTAGE, - "class": DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": "20", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { "entity_id": "sensor.charge_mode", "unique_id": "vf1aaaaa555777999_charge_mode", "result": "always", - "class": DEVICE_CLASS_CHARGE_MODE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ICON: "mdi:calendar-remove", }, { "entity_id": "sensor.charge_state", "unique_id": "vf1aaaaa555777999_charge_state", "result": "charge_in_progress", - "class": DEVICE_CLASS_CHARGE_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ICON: "mdi:flash", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", - "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { "entity_id": "sensor.charging_remaining_time", "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": "145", - "unit": TIME_MINUTES, + ATTR_ICON: "mdi:timer", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { "entity_id": "sensor.mileage", "unique_id": "vf1aaaaa555777999_mileage", "result": "49114", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:sign-direction", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.outside_temperature", "unique_id": "vf1aaaaa555777999_outside_temperature", "result": "8.0", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { "entity_id": "sensor.plug_state", "unique_id": "vf1aaaaa555777999_plug_state", "result": "plugged", - "class": DEVICE_CLASS_PLUG_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ICON: "mdi:power-plug", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, ], }, @@ -173,13 +214,15 @@ MOCK_VEHICLES = { "entity_id": "binary_sensor.plugged_in", "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_OFF, - "class": DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_OFF, - "class": DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", }, ], SENSOR_DOMAIN: [ @@ -187,65 +230,86 @@ MOCK_VEHICLES = { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "128", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:ev-station", + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.battery_available_energy", "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "0", - "unit": ENERGY_KILO_WATT_HOUR, - "class": DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", "result": "50", - "unit": PERCENTAGE, - "class": DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": STATE_UNKNOWN, - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { "entity_id": "sensor.charge_mode", "unique_id": "vf1aaaaa555777999_charge_mode", "result": "schedule_mode", - "class": DEVICE_CLASS_CHARGE_MODE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ICON: "mdi:calendar-clock", }, { "entity_id": "sensor.charge_state", "unique_id": "vf1aaaaa555777999_charge_state", "result": "charge_error", - "class": DEVICE_CLASS_CHARGE_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ICON: "mdi:flash-off", + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, - "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { "entity_id": "sensor.charging_remaining_time", "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": STATE_UNKNOWN, - "unit": TIME_MINUTES, + ATTR_ICON: "mdi:timer", + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { "entity_id": "sensor.mileage", "unique_id": "vf1aaaaa555777999_mileage", "result": "49114", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:sign-direction", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.plug_state", "unique_id": "vf1aaaaa555777999_plug_state", "result": "unplugged", - "class": DEVICE_CLASS_PLUG_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ICON: "mdi:power-plug-off", + ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", }, ], }, @@ -273,13 +337,15 @@ MOCK_VEHICLES = { "entity_id": "binary_sensor.plugged_in", "unique_id": "vf1aaaaa555777123_plugged_in", "result": STATE_ON, - "class": DEVICE_CLASS_PLUG, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777123_charging", "result": STATE_ON, - "class": DEVICE_CLASS_BATTERY_CHARGING, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, ], SENSOR_DOMAIN: [ @@ -287,77 +353,102 @@ MOCK_VEHICLES = { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777123_battery_autonomy", "result": "141", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:ev-station", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.battery_available_energy", "unique_id": "vf1aaaaa555777123_battery_available_energy", "result": "31", - "unit": ENERGY_KILO_WATT_HOUR, - "class": DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777123_battery_level", "result": "60", - "unit": PERCENTAGE, - "class": DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777123_battery_temperature", "result": "20", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { "entity_id": "sensor.charge_mode", "unique_id": "vf1aaaaa555777123_charge_mode", "result": "always", - "class": DEVICE_CLASS_CHARGE_MODE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ICON: "mdi:calendar-remove", }, { "entity_id": "sensor.charge_state", "unique_id": "vf1aaaaa555777123_charge_state", "result": "charge_in_progress", - "class": DEVICE_CLASS_CHARGE_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ICON: "mdi:flash", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", - "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { "entity_id": "sensor.charging_remaining_time", "unique_id": "vf1aaaaa555777123_charging_remaining_time", "result": "145", - "unit": TIME_MINUTES, + ATTR_ICON: "mdi:timer", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { "entity_id": "sensor.fuel_autonomy", "unique_id": "vf1aaaaa555777123_fuel_autonomy", "result": "35", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:gas-station", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.fuel_quantity", "unique_id": "vf1aaaaa555777123_fuel_quantity", "result": "3", - "unit": VOLUME_LITERS, + ATTR_ICON: "mdi:fuel", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { "entity_id": "sensor.mileage", "unique_id": "vf1aaaaa555777123_mileage", "result": "5567", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:sign-direction", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.plug_state", "unique_id": "vf1aaaaa555777123_plug_state", "result": "plugged", - "class": DEVICE_CLASS_PLUG_STATE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ICON: "mdi:power-plug", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", }, ], }, @@ -382,19 +473,25 @@ MOCK_VEHICLES = { "entity_id": "sensor.fuel_autonomy", "unique_id": "vf1aaaaa555777123_fuel_autonomy", "result": "35", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:gas-station", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { "entity_id": "sensor.fuel_quantity", "unique_id": "vf1aaaaa555777123_fuel_quantity", "result": "3", - "unit": VOLUME_LITERS, + ATTR_ICON: "mdi:fuel", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { "entity_id": "sensor.mileage", "unique_id": "vf1aaaaa555777123_mileage", "result": "5567", - "unit": LENGTH_KILOMETERS, + ATTR_ICON: "mdi:sign-direction", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, ], }, diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index 71bb90f16a6..c357d9d7a5a 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -5,6 +5,7 @@ import pytest from renault_api.kamereon import exceptions from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component @@ -14,7 +15,7 @@ from . import ( setup_renault_integration_vehicle_with_no_data, setup_renault_integration_vehicle_with_side_effect, ) -from .const import MOCK_VEHICLES +from .const import CHECK_ATTRIBUTES, MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry @@ -40,10 +41,10 @@ async def test_binary_sensors(hass, vehicle_type): 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"] + for attr in CHECK_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -67,10 +68,13 @@ async def test_binary_sensor_empty(hass, vehicle_type): 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 + for attr in CHECK_ATTRIBUTES: + if attr == ATTR_LAST_UPDATE: + assert state.attributes.get(attr) is None + else: + assert state.attributes.get(attr) == expected_entity.get(attr) @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -101,10 +105,13 @@ async def test_binary_sensor_errors(hass, vehicle_type): 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 + for attr in CHECK_ATTRIBUTES: + if attr == ATTR_LAST_UPDATE: + assert state.attributes.get(attr) is None + else: + assert state.attributes.get(attr) == expected_entity.get(attr) async def test_binary_sensor_access_denied(hass): diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 41fceccb56c..4bc4d96d75f 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -4,8 +4,20 @@ from unittest.mock import patch import pytest from renault_api.kamereon import exceptions +from homeassistant.components.renault.const import ( + DEVICE_CLASS_CHARGE_MODE, + DEVICE_CLASS_CHARGE_STATE, + DEVICE_CLASS_PLUG_STATE, +) +from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import State from homeassistant.setup import async_setup_component from . import ( @@ -14,11 +26,28 @@ from . import ( setup_renault_integration_vehicle_with_no_data, setup_renault_integration_vehicle_with_side_effect, ) -from .const import MOCK_VEHICLES +from .const import CHECK_ATTRIBUTES, MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry +def check_inactive_attribute(state: State, attr: str, expected_entity: dict): + """Check attribute for icon for inactive sensors.""" + if attr == ATTR_LAST_UPDATE: + assert state.attributes.get(attr) is None + elif attr == ATTR_ICON: + if expected_entity.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CHARGE_MODE: + assert state.attributes.get(ATTR_ICON) == "mdi:calendar-remove" + elif expected_entity.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CHARGE_STATE: + assert state.attributes.get(ATTR_ICON) == "mdi:flash-off" + elif expected_entity.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PLUG_STATE: + assert state.attributes.get(ATTR_ICON) == "mdi:power-plug-off" + else: + assert state.attributes.get(ATTR_ICON) == expected_entity.get(ATTR_ICON) + else: + assert state.attributes.get(attr) == expected_entity.get(attr) + + @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_sensors(hass, vehicle_type): """Test for Renault sensors.""" @@ -40,10 +69,10 @@ async def test_sensors(hass, vehicle_type): 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"] + for attr in CHECK_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -67,10 +96,10 @@ async def test_sensor_empty(hass, vehicle_type): 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_UNKNOWN + for attr in CHECK_ATTRIBUTES: + check_inactive_attribute(state, attr, expected_entity) @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -101,10 +130,10 @@ async def test_sensor_errors(hass, vehicle_type): 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 + for attr in CHECK_ATTRIBUTES: + check_inactive_attribute(state, attr, expected_entity) async def test_sensor_access_denied(hass):