Add SMLIGHT sensor entities for second radio (#137403)

* Add sensors for second radio

* Add test for zigbee2 sensor

* Update homeassistant/components/smlight/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* drop useless replace

* Fix test failure

* Fix code coverage in config flow

* Update homeassistant/components/smlight/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix conversion of iterator to list

* Remove assert on radios

* simplify handling of radios further

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
TimL 2025-03-26 21:34:44 +11:00 committed by GitHub
parent e10801af80
commit 043603c9be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 32 deletions

View File

@ -51,14 +51,14 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
self.client = Api2(self._host, session=async_get_clientsession(self.hass))
try:
info = await self.client.get_info()
self._host = str(info.device_ip)
self._device_name = str(info.hostname)
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
if not await self._async_check_auth_required(user_input):
info = await self.client.get_info()
self._host = str(info.device_ip)
self._device_name = str(info.hostname)
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
return await self._async_complete_entry(user_input)
except SmlightConnectionError:
errors["base"] = "cannot_connect"
@ -128,13 +128,13 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
info = await self.client.get_info()
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
if not await self._async_check_auth_required(user_input):
return await self._async_complete_entry(user_input)
info = await self.client.get_info()
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
return await self._async_complete_entry(user_input)
except SmlightConnectionError:
return self.async_abort(reason="cannot_connect")

View File

@ -37,7 +37,7 @@ class SmSensorEntityDescription(SensorEntityDescription):
class SmInfoEntityDescription(SensorEntityDescription):
"""Class describing SMLIGHT information entities."""
value_fn: Callable[[Info], StateType]
value_fn: Callable[[Info, int], StateType]
INFO: list[SmInfoEntityDescription] = [
@ -46,24 +46,25 @@ INFO: list[SmInfoEntityDescription] = [
translation_key="device_mode",
device_class=SensorDeviceClass.ENUM,
options=["eth", "wifi", "usb"],
value_fn=lambda x: x.coord_mode,
value_fn=lambda x, idx: x.coord_mode,
),
SmInfoEntityDescription(
key="firmware_channel",
translation_key="firmware_channel",
device_class=SensorDeviceClass.ENUM,
options=["dev", "release"],
value_fn=lambda x: x.fw_channel,
),
SmInfoEntityDescription(
key="zigbee_type",
translation_key="zigbee_type",
device_class=SensorDeviceClass.ENUM,
options=["coordinator", "router", "thread"],
value_fn=lambda x: x.zb_type,
value_fn=lambda x, idx: x.fw_channel,
),
]
RADIO_INFO = SmInfoEntityDescription(
key="zigbee_type",
translation_key="zigbee_type",
device_class=SensorDeviceClass.ENUM,
options=["coordinator", "router", "thread"],
value_fn=lambda x, idx: x.radios[idx].zb_type,
)
SENSORS: list[SmSensorEntityDescription] = [
SmSensorEntityDescription(
@ -102,6 +103,16 @@ SENSORS: list[SmSensorEntityDescription] = [
),
]
EXTRA_SENSOR = SmSensorEntityDescription(
key="zigbee_temperature_2",
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_temp2,
)
UPTIME: list[SmSensorEntityDescription] = [
SmSensorEntityDescription(
key="core_uptime",
@ -127,8 +138,7 @@ async def async_setup_entry(
) -> None:
"""Set up SMLIGHT sensor based on a config entry."""
coordinator = entry.runtime_data.data
async_add_entities(
entities: list[SmEntity] = list(
chain(
(SmInfoSensorEntity(coordinator, description) for description in INFO),
(SmSensorEntity(coordinator, description) for description in SENSORS),
@ -136,6 +146,16 @@ async def async_setup_entry(
)
)
entities.extend(
SmInfoSensorEntity(coordinator, RADIO_INFO, idx)
for idx, _ in enumerate(coordinator.data.info.radios)
)
if coordinator.data.sensors.zb_temp2 is not None:
entities.append(SmSensorEntity(coordinator, EXTRA_SENSOR))
async_add_entities(entities)
class SmSensorEntity(SmEntity, SensorEntity):
"""Representation of a slzb sensor."""
@ -172,17 +192,20 @@ class SmInfoSensorEntity(SmEntity, SensorEntity):
self,
coordinator: SmDataUpdateCoordinator,
description: SmInfoEntityDescription,
idx: int = 0,
) -> None:
"""Initiate slzb sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
self.idx = idx
sensor = f"_{idx}" if idx else ""
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}{sensor}"
@property
def native_value(self) -> StateType:
"""Return the sensor value."""
value = self.entity_description.value_fn(self.coordinator.data.info)
value = self.entity_description.value_fn(self.coordinator.data.info, self.idx)
options = self.entity_description.options
if isinstance(value, int) and options is not None:

View File

@ -193,7 +193,7 @@ async def test_zeroconf_flow_auth(
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smlight_client.get_info.mock_calls) == 3
assert len(mock_smlight_client.get_info.mock_calls) == 2
async def test_zeroconf_unsupported_abort(
@ -406,7 +406,7 @@ async def test_user_invalid_auth(
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smlight_client.get_info.mock_calls) == 3
assert len(mock_smlight_client.get_info.mock_calls) == 2
async def test_user_cannot_connect(
@ -443,7 +443,7 @@ async def test_user_cannot_connect(
assert result2["title"] == "SLZB-06p7"
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smlight_client.get_info.mock_calls) == 3
assert len(mock_smlight_client.get_info.mock_calls) == 2
async def test_auth_cannot_connect(

View File

@ -2,17 +2,18 @@
from unittest.mock import MagicMock
from pysmlight import Sensors
from pysmlight import Info, Sensors
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smlight.const import DOMAIN
from homeassistant.const import STATE_UNKNOWN, 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
from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform
pytestmark = [
pytest.mark.usefixtures(
@ -73,3 +74,38 @@ async def test_zigbee_uptime_disconnected(
state = hass.states.get("sensor.mock_title_zigbee_uptime")
assert state.state == STATE_UNKNOWN
async def test_zigbee2_temp_sensor(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock,
) -> None:
"""Test for zb_temp2 if device has second radio."""
mock_smlight_client.get_sensors.return_value = Sensors(zb_temp2=20.45)
await setup_integration(hass, mock_config_entry)
state = hass.states.get("sensor.mock_title_zigbee_chip_temp_2")
assert state
assert state.state == "20.45"
async def test_zigbee_type_sensors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock,
) -> None:
"""Test for zigbee type sensor with second radio."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info.from_dict(
load_json_object_fixture("info-MR1.json", DOMAIN)
)
await setup_integration(hass, mock_config_entry)
state = hass.states.get("sensor.mock_title_zigbee_type")
assert state
assert state.state == "coordinator"
state = hass.states.get("sensor.mock_title_zigbee_type_2")
assert state
assert state.state == "router"