mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Add is_on state to LunatoneLineBroadcastLight of lunatone (#180897)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Coordinator for handling data fetching and updates."""
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -79,7 +80,9 @@ class LunatoneInfoDataUpdateCoordinator(DataUpdateCoordinator[InfoData]):
|
||||
return self.info_api.data
|
||||
|
||||
|
||||
class LunatoneDevicesDataUpdateCoordinator(DataUpdateCoordinator[dict[int, Device]]):
|
||||
class LunatoneDevicesDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[dict[int, dict[int, Device]]]
|
||||
):
|
||||
"""Data update coordinator for Lunatone devices."""
|
||||
|
||||
config_entry: LunatoneConfigEntry
|
||||
@@ -102,7 +105,7 @@ class LunatoneDevicesDataUpdateCoordinator(DataUpdateCoordinator[dict[int, Devic
|
||||
self.devices_api = devices_api
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[int, Device]:
|
||||
async def _async_update_data(self) -> dict[int, dict[int, Device]]:
|
||||
"""Update devices data."""
|
||||
try:
|
||||
await self.devices_api.async_update()
|
||||
@@ -113,7 +116,11 @@ class LunatoneDevicesDataUpdateCoordinator(DataUpdateCoordinator[dict[int, Devic
|
||||
|
||||
if self.devices_api.data is None:
|
||||
raise UpdateFailed("Did not receive devices data from Lunatone REST API")
|
||||
return {device.data.id: device for device in self.devices_api.devices}
|
||||
|
||||
data: dict[int, dict[int, Device]] = defaultdict(dict)
|
||||
for device in self.devices_api.devices:
|
||||
data[device.data.line].update({device.data.id: device})
|
||||
return dict(data)
|
||||
|
||||
|
||||
class LunatoneSensorsDataUpdateCoordinator(DataUpdateCoordinator[dict[int, Sensor]]):
|
||||
|
||||
@@ -11,10 +11,13 @@ async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: LunatoneConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
info_data = entry.runtime_data.coordinator_info.data
|
||||
devices_data = entry.runtime_data.coordinator_devices.data
|
||||
return {
|
||||
"info": entry.runtime_data.coordinator_info.data.model_dump(),
|
||||
"info": info_data.model_dump(),
|
||||
"devices": [
|
||||
v.data.model_dump()
|
||||
for v in entry.runtime_data.coordinator_devices.data.values()
|
||||
device.data.model_dump()
|
||||
for devices in devices_data.values()
|
||||
for device in devices.values()
|
||||
],
|
||||
}
|
||||
|
||||
@@ -45,18 +45,17 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[LightEntity] = [
|
||||
LunatoneLineBroadcastLight(
|
||||
coordinator_info,
|
||||
coordinator_devices,
|
||||
coordinator_info,
|
||||
dali_line_broadcast,
|
||||
config_entry.unique_id,
|
||||
)
|
||||
for dali_line_broadcast in dali_line_broadcasts
|
||||
]
|
||||
entities.extend(
|
||||
[
|
||||
LunatoneLight(coordinator_devices, device_id, config_entry.unique_id)
|
||||
for device_id in coordinator_devices.data
|
||||
]
|
||||
LunatoneLight(coordinator_devices, line_id, device_id, config_entry.unique_id)
|
||||
for line_id, devices in coordinator_devices.data.items()
|
||||
for device_id in devices
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -80,14 +79,17 @@ class LunatoneLight(
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LunatoneDevicesDataUpdateCoordinator,
|
||||
line_id: int,
|
||||
device_id: int,
|
||||
config_entry_unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize a Lunatone light."""
|
||||
super().__init__(coordinator)
|
||||
self._line_id = line_id
|
||||
self._device_id = device_id
|
||||
self._config_entry_unique_id = config_entry_unique_id
|
||||
self._device = self.coordinator.data[device_id]
|
||||
self._device = self.coordinator.data[line_id][device_id]
|
||||
|
||||
self._attr_unique_id = f"{config_entry_unique_id}-device{device_id}"
|
||||
|
||||
@property
|
||||
@@ -183,7 +185,7 @@ class LunatoneLight(
|
||||
@override
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._device = self.coordinator.data[self._device_id]
|
||||
self._device = self.coordinator.data[self._line_id][self._device_id]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
@@ -225,13 +227,12 @@ class LunatoneLight(
|
||||
|
||||
|
||||
class LunatoneLineBroadcastLight(
|
||||
CoordinatorEntity[LunatoneInfoDataUpdateCoordinator], LightEntity
|
||||
CoordinatorEntity[LunatoneDevicesDataUpdateCoordinator], LightEntity
|
||||
):
|
||||
"""Representation of a Lunatone line broadcast light."""
|
||||
|
||||
BRIGHTNESS_SCALE = (1, 100)
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
@@ -239,23 +240,23 @@ class LunatoneLineBroadcastLight(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator_info: LunatoneInfoDataUpdateCoordinator,
|
||||
coordinator_devices: LunatoneDevicesDataUpdateCoordinator,
|
||||
coordinator_info: LunatoneInfoDataUpdateCoordinator,
|
||||
broadcast: DALIBroadcast,
|
||||
config_entry_unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize a Lunatone line broadcast light."""
|
||||
super().__init__(coordinator_info)
|
||||
self._coordinator_devices = coordinator_devices
|
||||
super().__init__(coordinator_devices)
|
||||
self._coordinator_info = coordinator_info
|
||||
self._broadcast = broadcast
|
||||
|
||||
line = broadcast.line
|
||||
|
||||
self._attr_unique_id = f"{config_entry_unique_id}-line{line}"
|
||||
|
||||
line_device = self.coordinator.data.lines[str(line)].device
|
||||
line_device = self._coordinator_info.data.lines[str(line)].device
|
||||
extra_info: dict = {}
|
||||
if line_device.serial != coordinator_info.data.device.serial:
|
||||
if line_device.serial != self._coordinator_info.data.device.serial:
|
||||
extra_info.update(
|
||||
serial_number=str(line_device.serial),
|
||||
hw_version=line_device.pcb,
|
||||
@@ -274,12 +275,42 @@ class LunatoneLineBroadcastLight(
|
||||
**extra_info,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._coordinator_info.async_add_listener(self._handle_info_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_info_update(self) -> None:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
line_status = self.coordinator.data.lines[str(self._broadcast.line)].line_status
|
||||
return super().available and line_status == LineStatus.OK
|
||||
info_data = self._coordinator_info.data
|
||||
line_id = self._broadcast.line
|
||||
return (
|
||||
super().available
|
||||
and self._coordinator_info.last_update_success
|
||||
and line_id is not None
|
||||
and str(line_id) in info_data.lines
|
||||
and info_data.lines[str(line_id)].line_status == LineStatus.OK
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if light is on."""
|
||||
line_id = self._broadcast.line
|
||||
return (
|
||||
any(device.is_on for device in self.coordinator.data[line_id].values())
|
||||
if line_id is not None and line_id in self.coordinator.data
|
||||
else False
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
@@ -287,10 +318,10 @@ class LunatoneLineBroadcastLight(
|
||||
await self._broadcast.fade_to_brightness(
|
||||
brightness_to_value(self.BRIGHTNESS_SCALE, kwargs.get(ATTR_BRIGHTNESS, 255))
|
||||
)
|
||||
await self._coordinator_devices.async_refresh()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the line to turn off."""
|
||||
await self._broadcast.fade_to_brightness(0)
|
||||
await self._coordinator_devices.async_refresh()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -189,6 +189,15 @@ def build_device_data_list() -> list[DeviceData]:
|
||||
address=4,
|
||||
line=0,
|
||||
),
|
||||
DeviceData(
|
||||
id=6,
|
||||
name="Device 6",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(switchable=Status[bool](status=False)),
|
||||
address=0,
|
||||
line=1,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -305,6 +305,61 @@
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
dict({
|
||||
'address': 0,
|
||||
'available': True,
|
||||
'dali_types': list([
|
||||
]),
|
||||
'features': dict({
|
||||
'color_kelvin': None,
|
||||
'color_kelvin_with_fade': None,
|
||||
'color_rgb': None,
|
||||
'color_rgb_with_fade': None,
|
||||
'color_waf': None,
|
||||
'color_waf_with_fade': None,
|
||||
'color_xy': None,
|
||||
'color_xy_with_fade': None,
|
||||
'dali_cmd16': None,
|
||||
'dim_down': None,
|
||||
'dim_up': None,
|
||||
'dimmable': None,
|
||||
'dimmable_kelvin': None,
|
||||
'dimmable_rgb': None,
|
||||
'dimmable_waf': None,
|
||||
'dimmable_with_fade': None,
|
||||
'dimmable_xy': None,
|
||||
'fade_rate': None,
|
||||
'fade_time': None,
|
||||
'goto_last_active': None,
|
||||
'goto_last_active_with_fade': None,
|
||||
'save_to_scene': None,
|
||||
'scene': None,
|
||||
'scene_with_fade': None,
|
||||
'switchable': dict({
|
||||
'status': False,
|
||||
}),
|
||||
}),
|
||||
'groups': list([
|
||||
]),
|
||||
'id': 6,
|
||||
'line': 1,
|
||||
'name': 'Device 6',
|
||||
'scenes': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'control_gear_failure': False,
|
||||
'fade_running': False,
|
||||
'is_unaddressed': False,
|
||||
'lamp_failure': False,
|
||||
'lamp_on': False,
|
||||
'limit_error': False,
|
||||
'power_cycle_see': False,
|
||||
'raw': 0,
|
||||
'reset_state': False,
|
||||
}),
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
]),
|
||||
'info': dict({
|
||||
'descriptor': dict({
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
# name: test_setup[light.dali_line_0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.ASSUMED_STATE: 'assumed_state'>: True,
|
||||
<LightEntityStateAttribute.BRIGHTNESS: 'brightness'>: None,
|
||||
<LightEntityStateAttribute.COLOR_MODE: 'color_mode'>: None,
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'DALI Line 0',
|
||||
@@ -57,7 +56,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.dali_line_1-entry]
|
||||
@@ -104,7 +103,6 @@
|
||||
# name: test_setup[light.dali_line_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.ASSUMED_STATE: 'assumed_state'>: True,
|
||||
<LightEntityStateAttribute.BRIGHTNESS: 'brightness'>: None,
|
||||
<LightEntityStateAttribute.COLOR_MODE: 'color_mode'>: None,
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'DALI Line 1',
|
||||
@@ -118,7 +116,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_1-entry]
|
||||
@@ -435,3 +433,62 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_6-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES: 'supported_color_modes'>: list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_6',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'be37ca9c47c24498a38bc62c7c711840-device6',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_6-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<LightEntityStateAttribute.COLOR_MODE: 'color_mode'>: None,
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Device 6',
|
||||
<LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES: 'supported_color_modes'>: list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_6',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import copy
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from lunatone_rest_api_client.models import LineStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -166,10 +167,21 @@ async def test_turn_on_off_broadcast(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the broadcast light can be turned on and off."""
|
||||
entity_id = f"light.dali_line_{mock_lunatone_dali_broadcast.line}"
|
||||
line_id = mock_lunatone_dali_broadcast.line
|
||||
entity_id = f"light.dali_line_{line_id}"
|
||||
light_status = iter((True, True, False))
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
status = next(light_status)
|
||||
for device in mock_lunatone_devices.data.devices:
|
||||
device.features.switchable.status = (
|
||||
status if device.line == line_id else True
|
||||
)
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = fake_update
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -180,6 +192,10 @@ async def test_turn_on_off_broadcast(
|
||||
assert mock_lunatone_dali_broadcast.fade_to_brightness.await_count == 1
|
||||
mock_lunatone_dali_broadcast.fade_to_brightness.assert_awaited()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "on"
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -200,6 +216,10 @@ async def test_turn_on_off_broadcast(
|
||||
assert mock_lunatone_dali_broadcast.fade_to_brightness.await_count == 3
|
||||
mock_lunatone_dali_broadcast.fade_to_brightness.assert_awaited()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "off"
|
||||
|
||||
|
||||
async def test_line_broadcast_available_status(
|
||||
hass: HomeAssistant,
|
||||
@@ -209,15 +229,17 @@ async def test_line_broadcast_available_status(
|
||||
mock_lunatone_scan: AsyncMock,
|
||||
mock_lunatone_dali_broadcast: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test if the broadcast light is available."""
|
||||
entity_id = f"light.dali_line_{mock_lunatone_dali_broadcast.line}"
|
||||
line_id = str(mock_lunatone_dali_broadcast.line)
|
||||
entity_id = f"light.dali_line_{line_id}"
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
info_data = copy.deepcopy(mock_lunatone_info.data)
|
||||
info_data.lines["0"].line_status = LineStatus.NOT_REACHABLE
|
||||
info_data.lines[line_id].line_status = LineStatus.NOT_REACHABLE
|
||||
mock_lunatone_info.data = info_data
|
||||
|
||||
mock_lunatone_info.async_update.side_effect = fake_update
|
||||
|
||||
Reference in New Issue
Block a user