Skip to content
Merged
9 changes: 7 additions & 2 deletions homeassistant/components/sunricher_dali/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import logging

from PySrDaliGateway import DaliGateway
Expand All @@ -23,7 +24,7 @@
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__)


Expand All @@ -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"
Expand All @@ -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)

Expand Down
57 changes: 57 additions & 0 deletions homeassistant/components/sunricher_dali/entity.py
Original file line number Diff line number Diff line change
@@ -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"
27 changes: 4 additions & 23 deletions homeassistant/components/sunricher_dali/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN, MANUFACTURER
from .entity import DaliDeviceEntity
from .types import DaliCenterConfigEntry

_LOGGER = logging.getLogger(__name__)
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -111,34 +108,18 @@ async def async_turn_off(self, **kwargs: Any) -> None:

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(
CallbackEventType.LIGHT_STATUS, self._handle_device_update
)
)

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:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sunricher_dali/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
45 changes: 45 additions & 0 deletions homeassistant/components/sunricher_dali/scene.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion homeassistant/components/sunricher_dali/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -13,6 +13,7 @@ class DaliCenterData:

gateway: DaliGateway
devices: list[Device]
scenes: list[Scene]


type DaliCenterConfigEntry = ConfigEntry[DaliCenterData]
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions tests/components/sunricher_dali/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading
Loading