"""Support for Honeywell Lyric sensor platform.""" from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from typing import override from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation from aiolyric.objects.priority import LyricAccessory, LyricRoom from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .const import ( PRESET_HOLD_UNTIL, PRESET_NO_HOLD, PRESET_PERMANENT_HOLD, PRESET_TEMPORARY_HOLD, PRESET_VACATION_HOLD, ) from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator from .entity import LyricAccessoryEntity, LyricDeviceEntity LYRIC_SETPOINT_STATUS_NAMES = { PRESET_NO_HOLD: "Following Schedule", PRESET_PERMANENT_HOLD: "Held Permanently", PRESET_TEMPORARY_HOLD: "Held Temporarily", PRESET_VACATION_HOLD: "Holiday", } PRIORITY_STATUS_OPTIONS = { PRESET_NO_HOLD: "no_hold", PRESET_TEMPORARY_HOLD: "temporary_hold", PRESET_HOLD_UNTIL: "hold_until", PRESET_PERMANENT_HOLD: "permanent_hold", PRESET_VACATION_HOLD: "vacation_hold", } @dataclass(frozen=True, kw_only=True) class LyricSensorEntityDescription(SensorEntityDescription): """Class describing Honeywell Lyric sensor entities.""" value_fn: Callable[[LyricDevice], StateType | datetime] suitable_fn: Callable[[LyricDevice], bool] @dataclass(frozen=True, kw_only=True) class LyricSensorAccessoryEntityDescription(SensorEntityDescription): """Class describing Honeywell Lyric room sensor entities.""" value_fn: Callable[[LyricRoom, LyricAccessory], StateType | datetime] suitable_fn: Callable[[LyricRoom, LyricAccessory], bool] DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ LyricSensorEntityDescription( key="indoor_temperature", translation_key="indoor_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.indoor_temperature, suitable_fn=lambda device: device.indoor_temperature, ), LyricSensorEntityDescription( key="indoor_humidity", translation_key="indoor_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.indoor_humidity, suitable_fn=lambda device: device.indoor_humidity, ), LyricSensorEntityDescription( key="outdoor_temperature", translation_key="outdoor_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.outdoor_temperature, suitable_fn=lambda device: device.outdoor_temperature, ), LyricSensorEntityDescription( key="outdoor_humidity", translation_key="outdoor_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.displayed_outdoor_humidity, suitable_fn=lambda device: device.displayed_outdoor_humidity, ), LyricSensorEntityDescription( key="next_period_time", translation_key="next_period_time", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: get_datetime_from_future_time( device.changeable_values.next_period_time ), suitable_fn=lambda device: ( device.changeable_values and device.changeable_values.next_period_time ), ), LyricSensorEntityDescription( key="schedule_status", translation_key="schedule_status", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.schedule_status, suitable_fn=lambda device: device.schedule_status, ), LyricSensorEntityDescription( key="setpoint_status", translation_key="setpoint_status", value_fn=lambda device: get_setpoint_status( device.changeable_values.thermostat_setpoint_status, device.changeable_values.next_period_time, ), suitable_fn=lambda device: ( device.changeable_values and device.changeable_values.thermostat_setpoint_status ), ), ] ACCESSORY_SENSORS: list[LyricSensorAccessoryEntityDescription] = [ LyricSensorAccessoryEntityDescription( key="room_temperature", translation_key="room_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda _, accessory: accessory.temperature, suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", ), LyricSensorAccessoryEntityDescription( key="room_humidity", translation_key="room_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda room, _: room.room_avg_humidity, suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", ), LyricSensorAccessoryEntityDescription( key="room_average_temperature", translation_key="room_average_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda room, _: room.room_avg_temp, suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", ), ] def get_setpoint_status(status: str, time: str) -> str | None: """Get status of the setpoint.""" if status == PRESET_HOLD_UNTIL: return f"Held until {time}" return LYRIC_SETPOINT_STATUS_NAMES.get(status) def get_datetime_from_future_time(time_str: str | None) -> datetime | None: """Get datetime from future time provided.""" if time_str is None: return None time = dt_util.parse_time(time_str) if time is None: return None now = dt_util.utcnow() if time <= now.time(): now = now + timedelta(days=1) return dt_util.as_utc(datetime.combine(now.date(), time)) async def async_setup_entry( hass: HomeAssistant, entry: LyricConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Honeywell Lyric sensor platform based on a config entry.""" coordinator = entry.runtime_data async_add_entities( LyricSensor( coordinator, device_sensor, location, device, ) for location in coordinator.data.locations for device in location.devices for device_sensor in DEVICE_SENSORS if device_sensor.suitable_fn(device) ) async_add_entities( LyricAccessorySensor( coordinator, accessory_sensor, location, device, room, accessory ) for location in coordinator.data.locations for device in location.devices for room in coordinator.data.rooms_dict.get(device.mac_id, {}).values() for accessory in room.accessories for accessory_sensor in ACCESSORY_SENSORS if accessory_sensor.suitable_fn(room, accessory) ) async_add_entities( LyricPriorityStatusSensor(coordinator, location, device) for location in coordinator.data.locations for device in location.devices if device.device_class == "Thermostat" and coordinator.data.rooms_dict.get(device.mac_id) ) class LyricSensor(LyricDeviceEntity, SensorEntity): """Define a Honeywell Lyric sensor.""" entity_description: LyricSensorEntityDescription def __init__( self, coordinator: LyricDataUpdateCoordinator, description: LyricSensorEntityDescription, location: LyricLocation, device: LyricDevice, ) -> None: """Initialize.""" super().__init__( coordinator, location, device, f"{device.mac_id}_{description.key}", ) self.entity_description = description if description.device_class == SensorDeviceClass.TEMPERATURE: if device.units == "Fahrenheit": self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT else: self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS @property @override def native_value(self) -> StateType | datetime: """Return the state.""" return self.entity_description.value_fn(self.device) class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): """Define a Honeywell Lyric sensor.""" entity_description: LyricSensorAccessoryEntityDescription def __init__( self, coordinator: LyricDataUpdateCoordinator, description: LyricSensorAccessoryEntityDescription, location: LyricLocation, parentDevice: LyricDevice, room: LyricRoom, accessory: LyricAccessory, ) -> None: """Initialize.""" super().__init__( coordinator, location, parentDevice, room, accessory, f"{parentDevice.mac_id}_room{room.id}_acc{accessory.id}_{description.key}", ) self.entity_description = description if description.device_class == SensorDeviceClass.TEMPERATURE: if parentDevice.units == "Fahrenheit": self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT else: self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS @property @override def native_value(self) -> StateType | datetime: """Return the state.""" return self.entity_description.value_fn(self.room, self.accessory) class LyricPriorityStatusSensor(LyricDeviceEntity, SensorEntity): """Define a Honeywell Lyric room priority hold status sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "priority_status" _attr_device_class = SensorDeviceClass.ENUM _attr_options = list(PRIORITY_STATUS_OPTIONS.values()) def __init__( self, coordinator: LyricDataUpdateCoordinator, location: LyricLocation, device: LyricDevice, ) -> None: """Initialize.""" super().__init__( coordinator, location, device, f"{device.mac_id}_priority_status", ) @property @override def native_value(self) -> str | None: """Return the state.""" priority = self.coordinator.data.priorities_dict.get(self._mac_id) if priority is None: return None return PRIORITY_STATUS_OPTIONS.get(priority.status)