diff --git a/homeassistant/components/sunricher_dali/quality_scale.yaml b/homeassistant/components/sunricher_dali/quality_scale.yaml index b6dc471fbc3..27b40e9335d 100644 --- a/homeassistant/components/sunricher_dali/quality_scale.yaml +++ b/homeassistant/components/sunricher_dali/quality_scale.yaml @@ -59,14 +59,10 @@ rules: docs-troubleshooting: todo docs-use-cases: todo dynamic-devices: todo - entity-category: - status: exempt - comment: Integration exposes only primary light entities. - entity-device-class: - status: exempt - comment: Light entities do not support device classes. + entity-category: done + entity-device-class: done entity-disabled-by-default: todo - entity-translations: todo + entity-translations: done exception-translations: todo icon-translations: todo reconfiguration-flow: todo diff --git a/homeassistant/components/sunricher_dali/sensor.py b/homeassistant/components/sunricher_dali/sensor.py index 19dc58d5168..7b82a8d1dc3 100644 --- a/homeassistant/components/sunricher_dali/sensor.py +++ b/homeassistant/components/sunricher_dali/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from PySrDaliGateway import CallbackEventType, Device -from PySrDaliGateway.helper import is_illuminance_sensor +from PySrDaliGateway.helper import is_illuminance_sensor, is_light_device from PySrDaliGateway.types import IlluminanceStatus from homeassistant.components.sensor import ( @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.const import LIGHT_LUX +from homeassistant.const import LIGHT_LUX, EntityCategory, UnitOfEnergy from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -35,11 +35,12 @@ async def async_setup_entry( """Set up Sunricher DALI sensor entities from config entry.""" devices = entry.runtime_data.devices - entities: list[SensorEntity] = [ - SunricherDaliIlluminanceSensor(device) - for device in devices - if is_illuminance_sensor(device.dev_type) - ] + entities: list[SensorEntity] = [] + for device in devices: + if is_illuminance_sensor(device.dev_type): + entities.append(SunricherDaliIlluminanceSensor(device)) + if is_light_device(device.dev_type): + entities.append(SunricherDaliEnergySensor(device)) if entities: async_add_entities(entities) @@ -119,3 +120,41 @@ class SunricherDaliIlluminanceSensor(DaliDeviceEntity, SensorEntity): on_off, ) self.schedule_update_ha_state() + + +class SunricherDaliEnergySensor(DaliDeviceEntity, SensorEntity): + """Representation of a Sunricher DALI Energy Sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_suggested_display_precision = 2 + + def __init__(self, device: Device) -> None: + """Initialize the energy sensor.""" + super().__init__(device) + self._device = device + self._attr_unique_id = f"{device.unique_id}_energy" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.dev_id)}, + name=device.name, + manufacturer=MANUFACTURER, + model=device.model, + via_device=(DOMAIN, device.gw_sn), + ) + + async def async_added_to_hass(self) -> None: + """Register energy report listener.""" + await super().async_added_to_hass() + self.async_on_remove( + self._device.register_listener( + CallbackEventType.ENERGY_REPORT, self._handle_energy_update + ) + ) + + @callback + def _handle_energy_update(self, energy_value: float) -> None: + """Update energy value.""" + self._attr_native_value = energy_value + self.schedule_update_ha_state() diff --git a/tests/components/sunricher_dali/conftest.py b/tests/components/sunricher_dali/conftest.py index 270466899cd..1c1bf319e8c 100644 --- a/tests/components/sunricher_dali/conftest.py +++ b/tests/components/sunricher_dali/conftest.py @@ -73,6 +73,9 @@ ILLUMINANCE_SENSOR_DATA: dict[str, Any] = { "channel": 0, } +# Light device data for energy sensor testing (reuse first device from DEVICE_DATA) +LIGHT_DEVICE_DATA: dict[str, Any] = DEVICE_DATA[0] + MOTION_SENSOR_DATA: dict[str, Any] = { "dev_id": "02010000106A242121110E", "dev_type": "0201", @@ -152,6 +155,12 @@ def mock_illuminance_device() -> MagicMock: return _create_mock_device(ILLUMINANCE_SENSOR_DATA) +@pytest.fixture +def mock_light_device() -> MagicMock: + """Return a mocked light device for energy sensor testing.""" + return _create_mock_device(LIGHT_DEVICE_DATA) + + @pytest.fixture def mock_motion_sensor_device() -> MagicMock: """Return a mocked motion sensor device.""" diff --git a/tests/components/sunricher_dali/snapshots/test_sensor.ambr b/tests/components/sunricher_dali/snapshots/test_sensor.ambr index fead384b976..3503ef1cb3b 100644 --- a/tests/components/sunricher_dali/snapshots/test_sensor.ambr +++ b/tests/components/sunricher_dali/snapshots/test_sensor.ambr @@ -1,4 +1,61 @@ # serializer version: 1 +# name: test_setup_entry[sensor.dimmer_0000_02_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dimmer_0000_02_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'sunricher_dali', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01010000026A242121110E_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_entry[sensor.dimmer_0000_02_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Dimmer 0000-02 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dimmer_0000_02_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup_entry[sensor.illuminance_sensor_0000_20-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/sunricher_dali/test_sensor.py b/tests/components/sunricher_dali/test_sensor.py index 0f6ba6aacba..4306b6e4673 100644 --- a/tests/components/sunricher_dali/test_sensor.py +++ b/tests/components/sunricher_dali/test_sensor.py @@ -17,9 +17,11 @@ ENTITY_ID = "sensor.illuminance_sensor_0000_20" @pytest.fixture -def mock_devices(mock_illuminance_device: MagicMock) -> list[MagicMock]: - """Override mock_devices to use illuminance sensor only.""" - return [mock_illuminance_device] +def mock_devices( + mock_illuminance_device: MagicMock, mock_light_device: MagicMock +) -> list[MagicMock]: + """Override mock_devices to use illuminance sensor and light device.""" + return [mock_illuminance_device, mock_light_device] @pytest.fixture @@ -28,6 +30,9 @@ def platforms() -> list[Platform]: return [Platform.SENSOR] +ENERGY_ENTITY_ID = "sensor.dimmer_0000_02_energy" + + @pytest.mark.usefixtures("init_integration") async def test_setup_entry( hass: HomeAssistant, @@ -42,8 +47,11 @@ async def test_setup_entry( entity_registry, mock_config_entry.entry_id ) - assert len(entity_entries) == 1 - assert entity_entries[0].entity_id == ENTITY_ID + # Should have illuminance sensor and energy sensor + assert len(entity_entries) == 2 + entity_ids = {entry.entity_id for entry in entity_entries} + assert ENTITY_ID in entity_ids + assert ENERGY_ENTITY_ID in entity_ids @pytest.mark.usefixtures("init_integration") @@ -129,3 +137,59 @@ async def test_availability( state = hass.states.get(ENTITY_ID) assert state is not None assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("init_integration") +async def test_energy_callback( + hass: HomeAssistant, + mock_light_device: MagicMock, +) -> None: + """Test EnergySensor handles energy report callback correctly.""" + callback = find_device_listener(mock_light_device, CallbackEventType.ENERGY_REPORT) + + # Update energy value + callback(123.45) + await hass.async_block_till_done() + + state = hass.states.get(ENERGY_ENTITY_ID) + assert state is not None + assert float(state.state) == 123.45 + + # Update to new value + callback(200.0) + await hass.async_block_till_done() + + state = hass.states.get(ENERGY_ENTITY_ID) + assert state is not None + assert float(state.state) == 200.0 + + +@pytest.mark.usefixtures("init_integration") +async def test_energy_initial_state( + hass: HomeAssistant, +) -> None: + """Test EnergySensor initial state is unknown.""" + state = hass.states.get(ENERGY_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNKNOWN + + +@pytest.mark.usefixtures("init_integration") +async def test_energy_availability( + hass: HomeAssistant, + mock_light_device: MagicMock, +) -> None: + """Test availability changes are reflected in energy sensor state.""" + trigger_availability_callback(mock_light_device, False) + await hass.async_block_till_done() + + state = hass.states.get(ENERGY_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + trigger_availability_callback(mock_light_device, True) + await hass.async_block_till_done() + + state = hass.states.get(ENERGY_ENTITY_ID) + assert state is not None + assert state.state != STATE_UNAVAILABLE