mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 10:59:24 +00:00
Add DALI Center integration (#151479)
Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1543,6 +1543,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/suez_water/ @ooii @jb101010-2
|
/tests/components/suez_water/ @ooii @jb101010-2
|
||||||
/homeassistant/components/sun/ @home-assistant/core
|
/homeassistant/components/sun/ @home-assistant/core
|
||||||
/tests/components/sun/ @home-assistant/core
|
/tests/components/sun/ @home-assistant/core
|
||||||
|
/homeassistant/components/sunricher_dali_center/ @niracler
|
||||||
|
/tests/components/sunricher_dali_center/ @niracler
|
||||||
/homeassistant/components/supla/ @mwegrzynek
|
/homeassistant/components/supla/ @mwegrzynek
|
||||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||||
|
|||||||
88
homeassistant/components/sunricher_dali_center/__init__.py
Normal file
88
homeassistant/components/sunricher_dali_center/__init__.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""The DALI Center integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PySrDaliGateway import DaliGateway
|
||||||
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
|
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
|
||||||
|
from .types import DaliCenterConfigEntry, DaliCenterData
|
||||||
|
|
||||||
|
_PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -> bool:
|
||||||
|
"""Set up DALI Center from a config entry."""
|
||||||
|
|
||||||
|
gateway = DaliGateway(
|
||||||
|
entry.data[CONF_SERIAL_NUMBER],
|
||||||
|
entry.data[CONF_HOST],
|
||||||
|
entry.data[CONF_PORT],
|
||||||
|
entry.data[CONF_USERNAME],
|
||||||
|
entry.data[CONF_PASSWORD],
|
||||||
|
name=entry.data[CONF_NAME],
|
||||||
|
)
|
||||||
|
gw_sn = gateway.gw_sn
|
||||||
|
|
||||||
|
try:
|
||||||
|
await gateway.connect()
|
||||||
|
except DaliGatewayError as exc:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
"You can try to delete the gateway and add it again"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def on_online_status(dev_id: str, available: bool) -> None:
|
||||||
|
signal = f"{DOMAIN}_update_available_{dev_id}"
|
||||||
|
hass.add_job(async_dispatcher_send, hass, signal, available)
|
||||||
|
|
||||||
|
gateway.on_online_status = on_online_status
|
||||||
|
|
||||||
|
try:
|
||||||
|
devices = await gateway.discover_devices()
|
||||||
|
except DaliGatewayError as exc:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
"Unable to discover devices from the gateway"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
_LOGGER.debug("Discovered %d devices on gateway %s", len(devices), gw_sn)
|
||||||
|
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, gw_sn)},
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
name=gateway.name,
|
||||||
|
model="SR-GW-EDA",
|
||||||
|
serial_number=gw_sn,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.runtime_data = DaliCenterData(
|
||||||
|
gateway=gateway,
|
||||||
|
devices=devices,
|
||||||
|
)
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, _PLATFORMS):
|
||||||
|
await entry.runtime_data.gateway.disconnect()
|
||||||
|
return unload_ok
|
||||||
134
homeassistant/components/sunricher_dali_center/config_flow.py
Normal file
134
homeassistant/components/sunricher_dali_center/config_flow.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""Config flow for the DALI Center integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from PySrDaliGateway import DaliGateway
|
||||||
|
from PySrDaliGateway.discovery import DaliGatewayDiscovery
|
||||||
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DaliCenterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for DALI Center."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self._discovered_gateways: dict[str, DaliGateway] = {}
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
if user_input is not None:
|
||||||
|
return await self.async_step_select_gateway()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_select_gateway(
|
||||||
|
self, discovery_info: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle gateway discovery."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if discovery_info and "selected_gateway" in discovery_info:
|
||||||
|
selected_sn = discovery_info["selected_gateway"]
|
||||||
|
selected_gateway = self._discovered_gateways[selected_sn]
|
||||||
|
|
||||||
|
await self.async_set_unique_id(selected_gateway.gw_sn)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await selected_gateway.connect()
|
||||||
|
except DaliGatewayError as err:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Failed to connect to gateway %s during config flow",
|
||||||
|
selected_gateway.gw_sn,
|
||||||
|
exc_info=err,
|
||||||
|
)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await selected_gateway.disconnect()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=selected_gateway.name,
|
||||||
|
data={
|
||||||
|
CONF_SERIAL_NUMBER: selected_gateway.gw_sn,
|
||||||
|
CONF_HOST: selected_gateway.gw_ip,
|
||||||
|
CONF_PORT: selected_gateway.port,
|
||||||
|
CONF_NAME: selected_gateway.name,
|
||||||
|
CONF_USERNAME: selected_gateway.username,
|
||||||
|
CONF_PASSWORD: selected_gateway.passwd,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._discovered_gateways:
|
||||||
|
_LOGGER.debug("Starting gateway discovery")
|
||||||
|
discovery = DaliGatewayDiscovery()
|
||||||
|
try:
|
||||||
|
discovered = await discovery.discover_gateways()
|
||||||
|
except DaliGatewayError as err:
|
||||||
|
_LOGGER.debug("Gateway discovery failed", exc_info=err)
|
||||||
|
errors["base"] = "discovery_failed"
|
||||||
|
else:
|
||||||
|
configured_gateways = {
|
||||||
|
entry.data[CONF_SERIAL_NUMBER]
|
||||||
|
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
self._discovered_gateways = {
|
||||||
|
gw.gw_sn: gw
|
||||||
|
for gw in discovered
|
||||||
|
if gw.gw_sn not in configured_gateways
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self._discovered_gateways:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="select_gateway",
|
||||||
|
errors=errors if errors else {"base": "no_devices_found"},
|
||||||
|
data_schema=vol.Schema({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
gateway_options = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=sn,
|
||||||
|
label=f"{gateway.name} [SN {sn}, IP {gateway.gw_ip}]",
|
||||||
|
)
|
||||||
|
for sn, gateway in self._discovered_gateways.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="select_gateway",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("selected_gateway"): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=gateway_options, sort=True)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
5
homeassistant/components/sunricher_dali_center/const.py
Normal file
5
homeassistant/components/sunricher_dali_center/const.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the DALI Center integration."""
|
||||||
|
|
||||||
|
DOMAIN = "sunricher_dali_center"
|
||||||
|
MANUFACTURER = "Sunricher"
|
||||||
|
CONF_SERIAL_NUMBER = "serial_number"
|
||||||
190
homeassistant/components/sunricher_dali_center/light.py
Normal file
190
homeassistant/components/sunricher_dali_center/light.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""Platform for light integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from PySrDaliGateway import Device
|
||||||
|
from PySrDaliGateway.helper import is_light_device
|
||||||
|
from PySrDaliGateway.types import LightStatus
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
|
ATTR_HS_COLOR,
|
||||||
|
ATTR_RGBW_COLOR,
|
||||||
|
ColorMode,
|
||||||
|
LightEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN, MANUFACTURER
|
||||||
|
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 DALI Center light entities from config entry."""
|
||||||
|
runtime_data = entry.runtime_data
|
||||||
|
gateway = runtime_data.gateway
|
||||||
|
devices = runtime_data.devices
|
||||||
|
|
||||||
|
def _on_light_status(dev_id: str, status: LightStatus) -> None:
|
||||||
|
signal = f"{DOMAIN}_update_{dev_id}"
|
||||||
|
hass.add_job(async_dispatcher_send, hass, signal, status)
|
||||||
|
|
||||||
|
gateway.on_light_status = _on_light_status
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
DaliCenterLight(device)
|
||||||
|
for device in devices
|
||||||
|
if is_light_device(device.dev_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DaliCenterLight(LightEntity):
|
||||||
|
"""Representation of a DALI Center Light."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_name = None
|
||||||
|
_attr_is_on: bool | None = None
|
||||||
|
_attr_brightness: int | None = None
|
||||||
|
_white_level: int | None = None
|
||||||
|
_attr_color_mode: ColorMode | str | None = None
|
||||||
|
_attr_color_temp_kelvin: int | None = None
|
||||||
|
_attr_hs_color: tuple[float, float] | None = None
|
||||||
|
_attr_rgbw_color: tuple[int, int, int, int] | None = None
|
||||||
|
|
||||||
|
def __init__(self, light: Device) -> None:
|
||||||
|
"""Initialize the light entity."""
|
||||||
|
|
||||||
|
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,
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=light.model,
|
||||||
|
via_device=(DOMAIN, light.gw_sn),
|
||||||
|
)
|
||||||
|
self._attr_min_color_temp_kelvin = 1000
|
||||||
|
self._attr_max_color_temp_kelvin = 8000
|
||||||
|
|
||||||
|
self._determine_features()
|
||||||
|
|
||||||
|
def _determine_features(self) -> None:
|
||||||
|
supported_modes: set[ColorMode] = set()
|
||||||
|
color_mode = self._light.color_mode
|
||||||
|
color_mode_map: dict[str, ColorMode] = {
|
||||||
|
"color_temp": ColorMode.COLOR_TEMP,
|
||||||
|
"hs": ColorMode.HS,
|
||||||
|
"rgbw": ColorMode.RGBW,
|
||||||
|
}
|
||||||
|
self._attr_color_mode = color_mode_map.get(color_mode, ColorMode.BRIGHTNESS)
|
||||||
|
supported_modes.add(self._attr_color_mode)
|
||||||
|
self._attr_supported_color_modes = supported_modes
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Turning on light %s with kwargs: %s", self._attr_unique_id, kwargs
|
||||||
|
)
|
||||||
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
|
||||||
|
hs_color = kwargs.get(ATTR_HS_COLOR)
|
||||||
|
rgbw_color = kwargs.get(ATTR_RGBW_COLOR)
|
||||||
|
self._light.turn_on(
|
||||||
|
brightness=brightness,
|
||||||
|
color_temp_kelvin=color_temp_kelvin,
|
||||||
|
hs_color=hs_color,
|
||||||
|
rgbw_color=rgbw_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
self._light.turn_off()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity addition to Home Assistant."""
|
||||||
|
|
||||||
|
signal = f"{DOMAIN}_update_{self._attr_unique_id}"
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, self._handle_device_update)
|
||||||
|
)
|
||||||
|
|
||||||
|
signal = f"{DOMAIN}_update_available_{self._attr_unique_id}"
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, 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:
|
||||||
|
self._attr_is_on = status["is_on"]
|
||||||
|
|
||||||
|
if status.get("brightness") is not None:
|
||||||
|
self._attr_brightness = status["brightness"]
|
||||||
|
|
||||||
|
if status.get("white_level") is not None:
|
||||||
|
self._white_level = status["white_level"]
|
||||||
|
if self._attr_rgbw_color is not None and self._white_level is not None:
|
||||||
|
self._attr_rgbw_color = (
|
||||||
|
self._attr_rgbw_color[0],
|
||||||
|
self._attr_rgbw_color[1],
|
||||||
|
self._attr_rgbw_color[2],
|
||||||
|
self._white_level,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get("color_temp_kelvin") is not None
|
||||||
|
and self._attr_supported_color_modes
|
||||||
|
and ColorMode.COLOR_TEMP in self._attr_supported_color_modes
|
||||||
|
):
|
||||||
|
self._attr_color_temp_kelvin = status["color_temp_kelvin"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get("hs_color") is not None
|
||||||
|
and self._attr_supported_color_modes
|
||||||
|
and ColorMode.HS in self._attr_supported_color_modes
|
||||||
|
):
|
||||||
|
self._attr_hs_color = status["hs_color"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get("rgbw_color") is not None
|
||||||
|
and self._attr_supported_color_modes
|
||||||
|
and ColorMode.RGBW in self._attr_supported_color_modes
|
||||||
|
):
|
||||||
|
self._attr_rgbw_color = status["rgbw_color"]
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
10
homeassistant/components/sunricher_dali_center/manifest.json
Normal file
10
homeassistant/components/sunricher_dali_center/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "sunricher_dali_center",
|
||||||
|
"name": "DALI Center",
|
||||||
|
"codeowners": ["@niracler"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali_center",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["PySrDaliGateway==0.13.1"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom actions.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: todo
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: todo
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: todo
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
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-disabled-by-default: todo
|
||||||
|
entity-translations: todo
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: todo
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
||||||
29
homeassistant/components/sunricher_dali_center/strings.json
Normal file
29
homeassistant/components/sunricher_dali_center/strings.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Set up DALI Center gateway",
|
||||||
|
"description": "**Three-step process:**\n\n1. Ensure the gateway is powered and on the same network.\n2. Select **Submit** to start discovery (searches for up to 3 minutes)\n3. While discovery is in progress, press the **Reset** button on your DALI gateway device **once**.\n\nThe gateway will respond immediately after the button press."
|
||||||
|
},
|
||||||
|
"select_gateway": {
|
||||||
|
"title": "Select DALI gateway",
|
||||||
|
"description": "Select the gateway to configure.",
|
||||||
|
"data": {
|
||||||
|
"selected_gateway": "Gateway"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"selected_gateway": "Each option shows the gateway name, serial number, and IP address."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"discovery_failed": "Failed to discover DALI gateways on the network",
|
||||||
|
"no_devices_found": "No DALI gateways found on the network",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
homeassistant/components/sunricher_dali_center/types.py
Normal file
18
homeassistant/components/sunricher_dali_center/types.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Type definitions for the DALI Center integration."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from PySrDaliGateway import DaliGateway, Device
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DaliCenterData:
|
||||||
|
"""Runtime data for the DALI Center integration."""
|
||||||
|
|
||||||
|
gateway: DaliGateway
|
||||||
|
devices: list[Device]
|
||||||
|
|
||||||
|
|
||||||
|
type DaliCenterConfigEntry = ConfigEntry[DaliCenterData]
|
||||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -641,6 +641,7 @@ FLOWS = {
|
|||||||
"subaru",
|
"subaru",
|
||||||
"suez_water",
|
"suez_water",
|
||||||
"sun",
|
"sun",
|
||||||
|
"sunricher_dali_center",
|
||||||
"sunweg",
|
"sunweg",
|
||||||
"surepetcare",
|
"surepetcare",
|
||||||
"swiss_public_transport",
|
"swiss_public_transport",
|
||||||
|
|||||||
@@ -6489,6 +6489,12 @@
|
|||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
},
|
},
|
||||||
|
"sunricher_dali_center": {
|
||||||
|
"name": "DALI Center",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_push"
|
||||||
|
},
|
||||||
"sunweg": {
|
"sunweg": {
|
||||||
"name": "Sun WEG",
|
"name": "Sun WEG",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
|||||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -83,6 +83,9 @@ PyQRCode==1.2.1
|
|||||||
# homeassistant.components.rmvtransport
|
# homeassistant.components.rmvtransport
|
||||||
PyRMVtransport==0.3.3
|
PyRMVtransport==0.3.3
|
||||||
|
|
||||||
|
# homeassistant.components.sunricher_dali_center
|
||||||
|
PySrDaliGateway==0.13.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.72.0
|
PySwitchbot==0.72.0
|
||||||
|
|
||||||
|
|||||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -80,6 +80,9 @@ PyQRCode==1.2.1
|
|||||||
# homeassistant.components.rmvtransport
|
# homeassistant.components.rmvtransport
|
||||||
PyRMVtransport==0.3.3
|
PyRMVtransport==0.3.3
|
||||||
|
|
||||||
|
# homeassistant.components.sunricher_dali_center
|
||||||
|
PySrDaliGateway==0.13.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.72.0
|
PySwitchbot==0.72.0
|
||||||
|
|
||||||
|
|||||||
1
tests/components/sunricher_dali_center/__init__.py
Normal file
1
tests/components/sunricher_dali_center/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Sunricher DALI Center integration."""
|
||||||
150
tests/components/sunricher_dali_center/conftest.py
Normal file
150
tests/components/sunricher_dali_center/conftest.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Common fixtures for the Dali Center tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.sunricher_dali_center.const import (
|
||||||
|
CONF_SERIAL_NUMBER,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_SERIAL_NUMBER: "6A242121110E",
|
||||||
|
CONF_HOST: "192.168.1.100",
|
||||||
|
CONF_PORT: 1883,
|
||||||
|
CONF_NAME: "Test Gateway",
|
||||||
|
CONF_USERNAME: "gateway_user",
|
||||||
|
CONF_PASSWORD: "gateway_pass",
|
||||||
|
},
|
||||||
|
unique_id="6A242121110E",
|
||||||
|
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."""
|
||||||
|
device = MagicMock()
|
||||||
|
device.dev_id = dev_id
|
||||||
|
device.unique_id = 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.turn_on = MagicMock()
|
||||||
|
device.turn_off = MagicMock()
|
||||||
|
device.read_status = MagicMock()
|
||||||
|
return 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",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_discovery(mock_gateway: MagicMock) -> Generator[MagicMock]:
|
||||||
|
"""Mock DaliGatewayDiscovery."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.sunricher_dali_center.config_flow.DaliGatewayDiscovery"
|
||||||
|
) as mock_discovery_class:
|
||||||
|
mock_discovery = mock_discovery_class.return_value
|
||||||
|
mock_discovery.discover_gateways = AsyncMock(return_value=[mock_gateway])
|
||||||
|
yield mock_discovery
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_gateway(mock_devices: list[MagicMock]) -> Generator[MagicMock]:
|
||||||
|
"""Return a mocked DaliGateway."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.sunricher_dali_center.DaliGateway", autospec=True
|
||||||
|
) as mock_gateway_class,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.sunricher_dali_center.config_flow.DaliGateway",
|
||||||
|
new=mock_gateway_class,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
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.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)
|
||||||
|
yield mock_gateway
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.sunricher_dali_center.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
253
tests/components/sunricher_dali_center/snapshots/test_light.ambr
Normal file
253
tests/components/sunricher_dali_center/snapshots/test_light.ambr
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_entities[light.cct_0000_03-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'max_color_temp_kelvin': 8000,
|
||||||
|
'max_mireds': 1000,
|
||||||
|
'min_color_temp_kelvin': 1000,
|
||||||
|
'min_mireds': 125,
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'light',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'light.cct_0000_03',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'sunricher_dali_center',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '01020000036A242121110E',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.cct_0000_03-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'brightness': None,
|
||||||
|
'color_mode': None,
|
||||||
|
'color_temp': None,
|
||||||
|
'color_temp_kelvin': None,
|
||||||
|
'friendly_name': 'CCT 0000-03',
|
||||||
|
'hs_color': None,
|
||||||
|
'max_color_temp_kelvin': 8000,
|
||||||
|
'max_mireds': 1000,
|
||||||
|
'min_color_temp_kelvin': 1000,
|
||||||
|
'min_mireds': 125,
|
||||||
|
'rgb_color': None,
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||||
|
]),
|
||||||
|
'supported_features': <LightEntityFeature: 0>,
|
||||||
|
'xy_color': None,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'light.cct_0000_03',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.dimmer_0000_02-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'light',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'light.dimmer_0000_02',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'sunricher_dali_center',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '01010000026A242121110E',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.dimmer_0000_02-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'brightness': None,
|
||||||
|
'color_mode': None,
|
||||||
|
'friendly_name': 'Dimmer 0000-02',
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||||
|
]),
|
||||||
|
'supported_features': <LightEntityFeature: 0>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'light.dimmer_0000_02',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.hs_color_light-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.HS: 'hs'>,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'light',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'light.hs_color_light',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'sunricher_dali_center',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '01030000046A242121110E',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.hs_color_light-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'brightness': None,
|
||||||
|
'color_mode': None,
|
||||||
|
'friendly_name': 'HS Color Light',
|
||||||
|
'hs_color': None,
|
||||||
|
'rgb_color': None,
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.HS: 'hs'>,
|
||||||
|
]),
|
||||||
|
'supported_features': <LightEntityFeature: 0>,
|
||||||
|
'xy_color': None,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'light.hs_color_light',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.rgbw_light-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.RGBW: 'rgbw'>,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'light',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'light.rgbw_light',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'sunricher_dali_center',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '01040000056A242121110E',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[light.rgbw_light-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'brightness': None,
|
||||||
|
'color_mode': None,
|
||||||
|
'friendly_name': 'RGBW Light',
|
||||||
|
'hs_color': None,
|
||||||
|
'rgb_color': None,
|
||||||
|
'rgbw_color': None,
|
||||||
|
'supported_color_modes': list([
|
||||||
|
<ColorMode.RGBW: 'rgbw'>,
|
||||||
|
]),
|
||||||
|
'supported_features': <LightEntityFeature: 0>,
|
||||||
|
'xy_color': None,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'light.rgbw_light',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
224
tests/components/sunricher_dali_center/test_config_flow.py
Normal file
224
tests/components/sunricher_dali_center/test_config_flow.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
"""Test the DALI Center config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
|
|
||||||
|
from homeassistant.components.sunricher_dali_center.const import (
|
||||||
|
CONF_SERIAL_NUMBER,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_flow_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test a successful discovery flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result.get("title") == mock_gateway.name
|
||||||
|
assert result.get("data") == {
|
||||||
|
CONF_SERIAL_NUMBER: mock_gateway.gw_sn,
|
||||||
|
CONF_HOST: mock_gateway.gw_ip,
|
||||||
|
CONF_PORT: mock_gateway.port,
|
||||||
|
CONF_NAME: mock_gateway.name,
|
||||||
|
CONF_USERNAME: mock_gateway.username,
|
||||||
|
CONF_PASSWORD: mock_gateway.passwd,
|
||||||
|
}
|
||||||
|
result_entry = result.get("result")
|
||||||
|
assert result_entry is not None
|
||||||
|
assert result_entry.unique_id == mock_gateway.gw_sn
|
||||||
|
mock_setup_entry.assert_called_once()
|
||||||
|
mock_gateway.connect.assert_awaited_once()
|
||||||
|
mock_gateway.disconnect.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_no_gateways_found(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test discovery step when no gateways are found."""
|
||||||
|
mock_discovery.discover_gateways.return_value = []
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
errors = result.get("errors")
|
||||||
|
assert errors is not None
|
||||||
|
assert errors["base"] == "no_devices_found"
|
||||||
|
|
||||||
|
mock_discovery.discover_gateways.return_value = [mock_gateway]
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_gateway_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test discovery error handling when gateway search fails."""
|
||||||
|
mock_discovery.discover_gateways.side_effect = DaliGatewayError("failure")
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
errors = result.get("errors")
|
||||||
|
assert errors is not None
|
||||||
|
assert errors["base"] == "discovery_failed"
|
||||||
|
|
||||||
|
mock_discovery.discover_gateways.side_effect = None
|
||||||
|
mock_discovery.discover_gateways.return_value = [mock_gateway]
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_connection_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test connection failure when validating the selected gateway."""
|
||||||
|
mock_gateway.connect.side_effect = DaliGatewayError("failure")
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
errors = result.get("errors")
|
||||||
|
assert errors is not None
|
||||||
|
assert errors["base"] == "cannot_connect"
|
||||||
|
mock_gateway.connect.assert_awaited_once()
|
||||||
|
mock_gateway.disconnect.assert_not_awaited()
|
||||||
|
|
||||||
|
mock_gateway.connect.side_effect = None
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_duplicate_filtered(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that already configured gateways are filtered out."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
errors = result.get("errors")
|
||||||
|
assert errors is not None
|
||||||
|
assert errors["base"] == "no_devices_found"
|
||||||
|
|
||||||
|
await hass.config_entries.async_remove(mock_config_entry.entry_id)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == "select_gateway"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_unique_id_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_discovery: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test duplicate protection when the entry appears during the flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"selected_gateway": mock_gateway.gw_sn},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.get("type") is FlowResultType.ABORT
|
||||||
|
assert result.get("reason") == "already_configured"
|
||||||
61
tests/components/sunricher_dali_center/test_init.py
Normal file
61
tests/components/sunricher_dali_center/test_init.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""Test the Dali Center integration initialization."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test successful setup of config entry."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
mock_gateway.connect.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_connection_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup fails when gateway connection fails."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
mock_gateway.connect.side_effect = DaliGatewayError("Connection failed")
|
||||||
|
|
||||||
|
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
mock_gateway.connect.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test successful unloading of config entry."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
177
tests/components/sunricher_dali_center/test_light.py
Normal file
177
tests/components/sunricher_dali_center/test_light.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
"""Test the Dali Center light platform."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
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 tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
||||||
|
|
||||||
|
TEST_DIMMER_ENTITY_ID = "light.dimmer_0000_02"
|
||||||
|
TEST_DIMMER_DEVICE_ID = "01010000026A242121110E"
|
||||||
|
TEST_CCT_DEVICE_ID = "01020000036A242121110E"
|
||||||
|
TEST_HS_DEVICE_ID = "01030000046A242121110E"
|
||||||
|
TEST_RGBW_DEVICE_ID = "01040000056A242121110E"
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_status(
|
||||||
|
gateway: MagicMock, device_id: str, status: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Invoke the status callback registered on the gateway mock."""
|
||||||
|
callback = gateway.on_light_status
|
||||||
|
assert callable(callback)
|
||||||
|
callback(device_id, status)
|
||||||
|
|
||||||
|
|
||||||
|
@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_center._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,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test the light entities."""
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
device_entries = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
assert len(device_entries) == 5
|
||||||
|
|
||||||
|
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 is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_light(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_devices: list[MagicMock],
|
||||||
|
) -> None:
|
||||||
|
"""Test turning on a light."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: TEST_DIMMER_ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_devices[0].turn_on.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_off_light(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_devices: list[MagicMock],
|
||||||
|
) -> None:
|
||||||
|
"""Test turning off a light."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: TEST_DIMMER_ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_devices[0].turn_off.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_with_brightness(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_devices: list[MagicMock],
|
||||||
|
) -> None:
|
||||||
|
"""Test turning on light with brightness."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: TEST_DIMMER_ENTITY_ID, ATTR_BRIGHTNESS: 128},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_devices[0].turn_on.assert_called_once_with(
|
||||||
|
brightness=128,
|
||||||
|
color_temp_kelvin=None,
|
||||||
|
hs_color=None,
|
||||||
|
rgbw_color=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dispatcher_connection(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_devices: list[MagicMock],
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that dispatcher signals are properly connected."""
|
||||||
|
entity_entry = er.async_get(hass).async_get(TEST_DIMMER_ENTITY_ID)
|
||||||
|
assert entity_entry is not None
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_DIMMER_ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
status_update: dict[str, Any] = {"is_on": True, "brightness": 128}
|
||||||
|
|
||||||
|
_dispatch_status(mock_gateway, TEST_DIMMER_DEVICE_ID, status_update)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state_after = hass.states.get(TEST_DIMMER_ENTITY_ID)
|
||||||
|
assert state_after is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("device_id", "status_update"),
|
||||||
|
[
|
||||||
|
(TEST_CCT_DEVICE_ID, {"color_temp_kelvin": 3000}),
|
||||||
|
(TEST_HS_DEVICE_ID, {"hs_color": (120.0, 50.0)}),
|
||||||
|
(TEST_RGBW_DEVICE_ID, {"rgbw_color": (255, 128, 64, 32)}),
|
||||||
|
(TEST_RGBW_DEVICE_ID, {"white_level": 200}),
|
||||||
|
],
|
||||||
|
ids=["cct_color_temp", "hs_color", "rgbw_color", "rgbw_white_level"],
|
||||||
|
)
|
||||||
|
async def test_status_updates(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
device_id: str,
|
||||||
|
status_update: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test various status updates for different device types."""
|
||||||
|
_dispatch_status(mock_gateway, device_id, status_update)
|
||||||
|
await hass.async_block_till_done()
|
||||||
Reference in New Issue
Block a user