From 921ef2ec6579c40f5640c210883977437aef9b86 Mon Sep 17 00:00:00 2001 From: trip-g Date: Tue, 21 Jul 2026 17:08:43 -0400 Subject: [PATCH 1/5] Add Priority Status sensor to Lyric integration Resideo's priority endpoint reports a top-level hold status (e.g. "NoHold") alongside the room priority data already used by the Room Priority select entity. This was parsed but never surfaced. Adds a dedicated diagnostic sensor reading from priorities_dict, gated the same way as the existing select entity (thermostat device with populated room data). --- homeassistant/components/lyric/sensor.py | 40 ++++++++++++++++++++- homeassistant/components/lyric/strings.json | 3 ++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 4cdcbe5cfdd00..0766d0a6b0cb5 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -15,7 +15,7 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import PERCENTAGE, UnitOfTemperature +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 @@ -189,6 +189,14 @@ async def async_setup_entry( 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.""" @@ -258,3 +266,33 @@ def __init__( 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" + + 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) -> StateType: + """Return the state.""" + priority = self.coordinator.data.priorities_dict.get(self._mac_id) + if priority is None: + return None + return priority.status diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index b9547c8251475..3c4f80671dd52 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -61,6 +61,9 @@ "outdoor_temperature": { "name": "Outdoor temperature" }, + "priority_status": { + "name": "Priority status" + }, "room_humidity": { "name": "Room humidity" }, From f13b3ef48b8e14e424e1ca94baa9269e51d98f36 Mon Sep 17 00:00:00 2001 From: clutch2sft Date: Mon, 27 Jul 2026 08:49:25 -0400 Subject: [PATCH 2/5] Add test coverage for the Priority Status sensor - Port the shared conftest.py fixtures from the sibling binary-sensor branch (real config-entry setup, aiolyric client patched at the Lyric.get_locations/get_thermostat_rooms boundary rather than mocked HTTP), since this branch predates that infrastructure. - Add an xfail(strict=True) end-to-end test documenting that the entity isn't created at all today: its creation gate reads rooms_dict, which never populates under the currently-pinned aiolyric (same currentPriority/priority key bug as the room_motion entity), and even if it did, LyricPriority.status reads the wrong JSON key too. Both are fixed in the same upstream aiolyric#165 diff. - Add direct unit tests for LyricPriorityStatusSensor.native_value (present/missing priority data), which don't depend on that fix and give real, non-xfail coverage of the new entity class and the async_add_entities gating logic - closing the codecov/patch gap Copilot flagged (new code in an already well-covered sensor.py). Co-Authored-By: Claude Sonnet 5 --- tests/components/lyric/conftest.py | 157 ++++++++++++++++++++++++++ tests/components/lyric/test_sensor.py | 68 ++++++++++- 2 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/components/lyric/conftest.py diff --git a/tests/components/lyric/conftest.py b/tests/components/lyric/conftest.py new file mode 100644 index 0000000000000..c43194f46ee7e --- /dev/null +++ b/tests/components/lyric/conftest.py @@ -0,0 +1,157 @@ +"""Fixtures for the Honeywell Lyric integration tests.""" + +from collections.abc import Generator +from time import time +from unittest.mock import patch + +from aiolyric import Lyric +from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricPriority +import pytest + +from homeassistant.components.application_credentials import ( + DOMAIN as APPLICATION_CREDENTIALS_DOMAIN, + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.lyric.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + +BASE_URL = "https://api.honeywellhome.com/v2" + +# Real payload shapes captured from a live T9-T10 account with paired +# RCHTSENSOR room accessories. Deliberately using the actual field names +# Resideo returns (e.g. vacationHold.Enabled, accessories[].sensorType), +# not the ones aiolyric happens to read, so tests exercise real parsing +# rather than hiding behind pre-parsed mocks. +LOCATION_ID = "35202000168931" +# Use an LCC-prefixed ID so the current coordinator fetches room data. +DEVICE_ID = "LCC-7f86b153-8480-f111-b78f-6045bdb25006" +MAC_ID = "5CFCE1B67035" + +LOCATIONS_RESPONSE = [ + { + "locationID": LOCATION_ID, + "name": "Ocala P01", + "devices": [ + { + "vacationHold": {"Enabled": True}, + "scheduleStatus": "Resume", + "settings": {"devicePairingEnabled": True}, + "deviceClass": "Thermostat", + "deviceType": "Thermostat", + "deviceID": DEVICE_ID, + "name": "Ocala", + "macID": MAC_ID, + "units": "Fahrenheit", + "indoorTemperature": 79, + "deviceModel": "T9-T10", + } + ], + "users": [], + } +] + +PRIORITY_RESPONSE = { + "deviceId": MAC_ID, + "priorityStatus": "NoHold", + "priority": { + "priorityType": "PickARoom", + "selectedRooms": [1], + "rooms": [ + { + "id": 1, + "name": "Primary Bedroom", + "avgTemperature": 79, + "avgHumidity": 54, + "overallMotion": False, + "accessories": [ + { + "id": 1, + "sensorType": "IndoorAirSensor", + "temperature": 79, + "status": "Ok", + "detectMotion": True, + } + ], + } + ], + }, +} + + +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Register lyric application credentials.""" + assert await async_setup_component(hass, APPLICATION_CREDENTIALS_DOMAIN, {}) + + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return an already-authenticated Lyric config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": time() + 3600, + "token_type": "Bearer", + }, + }, + ) + + +@pytest.fixture +def mock_lyric_api() -> Generator[None]: + """Patch the aiolyric client to build real Location/Priority objects. + + Patches Lyric.get_locations/get_thermostat_rooms directly rather than + mocking HTTP responses, so tests exercise real aiolyric parsing (the + same LyricLocation/LyricPriority code reading the actual field names + Resideo returns) without depending on network-mocking machinery. + """ + + async def get_locations(self: Lyric) -> None: + self._locations = [ + LyricLocation(self._client, location) for location in LOCATIONS_RESPONSE + ] + self._locations_dict = { + location.location_id: location for location in self._locations + } + + async def get_thermostat_rooms( + self: Lyric, location_id: str, device_id: str + ) -> None: + priority = LyricPriority(PRIORITY_RESPONSE) + self._priorities_dict[priority.device_id] = priority + self._rooms_dict[priority.device_id] = { + room.id: room for room in priority.current_priority.rooms + } + + with ( + patch.object(Lyric, "get_locations", get_locations), + patch.object(Lyric, "get_thermostat_rooms", get_thermostat_rooms), + ): + yield + + +async def async_setup_lyric_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Set up the mock config entry and wait for it to settle.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/lyric/test_sensor.py b/tests/components/lyric/test_sensor.py index 7f74b5e8d63ea..c1a9a54c35ce9 100644 --- a/tests/components/lyric/test_sensor.py +++ b/tests/components/lyric/test_sensor.py @@ -1,8 +1,20 @@ """Tests for the Honeywell Lyric sensor platform.""" from datetime import datetime +from unittest.mock import MagicMock -from homeassistant.components.lyric.sensor import get_datetime_from_future_time +import pytest + +from homeassistant.components.lyric.sensor import ( + LyricPriorityStatusSensor, + get_datetime_from_future_time, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MAC_ID, async_setup_lyric_entry + +from tests.common import MockConfigEntry def test_get_datetime_from_future_time_none() -> None: @@ -19,3 +31,57 @@ def test_get_datetime_from_future_time_valid() -> None: """Test that a valid time string returns a datetime.""" result = get_datetime_from_future_time("13:30:00") assert isinstance(result, datetime) + + +@pytest.mark.xfail( + strict=True, + reason=( + "aiolyric 2.1.1's LyricPriority.current_priority reads JSON key " + "'currentPriority' instead of the live API's 'priority', so " + "rooms_dict never populates and this entity's creation gate never " + "passes. Fixed upstream in timmo001/aiolyric#165 (which also fixes " + "LyricPriority.status reading 'status' instead of the live API's " + "'priorityStatus'); once that's released and the manifest pin is " + "bumped, this will start passing for real and this marker must be " + "removed." + ), +) +async def test_priority_status_created( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + setup_credentials: None, + mock_lyric_api: None, + mock_config_entry: MockConfigEntry, +) -> None: + """Priority Status should be created via a real config entry setup.""" + await async_setup_lyric_entry(hass, mock_config_entry) + + entity_id = entity_registry.async_get_entity_id( + "sensor", "lyric", f"{MAC_ID}_priority_status" + ) + assert entity_id + state = hass.states.get(entity_id) + assert state + assert state.state == "NoHold" + + +def test_priority_status_native_value_returns_status() -> None: + """Priority Status reads priority.status when data is present.""" + coordinator = MagicMock() + coordinator.data.priorities_dict = {MAC_ID: MagicMock(status="NoHold")} + device = MagicMock(mac_id=MAC_ID) + + sensor = LyricPriorityStatusSensor(coordinator, MagicMock(), device) + + assert sensor.native_value == "NoHold" + + +def test_priority_status_native_value_returns_none_when_missing() -> None: + """Priority Status is None when no priority data exists for the device.""" + coordinator = MagicMock() + coordinator.data.priorities_dict = {} + device = MagicMock(mac_id=MAC_ID) + + sensor = LyricPriorityStatusSensor(coordinator, MagicMock(), device) + + assert sensor.native_value is None From f52d4a9acd2fa5eefa8deee1ee7eeba272089b4a Mon Sep 17 00:00:00 2001 From: clutch2sft Date: Tue, 28 Jul 2026 16:11:06 -0400 Subject: [PATCH 3/5] Address review patterns: JSON fixtures, mealie patching, drop xfail entirely Same fixture/patching cleanup already applied to the sibling branches, plus the bigger change joostlek's "let's not add failing tests directly" pushed toward: replace the xfail end-to-end test and the two direct LyricPriorityStatusSensor.native_value unit tests with a single real snapshot_platform test. - fixtures/locations.json now has two devices: one with a priority entry (state "NoHold"), one with room data but no priority entry (state "unknown") - both branches of native_value get exercised through real entity state instead of direct property calls. - rooms_dict/priorities_dict are set directly with lightweight stand-ins rather than real LyricPriority objects, since LyricPriority.status/ current_priority's field-name bugs (aiolyric#165) are aiolyric's own test suite's concern, not this integration's - this fixture tests our gating/display logic assuming the data contract is met, independent of whether the currently-pinned library can produce it today. - Verified new code (the async_add_entities generator + the whole LyricPriorityStatusSensor class) has zero coverage gaps; the only remaining "missing" lines are pre-existing, unrelated to this PR. --- tests/components/lyric/__init__.py | 12 + tests/components/lyric/conftest.py | 134 +++-------- .../components/lyric/fixtures/locations.json | 35 +++ .../lyric/snapshots/test_sensor.ambr | 217 ++++++++++++++++++ tests/components/lyric/test_sensor.py | 67 ++---- 5 files changed, 315 insertions(+), 150 deletions(-) create mode 100644 tests/components/lyric/fixtures/locations.json create mode 100644 tests/components/lyric/snapshots/test_sensor.ambr diff --git a/tests/components/lyric/__init__.py b/tests/components/lyric/__init__.py index 794c6bf1ba095..4b4ee70008487 100644 --- a/tests/components/lyric/__init__.py +++ b/tests/components/lyric/__init__.py @@ -1 +1,13 @@ """Tests for the Honeywell Lyric integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the Lyric integration for tests.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/lyric/conftest.py b/tests/components/lyric/conftest.py index c43194f46ee7e..50b9444ba0131 100644 --- a/tests/components/lyric/conftest.py +++ b/tests/components/lyric/conftest.py @@ -2,11 +2,9 @@ from collections.abc import Generator from time import time -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from aiolyric import Lyric from aiolyric.objects.location import LyricLocation -from aiolyric.objects.priority import LyricPriority import pytest from homeassistant.components.application_credentials import ( @@ -18,72 +16,18 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_array_fixture CLIENT_ID = "1234" CLIENT_SECRET = "5678" -BASE_URL = "https://api.honeywellhome.com/v2" - -# Real payload shapes captured from a live T9-T10 account with paired -# RCHTSENSOR room accessories. Deliberately using the actual field names -# Resideo returns (e.g. vacationHold.Enabled, accessories[].sensorType), -# not the ones aiolyric happens to read, so tests exercise real parsing -# rather than hiding behind pre-parsed mocks. +# Matches the values baked into fixtures/locations.json. LOCATION_ID = "35202000168931" -# Use an LCC-prefixed ID so the current coordinator fetches room data. DEVICE_ID = "LCC-7f86b153-8480-f111-b78f-6045bdb25006" MAC_ID = "5CFCE1B67035" - -LOCATIONS_RESPONSE = [ - { - "locationID": LOCATION_ID, - "name": "Ocala P01", - "devices": [ - { - "vacationHold": {"Enabled": True}, - "scheduleStatus": "Resume", - "settings": {"devicePairingEnabled": True}, - "deviceClass": "Thermostat", - "deviceType": "Thermostat", - "deviceID": DEVICE_ID, - "name": "Ocala", - "macID": MAC_ID, - "units": "Fahrenheit", - "indoorTemperature": 79, - "deviceModel": "T9-T10", - } - ], - "users": [], - } -] - -PRIORITY_RESPONSE = { - "deviceId": MAC_ID, - "priorityStatus": "NoHold", - "priority": { - "priorityType": "PickARoom", - "selectedRooms": [1], - "rooms": [ - { - "id": 1, - "name": "Primary Bedroom", - "avgTemperature": 79, - "avgHumidity": 54, - "overallMotion": False, - "accessories": [ - { - "id": 1, - "sensorType": "IndoorAirSensor", - "temperature": 79, - "status": "Ok", - "detectMotion": True, - } - ], - } - ], - }, -} +# Second device: has room data but no priority data yet, exercising the +# defensive "no priority entry" branch of LyricPriorityStatusSensor. +NO_PRIORITY_DATA_MAC_ID = "5CFCE1B67036" @pytest.fixture @@ -114,44 +58,42 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_lyric_api() -> Generator[None]: - """Patch the aiolyric client to build real Location/Priority objects. - - Patches Lyric.get_locations/get_thermostat_rooms directly rather than - mocking HTTP responses, so tests exercise real aiolyric parsing (the - same LyricLocation/LyricPriority code reading the actual field names - Resideo returns) without depending on network-mocking machinery. +def mock_lyric_api() -> Generator[MagicMock]: + """Mock the aiolyric client, backed by a real Location and directly-set priority data. + + Patches Lyric where the integration imports it (autospec, like the + mealie client mock). Location/device data comes from a real + LyricLocation parsed from a live-shaped fixture, so field-name + parsing is still exercised for real. + + rooms_dict/priorities_dict are set directly with lightweight stand-ins + rather than real LyricPriority objects: LyricPriority.status/ + current_priority currently read the wrong JSON keys (tracked upstream + in aiolyric#165), and that parsing bug belongs to aiolyric's own test + suite, not this integration's. This fixture tests that Home + Assistant's own gating/display logic is correct once priority data + exists, independent of whether the currently-pinned aiolyric can + actually produce it. + + The fixture's second device has room data but no priority entry, so + both branches of LyricPriorityStatusSensor.native_value get exercised + through real entity state rather than a direct unit test. """ + with patch("homeassistant.components.lyric.Lyric", autospec=True) as mock_lyric_cls: + lyric = mock_lyric_cls.return_value - async def get_locations(self: Lyric) -> None: - self._locations = [ - LyricLocation(self._client, location) for location in LOCATIONS_RESPONSE + locations_json = load_json_array_fixture("locations.json", DOMAIN) + lyric.locations = [ + LyricLocation(MagicMock(), location) for location in locations_json ] - self._locations_dict = { - location.location_id: location for location in self._locations + lyric.locations_dict = { + location.location_id: location for location in lyric.locations } - async def get_thermostat_rooms( - self: Lyric, location_id: str, device_id: str - ) -> None: - priority = LyricPriority(PRIORITY_RESPONSE) - self._priorities_dict[priority.device_id] = priority - self._rooms_dict[priority.device_id] = { - room.id: room for room in priority.current_priority.rooms + lyric.priorities_dict = {MAC_ID: MagicMock(status="NoHold")} + lyric.rooms_dict = { + MAC_ID: {1: MagicMock()}, + NO_PRIORITY_DATA_MAC_ID: {1: MagicMock()}, } - with ( - patch.object(Lyric, "get_locations", get_locations), - patch.object(Lyric, "get_thermostat_rooms", get_thermostat_rooms), - ): - yield - - -async def async_setup_lyric_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Set up the mock config entry and wait for it to settle.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + yield lyric diff --git a/tests/components/lyric/fixtures/locations.json b/tests/components/lyric/fixtures/locations.json new file mode 100644 index 0000000000000..e8e662ba1c51b --- /dev/null +++ b/tests/components/lyric/fixtures/locations.json @@ -0,0 +1,35 @@ +[ + { + "locationID": "35202000168931", + "name": "Ocala P01", + "devices": [ + { + "vacationHold": { "Enabled": true }, + "scheduleStatus": "Resume", + "settings": { "devicePairingEnabled": true }, + "deviceClass": "Thermostat", + "deviceType": "Thermostat", + "deviceID": "LCC-7f86b153-8480-f111-b78f-6045bdb25006", + "name": "Ocala", + "macID": "5CFCE1B67035", + "units": "Fahrenheit", + "indoorTemperature": 79, + "deviceModel": "T9-T10" + }, + { + "vacationHold": { "Enabled": false }, + "scheduleStatus": "Resume", + "settings": { "devicePairingEnabled": true }, + "deviceClass": "Thermostat", + "deviceType": "Thermostat", + "deviceID": "LCC-8f86b153-8480-f111-b78f-6045bdb25007", + "name": "Bedroom", + "macID": "5CFCE1B67036", + "units": "Fahrenheit", + "indoorTemperature": 72, + "deviceModel": "T9-T10" + } + ], + "users": [] + } +] diff --git a/tests/components/lyric/snapshots/test_sensor.ambr b/tests/components/lyric/snapshots/test_sensor.ambr new file mode 100644 index 0000000000000..6f73213d5be2c --- /dev/null +++ b/tests/components/lyric/snapshots/test_sensor.ambr @@ -0,0 +1,217 @@ +# serializer version: 1 +# name: test_sensor[sensor.bedroom_thermostat_indoor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bedroom_thermostat_indoor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Indoor temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor temperature', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_temperature', + 'unique_id': '5CFCE1B67036_indoor_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.bedroom_thermostat_indoor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Bedroom Thermostat Indoor temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.bedroom_thermostat_indoor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2222222222222', + }) +# --- +# name: test_sensor[sensor.bedroom_thermostat_priority_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.bedroom_thermostat_priority_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Priority status', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Priority status', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'priority_status', + 'unique_id': '5CFCE1B67036_priority_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.bedroom_thermostat_priority_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Bedroom Thermostat Priority status', + }), + 'context': , + 'entity_id': 'sensor.bedroom_thermostat_priority_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.ocala_thermostat_indoor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ocala_thermostat_indoor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Indoor temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor temperature', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_temperature', + 'unique_id': '5CFCE1B67035_indoor_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.ocala_thermostat_indoor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Ocala Thermostat Indoor temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.ocala_thermostat_indoor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.1111111111111', + }) +# --- +# name: test_sensor[sensor.ocala_thermostat_priority_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ocala_thermostat_priority_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Priority status', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Priority status', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'priority_status', + 'unique_id': '5CFCE1B67035_priority_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.ocala_thermostat_priority_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Ocala Thermostat Priority status', + }), + 'context': , + 'entity_id': 'sensor.ocala_thermostat_priority_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'NoHold', + }) +# --- diff --git a/tests/components/lyric/test_sensor.py b/tests/components/lyric/test_sensor.py index c1a9a54c35ce9..87447cdd06a1a 100644 --- a/tests/components/lyric/test_sensor.py +++ b/tests/components/lyric/test_sensor.py @@ -1,20 +1,18 @@ """Tests for the Honeywell Lyric sensor platform.""" from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch -import pytest +from syrupy.assertion import SnapshotAssertion -from homeassistant.components.lyric.sensor import ( - LyricPriorityStatusSensor, - get_datetime_from_future_time, -) +from homeassistant.components.lyric.sensor import get_datetime_from_future_time +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MAC_ID, async_setup_lyric_entry +from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform def test_get_datetime_from_future_time_none() -> None: @@ -33,55 +31,16 @@ def test_get_datetime_from_future_time_valid() -> None: assert isinstance(result, datetime) -@pytest.mark.xfail( - strict=True, - reason=( - "aiolyric 2.1.1's LyricPriority.current_priority reads JSON key " - "'currentPriority' instead of the live API's 'priority', so " - "rooms_dict never populates and this entity's creation gate never " - "passes. Fixed upstream in timmo001/aiolyric#165 (which also fixes " - "LyricPriority.status reading 'status' instead of the live API's " - "'priorityStatus'); once that's released and the manifest pin is " - "bumped, this will start passing for real and this marker must be " - "removed." - ), -) -async def test_priority_status_created( +async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, setup_credentials: None, - mock_lyric_api: None, + mock_lyric_api: MagicMock, mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: - """Priority Status should be created via a real config entry setup.""" - await async_setup_lyric_entry(hass, mock_config_entry) + """Test the Lyric sensor platform via a real config entry setup.""" + with patch("homeassistant.components.lyric.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) - entity_id = entity_registry.async_get_entity_id( - "sensor", "lyric", f"{MAC_ID}_priority_status" - ) - assert entity_id - state = hass.states.get(entity_id) - assert state - assert state.state == "NoHold" - - -def test_priority_status_native_value_returns_status() -> None: - """Priority Status reads priority.status when data is present.""" - coordinator = MagicMock() - coordinator.data.priorities_dict = {MAC_ID: MagicMock(status="NoHold")} - device = MagicMock(mac_id=MAC_ID) - - sensor = LyricPriorityStatusSensor(coordinator, MagicMock(), device) - - assert sensor.native_value == "NoHold" - - -def test_priority_status_native_value_returns_none_when_missing() -> None: - """Priority Status is None when no priority data exists for the device.""" - coordinator = MagicMock() - coordinator.data.priorities_dict = {} - device = MagicMock(mac_id=MAC_ID) - - sensor = LyricPriorityStatusSensor(coordinator, MagicMock(), device) - - assert sensor.native_value is None + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From c13998a649b4bb5758c42915e9b351ec68dbf34e Mon Sep 17 00:00:00 2001 From: clutch2sft Date: Tue, 28 Jul 2026 16:56:39 -0400 Subject: [PATCH 4/5] Fix locationID fixture/constant type to match the documented API contract Resideo's own API docs document locationID as an Integer, and aiolyric's get_thermostat_rooms already types it int, but this fixture quoted it as a string - inherited from early in this session before checking the real docs. No behavior change (the value is only ever interpolated into a URL), but the fixture now matches reality. --- tests/components/lyric/conftest.py | 2 +- tests/components/lyric/fixtures/locations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/lyric/conftest.py b/tests/components/lyric/conftest.py index 50b9444ba0131..5dca5bc0b81e0 100644 --- a/tests/components/lyric/conftest.py +++ b/tests/components/lyric/conftest.py @@ -22,7 +22,7 @@ CLIENT_SECRET = "5678" # Matches the values baked into fixtures/locations.json. -LOCATION_ID = "35202000168931" +LOCATION_ID = 35202000168931 DEVICE_ID = "LCC-7f86b153-8480-f111-b78f-6045bdb25006" MAC_ID = "5CFCE1B67035" # Second device: has room data but no priority data yet, exercising the diff --git a/tests/components/lyric/fixtures/locations.json b/tests/components/lyric/fixtures/locations.json index e8e662ba1c51b..efc5cdfea5e1c 100644 --- a/tests/components/lyric/fixtures/locations.json +++ b/tests/components/lyric/fixtures/locations.json @@ -1,6 +1,6 @@ [ { - "locationID": "35202000168931", + "locationID": 35202000168931, "name": "Ocala P01", "devices": [ { From b332e44a960d7938aa56053448662b1cf137cb12 Mon Sep 17 00:00:00 2001 From: clutch2sft Date: Tue, 28 Jul 2026 17:11:06 -0400 Subject: [PATCH 5/5] Address Copilot's low-confidence review comments - Condense mock_lyric_api's docstring to the non-obvious constraint (the aiolyric field-name bug and why the second device exists); drop the narration of setup mechanics already visible in the code below. - Apply setup_credentials/mock_lyric_api via usefixtures in test_sensor instead of injecting them as unused parameters, matching our own "don't inject unused fixture arguments" rule. --- tests/components/lyric/conftest.py | 23 ++++++----------------- tests/components/lyric/test_sensor.py | 6 +++--- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/tests/components/lyric/conftest.py b/tests/components/lyric/conftest.py index 5dca5bc0b81e0..8598c8cd2e450 100644 --- a/tests/components/lyric/conftest.py +++ b/tests/components/lyric/conftest.py @@ -61,23 +61,12 @@ def mock_config_entry() -> MockConfigEntry: def mock_lyric_api() -> Generator[MagicMock]: """Mock the aiolyric client, backed by a real Location and directly-set priority data. - Patches Lyric where the integration imports it (autospec, like the - mealie client mock). Location/device data comes from a real - LyricLocation parsed from a live-shaped fixture, so field-name - parsing is still exercised for real. - - rooms_dict/priorities_dict are set directly with lightweight stand-ins - rather than real LyricPriority objects: LyricPriority.status/ - current_priority currently read the wrong JSON keys (tracked upstream - in aiolyric#165), and that parsing bug belongs to aiolyric's own test - suite, not this integration's. This fixture tests that Home - Assistant's own gating/display logic is correct once priority data - exists, independent of whether the currently-pinned aiolyric can - actually produce it. - - The fixture's second device has room data but no priority entry, so - both branches of LyricPriorityStatusSensor.native_value get exercised - through real entity state rather than a direct unit test. + rooms_dict/priorities_dict are set directly rather than parsed from + real LyricPriority objects: LyricPriority.status/current_priority + currently read the wrong JSON keys (aiolyric#165), and that parsing + bug is aiolyric's own test suite's concern, not this integration's. + The second device has room data but no priority entry, exercising + both branches of LyricPriorityStatusSensor.native_value. """ with patch("homeassistant.components.lyric.Lyric", autospec=True) as mock_lyric_cls: lyric = mock_lyric_cls.return_value diff --git a/tests/components/lyric/test_sensor.py b/tests/components/lyric/test_sensor.py index 87447cdd06a1a..8243eb09be5d9 100644 --- a/tests/components/lyric/test_sensor.py +++ b/tests/components/lyric/test_sensor.py @@ -1,8 +1,9 @@ """Tests for the Honeywell Lyric sensor platform.""" from datetime import datetime -from unittest.mock import MagicMock, patch +from unittest.mock import patch +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.lyric.sensor import get_datetime_from_future_time @@ -31,11 +32,10 @@ def test_get_datetime_from_future_time_valid() -> None: assert isinstance(result, datetime) +@pytest.mark.usefixtures("setup_credentials", "mock_lyric_api") async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, - setup_credentials: None, - mock_lyric_api: MagicMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: