diff --git a/.coveragerc b/.coveragerc index 350c39ca3d2..43dc6edafc5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -87,6 +87,7 @@ omit = homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/entity.py + homeassistant/components/aprilaire/sensor.py homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py homeassistant/components/apsystems/entity.py diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index 4fa5cdac68d..ba310615567 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN from .coordinator import AprilaireCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprilaire/sensor.py b/homeassistant/components/aprilaire/sensor.py new file mode 100644 index 00000000000..249c1b3850f --- /dev/null +++ b/homeassistant/components/aprilaire/sensor.py @@ -0,0 +1,308 @@ +"""The Aprilaire sensor component.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from pyaprilaire.const import Attribute + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import AprilaireCoordinator +from .entity import BaseAprilaireEntity + +DEHUMIDIFICATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "on", + 4: "off", +} + +HUMIDIFICATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "off", +} + +VENTILATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "idle", + 4: "idle", + 5: "idle", + 6: "off", +} + +AIR_CLEANING_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "off", +} + +FAN_STATUS_MAP: dict[StateType, str] = {0: "off", 1: "on"} + + +def get_entities( + entity_class: type[BaseAprilaireSensor], + coordinator: AprilaireCoordinator, + unique_id: str, + descriptions: tuple[AprilaireSensorDescription, ...], +) -> list[BaseAprilaireSensor]: + """Get the entities for a list of sensor descriptions.""" + + entities = ( + entity_class(coordinator, description, unique_id) + for description in descriptions + ) + + return [entity for entity in entities if entity.exists] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Aprilaire sensor devices.""" + + coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id] + + assert config_entry.unique_id is not None + + entities = ( + get_entities( + AprilaireHumiditySensor, + coordinator, + config_entry.unique_id, + HUMIDITY_SENSORS, + ) + + get_entities( + AprilaireTemperatureSensor, + coordinator, + config_entry.unique_id, + TEMPERATURE_SENSORS, + ) + + get_entities( + AprilaireStatusSensor, coordinator, config_entry.unique_id, STATUS_SENSORS + ) + ) + + async_add_entities(entities) + + +@dataclass(frozen=True, kw_only=True) +class AprilaireSensorDescription(SensorEntityDescription): + """Class describing Aprilaire sensor entities.""" + + status_key: str | None + value_key: str + + +@dataclass(frozen=True, kw_only=True) +class AprilaireStatusSensorDescription(AprilaireSensorDescription): + """Class describing Aprilaire status sensor entities.""" + + status_map: dict[StateType, str] + + +HUMIDITY_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireSensorDescription( + key="indoor_humidity_controlling_sensor", + translation_key="indoor_humidity_controlling_sensor", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + status_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + ), + AprilaireSensorDescription( + key="outdoor_humidity_controlling_sensor", + translation_key="outdoor_humidity_controlling_sensor", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + status_key=Attribute.OUTDOOR_HUMIDITY_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.OUTDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + ), +) + +TEMPERATURE_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireSensorDescription( + key="indoor_temperature_controlling_sensor", + translation_key="indoor_temperature_controlling_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + status_key=Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_VALUE, + ), + AprilaireSensorDescription( + key="outdoor_temperature_controlling_sensor", + translation_key="outdoor_temperature_controlling_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + status_key=Attribute.OUTDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.OUTDOOR_TEMPERATURE_CONTROLLING_SENSOR_VALUE, + ), +) + +STATUS_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireStatusSensorDescription( + key="dehumidification_status", + translation_key="dehumidification_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.DEHUMIDIFICATION_AVAILABLE, + value_key=Attribute.DEHUMIDIFICATION_STATUS, + status_map=DEHUMIDIFICATION_STATUS_MAP, + options=list(set(DEHUMIDIFICATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="humidification_status", + translation_key="humidification_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.HUMIDIFICATION_AVAILABLE, + value_key=Attribute.HUMIDIFICATION_STATUS, + status_map=HUMIDIFICATION_STATUS_MAP, + options=list(set(HUMIDIFICATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="ventilation_status", + translation_key="ventilation_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.VENTILATION_AVAILABLE, + value_key=Attribute.VENTILATION_STATUS, + status_map=VENTILATION_STATUS_MAP, + options=list(set(VENTILATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="air_cleaning_status", + translation_key="air_cleaning_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.AIR_CLEANING_AVAILABLE, + value_key=Attribute.AIR_CLEANING_STATUS, + status_map=AIR_CLEANING_STATUS_MAP, + options=list(set(AIR_CLEANING_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="fan_status", + translation_key="fan_status", + device_class=SensorDeviceClass.ENUM, + status_key=None, + value_key=Attribute.FAN_STATUS, + status_map=FAN_STATUS_MAP, + options=list(set(FAN_STATUS_MAP.values())), + ), +) + + +class BaseAprilaireSensor(BaseAprilaireEntity, SensorEntity): + """Base sensor entity for Aprilaire.""" + + entity_description: AprilaireSensorDescription + status_sensor_available_value: int | None = None + status_sensor_exists_values: list[int] + + def __init__( + self, + coordinator: AprilaireCoordinator, + description: AprilaireSensorDescription, + unique_id: str, + ) -> None: + """Initialize a sensor for an Aprilaire device.""" + + self.entity_description = description + + super().__init__(coordinator, unique_id) + + @property + def exists(self) -> bool: + """Return True if the sensor exists.""" + + if self.entity_description.status_key is None: + return True + + return ( + self.coordinator.data.get(self.entity_description.status_key) + in self.status_sensor_exists_values + ) + + @property + def available(self) -> bool: + """Return True if the sensor is available.""" + + if ( + self.entity_description.status_key is None + or self.status_sensor_available_value is None + ): + return True + + if not super().available: + return False + + return ( + self.coordinator.data.get(self.entity_description.status_key) + == self.status_sensor_available_value + ) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + + # Valid cast as pyaprilaire only provides str | int | float + return cast( + StateType, self.coordinator.data.get(self.entity_description.value_key) + ) + + +class AprilaireHumiditySensor(BaseAprilaireSensor): + """Humidity sensor entity for Aprilaire.""" + + status_sensor_available_value = 0 + status_sensor_exists_values = [0, 1, 2] + + +class AprilaireTemperatureSensor(BaseAprilaireSensor): + """Temperature sensor entity for Aprilaire.""" + + status_sensor_available_value = 0 + status_sensor_exists_values = [0, 1, 2] + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + if self.unit_of_measurement == UnitOfTemperature.CELSIUS: + return 1 + + return 0 + + +class AprilaireStatusSensor(BaseAprilaireSensor): + """Status sensor entity for Aprilaire.""" + + status_sensor_exists_values = [1, 2] + entity_description: AprilaireStatusSensorDescription + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor mapped to the status option.""" + + raw_value = super().native_value + + return self.entity_description.status_map.get(raw_value) diff --git a/homeassistant/components/aprilaire/strings.json b/homeassistant/components/aprilaire/strings.json index e996691f21f..72005e0215c 100644 --- a/homeassistant/components/aprilaire/strings.json +++ b/homeassistant/components/aprilaire/strings.json @@ -23,6 +23,59 @@ "thermostat": { "name": "Thermostat" } + }, + "sensor": { + "indoor_humidity_controlling_sensor": { + "name": "Indoor humidity controlling sensor" + }, + "outdoor_humidity_controlling_sensor": { + "name": "Outdoor humidity controlling sensor" + }, + "indoor_temperature_controlling_sensor": { + "name": "Indoor temperature controlling sensor" + }, + "outdoor_temperature_controlling_sensor": { + "name": "Outdoor temperature controlling sensor" + }, + "dehumidification_status": { + "name": "Dehumidification status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "humidification_status": { + "name": "Humidification status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "ventilation_status": { + "name": "Ventilation status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "air_cleaning_status": { + "name": "Air cleaning status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "fan_status": { + "name": "Fan status", + "state": { + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + } } } }