From 046db487f702d66bf196950464ed5aa94231dba0 Mon Sep 17 00:00:00 2001 From: David Bugl Date: Thu, 13 Aug 2026 10:35:17 +0000 Subject: [PATCH 1/6] Rename Lunatone scan interval constants to update interval Align constant names with the `update_interval` parameter used by `DataUpdateCoordinator` for improved clarity. --- homeassistant/components/lunatone/coordinator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lunatone/coordinator.py b/homeassistant/components/lunatone/coordinator.py index f5a591e5f2966b..3ebb11ba7c7e04 100644 --- a/homeassistant/components/lunatone/coordinator.py +++ b/homeassistant/components/lunatone/coordinator.py @@ -24,9 +24,9 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_INFO_SCAN_INTERVAL = timedelta(seconds=60) -DEFAULT_DEVICES_SCAN_INTERVAL = timedelta(seconds=10) -DEFAULT_SENSORS_SCAN_INTERVAL = timedelta(seconds=30) +DEFAULT_INFO_UPDATE_INTERVAL = timedelta(seconds=60) +DEFAULT_DEVICES_UPDATE_INTERVAL = timedelta(seconds=10) +DEFAULT_SENSORS_UPDATE_INTERVAL = timedelta(seconds=30) @dataclass @@ -57,7 +57,7 @@ def __init__( config_entry=config_entry, name=f"{DOMAIN}-info", always_update=False, - update_interval=DEFAULT_INFO_SCAN_INTERVAL, + update_interval=DEFAULT_INFO_UPDATE_INTERVAL, ) self.info_api = info_api @@ -94,7 +94,7 @@ def __init__( config_entry=config_entry, name=f"{DOMAIN}-devices", always_update=False, - update_interval=DEFAULT_DEVICES_SCAN_INTERVAL, + update_interval=DEFAULT_DEVICES_UPDATE_INTERVAL, ) self.devices_api = devices_api @@ -131,7 +131,7 @@ def __init__( config_entry=config_entry, name=f"{DOMAIN}-sensors", always_update=False, - update_interval=DEFAULT_SENSORS_SCAN_INTERVAL, + update_interval=DEFAULT_SENSORS_UPDATE_INTERVAL, ) self.sensors_api = sensors_api From 40757fb5451770ca60152d2fe669c2a618894d8c Mon Sep 17 00:00:00 2001 From: David Bugl Date: Thu, 13 Aug 2026 10:41:21 +0000 Subject: [PATCH 2/6] Add basic DALI scan update coordinator --- homeassistant/components/lunatone/__init__.py | 15 ++++- .../components/lunatone/coordinator.py | 43 +++++++++++++- tests/components/lunatone/conftest.py | 11 ++++ tests/components/lunatone/test_config_flow.py | 2 + tests/components/lunatone/test_diagnostics.py | 1 + tests/components/lunatone/test_init.py | 56 +++++++++++++++++++ tests/components/lunatone/test_light.py | 9 +++ tests/components/lunatone/test_sensor.py | 2 + 8 files changed, 137 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lunatone/__init__.py b/homeassistant/components/lunatone/__init__.py index 5a2f28674755dd..7fb9631aa55fec 100644 --- a/homeassistant/components/lunatone/__init__.py +++ b/homeassistant/components/lunatone/__init__.py @@ -3,7 +3,14 @@ import logging from typing import Final -from lunatone_rest_api_client import Auth, DALIBroadcast, Devices, Info, Sensors +from lunatone_rest_api_client import ( + Auth, + DALIBroadcast, + DALIScan, + Devices, + Info, + Sensors, +) from homeassistant.const import CONF_URL, Platform from homeassistant.core import HomeAssistant @@ -18,6 +25,7 @@ LunatoneData, LunatoneDevicesDataUpdateCoordinator, LunatoneInfoDataUpdateCoordinator, + LunatoneScanDataUpdateCoordinator, LunatoneSensorsDataUpdateCoordinator, ) @@ -70,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> """Set up Lunatone from a config entry.""" auth_api = Auth(async_get_clientsession(hass), entry.data[CONF_URL]) info_api = Info(auth_api) + dali_scan_api = DALIScan(auth_api) devices_api = Devices(info_api) sensors_api = Sensors(auth_api) @@ -110,6 +119,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> coordinator_sensors = LunatoneSensorsDataUpdateCoordinator(hass, entry, sensors_api) await coordinator_sensors.async_config_entry_first_refresh() + coordinator_scan = LunatoneScanDataUpdateCoordinator(hass, entry, dali_scan_api) + await coordinator_scan.async_config_entry_first_refresh() + dali_line_broadcasts = [ DALIBroadcast(auth_api, int(line)) for line in coordinator_info.data.lines ] @@ -118,6 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> coordinator_info, coordinator_devices, coordinator_sensors, + coordinator_scan, dali_line_broadcasts, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/lunatone/coordinator.py b/homeassistant/components/lunatone/coordinator.py index 3ebb11ba7c7e04..b2ae1c2daa054f 100644 --- a/homeassistant/components/lunatone/coordinator.py +++ b/homeassistant/components/lunatone/coordinator.py @@ -8,13 +8,14 @@ import aiohttp from lunatone_rest_api_client import ( DALIBroadcast, + DALIScan, Device, Devices, Info, Sensor, Sensors, ) -from lunatone_rest_api_client.models import InfoData +from lunatone_rest_api_client.models import InfoData, ScanData from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -27,6 +28,7 @@ DEFAULT_INFO_UPDATE_INTERVAL = timedelta(seconds=60) DEFAULT_DEVICES_UPDATE_INTERVAL = timedelta(seconds=10) DEFAULT_SENSORS_UPDATE_INTERVAL = timedelta(seconds=30) +DEFAULT_SCAN_UPDATE_INTERVAL = timedelta(seconds=10) @dataclass @@ -36,6 +38,7 @@ class LunatoneData: coordinator_info: LunatoneInfoDataUpdateCoordinator coordinator_devices: LunatoneDevicesDataUpdateCoordinator coordinator_sensors: LunatoneSensorsDataUpdateCoordinator + coordinator_scan: LunatoneScanDataUpdateCoordinator dali_line_broadcasts: list[DALIBroadcast] @@ -149,3 +152,41 @@ async def _async_update_data(self) -> dict[int, Sensor]: if self.sensors_api.data is None: raise UpdateFailed("Did not receive sensors data from Lunatone REST API") return {sensor.id: sensor for sensor in self.sensors_api.sensors} + + +class LunatoneScanDataUpdateCoordinator(DataUpdateCoordinator[ScanData]): + """Data update coordinator for Lunatone scan.""" + + config_entry: LunatoneConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: LunatoneConfigEntry, + dali_scan_api: DALIScan, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=f"{DOMAIN}-scan", + always_update=False, + update_interval=DEFAULT_SCAN_UPDATE_INTERVAL, + ) + self.dali_scan_api = dali_scan_api + + @override + async def _async_update_data(self) -> ScanData: + """Update scan data.""" + try: + await self.dali_scan_api.async_update() + except aiohttp.ClientConnectionError as ex: + raise UpdateFailed( + "Unable to retrieve scan data from Lunatone REST API" + ) from ex + + if self.dali_scan_api.data is None: + raise UpdateFailed("Did not receive scan data from Lunatone REST API") + + return self.dali_scan_api.data diff --git a/tests/components/lunatone/conftest.py b/tests/components/lunatone/conftest.py index ba5e5f364b9c13..7fa398cbede573 100644 --- a/tests/components/lunatone/conftest.py +++ b/tests/components/lunatone/conftest.py @@ -161,6 +161,17 @@ def _set_data(data: SensorsData) -> None: yield sensors +@pytest.fixture +def mock_lunatone_scan() -> Generator[AsyncMock]: + """Mock a Lunatone DALI scan object.""" + with patch( + "homeassistant.components.lunatone.DALIScan", + autospec=True, + ) as mock_dali_scan: + scan = mock_dali_scan.return_value + yield scan + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/lunatone/test_config_flow.py b/tests/components/lunatone/test_config_flow.py index 3db320d2a50757..16fc432f531f4f 100644 --- a/tests/components/lunatone/test_config_flow.py +++ b/tests/components/lunatone/test_config_flow.py @@ -158,6 +158,7 @@ async def test_zeroconf_flow( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, ) -> None: """Test zeroconf flow.""" result = await hass.config_entries.flow.async_init( @@ -180,6 +181,7 @@ async def test_zeroconf_flow_abort_duplicate( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test zeroconf flow aborts with duplicate.""" diff --git a/tests/components/lunatone/test_diagnostics.py b/tests/components/lunatone/test_diagnostics.py index 044ff2d30af590..fbf0e42995bbe7 100644 --- a/tests/components/lunatone/test_diagnostics.py +++ b/tests/components/lunatone/test_diagnostics.py @@ -19,6 +19,7 @@ async def test_config_entry_diagnostics( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/lunatone/test_init.py b/tests/components/lunatone/test_init.py index 0f644d905ea80f..9c0b5275b3fc89 100644 --- a/tests/components/lunatone/test_init.py +++ b/tests/components/lunatone/test_init.py @@ -20,6 +20,7 @@ async def test_load_unload_config_entry( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: @@ -50,6 +51,7 @@ async def test_config_entry_not_ready_info_api_fail( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test config entry not ready due to info API failure.""" @@ -74,6 +76,7 @@ async def test_config_entry_not_ready_devices_api_fail( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test config entry not ready due to devices API failure.""" @@ -100,6 +103,7 @@ async def test_config_entry_not_ready_sensors_api_fail( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test config entry not ready due to sensors API failure.""" @@ -123,6 +127,37 @@ async def test_config_entry_not_ready_sensors_api_fail( assert mock_config_entry.state is ConfigEntryState.LOADED +async def test_config_entry_not_ready_scan_api_fail( + hass: HomeAssistant, + mock_lunatone_info: AsyncMock, + mock_lunatone_devices: AsyncMock, + mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry not ready due to sensors API failure.""" + mock_lunatone_scan.async_update.side_effect = aiohttp.ClientConnectionError() + + await setup_integration(hass, mock_config_entry) + + mock_lunatone_info.async_update.assert_called_once() + mock_lunatone_devices.async_update.assert_called_once() + mock_lunatone_sensors.async_update.assert_called_once() + mock_lunatone_scan.async_update.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + mock_lunatone_scan.async_update.side_effect = None + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_lunatone_info.async_update.assert_called() + mock_lunatone_devices.async_update.assert_called() + mock_lunatone_sensors.async_update.assert_called() + mock_lunatone_scan.async_update.assert_called() + assert mock_config_entry.state is ConfigEntryState.LOADED + + async def test_config_entry_not_ready_no_info_data( hass: HomeAssistant, mock_lunatone_info: AsyncMock, @@ -172,6 +207,26 @@ async def test_config_entry_not_ready_no_sensors_data( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_config_entry_not_ready_no_dali_scan_data( + hass: HomeAssistant, + mock_lunatone_info: AsyncMock, + mock_lunatone_devices: AsyncMock, + mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Lunatone configuration entry not ready due to missing DALI scan data.""" + mock_lunatone_scan.data = None + + await setup_integration(hass, mock_config_entry) + + mock_lunatone_info.async_update.assert_called_once() + mock_lunatone_devices.async_update.assert_called_once() + mock_lunatone_sensors.async_update.assert_called_once() + mock_lunatone_scan.async_update.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + async def test_config_entry_not_ready_no_serial_number( hass: HomeAssistant, mock_lunatone_info: AsyncMock, @@ -192,6 +247,7 @@ async def test_config_entry_unique_id_update( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, diff --git a/tests/components/lunatone/test_light.py b/tests/components/lunatone/test_light.py index 4ccbf1964335b5..0b88287f49eb04 100644 --- a/tests/components/lunatone/test_light.py +++ b/tests/components/lunatone/test_light.py @@ -35,6 +35,7 @@ async def test_setup( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -56,6 +57,7 @@ async def test_turn_on_off( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test the light can be turned on and off.""" @@ -98,6 +100,7 @@ async def test_turn_on_off_with_brightness( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test the light can be turned on with brightness.""" @@ -158,6 +161,7 @@ async def test_turn_on_off_broadcast( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_lunatone_dali_broadcast: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: @@ -202,6 +206,7 @@ async def test_line_broadcast_available_status( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_lunatone_dali_broadcast: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: @@ -234,6 +239,7 @@ async def test_line_broadcast_line_present( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_lunatone_dali_broadcast: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: @@ -254,6 +260,7 @@ async def test_turn_on_with_color_temperature( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, color_temp_kelvin: int, ) -> None: @@ -294,6 +301,7 @@ async def test_turn_on_with_rgb_color( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, rgb_color: tuple[int, int, int], ) -> None: @@ -336,6 +344,7 @@ async def test_turn_on_with_rgbw_color( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, rgbw_color: tuple[int, int, int, int], ) -> None: diff --git a/tests/components/lunatone/test_sensor.py b/tests/components/lunatone/test_sensor.py index a630aa8fc06a02..cccfb2f58fa494 100644 --- a/tests/components/lunatone/test_sensor.py +++ b/tests/components/lunatone/test_sensor.py @@ -21,6 +21,7 @@ async def test_setup( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -41,6 +42,7 @@ async def test_sensor_value_update( mock_lunatone_info: AsyncMock, mock_lunatone_devices: AsyncMock, mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: From 7de7d3b1204f5ed9a37c75652a4a4a0b17260d3b Mon Sep 17 00:00:00 2001 From: David Bugl Date: Thu, 13 Aug 2026 10:43:33 +0000 Subject: [PATCH 3/6] Reduce Lunatone DALI scan interval when API is busy When the DALI scan API reports as busy, temporarily reduce the update interval to 1 second. This allows for quicker status updates during active scanning improving responsiveness. --- homeassistant/components/lunatone/coordinator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/lunatone/coordinator.py b/homeassistant/components/lunatone/coordinator.py index b2ae1c2daa054f..6170309d09110e 100644 --- a/homeassistant/components/lunatone/coordinator.py +++ b/homeassistant/components/lunatone/coordinator.py @@ -189,4 +189,9 @@ async def _async_update_data(self) -> ScanData: if self.dali_scan_api.data is None: raise UpdateFailed("Did not receive scan data from Lunatone REST API") + update_interval = DEFAULT_SCAN_UPDATE_INTERVAL + if self.dali_scan_api.is_busy: + update_interval = timedelta(seconds=1) + self.update_interval = update_interval + return self.dali_scan_api.data From ef7f205e4970730de4e9162747c3b94e51b33d6b Mon Sep 17 00:00:00 2001 From: David Bugl Date: Thu, 13 Aug 2026 10:46:09 +0000 Subject: [PATCH 4/6] Add Lunatone DALI scan status sensor --- homeassistant/components/lunatone/sensor.py | 50 ++++++++++++++++++- .../components/lunatone/strings.json | 11 ++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lunatone/sensor.py b/homeassistant/components/lunatone/sensor.py index 0335a848c1e30a..8b96657c815a3e 100644 --- a/homeassistant/components/lunatone/sensor.py +++ b/homeassistant/components/lunatone/sensor.py @@ -2,7 +2,7 @@ from typing import Final, override -from lunatone_rest_api_client import Sensor +from lunatone_rest_api_client import DALIScan, Sensor from lunatone_rest_api_client.models import SensorAddressType, SensorType from homeassistant.components.sensor import ( @@ -13,6 +13,7 @@ ) from homeassistant.const import ( LIGHT_LUX, + EntityCategory, UnitOfPressure, UnitOfRatio, UnitOfTemperature, @@ -24,7 +25,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import LunatoneConfigEntry, LunatoneSensorsDataUpdateCoordinator +from .coordinator import ( + LunatoneConfigEntry, + LunatoneScanDataUpdateCoordinator, + LunatoneSensorsDataUpdateCoordinator, +) PARALLEL_UPDATES = 0 SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { @@ -78,6 +83,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Lunatone sensors from the config entry.""" + coordinator_scan = config_entry.runtime_data.coordinator_scan coordinator_sensors = config_entry.runtime_data.coordinator_sensors assert config_entry.unique_id is not None @@ -89,6 +95,9 @@ async def async_setup_entry( for sensor_id, sensor_data in coordinator_sensors.data.items() if (description := SENSOR_TYPES.get(sensor_data.data.type)) ) + async_add_entities( + [LunatoneDALIScanSensor(coordinator_scan, config_entry.unique_id)] + ) class LunatoneSensor( @@ -160,3 +169,40 @@ def available(self) -> bool: def native_value(self) -> float | None: """Return the measurement value of the sensor.""" return self.sensor.data.value + + +class LunatoneDALIScanSensor( + CoordinatorEntity[LunatoneScanDataUpdateCoordinator], SensorEntity +): + """Representation of a Lunatone Sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LunatoneScanDataUpdateCoordinator, + config_entry_unique_id: str, + ) -> None: + """Initialize a Lunatone Sensor.""" + super().__init__(coordinator) + self.entity_category = EntityCategory.DIAGNOSTIC + + self._config_entry_unique_id = config_entry_unique_id + + # self._attr_name = "DALI Scan Progress" + self._attr_unique_id = f"{config_entry_unique_id}-scan-progress" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config_entry_unique_id)}, + ) + self._attr_translation_key = "scan_status" + + @property + def scan(self) -> DALIScan: + """Return the sensor data.""" + return self.coordinator.dali_scan_api + + @property + @override + def native_value(self) -> str | None: + """Return the measurement value of the sensor.""" + return "in_progress" if self.scan.is_busy and self.scan.progress else "idle" diff --git a/homeassistant/components/lunatone/strings.json b/homeassistant/components/lunatone/strings.json index ff57187e1e16c1..94398ff197f3ac 100644 --- a/homeassistant/components/lunatone/strings.json +++ b/homeassistant/components/lunatone/strings.json @@ -37,6 +37,17 @@ } } }, + "entity": { + "sensor": { + "scan_status": { + "name": "DALI scan status", + "state": { + "idle": "Idle", + "in_progress": "In progress" + } + } + } + }, "exceptions": { "missing_device_info": { "message": "Unable to read device information. Please verify the device's network connection." From 5ffde0c57b4f344bb02c71e6f8e427f9616f59b6 Mon Sep 17 00:00:00 2001 From: David Bugl Date: Thu, 13 Aug 2026 11:12:01 +0000 Subject: [PATCH 5/6] Replace the Lunatone DALI scan status sensor with binary sensor --- homeassistant/components/lunatone/__init__.py | 6 +- .../components/lunatone/binary_sensor.py | 65 +++++++++++++++++++ homeassistant/components/lunatone/sensor.py | 50 +------------- .../components/lunatone/strings.json | 8 +-- 4 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/lunatone/binary_sensor.py diff --git a/homeassistant/components/lunatone/__init__.py b/homeassistant/components/lunatone/__init__.py index 7fb9631aa55fec..dde1f64bb4e382 100644 --- a/homeassistant/components/lunatone/__init__.py +++ b/homeassistant/components/lunatone/__init__.py @@ -30,7 +30,11 @@ ) _LOGGER = logging.getLogger(__name__) -PLATFORMS: Final[list[Platform]] = [Platform.LIGHT, Platform.SENSOR] +PLATFORMS: Final[list[Platform]] = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.SENSOR, +] async def _update_unique_id( diff --git a/homeassistant/components/lunatone/binary_sensor.py b/homeassistant/components/lunatone/binary_sensor.py new file mode 100644 index 00000000000000..06fc898f5ef0f3 --- /dev/null +++ b/homeassistant/components/lunatone/binary_sensor.py @@ -0,0 +1,65 @@ +"""Platform for Lunatone binary sensor integration.""" + +from typing import override + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LunatoneConfigEntry, LunatoneScanDataUpdateCoordinator + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: LunatoneConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Lunatone sensors from the config entry.""" + coordinator_scan = config_entry.runtime_data.coordinator_scan + + assert config_entry.unique_id is not None + + async_add_entities( + [LunatoneDALIScanStatus(coordinator_scan, config_entry.unique_id)] + ) + + +class LunatoneDALIScanStatus( + CoordinatorEntity[LunatoneScanDataUpdateCoordinator], BinarySensorEntity +): + """Representation of a Lunatone DALI scan status.""" + + _attr_device_class = BinarySensorDeviceClass.RUNNING + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LunatoneScanDataUpdateCoordinator, + config_entry_unique_id: str, + ) -> None: + """Initialize a Lunatone Sensor.""" + super().__init__(coordinator) + self.entity_category = EntityCategory.DIAGNOSTIC + + self._config_entry_unique_id = config_entry_unique_id + + self._attr_unique_id = f"{config_entry_unique_id}-scan-progress" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config_entry_unique_id)}, + ) + self._attr_translation_key = "scan_status" + + @property + @override + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.dali_scan_api.is_busy diff --git a/homeassistant/components/lunatone/sensor.py b/homeassistant/components/lunatone/sensor.py index 8b96657c815a3e..0335a848c1e30a 100644 --- a/homeassistant/components/lunatone/sensor.py +++ b/homeassistant/components/lunatone/sensor.py @@ -2,7 +2,7 @@ from typing import Final, override -from lunatone_rest_api_client import DALIScan, Sensor +from lunatone_rest_api_client import Sensor from lunatone_rest_api_client.models import SensorAddressType, SensorType from homeassistant.components.sensor import ( @@ -13,7 +13,6 @@ ) from homeassistant.const import ( LIGHT_LUX, - EntityCategory, UnitOfPressure, UnitOfRatio, UnitOfTemperature, @@ -25,11 +24,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import ( - LunatoneConfigEntry, - LunatoneScanDataUpdateCoordinator, - LunatoneSensorsDataUpdateCoordinator, -) +from .coordinator import LunatoneConfigEntry, LunatoneSensorsDataUpdateCoordinator PARALLEL_UPDATES = 0 SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { @@ -83,7 +78,6 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Lunatone sensors from the config entry.""" - coordinator_scan = config_entry.runtime_data.coordinator_scan coordinator_sensors = config_entry.runtime_data.coordinator_sensors assert config_entry.unique_id is not None @@ -95,9 +89,6 @@ async def async_setup_entry( for sensor_id, sensor_data in coordinator_sensors.data.items() if (description := SENSOR_TYPES.get(sensor_data.data.type)) ) - async_add_entities( - [LunatoneDALIScanSensor(coordinator_scan, config_entry.unique_id)] - ) class LunatoneSensor( @@ -169,40 +160,3 @@ def available(self) -> bool: def native_value(self) -> float | None: """Return the measurement value of the sensor.""" return self.sensor.data.value - - -class LunatoneDALIScanSensor( - CoordinatorEntity[LunatoneScanDataUpdateCoordinator], SensorEntity -): - """Representation of a Lunatone Sensor.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: LunatoneScanDataUpdateCoordinator, - config_entry_unique_id: str, - ) -> None: - """Initialize a Lunatone Sensor.""" - super().__init__(coordinator) - self.entity_category = EntityCategory.DIAGNOSTIC - - self._config_entry_unique_id = config_entry_unique_id - - # self._attr_name = "DALI Scan Progress" - self._attr_unique_id = f"{config_entry_unique_id}-scan-progress" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._config_entry_unique_id)}, - ) - self._attr_translation_key = "scan_status" - - @property - def scan(self) -> DALIScan: - """Return the sensor data.""" - return self.coordinator.dali_scan_api - - @property - @override - def native_value(self) -> str | None: - """Return the measurement value of the sensor.""" - return "in_progress" if self.scan.is_busy and self.scan.progress else "idle" diff --git a/homeassistant/components/lunatone/strings.json b/homeassistant/components/lunatone/strings.json index 94398ff197f3ac..2d2b6b20800a0e 100644 --- a/homeassistant/components/lunatone/strings.json +++ b/homeassistant/components/lunatone/strings.json @@ -38,13 +38,9 @@ } }, "entity": { - "sensor": { + "binary_sensor": { "scan_status": { - "name": "DALI scan status", - "state": { - "idle": "Idle", - "in_progress": "In progress" - } + "name": "DALI scan" } } }, From bc46ff2c6ad7f719ec5b08e5693bcc2b0dd217e9 Mon Sep 17 00:00:00 2001 From: David Bugl Date: Fri, 14 Aug 2026 10:45:17 +0000 Subject: [PATCH 6/6] Update the doc strings of `binary_sensor.py` --- homeassistant/components/lunatone/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lunatone/binary_sensor.py b/homeassistant/components/lunatone/binary_sensor.py index 06fc898f5ef0f3..b4ad44838de674 100644 --- a/homeassistant/components/lunatone/binary_sensor.py +++ b/homeassistant/components/lunatone/binary_sensor.py @@ -23,7 +23,7 @@ async def async_setup_entry( config_entry: LunatoneConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Lunatone sensors from the config entry.""" + """Set up Lunatone binary sensors from the config entry.""" coordinator_scan = config_entry.runtime_data.coordinator_scan assert config_entry.unique_id is not None @@ -46,7 +46,7 @@ def __init__( coordinator: LunatoneScanDataUpdateCoordinator, config_entry_unique_id: str, ) -> None: - """Initialize a Lunatone Sensor.""" + """Initialize a Lunatone DALI scan status.""" super().__init__(coordinator) self.entity_category = EntityCategory.DIAGNOSTIC @@ -61,5 +61,5 @@ def __init__( @property @override def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" + """Return true if the DALI scan is on.""" return self.coordinator.dali_scan_api.is_busy