Add Priority Status sensor to Lyric integration (#177065)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
clutch2sft
2026-08-29 17:35:23 +02:00
committed by GitHub
co-authored by Claude Sonnet 5 Joostlek
parent 381db53586
commit 52adc20f85
7 changed files with 384 additions and 7 deletions
+48
View File
@@ -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)
@@ -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"
},
+14 -7
View File
@@ -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
@@ -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": []
@@ -0,0 +1,9 @@
{
"deviceId": "LCC-7f86b153-8480-f111-b78f-6045bdb25006",
"priorityStatus": "NoHold",
"priority": {
"priorityType": "WholeHouse",
"selectedRooms": [],
"rooms": []
}
}
@@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.bedroom_thermostat_device_pairing_enabled',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom Thermostat Device pairing enabled',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.bedroom_thermostat_device_pairing_enabled',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensor[binary_sensor.ocala_thermostat_device_pairing_enabled-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -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({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Indoor temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'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': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensor[sensor.bedroom_thermostat_indoor_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom Thermostat Indoor temperature',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_thermostat_indoor_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.2222222222222',
})
# ---
# name: test_sensor[sensor.bedroom_thermostat_priority_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'no_hold',
'temporary_hold',
'hold_until',
'permanent_hold',
'vacation_hold',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.bedroom_thermostat_priority_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Priority status',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom Thermostat Priority status',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'no_hold',
'temporary_hold',
'hold_until',
'permanent_hold',
'vacation_hold',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_thermostat_priority_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor[sensor.bedroom_thermostat_schedule_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.bedroom_thermostat_schedule_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom Thermostat Schedule status',
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_thermostat_schedule_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'no_hold',
'temporary_hold',
'hold_until',
'permanent_hold',
'vacation_hold',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.ocala_thermostat_priority_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Priority status',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Ocala Thermostat Priority status',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'no_hold',
'temporary_hold',
'hold_until',
'permanent_hold',
'vacation_hold',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.ocala_thermostat_priority_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'no_hold',
})
# ---
# name: test_sensor[sensor.ocala_thermostat_schedule_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([