From 4252ad7befbc7b944bcdc0f01cf8a8307dfd5028 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 10 Apr 2026 17:44:16 -0400 Subject: [PATCH 1/8] Add room priority select entity to Lyric integration Adds a select entity per LCC thermostat that has room sensors, allowing users to choose which room's temperature reading the thermostat should prioritize. Options are dynamically built from the thermostat's linked rooms. Supported priority modes: - Follow Me: auto-switch based on motion detection - : use a specific room sensor (PickARoom) Depends on: - aiolyric storing LyricPriority in priorities_dict (library PR pending) - aiolyric PR #134 (fix update_priority POST->PUT) --- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/lyric/icons.json | 5 + homeassistant/components/lyric/select.py | 134 ++++++++++++++++++++ homeassistant/components/lyric/strings.json | 8 ++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lyric/select.py diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 95fb559491d325..90b95a7cbd1828 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -23,7 +23,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SELECT, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool: diff --git a/homeassistant/components/lyric/icons.json b/homeassistant/components/lyric/icons.json index edb61c3f8e2bd3..6203a932e6b5aa 100644 --- a/homeassistant/components/lyric/icons.json +++ b/homeassistant/components/lyric/icons.json @@ -1,5 +1,10 @@ { "entity": { + "select": { + "room_priority": { + "default": "mdi:home-thermometer" + } + }, "sensor": { "setpoint_status": { "default": "mdi:thermostat" diff --git a/homeassistant/components/lyric/select.py b/homeassistant/components/lyric/select.py new file mode 100644 index 00000000000000..c84b0ba8316dca --- /dev/null +++ b/homeassistant/components/lyric/select.py @@ -0,0 +1,134 @@ +"""Support for Honeywell Lyric select platform.""" + +from __future__ import annotations + +import logging + +from aiolyric.objects.device import LyricDevice +from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricRoom + +from homeassistant.components.select import SelectEntity +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import LYRIC_EXCEPTIONS +from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator +from .entity import LyricDeviceEntity + +_LOGGER = logging.getLogger(__name__) + +# Honeywell Lyric API priority types +PRIORITY_TYPE_PICK_A_ROOM = "PickARoom" +PRIORITY_TYPE_FOLLOW_ME = "FollowMe" +PRIORITY_TYPE_WHOLE_HOUSE = "WholeHouse" + +# Option shown in the select for the FollowMe mode +OPTION_FOLLOW_ME = "follow_me" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LyricConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Honeywell Lyric select entities based on a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + LyricRoomPrioritySelect(coordinator, location, device) + for location in coordinator.data.locations + for device in location.devices + if device.device_class == "Thermostat" + and device.device_id.startswith("LCC") + and coordinator.data.rooms_dict.get(device.mac_id) + ) + + +class LyricRoomPrioritySelect(LyricDeviceEntity, SelectEntity): + """Select entity for Honeywell Lyric thermostat room priority.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "room_priority" + + def __init__( + self, + coordinator: LyricDataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + ) -> None: + """Initialize the room priority select entity.""" + super().__init__( + coordinator, + location, + device, + f"{device.mac_id}_room_priority", + ) + + @property + def _rooms(self) -> dict[int, LyricRoom]: + """Return the rooms for this thermostat.""" + return self.coordinator.data.rooms_dict.get(self._mac_id, {}) + + @property + def options(self) -> list[str]: + """Return the list of available room priority options.""" + room_options = sorted( + room.room_name for room in self._rooms.values() if room.room_name + ) + return [OPTION_FOLLOW_ME, *room_options] + + @property + def current_option(self) -> str | None: + """Return the currently selected room priority.""" + priority = self.coordinator.data.priorities_dict.get(self._mac_id) + if priority is None: + return None + + current = priority.current_priority + if current.priority_type == PRIORITY_TYPE_FOLLOW_ME: + return OPTION_FOLLOW_ME + + if current.priority_type == PRIORITY_TYPE_PICK_A_ROOM: + selected = current.selected_rooms + if selected: + room = self._rooms.get(selected[0]) + if room is not None: + return room.room_name + + return None + + async def async_select_option(self, option: str) -> None: + """Set the room priority.""" + if option == OPTION_FOLLOW_ME: + priority_type = PRIORITY_TYPE_FOLLOW_ME + rooms: list[int] = [] + else: + priority_type = PRIORITY_TYPE_PICK_A_ROOM + room_id = next( + ( + rid + for rid, room in self._rooms.items() + if room.room_name == option + ), + None, + ) + if room_id is None: + _LOGGER.error("Room not found: %s", option) + return + rooms = [room_id] + + _LOGGER.debug( + "Set room priority: type=%s, rooms=%s", priority_type, rooms + ) + try: + await self.coordinator.data.update_priority( + self.location, + self.device, + priority_type=priority_type, + rooms=rooms, + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 51f1cff5269aca..7ea9cbfeee9562 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -37,6 +37,14 @@ } }, "entity": { + "select": { + "room_priority": { + "name": "Room priority", + "state": { + "follow_me": "Follow me" + } + } + }, "sensor": { "indoor_humidity": { "name": "Indoor humidity" From 417efcf107ed2b64045065c2e8a3239388f764e6 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 11 Apr 2026 11:01:41 -0400 Subject: [PATCH 2/8] Fetch priority data in coordinator as fallback for stock aiolyric The coordinator now fetches priority data directly from the Honeywell API if the aiolyric library doesn't expose priorities_dict. This allows the select entity to work with the current aiolyric 2.0.2 without requiring a library upgrade for the read path. --- homeassistant/components/lyric/coordinator.py | 34 ++++++++++++++++--- homeassistant/components/lyric/select.py | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lyric/coordinator.py b/homeassistant/components/lyric/coordinator.py index b9b36e56133e94..e0cd2e149e8ba3 100644 --- a/homeassistant/components/lyric/coordinator.py +++ b/homeassistant/components/lyric/coordinator.py @@ -16,6 +16,8 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from aiolyric.objects.priority import LyricPriority + from .api import OAuth2SessionLyric _LOGGER = logging.getLogger(__name__) @@ -45,6 +47,20 @@ def __init__( ) self.oauth_session = oauth_session self.lyric = lyric + self.priorities_dict: dict[str, LyricPriority] = {} + + async def _fetch_priorities( + self, lcc_devices: list[tuple] + ) -> None: + """Fetch priority data for LCC devices.""" + from aiolyric.const import BASE_URL + + for location, device in lcc_devices: + url = f"{BASE_URL}/devices/thermostats/{device.device_id}/priority?apikey={self.lyric.client_id}&locationId={location.location_id}" + response = await self.lyric._client.get(url) + json_data = await response.json() + priority = LyricPriority(json_data) + self.priorities_dict[priority.device_id] = priority async def _async_update_data(self) -> Lyric: """Fetch data from Lyric.""" @@ -65,17 +81,27 @@ async def _run_update(self, force_refresh_token: bool) -> Lyric: try: async with asyncio.timeout(60): await self.lyric.get_locations() + lcc_devices = [ + (location, device) + for location in self.lyric.locations + for device in location.devices + if device.device_class == "Thermostat" + and device.device_id.startswith("LCC") + ] await asyncio.gather( *( self.lyric.get_thermostat_rooms( location.location_id, device.device_id ) - for location in self.lyric.locations - for device in location.devices - if device.device_class == "Thermostat" - and device.device_id.startswith("LCC") + for location, device in lcc_devices ) ) + # Store priority data from the library if available, + # otherwise fetch it directly + if hasattr(self.lyric, "priorities_dict"): + self.priorities_dict = self.lyric.priorities_dict + else: + await self._fetch_priorities(lcc_devices) except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. diff --git a/homeassistant/components/lyric/select.py b/homeassistant/components/lyric/select.py index c84b0ba8316dca..b6740d918aee5e 100644 --- a/homeassistant/components/lyric/select.py +++ b/homeassistant/components/lyric/select.py @@ -82,7 +82,7 @@ def options(self) -> list[str]: @property def current_option(self) -> str | None: """Return the currently selected room priority.""" - priority = self.coordinator.data.priorities_dict.get(self._mac_id) + priority = self.coordinator.priorities_dict.get(self._mac_id) if priority is None: return None From 97bcce2c3fa1407e47a252de63d3fcbf18e89bb0 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 11 Apr 2026 11:15:22 -0400 Subject: [PATCH 3/8] Fix ruff lint: move import to top-level, remove private member access --- homeassistant/components/lyric/coordinator.py | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/lyric/coordinator.py b/homeassistant/components/lyric/coordinator.py index e0cd2e149e8ba3..ae0cb71323191e 100644 --- a/homeassistant/components/lyric/coordinator.py +++ b/homeassistant/components/lyric/coordinator.py @@ -10,14 +10,13 @@ from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric from aiolyric.exceptions import LyricAuthenticationException, LyricException +from aiolyric.objects.priority import LyricPriority from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from aiolyric.objects.priority import LyricPriority - from .api import OAuth2SessionLyric _LOGGER = logging.getLogger(__name__) @@ -49,19 +48,6 @@ def __init__( self.lyric = lyric self.priorities_dict: dict[str, LyricPriority] = {} - async def _fetch_priorities( - self, lcc_devices: list[tuple] - ) -> None: - """Fetch priority data for LCC devices.""" - from aiolyric.const import BASE_URL - - for location, device in lcc_devices: - url = f"{BASE_URL}/devices/thermostats/{device.device_id}/priority?apikey={self.lyric.client_id}&locationId={location.location_id}" - response = await self.lyric._client.get(url) - json_data = await response.json() - priority = LyricPriority(json_data) - self.priorities_dict[priority.device_id] = priority - async def _async_update_data(self) -> Lyric: """Fetch data from Lyric.""" return await self._run_update(False) @@ -81,27 +67,19 @@ async def _run_update(self, force_refresh_token: bool) -> Lyric: try: async with asyncio.timeout(60): await self.lyric.get_locations() - lcc_devices = [ - (location, device) - for location in self.lyric.locations - for device in location.devices - if device.device_class == "Thermostat" - and device.device_id.startswith("LCC") - ] await asyncio.gather( *( self.lyric.get_thermostat_rooms( location.location_id, device.device_id ) - for location, device in lcc_devices + for location in self.lyric.locations + for device in location.devices + if device.device_class == "Thermostat" + and device.device_id.startswith("LCC") ) ) - # Store priority data from the library if available, - # otherwise fetch it directly if hasattr(self.lyric, "priorities_dict"): self.priorities_dict = self.lyric.priorities_dict - else: - await self._fetch_priorities(lcc_devices) except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. From 7cbb73aec3aa4f3ec5156b748b6d2306e3f8c617 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 11 Apr 2026 11:23:23 -0400 Subject: [PATCH 4/8] Apply ruff format to select.py --- homeassistant/components/lyric/select.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lyric/select.py b/homeassistant/components/lyric/select.py index b6740d918aee5e..99d11191b297ab 100644 --- a/homeassistant/components/lyric/select.py +++ b/homeassistant/components/lyric/select.py @@ -107,11 +107,7 @@ async def async_select_option(self, option: str) -> None: else: priority_type = PRIORITY_TYPE_PICK_A_ROOM room_id = next( - ( - rid - for rid, room in self._rooms.items() - if room.room_name == option - ), + (rid for rid, room in self._rooms.items() if room.room_name == option), None, ) if room_id is None: @@ -119,9 +115,7 @@ async def async_select_option(self, option: str) -> None: return rooms = [room_id] - _LOGGER.debug( - "Set room priority: type=%s, rooms=%s", priority_type, rooms - ) + _LOGGER.debug("Set room priority: type=%s, rooms=%s", priority_type, rooms) try: await self.coordinator.data.update_priority( self.location, From e61ed673f997ea918427b7eab8337dfc16b21882 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 18 Apr 2026 21:46:45 -0400 Subject: [PATCH 5/8] Remove hasattr guard, access priorities_dict directly --- homeassistant/components/lyric/coordinator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/coordinator.py b/homeassistant/components/lyric/coordinator.py index ae0cb71323191e..72df2e8dd5204c 100644 --- a/homeassistant/components/lyric/coordinator.py +++ b/homeassistant/components/lyric/coordinator.py @@ -78,8 +78,7 @@ async def _run_update(self, force_refresh_token: bool) -> Lyric: and device.device_id.startswith("LCC") ) ) - if hasattr(self.lyric, "priorities_dict"): - self.priorities_dict = self.lyric.priorities_dict + self.priorities_dict = self.lyric.priorities_dict except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. From 1efcd77b0fc1b1cc111e26b8fbe582c3a807698d Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 8 May 2026 10:57:54 -0400 Subject: [PATCH 6/8] Bump aiolyric to 2.1.0 The upstream release includes priorities_dict property and update_priority fix needed for the room priority select entity. --- homeassistant/components/lyric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 62e5683722f270..1158c2f55a7338 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -22,5 +22,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aiolyric"], - "requirements": ["aiolyric==2.0.2"] + "requirements": ["aiolyric==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 99f6cf0d911590..9f0634be394f2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,7 +321,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.0.2 +aiolyric==2.1.0 # homeassistant.components.mealie aiomealie==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5c2ca2751f7e4..b9a65a0dcc2913 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -306,7 +306,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.0.2 +aiolyric==2.1.0 # homeassistant.components.mealie aiomealie==1.2.3 From 1a332c5e4baa8b27f78cba7b66c7ea039f1afafa Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sun, 17 May 2026 13:41:26 -0400 Subject: [PATCH 7/8] Address review: revert version bump, read priorities from coordinator.data - Revert aiolyric version bump (will be a separate PR) - Remove priorities_dict attribute from coordinator - Read priorities directly from coordinator.data.priorities_dict --- homeassistant/components/lyric/coordinator.py | 3 --- homeassistant/components/lyric/manifest.json | 2 +- homeassistant/components/lyric/select.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lyric/coordinator.py b/homeassistant/components/lyric/coordinator.py index 3dcda9eeb6992d..f7bd07123b8114 100644 --- a/homeassistant/components/lyric/coordinator.py +++ b/homeassistant/components/lyric/coordinator.py @@ -8,7 +8,6 @@ from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric from aiolyric.exceptions import LyricAuthenticationException, LyricException -from aiolyric.objects.priority import LyricPriority from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -44,7 +43,6 @@ def __init__( ) self.oauth_session = oauth_session self.lyric = lyric - self.priorities_dict: dict[str, LyricPriority] = {} async def _async_update_data(self) -> Lyric: """Fetch data from Lyric.""" @@ -76,7 +74,6 @@ async def _run_update(self, force_refresh_token: bool) -> Lyric: and device.device_id.startswith("LCC") ) ) - self.priorities_dict = self.lyric.priorities_dict except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 1158c2f55a7338..62e5683722f270 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -22,5 +22,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aiolyric"], - "requirements": ["aiolyric==2.1.0"] + "requirements": ["aiolyric==2.0.2"] } diff --git a/homeassistant/components/lyric/select.py b/homeassistant/components/lyric/select.py index 99d11191b297ab..1ccf4b219d3830 100644 --- a/homeassistant/components/lyric/select.py +++ b/homeassistant/components/lyric/select.py @@ -82,7 +82,7 @@ def options(self) -> list[str]: @property def current_option(self) -> str | None: """Return the currently selected room priority.""" - priority = self.coordinator.priorities_dict.get(self._mac_id) + priority = self.coordinator.data.priorities_dict.get(self._mac_id) if priority is None: return None diff --git a/requirements_all.txt b/requirements_all.txt index 693cf9b15d8a5a..0771cb1e5e38ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,7 +321,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.1.0 +aiolyric==2.0.2 # homeassistant.components.mealie aiomealie==1.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a9d95e6450d2b..97a7f34bf4896e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -306,7 +306,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.1.0 +aiolyric==2.0.2 # homeassistant.components.mealie aiomealie==1.2.4 From c8decba67f525007f4a3b03b1c972ddf6faf3d7e Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Wed, 20 May 2026 10:18:56 -0400 Subject: [PATCH 8/8] Fix pylint and ruff failures in select.py - Remove `from __future__ import annotations` (banned, Python 3.14+) - Raise HomeAssistantError instead of swallowing exception in async_select_option --- homeassistant/components/lyric/select.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/select.py b/homeassistant/components/lyric/select.py index 1ccf4b219d3830..683d2c6b2e3816 100644 --- a/homeassistant/components/lyric/select.py +++ b/homeassistant/components/lyric/select.py @@ -1,7 +1,5 @@ """Support for Honeywell Lyric select platform.""" -from __future__ import annotations - import logging from aiolyric.objects.device import LyricDevice @@ -11,6 +9,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import LYRIC_EXCEPTIONS @@ -124,5 +123,7 @@ async def async_select_option(self, option: str) -> None: rooms=rooms, ) except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) + raise HomeAssistantError( + f"Failed to set room priority: {exception}" + ) from exception await self.coordinator.async_refresh()