mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 09:38:58 +01:00
Add scene platform for Sunricher DALI integration (#157808)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from PySrDaliGateway import DaliGateway
|
||||
@@ -23,7 +24,7 @@ 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.LIGHT]
|
||||
_PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.SCENE]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -48,7 +49,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
|
||||
) from exc
|
||||
|
||||
try:
|
||||
devices = await gateway.discover_devices()
|
||||
devices, scenes = await asyncio.gather(
|
||||
gateway.discover_devices(),
|
||||
gateway.discover_scenes(),
|
||||
)
|
||||
except DaliGatewayError as exc:
|
||||
raise ConfigEntryNotReady(
|
||||
"Unable to discover devices from the gateway"
|
||||
@@ -70,6 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
|
||||
entry.runtime_data = DaliCenterData(
|
||||
gateway=gateway,
|
||||
devices=devices,
|
||||
scenes=scenes,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Base entity for Sunricher DALI integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PySrDaliGateway import CallbackEventType, DaliObjectBase, Device
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DaliCenterEntity(Entity):
|
||||
"""Base entity for DALI Center objects (devices, scenes, etc.)."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, dali_object: DaliObjectBase) -> None:
|
||||
"""Initialize base entity."""
|
||||
self._dali_object = dali_object
|
||||
self._attr_unique_id = dali_object.unique_id
|
||||
self._unavailable_logged = False
|
||||
self._attr_available = True
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register availability listener."""
|
||||
self.async_on_remove(
|
||||
self._dali_object.register_listener(
|
||||
CallbackEventType.ONLINE_STATUS,
|
||||
self._handle_availability,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_availability(self, available: bool) -> None:
|
||||
"""Handle availability changes."""
|
||||
if not available and not self._unavailable_logged:
|
||||
_LOGGER.info("Entity %s became unavailable", self.entity_id)
|
||||
self._unavailable_logged = True
|
||||
elif available and self._unavailable_logged:
|
||||
_LOGGER.info("Entity %s is back online", self.entity_id)
|
||||
self._unavailable_logged = False
|
||||
|
||||
self._attr_available = available
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class DaliDeviceEntity(DaliCenterEntity):
|
||||
"""Base entity for DALI Device objects."""
|
||||
|
||||
def __init__(self, device: Device) -> None:
|
||||
"""Initialize device entity."""
|
||||
super().__init__(device)
|
||||
self._attr_available = device.status == "online"
|
||||
@@ -22,6 +22,7 @@ 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__)
|
||||
@@ -45,10 +46,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class DaliCenterLight(LightEntity):
|
||||
class DaliCenterLight(DaliDeviceEntity, LightEntity):
|
||||
"""Representation of a Sunricher DALI Light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_is_on: bool | None = None
|
||||
_attr_brightness: int | None = None
|
||||
@@ -60,11 +60,8 @@ class DaliCenterLight(LightEntity):
|
||||
|
||||
def __init__(self, light: Device) -> None:
|
||||
"""Initialize the light entity."""
|
||||
|
||||
super().__init__(light)
|
||||
self._light = light
|
||||
self._unavailable_logged = False
|
||||
self._attr_unique_id = light.unique_id
|
||||
self._attr_available = light.status == "online"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, light.dev_id)},
|
||||
name=light.name,
|
||||
@@ -111,6 +108,7 @@ class DaliCenterLight(LightEntity):
|
||||
|
||||
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._light.register_listener(
|
||||
@@ -118,27 +116,10 @@ class DaliCenterLight(LightEntity):
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
self._light.register_listener(
|
||||
CallbackEventType.ONLINE_STATUS, self._handle_availability
|
||||
)
|
||||
)
|
||||
|
||||
# read_status() only queues a request on the gateway and relies on the
|
||||
# current event loop via call_later, so it must run in the loop thread.
|
||||
self._light.read_status()
|
||||
|
||||
@callback
|
||||
def _handle_availability(self, available: bool) -> None:
|
||||
self._attr_available = available
|
||||
if not available and not self._unavailable_logged:
|
||||
_LOGGER.info("Light %s became unavailable", self._attr_unique_id)
|
||||
self._unavailable_logged = True
|
||||
elif available and self._unavailable_logged:
|
||||
_LOGGER.info("Light %s is back online", self._attr_unique_id)
|
||||
self._unavailable_logged = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _handle_device_update(self, status: LightStatus) -> None:
|
||||
if status.get("is_on") is not None:
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["PySrDaliGateway==0.16.2"]
|
||||
"requirements": ["PySrDaliGateway==0.18.0"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Support for DALI Center Scene entities."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PySrDaliGateway import Scene
|
||||
|
||||
from homeassistant.components.scene import Scene as SceneEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import DaliCenterEntity
|
||||
from .types import DaliCenterConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: DaliCenterConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up DALI Center scene entities from config entry."""
|
||||
async_add_entities(DaliCenterScene(scene) for scene in entry.runtime_data.scenes)
|
||||
|
||||
|
||||
class DaliCenterScene(DaliCenterEntity, SceneEntity):
|
||||
"""Representation of a DALI Center Scene."""
|
||||
|
||||
def __init__(self, scene: Scene) -> None:
|
||||
"""Initialize the DALI scene."""
|
||||
super().__init__(scene)
|
||||
self._scene = scene
|
||||
self._attr_name = scene.name
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, scene.gw_sn)},
|
||||
)
|
||||
|
||||
async def async_activate(self, **kwargs: Any) -> None:
|
||||
"""Activate the DALI scene."""
|
||||
await self.hass.async_add_executor_job(self._scene.activate)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PySrDaliGateway import DaliGateway, Device
|
||||
from PySrDaliGateway import DaliGateway, Device, Scene
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -13,6 +13,7 @@ class DaliCenterData:
|
||||
|
||||
gateway: DaliGateway
|
||||
devices: list[Device]
|
||||
scenes: list[Scene]
|
||||
|
||||
|
||||
type DaliCenterConfigEntry = ConfigEntry[DaliCenterData]
|
||||
|
||||
Generated
+1
-1
@@ -80,7 +80,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.16.2
|
||||
PySrDaliGateway==0.18.0
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.74.0
|
||||
|
||||
Generated
+1
-1
@@ -80,7 +80,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.16.2
|
||||
PySrDaliGateway==0.18.0
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.74.0
|
||||
|
||||
@@ -16,3 +16,9 @@ def find_device_listener(
|
||||
raise AssertionError(
|
||||
f"Listener for event type {event_type} not found on device {device.dev_id}"
|
||||
)
|
||||
|
||||
|
||||
def trigger_availability_callback(device: MagicMock, available: bool) -> None:
|
||||
"""Trigger availability callbacks registered on the device mock."""
|
||||
callback = find_device_listener(device, CallbackEventType.ONLINE_STATUS)
|
||||
callback(available)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Common fixtures for the Sunricher DALI tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from PySrDaliGateway.helper import gen_device_unique_id, gen_group_unique_id
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sunricher_dali.const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
@@ -12,10 +14,73 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
GATEWAY_SERIAL = "6A242121110E"
|
||||
GATEWAY_HOST = "192.168.1.100"
|
||||
GATEWAY_PORT = 1883
|
||||
|
||||
DEVICE_DATA: list[dict[str, Any]] = [
|
||||
{
|
||||
"dev_id": "01010000026A242121110E",
|
||||
"dev_type": "0101",
|
||||
"name": "Dimmer 0000-02",
|
||||
"model": "DALI DT6 Dimmable Driver",
|
||||
"color_mode": "brightness",
|
||||
"address": 2,
|
||||
"channel": 0,
|
||||
},
|
||||
{
|
||||
"dev_id": "01020000036A242121110E",
|
||||
"dev_type": "0102",
|
||||
"name": "CCT 0000-03",
|
||||
"model": "DALI DT8 Tc Dimmable Driver",
|
||||
"color_mode": "color_temp",
|
||||
"address": 3,
|
||||
"channel": 0,
|
||||
},
|
||||
{
|
||||
"dev_id": "01030000046A242121110E",
|
||||
"dev_type": "0103",
|
||||
"name": "HS Color Light",
|
||||
"model": "DALI HS Color Driver",
|
||||
"color_mode": "hs",
|
||||
"address": 4,
|
||||
"channel": 0,
|
||||
},
|
||||
{
|
||||
"dev_id": "01040000056A242121110E",
|
||||
"dev_type": "0104",
|
||||
"name": "RGBW Light",
|
||||
"model": "DALI RGBW Driver",
|
||||
"color_mode": "rgbw",
|
||||
"address": 5,
|
||||
"channel": 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_gateway: MagicMock,
|
||||
mock_devices: list[MagicMock],
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.sunricher_dali._PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
@@ -23,36 +88,29 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_SERIAL_NUMBER: "6A242121110E",
|
||||
CONF_HOST: "192.168.1.100",
|
||||
CONF_PORT: 1883,
|
||||
CONF_SERIAL_NUMBER: GATEWAY_SERIAL,
|
||||
CONF_HOST: GATEWAY_HOST,
|
||||
CONF_PORT: GATEWAY_PORT,
|
||||
CONF_NAME: "Test Gateway",
|
||||
CONF_USERNAME: "gateway_user",
|
||||
CONF_PASSWORD: "gateway_pass",
|
||||
},
|
||||
unique_id="6A242121110E",
|
||||
unique_id=GATEWAY_SERIAL,
|
||||
title="Test Gateway",
|
||||
)
|
||||
|
||||
|
||||
def _create_mock_device(
|
||||
dev_id: str,
|
||||
dev_type: str,
|
||||
name: str,
|
||||
model: str,
|
||||
color_mode: str,
|
||||
gw_sn: str = "6A242121110E",
|
||||
) -> MagicMock:
|
||||
"""Create a mock device with standard attributes."""
|
||||
def _create_mock_device(device_data: dict[str, Any]) -> MagicMock:
|
||||
"""Create a mock device from device data dict."""
|
||||
device = MagicMock()
|
||||
device.dev_id = dev_id
|
||||
device.unique_id = dev_id
|
||||
device.dev_id = device_data["dev_id"]
|
||||
device.unique_id = device_data["dev_id"]
|
||||
device.status = "online"
|
||||
device.dev_type = dev_type
|
||||
device.name = name
|
||||
device.model = model
|
||||
device.gw_sn = gw_sn
|
||||
device.color_mode = color_mode
|
||||
device.dev_type = device_data["dev_type"]
|
||||
device.name = device_data["name"]
|
||||
device.model = device_data["model"]
|
||||
device.gw_sn = GATEWAY_SERIAL
|
||||
device.color_mode = device_data["color_mode"]
|
||||
device.turn_on = MagicMock()
|
||||
device.turn_off = MagicMock()
|
||||
device.read_status = MagicMock()
|
||||
@@ -63,43 +121,23 @@ def _create_mock_device(
|
||||
@pytest.fixture
|
||||
def mock_devices() -> list[MagicMock]:
|
||||
"""Return mocked Device objects."""
|
||||
return [
|
||||
_create_mock_device(
|
||||
"01010000026A242121110E",
|
||||
"0101",
|
||||
"Dimmer 0000-02",
|
||||
"DALI DT6 Dimmable Driver",
|
||||
"brightness",
|
||||
),
|
||||
_create_mock_device(
|
||||
"01020000036A242121110E",
|
||||
"0102",
|
||||
"CCT 0000-03",
|
||||
"DALI DT8 Tc Dimmable Driver",
|
||||
"color_temp",
|
||||
),
|
||||
_create_mock_device(
|
||||
"01030000046A242121110E",
|
||||
"0103",
|
||||
"HS Color Light",
|
||||
"DALI HS Color Driver",
|
||||
"hs",
|
||||
),
|
||||
_create_mock_device(
|
||||
"01040000056A242121110E",
|
||||
"0104",
|
||||
"RGBW Light",
|
||||
"DALI RGBW Driver",
|
||||
"rgbw",
|
||||
),
|
||||
_create_mock_device(
|
||||
"01010000026A242121110E",
|
||||
"0101",
|
||||
"Duplicate Dimmer",
|
||||
"DALI DT6 Dimmable Driver",
|
||||
"brightness",
|
||||
),
|
||||
]
|
||||
devices = [_create_mock_device(data) for data in DEVICE_DATA]
|
||||
devices.append(_create_mock_device(DEVICE_DATA[0]))
|
||||
return devices
|
||||
|
||||
|
||||
def _create_scene_device_property(
|
||||
dev_type: str, brightness: int = 128, **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Create scene device property dict with defaults."""
|
||||
return {
|
||||
"is_on": True,
|
||||
"brightness": brightness,
|
||||
"color_temp_kelvin": kwargs.get("color_temp_kelvin"),
|
||||
"hs_color": kwargs.get("hs_color"),
|
||||
"rgbw_color": kwargs.get("rgbw_color"),
|
||||
"white_level": kwargs.get("white_level"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -113,8 +151,105 @@ def mock_discovery(mock_gateway: MagicMock) -> Generator[MagicMock]:
|
||||
yield mock_discovery
|
||||
|
||||
|
||||
def _create_mock_scene(
|
||||
scene_id: int,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
channel: int,
|
||||
area_id: str,
|
||||
devices: list[dict[str, Any]],
|
||||
gw_sn: str = GATEWAY_SERIAL,
|
||||
) -> MagicMock:
|
||||
"""Create a mock scene with standard attributes."""
|
||||
devices_with_ids: list[dict[str, Any]] = []
|
||||
for device in devices:
|
||||
device_with_id = dict(device)
|
||||
device_with_id["unique_id"] = (
|
||||
gen_group_unique_id(device["address"], device["channel"], gw_sn)
|
||||
if device["dev_type"] == "0401"
|
||||
else gen_device_unique_id(
|
||||
device["dev_type"],
|
||||
device["channel"],
|
||||
device["address"],
|
||||
gw_sn,
|
||||
)
|
||||
)
|
||||
devices_with_ids.append(device_with_id)
|
||||
|
||||
scene = MagicMock()
|
||||
scene.scene_id = scene_id
|
||||
scene.name = name
|
||||
scene.unique_id = unique_id
|
||||
scene.gw_sn = gw_sn
|
||||
scene.channel = channel
|
||||
scene.activate = MagicMock()
|
||||
scene.devices = devices_with_ids
|
||||
|
||||
scene_details: dict[str, Any] = {
|
||||
"unique_id": unique_id,
|
||||
"id": scene_id,
|
||||
"name": name,
|
||||
"channel": channel,
|
||||
"area_id": area_id,
|
||||
"devices": devices_with_ids,
|
||||
}
|
||||
scene.read_scene = AsyncMock(return_value=scene_details)
|
||||
scene.register_listener = MagicMock(return_value=lambda: None)
|
||||
return scene
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gateway(mock_devices: list[MagicMock]) -> Generator[MagicMock]:
|
||||
def mock_scenes() -> list[MagicMock]:
|
||||
"""Return mocked Scene objects."""
|
||||
return [
|
||||
_create_mock_scene(
|
||||
scene_id=1,
|
||||
name="Living Room Evening",
|
||||
unique_id=f"scene_0001_0000_{GATEWAY_SERIAL}",
|
||||
channel=0,
|
||||
area_id="1",
|
||||
devices=[
|
||||
{
|
||||
"dev_type": DEVICE_DATA[0]["dev_type"],
|
||||
"channel": DEVICE_DATA[0]["channel"],
|
||||
"address": DEVICE_DATA[0]["address"],
|
||||
"gw_sn_obj": "",
|
||||
"property": _create_scene_device_property("0101", brightness=128),
|
||||
},
|
||||
{
|
||||
"dev_type": DEVICE_DATA[1]["dev_type"],
|
||||
"channel": DEVICE_DATA[1]["channel"],
|
||||
"address": DEVICE_DATA[1]["address"],
|
||||
"gw_sn_obj": "",
|
||||
"property": _create_scene_device_property(
|
||||
"0102", brightness=200, color_temp_kelvin=3000
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
_create_mock_scene(
|
||||
scene_id=2,
|
||||
name="Kitchen Bright",
|
||||
unique_id=f"scene_0002_0000_{GATEWAY_SERIAL}",
|
||||
channel=0,
|
||||
area_id="2",
|
||||
devices=[
|
||||
{
|
||||
"dev_type": "0401",
|
||||
"channel": 0,
|
||||
"address": 1,
|
||||
"gw_sn_obj": "",
|
||||
"property": _create_scene_device_property("0401", brightness=255),
|
||||
},
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gateway(
|
||||
mock_devices: list[MagicMock], mock_scenes: list[MagicMock]
|
||||
) -> Generator[MagicMock]:
|
||||
"""Return a mocked DaliGateway."""
|
||||
with (
|
||||
patch(
|
||||
@@ -126,15 +261,16 @@ def mock_gateway(mock_devices: list[MagicMock]) -> Generator[MagicMock]:
|
||||
),
|
||||
):
|
||||
mock_gateway = mock_gateway_class.return_value
|
||||
mock_gateway.gw_sn = "6A242121110E"
|
||||
mock_gateway.gw_ip = "192.168.1.100"
|
||||
mock_gateway.port = 1883
|
||||
mock_gateway.gw_sn = GATEWAY_SERIAL
|
||||
mock_gateway.gw_ip = GATEWAY_HOST
|
||||
mock_gateway.port = GATEWAY_PORT
|
||||
mock_gateway.name = "Test Gateway"
|
||||
mock_gateway.username = "gateway_user"
|
||||
mock_gateway.passwd = "gateway_pass"
|
||||
mock_gateway.connect = AsyncMock()
|
||||
mock_gateway.disconnect = AsyncMock()
|
||||
mock_gateway.discover_devices = AsyncMock(return_value=mock_devices)
|
||||
mock_gateway.discover_scenes = AsyncMock(return_value=mock_scenes)
|
||||
yield mock_gateway
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[scene.test_gateway_kitchen_bright-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'scene',
|
||||
'entity_category': None,
|
||||
'entity_id': 'scene.test_gateway_kitchen_bright',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Kitchen Bright',
|
||||
'platform': 'sunricher_dali',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'scene_0002_0000_6A242121110E',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[scene.test_gateway_kitchen_bright-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Gateway Kitchen Bright',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'scene.test_gateway_kitchen_bright',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[scene.test_gateway_living_room_evening-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'scene',
|
||||
'entity_category': None,
|
||||
'entity_id': 'scene.test_gateway_living_room_evening',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Living Room Evening',
|
||||
'platform': 'sunricher_dali',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'scene_0001_0000_6A242121110E',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[scene.test_gateway_living_room_evening-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Gateway Living Room Evening',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'scene.test_gateway_living_room_evening',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Test the Sunricher DALI light platform."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PySrDaliGateway import CallbackEventType
|
||||
import pytest
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import find_device_listener
|
||||
from . import find_device_listener, trigger_availability_callback
|
||||
|
||||
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
||||
|
||||
@@ -35,38 +35,12 @@ def _trigger_light_status_callback(
|
||||
callback(status)
|
||||
|
||||
|
||||
def _trigger_availability_callback(
|
||||
device: MagicMock, device_id: str, available: bool
|
||||
) -> None:
|
||||
"""Trigger the availability callbacks registered on the device mock."""
|
||||
callback = find_device_listener(device, CallbackEventType.ONLINE_STATUS)
|
||||
callback(available)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify which platforms to test."""
|
||||
return [Platform.LIGHT]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_gateway: MagicMock,
|
||||
mock_devices: list[MagicMock],
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.sunricher_dali._PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
@@ -192,13 +166,13 @@ async def test_device_availability(
|
||||
init_integration: MockConfigEntry,
|
||||
mock_devices: list[MagicMock],
|
||||
) -> None:
|
||||
"""Test device availability changes."""
|
||||
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, False)
|
||||
"""Test availability changes are reflected in entity state."""
|
||||
trigger_availability_callback(mock_devices[0], False)
|
||||
await hass.async_block_till_done()
|
||||
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
|
||||
assert state.state == "unavailable"
|
||||
|
||||
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, True)
|
||||
trigger_availability_callback(mock_devices[0], True)
|
||||
await hass.async_block_till_done()
|
||||
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
|
||||
assert state.state != "unavailable"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Test the Sunricher DALI scene platform."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import trigger_availability_callback
|
||||
|
||||
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
||||
|
||||
TEST_SCENE_1_ENTITY_ID = "scene.test_gateway_living_room_evening"
|
||||
TEST_SCENE_2_ENTITY_ID = "scene.test_gateway_kitchen_bright"
|
||||
TEST_DIMMER_ENTITY_ID = "light.dimmer_0000_02"
|
||||
TEST_CCT_ENTITY_ID = "light.cct_0000_03"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify which platforms to test."""
|
||||
return [Platform.SCENE]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_scenes: list[MagicMock],
|
||||
) -> None:
|
||||
"""Test the scene entities and their attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={("sunricher_dali", "6A242121110E")}
|
||||
)
|
||||
assert device_entry
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry.device_id == device_entry.id
|
||||
|
||||
state = hass.states.get(TEST_SCENE_1_ENTITY_ID)
|
||||
assert state is not None
|
||||
|
||||
|
||||
async def test_activate_scenes(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_scenes: list[MagicMock],
|
||||
) -> None:
|
||||
"""Test activating single and multiple scenes."""
|
||||
await hass.services.async_call(
|
||||
SCENE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_SCENE_1_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_scenes[0].activate.assert_called_once()
|
||||
|
||||
await hass.services.async_call(
|
||||
SCENE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: [TEST_SCENE_1_ENTITY_ID, TEST_SCENE_2_ENTITY_ID]},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_scenes[0].activate.call_count == 2
|
||||
mock_scenes[1].activate.assert_called_once()
|
||||
|
||||
|
||||
async def test_scene_availability(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_scenes: list[MagicMock],
|
||||
) -> None:
|
||||
"""Test scene availability changes when gateway goes offline."""
|
||||
state = hass.states.get(TEST_SCENE_1_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != "unavailable"
|
||||
|
||||
# Simulate gateway going offline
|
||||
trigger_availability_callback(mock_scenes[0], False)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_SCENE_1_ENTITY_ID)
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Simulate gateway coming back online
|
||||
trigger_availability_callback(mock_scenes[0], True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_SCENE_1_ENTITY_ID)
|
||||
assert state.state != "unavailable"
|
||||
Reference in New Issue
Block a user