Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add update platform for Smlight integration #125943

Merged
merged 12 commits into from
Sep 16, 2024
38 changes: 32 additions & 6 deletions homeassistant/components/smlight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,55 @@

from __future__ import annotations

from dataclasses import dataclass

from pysmlight import Api2

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .coordinator import SmDataUpdateCoordinator
from .coordinator import SmDataUpdateCoordinator, SmFirmwareUpdateCoordinator

PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
type SmConfigEntry = ConfigEntry[SmDataUpdateCoordinator]


@dataclass(kw_only=True)
class SmlightData:
"""Coordinator data class."""

data: SmDataUpdateCoordinator
firmware: SmFirmwareUpdateCoordinator


type SmConfigEntry = ConfigEntry[SmlightData]


async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
"""Set up SMLIGHT Zigbee from a config entry."""
coordinator = SmDataUpdateCoordinator(hass, entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
client = Api2(host=entry.data[CONF_HOST], session=async_get_clientsession(hass))
entry.async_create_background_task(hass, client.sse.client(), "smlight-sse-client")

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
data_coordinator = SmDataUpdateCoordinator(hass, entry.data[CONF_HOST], client)
firmware_coordinator = SmFirmwareUpdateCoordinator(
hass, entry.data[CONF_HOST], client
)

await data_coordinator.async_config_entry_first_refresh()
await firmware_coordinator.async_config_entry_first_refresh()

entry.runtime_data = SmlightData(
data=data_coordinator, firmware=firmware_coordinator
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/smlight/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up SMLIGHT sensor based on a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.data

async_add_entities(
SmBinarySensorEntity(coordinator, description) for description in SENSORS
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/smlight/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up SMLIGHT buttons based on a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.data

async_add_entities(SmButton(coordinator, button) for button in BUTTONS)


class SmButton(SmEntity, ButtonEntity):
"""Defines a SLZB-06 button."""

coordinator: SmDataUpdateCoordinator
entity_description: SmButtonDescription
_attr_entity_category = EntityCategory.CONFIG

Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/smlight/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
DOMAIN = "smlight"

ATTR_MANUFACTURER = "SMLIGHT"
DATA_COORDINATOR = "data"
FIRMWARE_COORDINATOR = "firmware"

SCAN_FIRMWARE_INTERVAL = timedelta(hours=6)
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(seconds=300)
UPTIME_DEVIATION = timedelta(seconds=5)
99 changes: 73 additions & 26 deletions homeassistant/components/smlight/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
"""DataUpdateCoordinator for Smlight."""

from __future__ import annotations

from abc import abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING

from pysmlight import Api2, Info, Sensors
from pysmlight.const import Settings, SettingsProp
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError
from pysmlight.web import Firmware

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, LOGGER, SCAN_INTERVAL
from .const import DOMAIN, LOGGER, SCAN_FIRMWARE_INTERVAL, SCAN_INTERVAL

if TYPE_CHECKING:
from . import SmConfigEntry


@dataclass
Expand All @@ -27,12 +33,21 @@ class SmData:
info: Info


class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
"""Class to manage fetching SMLIGHT data."""
@dataclass
class SmFwData:
"""SMLIGHT firmware data stored in the FirmwareUpdateCoordinator."""

info: Info
esp_firmware: list[Firmware] | None
zb_firmware: list[Firmware] | None


class SmBaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Base Coordinator for SMLIGHT."""

config_entry: ConfigEntry
config_entry: SmConfigEntry

def __init__(self, hass: HomeAssistant, host: str) -> None:
def __init__(self, hass: HomeAssistant, host: str, client: Api2) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
Expand All @@ -41,14 +56,10 @@ def __init__(self, hass: HomeAssistant, host: str) -> None:
update_interval=SCAN_INTERVAL,
)

self.client = client
self.unique_id: str | None = None
self.client = Api2(host=host, session=async_get_clientsession(hass))
self.legacy_api: int = 0

self.config_entry.async_create_background_task(
hass, self.client.sse.client(), "smlight-sse-client"
)

async def _async_setup(self) -> None:
"""Authenticate if needed during initial setup."""
if await self.client.check_auth_needed():
Expand Down Expand Up @@ -83,26 +94,62 @@ async def _async_setup(self) -> None:
translation_key="unsupported_firmware",
)

async def _async_update_data(self) -> _DataT:
try:
return await self._internal_update_data()
except SmlightAuthError as err:
raise ConfigEntryAuthFailed from err

except SmlightConnectionError as err:
raise UpdateFailed(err) from err

@abstractmethod
async def _internal_update_data(self) -> _DataT:
"""Update coordinator data."""


class SmDataUpdateCoordinator(SmBaseDataUpdateCoordinator[SmData]):
"""Class to manage fetching SMLIGHT sensor data."""

def update_setting(self, setting: Settings, value: bool | int) -> None:
"""Update the sensor value from event."""

prop = SettingsProp[setting.name].value
setattr(self.data.sensors, prop, value)

self.async_set_updated_data(self.data)

async def _async_update_data(self) -> SmData:
"""Fetch data from the SMLIGHT device."""
try:
sensors = Sensors()
if not self.legacy_api:
sensors = await self.client.get_sensors()
async def _internal_update_data(self) -> SmData:
"""Fetch sensor data from the SMLIGHT device."""
sensors = Sensors()
if not self.legacy_api:
sensors = await self.client.get_sensors()

return SmData(
sensors=sensors,
info=await self.client.get_info(),
)
except SmlightAuthError as err:
raise ConfigEntryAuthFailed from err
return SmData(
sensors=sensors,
info=await self.client.get_info(),
)

except SmlightConnectionError as err:
raise UpdateFailed(err) from err

class SmFirmwareUpdateCoordinator(SmBaseDataUpdateCoordinator[SmFwData]):
"""Class to manage fetching SMLIGHT firmware update data from cloud."""

def __init__(self, hass: HomeAssistant, host: str, client: Api2) -> None:
"""Initialize the coordinator."""
super().__init__(hass, host, client)

self.update_interval = SCAN_FIRMWARE_INTERVAL
# only one update can run at a time (core or zibgee)
self.in_progress = False

async def _internal_update_data(self) -> SmFwData:
"""Fetch data from the SMLIGHT device."""
info = await self.client.get_info()

return SmFwData(
info=info,
esp_firmware=await self.client.get_firmware_version(info.fw_channel),
zb_firmware=await self.client.get_firmware_version(
info.fw_channel, device=info.model, mode="zigbee"
),
)
6 changes: 3 additions & 3 deletions homeassistant/components/smlight/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import ATTR_MANUFACTURER
from .coordinator import SmDataUpdateCoordinator
from .coordinator import SmBaseDataUpdateCoordinator


class SmEntity(CoordinatorEntity[SmDataUpdateCoordinator]):
class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
"""Base class for all SMLight entities."""

_attr_has_entity_name = True

def __init__(self, coordinator: SmDataUpdateCoordinator) -> None:
def __init__(self, coordinator: SmBaseDataUpdateCoordinator) -> None:
"""Initialize entity with device."""
super().__init__(coordinator)
mac = format_mac(coordinator.data.info.MAC)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/smlight/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up SMLIGHT sensor based on a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.data

async_add_entities(
chain(
Expand All @@ -141,6 +141,7 @@ async def async_setup_entry(
class SmSensorEntity(SmEntity, SensorEntity):
"""Representation of a slzb sensor."""

coordinator: SmDataUpdateCoordinator
entity_description: SmSensorEntityDescription
_attr_entity_category = EntityCategory.DIAGNOSTIC

Expand All @@ -164,6 +165,7 @@ def native_value(self) -> datetime | str | float | None:
class SmInfoSensorEntity(SmEntity, SensorEntity):
"""Representation of a slzb info sensor."""

coordinator: SmDataUpdateCoordinator
entity_description: SmInfoEntityDescription
_attr_entity_category = EntityCategory.DIAGNOSTIC

Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/smlight/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@
"night_mode": {
"name": "LED night mode"
}
},
"update": {
"core_update": {
"name": "Core firmware"
},
"zigbee_update": {
"name": "Zigbee firmware"
}
}
},
"issues": {
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/smlight/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize switches for SLZB-06 device."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.data

async_add_entities(SmSwitch(coordinator, switch) for switch in SWITCHES)


class SmSwitch(SmEntity, SwitchEntity):
"""Representation of a SLZB-06 switch."""

coordinator: SmDataUpdateCoordinator
entity_description: SmSwitchEntityDescription
_attr_device_class = SwitchDeviceClass.SWITCH

Expand Down
Loading