"""Platform for Mazda sensor integration.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, PRESSURE_PSI, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.unit_system import UnitSystem from . import MazdaEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @dataclass class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" # Suffix to be appended to the vehicle name to obtain the sensor name name_suffix: str # Function to determine the value for this sensor, given the coordinator data and the configured unit system value: Callable[[dict, UnitSystem], StateType] @dataclass class MazdaSensorEntityDescription( SensorEntityDescription, MazdaSensorRequiredKeysMixin ): """Describes a Mazda sensor entity.""" # Function to determine whether the vehicle supports this sensor, given the coordinator data is_supported: Callable[[dict], bool] = lambda data: True # Function to determine the unit of measurement for this sensor, given the configured unit system # Falls back to description.native_unit_of_measurement if it is not provided unit: Callable[[UnitSystem], str | None] | None = None def _get_distance_unit(unit_system): """Return the distance unit for the given unit system.""" if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: return LENGTH_MILES return LENGTH_KILOMETERS def _fuel_remaining_percentage_supported(data): """Determine if fuel remaining percentage is supported.""" return (not data["isElectric"]) and ( data["status"]["fuelRemainingPercent"] is not None ) def _fuel_distance_remaining_supported(data): """Determine if fuel distance remaining is supported.""" return (not data["isElectric"]) and ( data["status"]["fuelDistanceRemainingKm"] is not None ) def _front_left_tire_pressure_supported(data): """Determine if front left tire pressure is supported.""" return data["status"]["tirePressure"]["frontLeftTirePressurePsi"] is not None def _front_right_tire_pressure_supported(data): """Determine if front right tire pressure is supported.""" return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None def _rear_left_tire_pressure_supported(data): """Determine if rear left tire pressure is supported.""" return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None def _rear_right_tire_pressure_supported(data): """Determine if rear right tire pressure is supported.""" return data["status"]["tirePressure"]["rearRightTirePressurePsi"] is not None def _ev_charge_level_supported(data): """Determine if charge level is supported.""" return ( data["isElectric"] and data["evStatus"]["chargeInfo"]["batteryLevelPercentage"] is not None ) def _ev_remaining_range_supported(data): """Determine if remaining range is supported.""" return ( data["isElectric"] and data["evStatus"]["chargeInfo"]["drivingRangeKm"] is not None ) def _fuel_distance_remaining_value(data, unit_system): """Get the fuel distance remaining value.""" return round( unit_system.length(data["status"]["fuelDistanceRemainingKm"], LENGTH_KILOMETERS) ) def _odometer_value(data, unit_system): """Get the odometer value.""" return round(unit_system.length(data["status"]["odometerKm"], LENGTH_KILOMETERS)) def _front_left_tire_pressure_value(data, unit_system): """Get the front left tire pressure value.""" return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"]) def _front_right_tire_pressure_value(data, unit_system): """Get the front right tire pressure value.""" return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"]) def _rear_left_tire_pressure_value(data, unit_system): """Get the rear left tire pressure value.""" return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"]) def _rear_right_tire_pressure_value(data, unit_system): """Get the rear right tire pressure value.""" return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"]) def _ev_charge_level_value(data, unit_system): """Get the charge level value.""" return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"]) def _ev_remaining_range_value(data, unit_system): """Get the remaining range value.""" return round( unit_system.length( data["evStatus"]["chargeInfo"]["drivingRangeKm"], LENGTH_KILOMETERS ) ) SENSOR_ENTITIES = [ MazdaSensorEntityDescription( key="fuel_remaining_percentage", name_suffix="Fuel Remaining Percentage", icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_remaining_percentage_supported, value=lambda data, unit_system: data["status"]["fuelRemainingPercent"], ), MazdaSensorEntityDescription( key="fuel_distance_remaining", name_suffix="Fuel Distance Remaining", icon="mdi:gas-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_distance_remaining_supported, value=_fuel_distance_remaining_value, ), MazdaSensorEntityDescription( key="odometer", name_suffix="Odometer", icon="mdi:speedometer", unit=_get_distance_unit, state_class=SensorStateClass.TOTAL_INCREASING, is_supported=lambda data: data["status"]["odometerKm"] is not None, value=_odometer_value, ), MazdaSensorEntityDescription( key="front_left_tire_pressure", name_suffix="Front Left Tire Pressure", icon="mdi:car-tire-alert", native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_front_left_tire_pressure_supported, value=_front_left_tire_pressure_value, ), MazdaSensorEntityDescription( key="front_right_tire_pressure", name_suffix="Front Right Tire Pressure", icon="mdi:car-tire-alert", native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_front_right_tire_pressure_supported, value=_front_right_tire_pressure_value, ), MazdaSensorEntityDescription( key="rear_left_tire_pressure", name_suffix="Rear Left Tire Pressure", icon="mdi:car-tire-alert", native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_rear_left_tire_pressure_supported, value=_rear_left_tire_pressure_value, ), MazdaSensorEntityDescription( key="rear_right_tire_pressure", name_suffix="Rear Right Tire Pressure", icon="mdi:car-tire-alert", native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_rear_right_tire_pressure_supported, value=_rear_right_tire_pressure_value, ), MazdaSensorEntityDescription( key="ev_charge_level", name_suffix="Charge Level", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, is_supported=_ev_charge_level_supported, value=_ev_charge_level_value, ), MazdaSensorEntityDescription( key="ev_remaining_range", name_suffix="Remaining Range", icon="mdi:ev-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, is_supported=_ev_remaining_range_supported, value=_ev_remaining_range_value, ), ] async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] entities: list[SensorEntity] = [] for index, data in enumerate(coordinator.data): for description in SENSOR_ENTITIES: if description.is_supported(data): entities.append( MazdaSensorEntity(client, coordinator, index, description) ) async_add_entities(entities) class MazdaSensorEntity(MazdaEntity, SensorEntity): """Representation of a Mazda vehicle sensor.""" entity_description: MazdaSensorEntityDescription def __init__(self, client, coordinator, index, description): """Initialize Mazda sensor.""" super().__init__(client, coordinator, index) self.entity_description = description self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property def native_unit_of_measurement(self): """Return the unit of measurement for the sensor, according to the configured unit system.""" if unit_fn := self.entity_description.unit: return unit_fn(self.hass.config.units) return self.entity_description.native_unit_of_measurement @property def native_value(self): """Return the state of the sensor.""" return self.entity_description.value(self.data, self.hass.config.units)