1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add sensor platform support to sunricher_dali integration (#159579)

This commit is contained in:
Niracler
2026-01-17 20:16:43 +08:00
committed by GitHub
parent 8555bc9da0
commit 48d1bd13fa
5 changed files with 329 additions and 1 deletions

View File

@@ -25,7 +25,12 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
from .types import DaliCenterConfigEntry, DaliCenterData
_PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.LIGHT, Platform.SCENE]
_PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.LIGHT,
Platform.SCENE,
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,121 @@
"""Platform for Sunricher DALI sensor entities."""
from __future__ import annotations
import logging
from PySrDaliGateway import CallbackEventType, Device
from PySrDaliGateway.helper import is_illuminance_sensor
from PySrDaliGateway.types import IlluminanceStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import LIGHT_LUX
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
from .entity import DaliDeviceEntity
from .types import DaliCenterConfigEntry
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: DaliCenterConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""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)
]
if entities:
async_add_entities(entities)
class SunricherDaliIlluminanceSensor(DaliDeviceEntity, SensorEntity):
"""Representation of a Sunricher DALI Illuminance Sensor."""
_attr_device_class = SensorDeviceClass.ILLUMINANCE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = LIGHT_LUX
_attr_name = None
def __init__(self, device: Device) -> None:
"""Initialize the illuminance sensor."""
super().__init__(device)
self._device = device
self._illuminance_value: float | None = None
self._sensor_enabled: bool = True
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.dev_id)},
name=device.name,
manufacturer=MANUFACTURER,
model=device.model,
via_device=(DOMAIN, device.gw_sn),
)
@property
def native_value(self) -> float | None:
"""Return the native value, or None if sensor is disabled."""
if not self._sensor_enabled:
return None
return self._illuminance_value
async def async_added_to_hass(self) -> None:
"""Handle entity addition to Home Assistant."""
await super().async_added_to_hass()
self.async_on_remove(
self._device.register_listener(
CallbackEventType.ILLUMINANCE_STATUS, self._handle_illuminance_status
)
)
self.async_on_remove(
self._device.register_listener(
CallbackEventType.SENSOR_ON_OFF, self._handle_sensor_on_off
)
)
self._device.read_status()
@callback
def _handle_illuminance_status(self, status: IlluminanceStatus) -> None:
"""Handle illuminance status updates."""
illuminance_value = status["illuminance_value"]
is_valid = status["is_valid"]
if not is_valid:
_LOGGER.debug(
"Illuminance value is not valid for device %s: %s lux",
self._device.dev_id,
illuminance_value,
)
return
self._illuminance_value = illuminance_value
self.schedule_update_ha_state()
@callback
def _handle_sensor_on_off(self, on_off: bool) -> None:
"""Handle sensor on/off updates."""
self._sensor_enabled = on_off
_LOGGER.debug(
"Illuminance sensor enable state for device %s updated to: %s",
self._device.dev_id,
on_off,
)
self.schedule_update_ha_state()

View File

@@ -63,6 +63,16 @@ DEVICE_DATA: list[dict[str, Any]] = [
},
]
ILLUMINANCE_SENSOR_DATA: dict[str, Any] = {
"dev_id": "02020000206A242121110E",
"dev_type": "0202",
"name": "Illuminance Sensor 0000-20",
"model": "DALI Illuminance Sensor",
"color_mode": None,
"address": 20,
"channel": 0,
}
@pytest.fixture
async def init_integration(
@@ -126,6 +136,12 @@ def mock_devices() -> list[MagicMock]:
return devices
@pytest.fixture
def mock_illuminance_device() -> MagicMock:
"""Return a mocked illuminance sensor device."""
return _create_mock_device(ILLUMINANCE_SENSOR_DATA)
def _create_scene_device_property(
dev_type: str, brightness: int = 128, **kwargs: Any
) -> dict[str, Any]:

View File

@@ -0,0 +1,55 @@
# serializer version: 1
# name: test_setup_entry[sensor.illuminance_sensor_0000_20-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'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.illuminance_sensor_0000_20',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ILLUMINANCE: 'illuminance'>,
'original_icon': None,
'original_name': None,
'platform': 'sunricher_dali',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '02020000206A242121110E',
'unit_of_measurement': 'lx',
})
# ---
# name: test_setup_entry[sensor.illuminance_sensor_0000_20-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'illuminance',
'friendly_name': 'Illuminance Sensor 0000-20',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'lx',
}),
'context': <ANY>,
'entity_id': 'sensor.illuminance_sensor_0000_20',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -0,0 +1,131 @@
"""Test the Sunricher DALI sensor platform."""
from unittest.mock import MagicMock
from PySrDaliGateway import CallbackEventType
import pytest
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import find_device_listener, trigger_availability_callback
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
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]
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify which platforms to test."""
return [Platform.SENSOR]
@pytest.mark.usefixtures("init_integration")
async def test_setup_entry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test that async_setup_entry correctly creates sensor entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entity_entries) == 1
assert entity_entries[0].entity_id == ENTITY_ID
@pytest.mark.usefixtures("init_integration")
async def test_illuminance_callback(
hass: HomeAssistant,
mock_illuminance_device: MagicMock,
) -> None:
"""Test IlluminanceSensor handles valid and invalid values correctly."""
callback = find_device_listener(
mock_illuminance_device, CallbackEventType.ILLUMINANCE_STATUS
)
# Valid value should update state
callback({"illuminance_value": 500.0, "is_valid": True})
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert float(state.state) == 500.0
# Invalid value should be ignored
callback({"illuminance_value": 9999.0, "is_valid": False})
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert float(state.state) == 500.0
@pytest.mark.usefixtures("init_integration")
async def test_sensor_on_off(
hass: HomeAssistant,
mock_illuminance_device: MagicMock,
) -> None:
"""Test IlluminanceSensor handles sensor on/off callback correctly."""
illuminance_callback = find_device_listener(
mock_illuminance_device, CallbackEventType.ILLUMINANCE_STATUS
)
illuminance_callback({"illuminance_value": 250.0, "is_valid": True})
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert float(state.state) == 250.0
on_off_callback = find_device_listener(
mock_illuminance_device, CallbackEventType.SENSOR_ON_OFF
)
# Turn off sensor -> state becomes unknown
on_off_callback(False)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNKNOWN
# Turn on sensor -> restore previous value
on_off_callback(True)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert float(state.state) == 250.0
@pytest.mark.usefixtures("init_integration")
async def test_availability(
hass: HomeAssistant,
mock_illuminance_device: MagicMock,
) -> None:
"""Test availability changes are reflected in sensor entity state."""
trigger_availability_callback(mock_illuminance_device, False)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
trigger_availability_callback(mock_illuminance_device, True)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state != STATE_UNAVAILABLE