Use EntityDescription - renault (#55061)
* Cleanup sensor.py * Add EntityDescription * Add checks for state attributes * Fix pylint * Simplify checks * Add icon checks * Update data type * Use mixin for required keys, and review class initialisation * Add constraint to TypeVar("T") * Enable lambda for icon handling * Enable lambda for value handling * Enable lambda for value handling
This commit is contained in:
parent
35d943ba56
commit
9315f3bdd9
6 changed files with 529 additions and 387 deletions
|
@ -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",
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue