Add sensors for Aprilaire integration (#113194)

* Add sensors for Aprilaire integration

* Exclude from coverage

* Exclude from coverage

* Add comment

* Update homeassistant/components/aprilaire/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Code review updates

* Code review updates

* Code review updates

* Code review updates

* Remove temperature conversion

* Add suggested display precision

* Code review updates

* Merge fix

* Code review fixes

* Fix type errors

* Revert change

* Fix type errors

* Type errors

* Use common keys

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Matthew FitzGerald-Chamberlain 2024-06-22 06:20:33 -05:00 committed by GitHub
parent 1c2aa9a49b
commit ea8d0ba2ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 363 additions and 1 deletions

View File

@ -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

View File

@ -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__)

View File

@ -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)

View File

@ -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%]"
}
}
}
}
}