From b6c74f46feeb52609a256109ec09674aa771743b Mon Sep 17 00:00:00 2001 From: trip-g Date: Tue, 21 Jul 2026 17:02:00 -0400 Subject: [PATCH 1/2] Add Accessory Status sensor to Lyric integration Resideo's priority endpoint reports a per-accessory status (e.g. "Ok") for paired room sensor accessories, which was parsed but never surfaced. Adds it as a diagnostic sensor so a failing/offline room sensor is visible instead of just its temperature/humidity readings silently going stale. --- homeassistant/components/lyric/sensor.py | 9 ++++++++- homeassistant/components/lyric/strings.json | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 4cdcbe5cfdd00..8485e126ca53f 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 @@ -133,6 +133,13 @@ class LyricSensorAccessoryEntityDescription(SensorEntityDescription): value_fn=lambda room, _: room.room_avg_humidity, suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", ), + LyricSensorAccessoryEntityDescription( + key="accessory_status", + translation_key="accessory_status", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda _, accessory: accessory.status, + suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", + ), ] diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index b9547c8251475..0f69b45052cce 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -46,6 +46,9 @@ } }, "sensor": { + "accessory_status": { + "name": "Accessory status" + }, "indoor_humidity": { "name": "Indoor humidity" }, From fd689245636e57e35a176c8600f64855e12e01b4 Mon Sep 17 00:00:00 2001 From: clutch2sft Date: Wed, 29 Jul 2026 06:24:36 -0400 Subject: [PATCH 2/2] Add test coverage for the Accessory Status sensor This branch had no dedicated tests at all - codecov/patch happened to pass anyway, but only because single-line lambda definitions count as "covered" on import, not because accessory_status was actually exercised. Following the pattern used on the sibling PRs: - fixtures/locations.json + fixtures/room.json hold the static test payloads (one thermostat device, one room with an IndoorAirSensor accessory). - mock_lyric_api patches Lyric where the integration imports it (autospec), building real LyricLocation/LyricRoom objects from the fixture JSON rather than mocking HTTP or hand-rolling MagicMocks. - setup_integration lives in __init__.py, matching the convention used elsewhere in this integration's test suite. - A single snapshot_platform test covers the whole sensor platform, including accessory_status (state "Ok"), room_temperature, and room_humidity - real coverage instead of the lambda-definition artifact. entity.py's LyricAccessoryEntity base class, previously 0% covered on this branch, now hits 100%. --- tests/components/lyric/__init__.py | 12 + tests/components/lyric/conftest.py | 76 ++++++ .../components/lyric/fixtures/locations.json | 22 ++ tests/components/lyric/fixtures/room.json | 15 ++ .../lyric/snapshots/test_sensor.ambr | 222 ++++++++++++++++++ tests/components/lyric/test_sensor.py | 25 ++ 6 files changed, 372 insertions(+) create mode 100644 tests/components/lyric/conftest.py create mode 100644 tests/components/lyric/fixtures/locations.json create mode 100644 tests/components/lyric/fixtures/room.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 new file mode 100644 index 0000000000000..7b24df5f00464 --- /dev/null +++ b/tests/components/lyric/conftest.py @@ -0,0 +1,76 @@ +"""Fixtures for the Honeywell Lyric integration tests.""" + +from collections.abc import Generator +from time import time +from unittest.mock import MagicMock, patch + +from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricRoom +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, + load_json_array_fixture, + load_json_object_fixture, +) + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +@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[MagicMock]: + """Mock the aiolyric client, backed by real Location/Room objects parsed from live-shaped fixtures.""" + with patch("homeassistant.components.lyric.Lyric", autospec=True) as mock_lyric_cls: + lyric = mock_lyric_cls.return_value + + locations_json = load_json_array_fixture("locations.json", DOMAIN) + lyric.locations = [ + LyricLocation(MagicMock(), location) for location in locations_json + ] + lyric.locations_dict = { + location.location_id: location for location in lyric.locations + } + + room_json = load_json_object_fixture("room.json", DOMAIN) + room = LyricRoom(room_json) + mac_id = lyric.locations[0].devices[0].mac_id + lyric.rooms_dict = {mac_id: {room.id: room}} + + yield lyric diff --git a/tests/components/lyric/fixtures/locations.json b/tests/components/lyric/fixtures/locations.json new file mode 100644 index 0000000000000..0d3f11736e2ab --- /dev/null +++ b/tests/components/lyric/fixtures/locations.json @@ -0,0 +1,22 @@ +[ + { + "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" + } + ], + "users": [] + } +] diff --git a/tests/components/lyric/fixtures/room.json b/tests/components/lyric/fixtures/room.json new file mode 100644 index 0000000000000..c3d223ffb955e --- /dev/null +++ b/tests/components/lyric/fixtures/room.json @@ -0,0 +1,15 @@ +{ + "id": 1, + "roomName": "Primary Bedroom", + "roomAvgTemp": 74.5, + "roomAvgHumidity": 54, + "accessories": [ + { + "id": 1, + "type": "IndoorAirSensor", + "temperature": 74.5, + "status": "Ok", + "detectMotion": true + } + ] +} diff --git a/tests/components/lyric/snapshots/test_sensor.ambr b/tests/components/lyric/snapshots/test_sensor.ambr new file mode 100644 index 0000000000000..1e74437a242c2 --- /dev/null +++ b/tests/components/lyric/snapshots/test_sensor.ambr @@ -0,0 +1,222 @@ +# serializer version: 1 +# 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.primary_bedroom_sensor_accessory_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.primary_bedroom_sensor_accessory_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Accessory status', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Accessory status', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'accessory_status', + 'unique_id': '5CFCE1B67035_room1_acc1_accessory_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.primary_bedroom_sensor_accessory_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Primary Bedroom Sensor Accessory status', + }), + 'context': , + 'entity_id': 'sensor.primary_bedroom_sensor_accessory_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Ok', + }) +# --- +# name: test_sensor[sensor.primary_bedroom_sensor_room_humidity-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.primary_bedroom_sensor_room_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Room humidity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Room humidity', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'room_humidity', + 'unique_id': '5CFCE1B67035_room1_acc1_room_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.primary_bedroom_sensor_room_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'humidity', + : 'Primary Bedroom Sensor Room humidity', + : , + : '%', + }), + 'context': , + 'entity_id': 'sensor.primary_bedroom_sensor_room_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54', + }) +# --- +# name: test_sensor[sensor.primary_bedroom_sensor_room_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.primary_bedroom_sensor_room_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Room temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Room temperature', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'room_temperature', + 'unique_id': '5CFCE1B67035_room1_acc1_room_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.primary_bedroom_sensor_room_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Primary Bedroom Sensor Room temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.primary_bedroom_sensor_room_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.6111111111111', + }) +# --- diff --git a/tests/components/lyric/test_sensor.py b/tests/components/lyric/test_sensor.py index 7f74b5e8d63ea..8243eb09be5d9 100644 --- a/tests/components/lyric/test_sensor.py +++ b/tests/components/lyric/test_sensor.py @@ -1,8 +1,19 @@ """Tests for the Honeywell Lyric sensor platform.""" from datetime import datetime +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion 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 . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform def test_get_datetime_from_future_time_none() -> None: @@ -19,3 +30,17 @@ 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.usefixtures("setup_credentials", "mock_lyric_api") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """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) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)