Add support for room sensor accessories assigned to a Honeywell (Lyric) Thermostat (#104343)

* Add support for room sensor accessories.

- Update coordinator to refresh and grab information about room sensor accessories assigned to a thermostat
- Add sensor entities for room humidity and room temperature
- Add devices to the registry for each room accessory
- "via_device" these entities through the assigned thermostat.

* fixed pre-commit issues.

* PR suggestions

- update docstring to reflect ownership by thermostat
- fixed potential issue where a sensor would not be added if its temperature value was 0

* fix bad github merge

* asyicio.gather futures for updating theromstat room stats
This commit is contained in:
Ryan Mattson 2024-04-18 13:50:11 -05:00 committed by GitHub
parent 5702ab3059
commit 05c37648c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 2 deletions

View File

@ -12,6 +12,7 @@ from aiolyric import Lyric
from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.exceptions import LyricAuthenticationException, LyricException
from aiolyric.objects.device import LyricDevice from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation from aiolyric.objects.location import LyricLocation
from aiolyric.objects.priority import LyricAccessories, LyricRoom
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
@ -77,6 +78,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
async with asyncio.timeout(60): async with asyncio.timeout(60):
await lyric.get_locations() await lyric.get_locations()
await asyncio.gather(
*(
lyric.get_thermostat_rooms(location.locationID, device.deviceID)
for location in lyric.locations
for device in location.devices
if device.deviceClass == "Thermostat"
)
)
except LyricAuthenticationException as exception: except LyricAuthenticationException as exception:
# Attempt to refresh the token before failing. # Attempt to refresh the token before failing.
# Honeywell appear to have issues keeping tokens saved. # Honeywell appear to have issues keeping tokens saved.
@ -159,8 +169,43 @@ class LyricDeviceEntity(LyricEntity):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this Honeywell Lyric instance.""" """Return device information about this Honeywell Lyric instance."""
return DeviceInfo( return DeviceInfo(
identifiers={(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
manufacturer="Honeywell", manufacturer="Honeywell",
model=self.device.deviceModel, model=self.device.deviceModel,
name=self.device.name, name=f"{self.device.name} Thermostat",
)
class LyricAccessoryEntity(LyricDeviceEntity):
"""Defines a Honeywell Lyric accessory entity, a sub-device of a thermostat."""
def __init__(
self,
coordinator: DataUpdateCoordinator[Lyric],
location: LyricLocation,
device: LyricDevice,
room: LyricRoom,
accessory: LyricAccessories,
key: str,
) -> None:
"""Initialize the Honeywell Lyric accessory entity."""
super().__init__(coordinator, location, device, key)
self._room = room
self._accessory = accessory
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Honeywell Lyric instance."""
return DeviceInfo(
identifiers={
(
f"{dr.CONNECTION_NETWORK_MAC}_room_accessory",
f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}",
)
},
manufacturer="Honeywell",
model="RCHTSENSOR",
name=f"{self._room.roomName} Sensor",
via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id),
) )

View File

@ -9,6 +9,7 @@ from datetime import datetime, timedelta
from aiolyric import Lyric from aiolyric import Lyric
from aiolyric.objects.device import LyricDevice from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation from aiolyric.objects.location import LyricLocation
from aiolyric.objects.priority import LyricAccessories, LyricRoom
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -24,7 +25,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import LyricDeviceEntity from . import LyricAccessoryEntity, LyricDeviceEntity
from .const import ( from .const import (
DOMAIN, DOMAIN,
PRESET_HOLD_UNTIL, PRESET_HOLD_UNTIL,
@ -50,6 +51,14 @@ class LyricSensorEntityDescription(SensorEntityDescription):
suitable_fn: Callable[[LyricDevice], bool] 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, LyricAccessories], StateType | datetime]
suitable_fn: Callable[[LyricRoom, LyricAccessories], bool]
DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ DEVICE_SENSORS: list[LyricSensorEntityDescription] = [
LyricSensorEntityDescription( LyricSensorEntityDescription(
key="indoor_temperature", key="indoor_temperature",
@ -109,6 +118,26 @@ DEVICE_SENSORS: list[LyricSensorEntityDescription] = [
), ),
] ]
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.roomAvgHumidity,
suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor",
),
]
def get_setpoint_status(status: str, time: str) -> str | None: def get_setpoint_status(status: str, time: str) -> str | None:
"""Get status of the setpoint.""" """Get status of the setpoint."""
@ -147,6 +176,18 @@ async def async_setup_entry(
if device_sensor.suitable_fn(device) 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.macID, {}).values()
for accessory in room.accessories
for accessory_sensor in ACCESSORY_SENSORS
if accessory_sensor.suitable_fn(room, accessory)
)
class LyricSensor(LyricDeviceEntity, SensorEntity): class LyricSensor(LyricDeviceEntity, SensorEntity):
"""Define a Honeywell Lyric sensor.""" """Define a Honeywell Lyric sensor."""
@ -178,3 +219,40 @@ class LyricSensor(LyricDeviceEntity, SensorEntity):
def native_value(self) -> StateType | datetime: def native_value(self) -> StateType | datetime:
"""Return the state.""" """Return the state."""
return self.entity_description.value_fn(self.device) return self.entity_description.value_fn(self.device)
class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity):
"""Define a Honeywell Lyric sensor."""
entity_description: LyricSensorAccessoryEntityDescription
def __init__(
self,
coordinator: DataUpdateCoordinator[Lyric],
description: LyricSensorAccessoryEntityDescription,
location: LyricLocation,
parentDevice: LyricDevice,
room: LyricRoom,
accessory: LyricAccessories,
) -> None:
"""Initialize."""
super().__init__(
coordinator,
location,
parentDevice,
room,
accessory,
f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}",
)
self.room = room
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
def native_value(self) -> StateType | datetime:
"""Return the state."""
return self.entity_description.value_fn(self._room, self._accessory)

View File

@ -41,6 +41,12 @@
}, },
"setpoint_status": { "setpoint_status": {
"name": "Setpoint status" "name": "Setpoint status"
},
"room_temperature": {
"name": "Room temperature"
},
"room_humidity": {
"name": "Room humidity"
} }
} }
}, },