diff --git a/homeassistant/components/lunatone/icons.json b/homeassistant/components/lunatone/icons.json new file mode 100644 index 000000000000..099c8ac27189 --- /dev/null +++ b/homeassistant/components/lunatone/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "dali_line_status": { + "default": "mdi:current-ac" + } + } + } +} diff --git a/homeassistant/components/lunatone/sensor.py b/homeassistant/components/lunatone/sensor.py index 7ec5903e52af..5ddc6492484f 100644 --- a/homeassistant/components/lunatone/sensor.py +++ b/homeassistant/components/lunatone/sensor.py @@ -3,7 +3,7 @@ from typing import Final, override from lunatone_rest_api_client import Sensor -from lunatone_rest_api_client.models import SensorAddressType, SensorType +from lunatone_rest_api_client.models import LineStatus, SensorAddressType, SensorType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( LIGHT_LUX, + EntityCategory, UnitOfPressure, UnitOfRatio, UnitOfTemperature, @@ -24,8 +25,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import LunatoneConfigEntry, LunatoneSensorsDataUpdateCoordinator +from .coordinator import ( + LunatoneConfigEntry, + LunatoneInfoDataUpdateCoordinator, + LunatoneSensorsDataUpdateCoordinator, +) +DALI_LINE_STATUS_SENSOR_MAPPING: dict[str, str] = { + LineStatus.LOW_POWER: "low_power", + LineStatus.NO_POWER: "no_power", + LineStatus.NOT_REACHABLE: "not_reachable", + LineStatus.OK: "ok", +} PARALLEL_UPDATES = 0 SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { SensorType.AIR_HUMIDITY: SensorEntityDescription( @@ -78,18 +89,25 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Lunatone sensors from the config entry.""" + coordinator_info = config_entry.runtime_data.coordinator_info coordinator_sensors = config_entry.runtime_data.coordinator_sensors assert config_entry.unique_id is not None - async_add_entities( + entities: list[SensorEntity] = [ LunatoneSensor( coordinator_sensors, description, sensor_id, config_entry.unique_id ) for sensor_id, sensor_data in coordinator_sensors.data.items() if (description := SENSOR_TYPES.get(sensor_data.data.type)) + ] + entities.extend( + LunatoneDALILineStatusSensor(coordinator_info, line_id, config_entry.unique_id) + for line_id in coordinator_info.data.lines ) + async_add_entities(entities) + class LunatoneSensor( CoordinatorEntity[LunatoneSensorsDataUpdateCoordinator], SensorEntity @@ -160,3 +178,42 @@ class LunatoneSensor( def native_value(self) -> float | None: """Return the measurement value of the sensor.""" return self.sensor.data.value + + +class LunatoneDALILineStatusSensor( + CoordinatorEntity[LunatoneInfoDataUpdateCoordinator], SensorEntity +): + """Representation of a Lunatone DALI line status sensor.""" + + _attr_device_class = SensorDeviceClass.ENUM + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_options = list(DALI_LINE_STATUS_SENSOR_MAPPING.values()) + _attr_state_class = None + _attr_translation_key = "dali_line_status" + + def __init__( + self, + coordinator: LunatoneInfoDataUpdateCoordinator, + line_id: str, + config_entry_unique_id: str, + ) -> None: + """Initialize a Lunatone DALI line status sensor.""" + super().__init__(coordinator) + + self._config_entry_unique_id = config_entry_unique_id + self._line_id = line_id + + line_unique_id = f"{config_entry_unique_id}-line{line_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, line_unique_id)}, + ) + self._attr_unique_id = f"{line_unique_id}-status" + + @property + @override + def native_value(self) -> str: + """Return the value of the sensor.""" + return DALI_LINE_STATUS_SENSOR_MAPPING[ + self.coordinator.data.lines[self._line_id].line_status + ] diff --git a/homeassistant/components/lunatone/strings.json b/homeassistant/components/lunatone/strings.json index 2d2b6b20800a..70d5ddde2ef3 100644 --- a/homeassistant/components/lunatone/strings.json +++ b/homeassistant/components/lunatone/strings.json @@ -42,6 +42,17 @@ "scan_status": { "name": "DALI scan" } + }, + "sensor": { + "dali_line_status": { + "name": "Status", + "state": { + "low_power": "Low bus power", + "no_power": "Bus power failure", + "not_reachable": "Not reachable", + "ok": "OK" + } + } } }, "exceptions": { diff --git a/tests/components/lunatone/snapshots/test_sensor.ambr b/tests/components/lunatone/snapshots/test_sensor.ambr index d7cf185cadca..2b5db2c564ee 100644 --- a/tests/components/lunatone/snapshots/test_sensor.ambr +++ b/tests/components/lunatone/snapshots/test_sensor.ambr @@ -57,6 +57,134 @@ 'state': 'unknown', }) # --- +# name: test_setup[sensor.dali_line_0_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'low_power', + 'no_power', + 'not_reachable', + 'ok', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dali_line_0_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'lunatone', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dali_line_status', + 'unique_id': 'be37ca9c47c24498a38bc62c7c711840-line0-status', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.dali_line_0_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'DALI Line 0 Status', + : list([ + 'low_power', + 'no_power', + 'not_reachable', + 'ok', + ]), + }), + 'context': , + 'entity_id': 'sensor.dali_line_0_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_setup[sensor.dali_line_1_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'low_power', + 'no_power', + 'not_reachable', + 'ok', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dali_line_1_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'lunatone', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dali_line_status', + 'unique_id': 'be37ca9c47c24498a38bc62c7c711840-line1-status', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.dali_line_1_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'DALI Line 1 Status', + : list([ + 'low_power', + 'no_power', + 'not_reachable', + 'ok', + ]), + }), + 'context': , + 'entity_id': 'sensor.dali_line_1_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- # name: test_setup[sensor.test_sensor_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/lunatone/test_sensor.py b/tests/components/lunatone/test_sensor.py index cccfb2f58fa4..774ecde829f9 100644 --- a/tests/components/lunatone/test_sensor.py +++ b/tests/components/lunatone/test_sensor.py @@ -1,10 +1,11 @@ """Tests for the lights provided by the Lunatone integration.""" +from copy import deepcopy from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from lunatone_rest_api_client.models import SensorData +from lunatone_rest_api_client.models import LineStatus, SensorData from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform @@ -71,3 +72,40 @@ async def test_sensor_value_update( assert entities[0].state == "22" assert entities[1].state == "55" assert entities[2].state == "20" + + +async def test_dali_line_status_value_update( + hass: HomeAssistant, + mock_lunatone_info: AsyncMock, + mock_lunatone_devices: AsyncMock, + mock_lunatone_sensors: AsyncMock, + mock_lunatone_scan: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Lunatone sensor value update.""" + line_id = 0 + entity_id = f"sensor.dali_line_{line_id}_status" + + line_statuses = iter((LineStatus.NO_POWER, LineStatus.OK)) + + async def fake_update() -> None: + info_data = deepcopy(mock_lunatone_info.data) + info_data.lines[str(line_id)].line_status = next(line_statuses) + mock_lunatone_info.data = info_data + + mock_lunatone_info.async_update.side_effect = fake_update + + await setup_integration(hass, mock_config_entry) + + entity = hass.states.get(entity_id) + assert entity + assert entity.state == "no_power" + + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity + assert entity.state == "ok"