diff --git a/CODEOWNERS b/CODEOWNERS index 367c6eee2bf8bccf8091937865bcd182f6b7de1e..1618b18a8be53a6412a3c8fc4697680f439dee2b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1329,6 +1329,8 @@ build.json @home-assistant/supervisor /homeassistant/components/smarty/ @z0mbieprocess /homeassistant/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST +/homeassistant/components/smlight/ @tl-sl +/tests/components/smlight/ @tl-sl /homeassistant/components/sms/ @ocalvo /tests/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 diff --git a/homeassistant/components/smlight/__init__.py b/homeassistant/components/smlight/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..16eb60b9c87593f7462b812811ed05d967648983 --- /dev/null +++ b/homeassistant/components/smlight/__init__.py @@ -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) diff --git a/homeassistant/components/smlight/config_flow.py b/homeassistant/components/smlight/config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..1b8cc4efeb1fc445c7039889cfa5016ddebc69d0 --- /dev/null +++ b/homeassistant/components/smlight/config_flow.py @@ -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) diff --git a/homeassistant/components/smlight/const.py b/homeassistant/components/smlight/const.py new file mode 100644 index 0000000000000000000000000000000000000000..de3270fe3be461fc311378057a992153ffb92a15 --- /dev/null +++ b/homeassistant/components/smlight/const.py @@ -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) diff --git a/homeassistant/components/smlight/coordinator.py b/homeassistant/components/smlight/coordinator.py new file mode 100644 index 0000000000000000000000000000000000000000..6a29f14fafdfb2544340728dcfc1cd239864bdc7 --- /dev/null +++ b/homeassistant/components/smlight/coordinator.py @@ -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 diff --git a/homeassistant/components/smlight/entity.py b/homeassistant/components/smlight/entity.py new file mode 100644 index 0000000000000000000000000000000000000000..50767d3bf748cd163ce4ddb23ad04d4096862b2d --- /dev/null +++ b/homeassistant/components/smlight/entity.py @@ -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}", + ) diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..0dbd25e90bd55e20b096a823738a936f095b231c --- /dev/null +++ b/homeassistant/components/smlight/manifest.json @@ -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." + } + ] +} diff --git a/homeassistant/components/smlight/sensor.py b/homeassistant/components/smlight/sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..d9c03760fb805b7963bcbbcc45272184f2373e4c --- /dev/null +++ b/homeassistant/components/smlight/sensor.py @@ -0,0 +1,103 @@ +"""Support for SLZB-06 sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pysmlight import Sensors + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import SmConfigEntry +from .coordinator import SmDataUpdateCoordinator +from .entity import SmEntity + + +@dataclass(frozen=True, kw_only=True) +class SmSensorEntityDescription(SensorEntityDescription): + """Class describing SMLIGHT sensor entities.""" + + entity_category = EntityCategory.DIAGNOSTIC + value_fn: Callable[[Sensors], float | None] + + +SENSORS = [ + SmSensorEntityDescription( + key="core_temperature", + translation_key="core_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda x: x.esp32_temp, + ), + SmSensorEntityDescription( + key="zigbee_temperature", + translation_key="zigbee_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda x: x.zb_temp, + ), + SmSensorEntityDescription( + key="ram_usage", + translation_key="ram_usage", + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + entity_registry_enabled_default=False, + value_fn=lambda x: x.ram_usage, + ), + SmSensorEntityDescription( + key="fs_usage", + translation_key="fs_usage", + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + entity_registry_enabled_default=False, + value_fn=lambda x: x.fs_used, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SmConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up SMLIGHT sensor based on a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + SmSensorEntity(coordinator, description) for description in SENSORS + ) + + +class SmSensorEntity(SmEntity, SensorEntity): + """Representation of a slzb sensor.""" + + entity_description: SmSensorEntityDescription + + def __init__( + self, + coordinator: SmDataUpdateCoordinator, + description: SmSensorEntityDescription, + ) -> None: + """Initiate slzb sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + + @property + def native_value(self) -> float | None: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.data.sensors) diff --git a/homeassistant/components/smlight/strings.json b/homeassistant/components/smlight/strings.json new file mode 100644 index 0000000000000000000000000000000000000000..02b9ebcc4e81b78181415aab917459f403c5554a --- /dev/null +++ b/homeassistant/components/smlight/strings.json @@ -0,0 +1,49 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up SMLIGHT Zigbee Integration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of the SMLIGHT SLZB-06x device" + } + }, + "auth": { + "description": "Please enter the username and password", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "confirm_discovery": { + "description": "Do you want to set up SMLIGHT at {host}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "zigbee_temperature": { + "name": "Zigbee chip temp" + }, + "core_temperature": { + "name": "Core chip temp" + }, + "fs_usage": { + "name": "Filesystem usage" + }, + "ram_usage": { + "name": "RAM usage" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b474fbaf54fbfe8bef65060ea8742308998ea83c..eecb3a76aacebaea68f394f26bfcd6b39b646203 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -525,6 +525,7 @@ FLOWS = { "smartthings", "smarttub", "smhi", + "smlight", "sms", "snapcast", "snooz", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9e1250e3b603673a4d672e1d68a770e4f01d3907..b14531e17312bda18beda70b44ec4f182e59a3be 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5588,6 +5588,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "smlight": { + "name": "SMLIGHT SLZB", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "sms": { "name": "SMS notifications via GSM-modem", "integration_type": "hub", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 7cd60da2d0eacfcb0924e55c85ebec96f832df0f..3e5e34090d1272ed376c6994f631b9b66ad652e2 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -747,6 +747,9 @@ ZEROCONF = { }, ], "_slzb-06._tcp.local.": [ + { + "domain": "smlight", + }, { "domain": "zha", "name": "slzb-06*", diff --git a/requirements_all.txt b/requirements_all.txt index f640bb15e5b9881df89c6f4503f1d6adce796487..ce1db53c844ee89ffadf2b285384c7e775a249c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2210,6 +2210,9 @@ pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 +# homeassistant.components.smlight +pysmlight==0.0.12 + # homeassistant.components.snmp pysnmp==6.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4db2963fe287f4aee4dd450c7f691784b35df84..ea248a662bd1d3a082b297b2e8e6e283043ea5cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1764,6 +1764,9 @@ pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 +# homeassistant.components.smlight +pysmlight==0.0.12 + # homeassistant.components.snmp pysnmp==6.2.5 diff --git a/tests/components/smlight/__init__.py b/tests/components/smlight/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..37184226507e736e474b09f23e5161835bc02372 --- /dev/null +++ b/tests/components/smlight/__init__.py @@ -0,0 +1 @@ +"""Tests for the SMLIGHT Zigbee adapter integration.""" diff --git a/tests/components/smlight/conftest.py b/tests/components/smlight/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..0338bf4b672be53c88558997d05a0904ad17dadc --- /dev/null +++ b/tests/components/smlight/conftest.py @@ -0,0 +1,74 @@ +"""Common fixtures for the SMLIGHT Zigbee tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from pysmlight.web import Info, Sensors +import pytest + +from homeassistant.components.smlight.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_json_object_fixture + +MOCK_HOST = "slzb-06.local" +MOCK_USERNAME = "test-user" +MOCK_PASSWORD = "test-pass" + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: MOCK_HOST, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.smlight.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]: + """Mock the SMLIGHT API client.""" + with ( + patch( + "homeassistant.components.smlight.coordinator.Api2", autospec=True + ) as smlight_mock, + patch("homeassistant.components.smlight.config_flow.Api2", new=smlight_mock), + ): + api = smlight_mock.return_value + api.host = MOCK_HOST + api.get_info.return_value = Info.from_dict( + load_json_object_fixture("info.json", DOMAIN) + ) + api.get_sensors.return_value = Sensors.from_dict( + load_json_object_fixture("sensors.json", DOMAIN) + ) + + api.check_auth_needed.return_value = False + api.authenticate.return_value = True + + yield api + + +async def setup_integration(hass: HomeAssistant, mock_config_entry: MockConfigEntry): + """Set up the integration.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/smlight/fixtures/info.json b/tests/components/smlight/fixtures/info.json new file mode 100644 index 0000000000000000000000000000000000000000..72bb7c1ed9b6c530a6d5c206c88dc23eba5de4d2 --- /dev/null +++ b/tests/components/smlight/fixtures/info.json @@ -0,0 +1,16 @@ +{ + "coord_mode": 0, + "device_ip": "192.168.1.161", + "fs_total": 3456, + "fw_channel": "dev", + "MAC": "AA:BB:CC:DD:EE:FF", + "model": "SLZB-06p7", + "ram_total": 296, + "sw_version": "v2.3.1.dev", + "wifi_mode": 0, + "zb_flash_size": 704, + "zb_hw": "CC2652P7", + "zb_ram_size": 152, + "zb_version": -1, + "zb_type": -1 +} diff --git a/tests/components/smlight/fixtures/sensors.json b/tests/components/smlight/fixtures/sensors.json new file mode 100644 index 0000000000000000000000000000000000000000..0b2f9055e01e58f5f6c66d2ed81b568e16cb8ef6 --- /dev/null +++ b/tests/components/smlight/fixtures/sensors.json @@ -0,0 +1,14 @@ +{ + "esp32_temp": 35.0, + "zb_temp": 32.7, + "uptime": 508125, + "socket_uptime": 127, + "ram_usage": 99, + "fs_used": 188, + "ethernet": true, + "wifi_connected": false, + "wifi_status": 255, + "disable_leds": false, + "night_mode": false, + "auto_zigbee": false +} diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr new file mode 100644 index 0000000000000000000000000000000000000000..528a7b7b340f63371931d190ddde950e6f0e424b --- /dev/null +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': 'http://slzb-06.local', + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'SMLIGHT', + 'model': 'SLZB-06p7', + 'model_id': None, + 'name': 'Mock Title', + 'name_by_user': None, + 'primary_config_entry': <ANY>, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/smlight/snapshots/test_sensor.ambr b/tests/components/smlight/snapshots/test_sensor.ambr new file mode 100644 index 0000000000000000000000000000000000000000..0ff3d37b735b608eb8fd1b197d3574570d42350f --- /dev/null +++ b/tests/components/smlight/snapshots/test_sensor.ambr @@ -0,0 +1,741 @@ +# serializer version: 1 +# name: test_sensors[sensor.mock_title_core_chip_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.mock_title_core_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Core chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'core_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_core_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.mock_title_core_chip_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Title Core chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.mock_title_core_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '35.0', + }) +# --- +# name: test_sensors[sensor.mock_title_filesystem_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.mock_title_filesystem_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'Filesystem usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fs_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_fs_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.mock_title_filesystem_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Mock Title Filesystem usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.mock_title_filesystem_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '188', + }) +# --- +# name: test_sensors[sensor.mock_title_ram_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.mock_title_ram_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'RAM usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ram_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_ram_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.mock_title_ram_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Mock Title RAM usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.mock_title_ram_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '99', + }) +# --- +# name: test_sensors[sensor.mock_title_zigbee_chip_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.mock_title_zigbee_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Zigbee chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'zigbee_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_zigbee_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.mock_title_zigbee_chip_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Title Zigbee chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.mock_title_zigbee_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '32.7', + }) +# --- +# name: test_sensors[sensor.slzb_06_core_chip_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_core_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Core chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'core_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_core_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_core_chip_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'slzb-06 Core chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_core_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '35.0', + }) +# --- +# name: test_sensors[sensor.slzb_06_core_chip_temp] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'slzb-06 Core chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_core_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '35.0', + }) +# --- +# name: test_sensors[sensor.slzb_06_core_chip_temp].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_core_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Core chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'core_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_core_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_core_chip_temp].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': 'http://slzb-06.local', + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'SMLIGHT', + 'model': 'SLZB-06p7', + 'model_id': None, + 'name': 'slzb-06', + 'name_by_user': None, + 'primary_config_entry': <ANY>, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'via_device_id': None, + }) +# --- +# name: test_sensors[sensor.slzb_06_filesystem_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_filesystem_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'Filesystem usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fs_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_fs_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_filesystem_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'slzb-06 Filesystem usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_filesystem_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '188', + }) +# --- +# name: test_sensors[sensor.slzb_06_filesystem_usage] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'slzb-06 Filesystem usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_filesystem_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '188', + }) +# --- +# name: test_sensors[sensor.slzb_06_filesystem_usage].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_filesystem_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'Filesystem usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fs_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_fs_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_filesystem_usage].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': 'http://slzb-06.local', + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'SMLIGHT', + 'model': 'SLZB-06p7', + 'model_id': None, + 'name': 'slzb-06', + 'name_by_user': None, + 'primary_config_entry': <ANY>, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'via_device_id': None, + }) +# --- +# name: test_sensors[sensor.slzb_06_ram_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_ram_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'RAM usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ram_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_ram_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_ram_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'slzb-06 RAM usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_ram_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '99', + }) +# --- +# name: test_sensors[sensor.slzb_06_ram_usage] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'slzb-06 RAM usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_ram_usage', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '99', + }) +# --- +# name: test_sensors[sensor.slzb_06_ram_usage].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_ram_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>, + 'original_icon': None, + 'original_name': 'RAM usage', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ram_usage', + 'unique_id': 'aa:bb:cc:dd:ee:ff_ram_usage', + 'unit_of_measurement': <UnitOfInformation.KILOBYTES: 'kB'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_ram_usage].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': 'http://slzb-06.local', + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'SMLIGHT', + 'model': 'SLZB-06p7', + 'model_id': None, + 'name': 'slzb-06', + 'name_by_user': None, + 'primary_config_entry': <ANY>, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'via_device_id': None, + }) +# --- +# name: test_sensors[sensor.slzb_06_zigbee_chip_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_zigbee_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Zigbee chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'zigbee_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_zigbee_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_zigbee_chip_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'slzb-06 Zigbee chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_zigbee_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '32.7', + }) +# --- +# name: test_sensors[sensor.slzb_06_zigbee_chip_temp] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'slzb-06 Zigbee chip temp', + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }), + 'context': <ANY>, + 'entity_id': 'sensor.slzb_06_zigbee_chip_temp', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': '32.7', + }) +# --- +# name: test_sensors[sensor.slzb_06_zigbee_chip_temp].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, + }), + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, + 'entity_id': 'sensor.slzb_06_zigbee_chip_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>, + 'original_icon': None, + 'original_name': 'Zigbee chip temp', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'zigbee_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff_zigbee_temperature', + 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, + }) +# --- +# name: test_sensors[sensor.slzb_06_zigbee_chip_temp].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': 'http://slzb-06.local', + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'SMLIGHT', + 'model': 'SLZB-06p7', + 'model_id': None, + 'name': 'slzb-06', + 'name_by_user': None, + 'primary_config_entry': <ANY>, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/smlight/test_config_flow.py b/tests/components/smlight/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..9a23a8de753216d295a580ab3983148405d1879d --- /dev/null +++ b/tests/components/smlight/test_config_flow.py @@ -0,0 +1,365 @@ +"""Test the SMLIGHT SLZB config flow.""" + +from ipaddress import ip_address +from unittest.mock import AsyncMock, MagicMock + +from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError +import pytest + +from homeassistant.components import zeroconf +from homeassistant.components.smlight.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME + +from tests.common import MockConfigEntry + +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + hostname="slzb-06.local.", + name="mock_name", + port=6638, + properties={"mac": "AA:BB:CC:DD:EE:FF"}, + type="mock_type", +) + +DISCOVERY_INFO_LEGACY = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + hostname="slzb-06.local.", + name="mock_name", + port=6638, + properties={}, + type="mock_type", +) + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test the full manual user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_HOST, + }, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "SLZB-06p7" + assert result2["data"] == { + CONF_HOST: MOCK_HOST, + } + assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_smlight_client: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the zeroconf flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO + ) + + assert result["description_placeholders"] == {"host": MOCK_HOST} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0]["flow_id"] == result["flow_id"] + assert progress[0]["context"]["confirm_only"] is True + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["context"]["source"] == "zeroconf" + assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert result2["title"] == "SLZB-06p7" + assert result2["data"] == { + CONF_HOST: MOCK_HOST, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 1 + + +async def test_zeroconf_flow_auth( + hass: HomeAssistant, + mock_smlight_client: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full zeroconf flow including authentication.""" + mock_smlight_client.check_auth_needed.return_value = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO + ) + + assert result["description_placeholders"] == {"host": MOCK_HOST} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0]["flow_id"] == result["flow_id"] + assert progress[0]["context"]["confirm_only"] is True + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "auth" + + progress2 = hass.config_entries.flow.async_progress() + assert len(progress2) == 1 + assert progress2[0]["flow_id"] == result["flow_id"] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + ) + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["context"]["source"] == "zeroconf" + assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert result3["title"] == "SLZB-06p7" + assert result3["data"] == { + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_HOST: MOCK_HOST, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_user_device_exists_abort( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we abort user flow if device already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: MOCK_HOST, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we abort zeroconf flow if device already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_user_invalid_auth( + hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock +) -> None: + """Test we handle invalid auth.""" + mock_smlight_client.check_auth_needed.return_value = True + mock_smlight_client.authenticate.side_effect = SmlightAuthError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: MOCK_HOST, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test", + CONF_PASSWORD: "bad", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + assert result2["step_id"] == "auth" + + mock_smlight_client.authenticate.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test", + CONF_PASSWORD: "good", + }, + ) + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "SLZB-06p7" + assert result3["data"] == { + CONF_HOST: MOCK_HOST, + CONF_USERNAME: "test", + CONF_PASSWORD: "good", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 1 + + +async def test_user_cannot_connect( + hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock +) -> None: + """Test we handle user cannot connect error.""" + mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "unknown.local", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + assert result["step_id"] == "user" + + mock_smlight_client.check_auth_needed.side_effect = None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_HOST, + }, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "SLZB-06p7" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 1 + + +async def test_auth_cannot_connect( + hass: HomeAssistant, mock_smlight_client: MagicMock +) -> None: + """Test we abort auth step on cannot connect error.""" + mock_smlight_client.check_auth_needed.return_value = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_HOST, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "cannot_connect" + + +async def test_zeroconf_cannot_connect( + hass: HomeAssistant, mock_smlight_client: MagicMock +) -> None: + """Test we abort flow on zeroconf cannot connect error.""" + mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "cannot_connect" + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_zeroconf_legacy_mac( + hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock +) -> None: + """Test we can get unique id MAC address for older firmwares.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=DISCOVERY_INFO_LEGACY, + ) + + assert result["description_placeholders"] == {"host": MOCK_HOST} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["context"]["source"] == "zeroconf" + assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert result2["title"] == "SLZB-06p7" + assert result2["data"] == { + CONF_HOST: MOCK_HOST, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 2 diff --git a/tests/components/smlight/test_init.py b/tests/components/smlight/test_init.py new file mode 100644 index 0000000000000000000000000000000000000000..682993cb9430e0003078e07cf4f07a5324ceda2d --- /dev/null +++ b/tests/components/smlight/test_init.py @@ -0,0 +1,94 @@ +"Test SMLIGHT SLZB device integration initialization." + +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.smlight.const import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed + +pytestmark = [ + pytest.mark.usefixtures( + "mock_smlight_client", + ) +] + + +async def test_async_setup_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test async_setup_entry.""" + entry = await setup_integration(hass, mock_config_entry) + + assert entry.state is ConfigEntryState.LOADED + assert entry.unique_id == "aa:bb:cc:dd:ee:ff" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_async_setup_auth_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test async_setup_entry when authentication fails.""" + mock_smlight_client.check_auth_needed.return_value = True + mock_smlight_client.authenticate.side_effect = SmlightAuthError + entry = await setup_integration(hass, mock_config_entry) + + assert entry.state is ConfigEntryState.SETUP_ERROR + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_update_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test update failed due to connection error.""" + + await setup_integration(hass, mock_config_entry) + entity = hass.states.get("sensor.mock_title_core_chip_temp") + assert entity.state is not STATE_UNAVAILABLE + + mock_smlight_client.get_info.side_effect = SmlightConnectionError + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity = hass.states.get("sensor.mock_title_core_chip_temp") + assert entity is not None + assert entity.state == STATE_UNAVAILABLE + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry information.""" + entry = await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot diff --git a/tests/components/smlight/test_sensor.py b/tests/components/smlight/test_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..4d16a73a0a77a353ec701b69c1a7e227a8a6fdef --- /dev/null +++ b/tests/components/smlight/test_sensor.py @@ -0,0 +1,54 @@ +"""Tests for the SMLIGHT sensor platform.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .conftest import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = [ + pytest.mark.usefixtures( + "mock_smlight_client", + ) +] + + +@pytest.fixture +def platforms() -> Platform | list[Platform]: + """Platforms, which should be loaded during the test.""" + return Platform.SENSOR + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the SMLIGHT sensors.""" + entry = await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +async def test_disabled_by_default_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the disabled by default SMLIGHT sensors.""" + await setup_integration(hass, mock_config_entry) + + for sensor in ("ram_usage", "filesystem_usage"): + assert not hass.states.get(f"sensor.mock_title_{sensor}") + + assert (entry := entity_registry.async_get(f"sensor.mock_title_{sensor}")) + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION