forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Integration: SMLIGHT SLZB-06 Adapters Integration (home-assistant…
…#118675) * Initial SMLIGHT integration Signed-off-by: Tim Lunn <tl@smlight.tech> * Generated content Signed-off-by: Tim Lunn <tl@smlight.tech> * Cleanup LOGGING * Use runtime data * Call super first * coordinator instance attributes * Move coordinatorEntity and attr to base class * cleanup sensors * update strings to use sentence case * Improve reauth flow on incorrect credentials * Use fixture for config_flow tests and test to completion * Split uptime hndling into a new uptime sensor entity * Drop server side events and internet callback will bring this back with binary sensor Platform * consolidate coordinator setup * entity always include connections * get_hostname tweak * Add tests for init, coordinator and sensor * Use custom type SmConfigEntry * update sensor snapshot * Drop reauth flow for later PR * Use _async_setup for initial setup * drop internet to be set later * sensor fixes * config flow re * typing fixes * Bump pysmlight dependency to 0.0.12 * dont trigger invalid auth message when first loading auth step * Merge uptime sensors back into main sensor class * clarify uptime handling * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * address review comments * pass host as parameter to the dataCoordinator * drop uptime sensors for a later PR * update sensor test snapshot * move coordinator unique_id to _async_setup * fix CI * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * drop invalid_auth test tag * use snapshot_platform, update fixtures * Finish all tests with abort or create entry * drop coordinator tests and remove hostname support * add test for update failure on connection error * use freezer for update_failed test * fix pysmlight imports --------- Signed-off-by: Tim Lunn <tl@smlight.tech> Co-authored-by: Tim Lunn <tim@feathertop.org> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
- Loading branch information
1 parent
b464813
commit 98a007c
Showing
23 changed files
with
1,871 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
"""SMLIGHT SLZB Zigbee device integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_HOST, Platform | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .coordinator import SmDataUpdateCoordinator | ||
|
||
PLATFORMS: list[Platform] = [ | ||
Platform.SENSOR, | ||
] | ||
type SmConfigEntry = ConfigEntry[SmDataUpdateCoordinator] | ||
|
||
|
||
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 | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
"""Config flow for SMLIGHT Zigbee integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from pysmlight import Api2 | ||
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError | ||
import voluptuous as vol | ||
|
||
from homeassistant.components import zeroconf | ||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.device_registry import format_mac | ||
|
||
from .const import DOMAIN | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST): str, | ||
} | ||
) | ||
|
||
STEP_AUTH_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_USERNAME): str, | ||
vol.Required(CONF_PASSWORD): str, | ||
} | ||
) | ||
|
||
|
||
class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for SMLIGHT Zigbee.""" | ||
|
||
def __init__(self) -> None: | ||
"""Initialize the config flow.""" | ||
self.client: Api2 | ||
self.host: str | None = None | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle the initial step.""" | ||
errors: dict[str, str] = {} | ||
|
||
if user_input is not None: | ||
host = user_input[CONF_HOST] | ||
self.client = Api2(host, session=async_get_clientsession(self.hass)) | ||
self.host = host | ||
|
||
try: | ||
if not await self._async_check_auth_required(user_input): | ||
return await self._async_complete_entry(user_input) | ||
except SmlightConnectionError: | ||
errors["base"] = "cannot_connect" | ||
except SmlightAuthError: | ||
return await self.async_step_auth() | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||
) | ||
|
||
async def async_step_auth( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle authentication to SLZB-06 device.""" | ||
errors: dict[str, str] = {} | ||
|
||
if user_input is not None: | ||
try: | ||
if not await self._async_check_auth_required(user_input): | ||
return await self._async_complete_entry(user_input) | ||
except SmlightConnectionError: | ||
return self.async_abort(reason="cannot_connect") | ||
except SmlightAuthError: | ||
errors["base"] = "invalid_auth" | ||
|
||
return self.async_show_form( | ||
step_id="auth", data_schema=STEP_AUTH_DATA_SCHEMA, errors=errors | ||
) | ||
|
||
async def async_step_zeroconf( | ||
self, discovery_info: zeroconf.ZeroconfServiceInfo | ||
) -> ConfigFlowResult: | ||
"""Handle a discovered Lan coordinator.""" | ||
local_name = discovery_info.hostname[:-1] | ||
node_name = local_name.removesuffix(".local") | ||
|
||
self.host = local_name | ||
self.context["title_placeholders"] = {CONF_NAME: node_name} | ||
self.client = Api2(self.host, session=async_get_clientsession(self.hass)) | ||
|
||
mac = discovery_info.properties.get("mac") | ||
# fallback for legacy firmware | ||
if mac is None: | ||
info = await self.client.get_info() | ||
mac = info.MAC | ||
await self.async_set_unique_id(format_mac(mac)) | ||
self._abort_if_unique_id_configured() | ||
|
||
return await self.async_step_confirm_discovery() | ||
|
||
async def async_step_confirm_discovery( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle discovery confirm.""" | ||
errors: dict[str, str] = {} | ||
|
||
if user_input is not None: | ||
user_input[CONF_HOST] = self.host | ||
try: | ||
if not await self._async_check_auth_required(user_input): | ||
return await self._async_complete_entry(user_input) | ||
|
||
except SmlightConnectionError: | ||
return self.async_abort(reason="cannot_connect") | ||
|
||
except SmlightAuthError: | ||
return await self.async_step_auth() | ||
|
||
self._set_confirm_only() | ||
|
||
return self.async_show_form( | ||
step_id="confirm_discovery", | ||
description_placeholders={"host": self.host}, | ||
errors=errors, | ||
) | ||
|
||
async def _async_check_auth_required(self, user_input: dict[str, Any]) -> bool: | ||
"""Check if auth required and attempt to authenticate.""" | ||
if await self.client.check_auth_needed(): | ||
if user_input.get(CONF_USERNAME) and user_input.get(CONF_PASSWORD): | ||
return not await self.client.authenticate( | ||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD] | ||
) | ||
raise SmlightAuthError | ||
return False | ||
|
||
async def _async_complete_entry( | ||
self, user_input: dict[str, Any] | ||
) -> ConfigFlowResult: | ||
info = await self.client.get_info() | ||
await self.async_set_unique_id(format_mac(info.MAC)) | ||
self._abort_if_unique_id_configured() | ||
|
||
if user_input.get(CONF_HOST) is None: | ||
user_input[CONF_HOST] = self.host | ||
|
||
assert info.model is not None | ||
return self.async_create_entry(title=info.model, data=user_input) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
"""Constants for the SMLIGHT Zigbee integration.""" | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
DOMAIN = "smlight" | ||
|
||
ATTR_MANUFACTURER = "SMLIGHT" | ||
|
||
LOGGER = logging.getLogger(__package__) | ||
SCAN_INTERVAL = timedelta(seconds=300) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
"""DataUpdateCoordinator for Smlight.""" | ||
|
||
from dataclasses import dataclass | ||
|
||
from pysmlight import Api2, Info, Sensors | ||
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryError | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.device_registry import format_mac | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL | ||
|
||
|
||
@dataclass | ||
class SmData: | ||
"""SMLIGHT data stored in the DataUpdateCoordinator.""" | ||
|
||
sensors: Sensors | ||
info: Info | ||
|
||
|
||
class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]): | ||
"""Class to manage fetching SMLIGHT data.""" | ||
|
||
config_entry: ConfigEntry | ||
|
||
def __init__(self, hass: HomeAssistant, host: str) -> None: | ||
"""Initialize the coordinator.""" | ||
super().__init__( | ||
hass, | ||
LOGGER, | ||
name=f"{DOMAIN}_{host}", | ||
update_interval=SCAN_INTERVAL, | ||
) | ||
|
||
self.unique_id: str | None = None | ||
self.client = Api2(host=host, session=async_get_clientsession(hass)) | ||
|
||
async def _async_setup(self) -> None: | ||
"""Authenticate if needed during initial setup.""" | ||
if await self.client.check_auth_needed(): | ||
if ( | ||
CONF_USERNAME in self.config_entry.data | ||
and CONF_PASSWORD in self.config_entry.data | ||
): | ||
try: | ||
await self.client.authenticate( | ||
self.config_entry.data[CONF_USERNAME], | ||
self.config_entry.data[CONF_PASSWORD], | ||
) | ||
except SmlightAuthError as err: | ||
LOGGER.error("Failed to authenticate: %s", err) | ||
raise ConfigEntryError from err | ||
|
||
info = await self.client.get_info() | ||
self.unique_id = format_mac(info.MAC) | ||
|
||
async def _async_update_data(self) -> SmData: | ||
"""Fetch data from the SMLIGHT device.""" | ||
try: | ||
return SmData( | ||
sensors=await self.client.get_sensors(), | ||
info=await self.client.get_info(), | ||
) | ||
except SmlightConnectionError as err: | ||
raise UpdateFailed(err) from err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
"""Base class for all SMLIGHT entities.""" | ||
|
||
from __future__ import annotations | ||
|
||
from homeassistant.helpers.device_registry import ( | ||
CONNECTION_NETWORK_MAC, | ||
DeviceInfo, | ||
format_mac, | ||
) | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import ATTR_MANUFACTURER | ||
from .coordinator import SmDataUpdateCoordinator | ||
|
||
|
||
class SmEntity(CoordinatorEntity[SmDataUpdateCoordinator]): | ||
"""Base class for all SMLight entities.""" | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__(self, coordinator: SmDataUpdateCoordinator) -> None: | ||
"""Initialize entity with device.""" | ||
super().__init__(coordinator) | ||
mac = format_mac(coordinator.data.info.MAC) | ||
self._attr_device_info = DeviceInfo( | ||
configuration_url=f"http://{coordinator.client.host}", | ||
connections={(CONNECTION_NETWORK_MAC, mac)}, | ||
manufacturer=ATTR_MANUFACTURER, | ||
model=coordinator.data.info.model, | ||
sw_version=f"core: {coordinator.data.info.sw_version} / zigbee: {coordinator.data.info.zb_version}", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"domain": "smlight", | ||
"name": "SMLIGHT SLZB", | ||
"codeowners": ["@tl-sl"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/smlight", | ||
"integration_type": "device", | ||
"iot_class": "local_polling", | ||
"requirements": ["pysmlight==0.0.12"], | ||
"zeroconf": [ | ||
{ | ||
"type": "_slzb-06._tcp.local." | ||
} | ||
] | ||
} |
Oops, something went wrong.