diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index b0f1d73f238..104f463306f 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -1,7 +1,8 @@ """Support for MelCloud device sensors.""" from __future__ import annotations -from typing import Any, Callable, NamedTuple +from dataclasses import dataclass +from typing import Any, Callable from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone @@ -11,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from homeassistant.util import dt as dt_util @@ -19,130 +21,146 @@ from . import MelCloudDevice from .const import DOMAIN -class SensorMetadata(NamedTuple): - """Metadata for an individual sensor.""" +@dataclass +class MelcloudSensorEntityDescription(SensorEntityDescription): + """Describes Melcloud sensor entity.""" - measurement_name: str - icon: str - unit: str - device_class: str - value_fn: Callable[[Any], float] - enabled: Callable[[Any], bool] + _value_fn: Callable[[Any], float] | None = None + _enabled: Callable[[Any], bool] | None = None + + def __post_init__(self) -> None: + """Ensure all required fields are set.""" + if self._value_fn is None: # pragma: no cover + raise TypeError + if self._enabled is None: # pragma: no cover + raise TypeError + self.value_fn = self._value_fn + self.enabled = self._enabled -ATA_SENSORS: dict[str, SensorMetadata] = { - "room_temperature": SensorMetadata( - "Room Temperature", +ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="room_temperature", + name="Room Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.room_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.room_temperature, + _enabled=lambda x: True, ), - "energy": SensorMetadata( - "Energy", + MelcloudSensorEntityDescription( + key="energy", + name="Energy", icon="mdi:factory", - unit=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - value_fn=lambda x: x.device.total_energy_consumed, - enabled=lambda x: x.device.has_energy_consumed_meter, + _value_fn=lambda x: x.device.total_energy_consumed, + _enabled=lambda x: x.device.has_energy_consumed_meter, ), -} -ATW_SENSORS: dict[str, SensorMetadata] = { - "outside_temperature": SensorMetadata( - "Outside Temperature", +) +ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="outside_temperature", + name="Outside Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.outside_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.outside_temperature, + _enabled=lambda x: True, ), - "tank_temperature": SensorMetadata( - "Tank Temperature", + MelcloudSensorEntityDescription( + key="tank_temperature", + name="Tank Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda x: x.device.tank_temperature, - enabled=lambda x: True, + _value_fn=lambda x: x.device.tank_temperature, + _enabled=lambda x: True, ), -} -ATW_ZONE_SENSORS: dict[str, SensorMetadata] = { - "room_temperature": SensorMetadata( - "Room Temperature", +) +ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( + MelcloudSensorEntityDescription( + key="room_temperature", + name="Room Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.room_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.room_temperature, + _enabled=lambda x: True, ), - "flow_temperature": SensorMetadata( - "Flow Temperature", + MelcloudSensorEntityDescription( + key="flow_temperature", + name="Flow Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.flow_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.flow_temperature, + _enabled=lambda x: True, ), - "return_temperature": SensorMetadata( - "Flow Return Temperature", + MelcloudSensorEntityDescription( + key="return_temperature", + name="Flow Return Temperature", icon="mdi:thermometer", - unit=TEMP_CELSIUS, + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - value_fn=lambda zone: zone.return_temperature, - enabled=lambda x: True, + _value_fn=lambda zone: zone.return_temperature, + _enabled=lambda x: True, ), -} +) async def async_setup_entry(hass, entry, async_add_entities): """Set up MELCloud device sensors based on config_entry.""" mel_devices = hass.data[DOMAIN].get(entry.entry_id) - async_add_entities( + + entities: list[MelDeviceSensor] = [ + MelDeviceSensor(mel_device, description) + for description in ATA_SENSORS + for mel_device in mel_devices[DEVICE_TYPE_ATA] + if description.enabled(mel_device) + ] + [ + MelDeviceSensor(mel_device, description) + for description in ATW_SENSORS + for mel_device in mel_devices[DEVICE_TYPE_ATW] + if description.enabled(mel_device) + ] + entities.extend( [ - MelDeviceSensor(mel_device, measurement, metadata) - for measurement, metadata in ATA_SENSORS.items() - for mel_device in mel_devices[DEVICE_TYPE_ATA] - if metadata.enabled(mel_device) - ] - + [ - MelDeviceSensor(mel_device, measurement, metadata) - for measurement, metadata in ATW_SENSORS.items() - for mel_device in mel_devices[DEVICE_TYPE_ATW] - if metadata.enabled(mel_device) - ] - + [ - AtwZoneSensor(mel_device, zone, measurement, metadata) + AtwZoneSensor(mel_device, zone, description) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - for measurement, metadata, in ATW_ZONE_SENSORS.items() - if metadata.enabled(zone) - ], - True, + for description in ATW_ZONE_SENSORS + if description.enabled(zone) + ] ) + async_add_entities(entities, True) class MelDeviceSensor(SensorEntity): """Representation of a Sensor.""" - def __init__(self, api: MelCloudDevice, measurement, metadata: SensorMetadata): + entity_description: MelcloudSensorEntityDescription + + def __init__( + self, + api: MelCloudDevice, + description: MelcloudSensorEntityDescription, + ) -> None: """Initialize the sensor.""" self._api = api - self._metadata = metadata + self.entity_description = description - self._attr_device_class = metadata.device_class - self._attr_icon = metadata.icon - self._attr_name = f"{api.name} {metadata.measurement_name}" - self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{measurement}" - self._attr_unit_of_measurement = metadata.unit + self._attr_name = f"{api.name} {description.name}" + self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" self._attr_state_class = STATE_CLASS_MEASUREMENT - if metadata.device_class == DEVICE_CLASS_ENERGY: + if description.device_class == DEVICE_CLASS_ENERGY: self._attr_last_reset = dt_util.utc_from_timestamp(0) @property def state(self): """Return the state of the sensor.""" - return self._metadata.value_fn(self._api) + return self.entity_description.value_fn(self._api) async def async_update(self): """Retrieve latest state.""" @@ -158,18 +176,19 @@ class AtwZoneSensor(MelDeviceSensor): """Air-to-Air device sensor.""" def __init__( - self, api: MelCloudDevice, zone: Zone, measurement, metadata: SensorMetadata - ): + self, + api: MelCloudDevice, + zone: Zone, + description: MelcloudSensorEntityDescription, + ) -> None: """Initialize the sensor.""" - if zone.zone_index == 1: - full_measurement = measurement - else: - full_measurement = f"{measurement}-zone-{zone.zone_index}" - super().__init__(api, full_measurement, metadata) + if zone.zone_index != 1: + description.key = f"{description.key}-zone-{zone.zone_index}" + super().__init__(api, description) self._zone = zone - self._attr_name = f"{api.name} {zone.name} {metadata.measurement_name}" + self._attr_name = f"{api.name} {zone.name} {description.name}" @property def state(self): """Return zone based state.""" - return self._metadata.value_fn(self._zone) + return self.entity_description.value_fn(self._zone) diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py index 6e6487a3774..5938f1af1f1 100644 --- a/tests/components/melcloud/test_atw_zone_sensor.py +++ b/tests/components/melcloud/test_atw_zone_sensor.py @@ -37,15 +37,13 @@ def test_zone_unique_ids(mock_device, mock_zone_1, mock_zone_2): sensor_1 = AtwZoneSensor( mock_device, mock_zone_1, - "room_temperature", - ATW_ZONE_SENSORS["room_temperature"], + ATW_ZONE_SENSORS[0], # room_temperature ) assert sensor_1.unique_id == "1234-11:11:11:11:11:11-room_temperature" sensor_2 = AtwZoneSensor( mock_device, mock_zone_2, - "room_temperature", - ATW_ZONE_SENSORS["flow_temperature"], + ATW_ZONE_SENSORS[0], # room_temperature ) assert sensor_2.unique_id == "1234-11:11:11:11:11:11-room_temperature-zone-2"