diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 68032fae8a2a..4f04db404bb5 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -38,6 +38,14 @@ LYRIC_SETPOINT_STATUS_NAMES = { 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): @@ -204,6 +212,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.""" @@ -273,3 +289,35 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): 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) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 3bcacdc7f0d3..595ba439e7ad 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -59,6 +59,16 @@ "outdoor_temperature": { "name": "Outdoor temperature" }, + "priority_status": { + "name": "Priority status", + "state": { + "hold_until": "Hold until", + "no_hold": "No hold", + "permanent_hold": "Permanent hold", + "temporary_hold": "Temporary hold", + "vacation_hold": "Vacation hold" + } + }, "room_average_temperature": { "name": "Room average temperature" }, diff --git a/tests/components/lyric/conftest.py b/tests/components/lyric/conftest.py index 979a3c79c1f7..f14aa63d4176 100644 --- a/tests/components/lyric/conftest.py +++ b/tests/components/lyric/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiolyric import Lyric from aiolyric.exceptions import LyricException from aiolyric.objects.location import LyricLocation -from aiolyric.objects.priority import LyricRoom +from aiolyric.objects.priority import LyricPriority, LyricRoom import pytest from homeassistant.components.application_credentials import ( @@ -28,6 +28,11 @@ from tests.common import ( CLIENT_ID = "1234" CLIENT_SECRET = "5678" +MAC_ID = "5CFCE1B67035" +# 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 async def setup_credentials(hass: HomeAssistant) -> None: @@ -163,12 +168,7 @@ async def mock_lyric_mixed_devices() -> AsyncGenerator[MagicMock]: @pytest.fixture def mock_lyric_api() -> Generator[MagicMock]: - """Mock the aiolyric client, backed by a real Location parsed from a live-shaped fixture. - - get_thermostat_rooms is left as an autospec'd no-op: this test only - covers device-level sensors, not the room/priority data it would - otherwise populate. - """ + """Mock the aiolyric client, backed by a real Location and a real LyricPriority.""" with patch("homeassistant.components.lyric.Lyric", autospec=True) as mock_lyric_cls: lyric = mock_lyric_cls.return_value @@ -180,4 +180,11 @@ def mock_lyric_api() -> Generator[MagicMock]: location.location_id: location for location in lyric.locations } + priority_json = load_json_object_fixture("priority.json", DOMAIN) + lyric.priorities_dict = {MAC_ID: LyricPriority(priority_json)} + lyric.rooms_dict = { + MAC_ID: {1: MagicMock()}, + NO_PRIORITY_DATA_MAC_ID: {1: MagicMock()}, + } + yield lyric diff --git a/tests/components/lyric/fixtures/locations.json b/tests/components/lyric/fixtures/locations.json index 0d3f11736e2a..efc5cdfea5e1 100644 --- a/tests/components/lyric/fixtures/locations.json +++ b/tests/components/lyric/fixtures/locations.json @@ -15,6 +15,19 @@ "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/fixtures/priority.json b/tests/components/lyric/fixtures/priority.json new file mode 100644 index 000000000000..203a9c8e836e --- /dev/null +++ b/tests/components/lyric/fixtures/priority.json @@ -0,0 +1,9 @@ +{ + "deviceId": "LCC-7f86b153-8480-f111-b78f-6045bdb25006", + "priorityStatus": "NoHold", + "priority": { + "priorityType": "WholeHouse", + "selectedRooms": [], + "rooms": [] + } +} diff --git a/tests/components/lyric/snapshots/test_binary_sensor.ambr b/tests/components/lyric/snapshots/test_binary_sensor.ambr index 25051f05620e..0828414db8e1 100644 --- a/tests/components/lyric/snapshots/test_binary_sensor.ambr +++ b/tests/components/lyric/snapshots/test_binary_sensor.ambr @@ -1,4 +1,54 @@ # serializer version: 1 +# name: test_binary_sensor[binary_sensor.bedroom_thermostat_device_pairing_enabled-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bedroom_thermostat_device_pairing_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Device pairing enabled', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Device pairing enabled', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_pairing_enabled', + 'unique_id': '5CFCE1B67036_device_pairing_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.bedroom_thermostat_device_pairing_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Bedroom Thermostat Device pairing enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom_thermostat_device_pairing_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensor[binary_sensor.ocala_thermostat_device_pairing_enabled-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/lyric/snapshots/test_sensor.ambr b/tests/components/lyric/snapshots/test_sensor.ambr index ee677d3ed716..4c19760f0063 100644 --- a/tests/components/lyric/snapshots/test_sensor.ambr +++ b/tests/components/lyric/snapshots/test_sensor.ambr @@ -1,4 +1,178 @@ # 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': dict({ + : list([ + 'no_hold', + 'temporary_hold', + 'hold_until', + 'permanent_hold', + 'vacation_hold', + ]), + }), + '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': , + '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({ + : 'enum', + : 'Bedroom Thermostat Priority status', + : list([ + 'no_hold', + 'temporary_hold', + 'hold_until', + 'permanent_hold', + 'vacation_hold', + ]), + }), + 'context': , + 'entity_id': 'sensor.bedroom_thermostat_priority_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.bedroom_thermostat_schedule_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_schedule_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule status', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Schedule status', + 'platform': 'lyric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_status', + 'unique_id': '5CFCE1B67036_schedule_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.bedroom_thermostat_schedule_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Bedroom Thermostat Schedule status', + }), + 'context': , + 'entity_id': 'sensor.bedroom_thermostat_schedule_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Resume', + }) +# --- # name: test_sensor[sensor.ocala_thermostat_indoor_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -57,6 +231,72 @@ 'state': '26.1111111111111', }) # --- +# name: test_sensor[sensor.ocala_thermostat_priority_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'no_hold', + 'temporary_hold', + 'hold_until', + 'permanent_hold', + 'vacation_hold', + ]), + }), + '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': , + '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({ + : 'enum', + : 'Ocala Thermostat Priority status', + : list([ + 'no_hold', + 'temporary_hold', + 'hold_until', + 'permanent_hold', + 'vacation_hold', + ]), + }), + 'context': , + 'entity_id': 'sensor.ocala_thermostat_priority_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_hold', + }) +# --- # name: test_sensor[sensor.ocala_thermostat_schedule_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([