Add uptime sensors for Smlight (#124408)

* Add uptime sensor as derived sensor class

* Add strings for uptime sensors

* Update sensor tests to include uptime sensors

* test zigbee uptime when disconnected
This commit is contained in:
TimL 2024-09-05 04:31:56 +10:00 committed by GitHub
parent c2b24dd355
commit f56c38d69b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 286 additions and 4 deletions

View File

@ -9,3 +9,4 @@ ATTR_MANUFACTURER = "SMLIGHT"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(seconds=300) SCAN_INTERVAL = timedelta(seconds=300)
UPTIME_DEVIATION = timedelta(seconds=5)

View File

@ -4,6 +4,8 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta
from itertools import chain
from pysmlight import Sensors from pysmlight import Sensors
@ -16,8 +18,10 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTemperature from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from . import SmConfigEntry from . import SmConfigEntry
from .const import UPTIME_DEVIATION
from .coordinator import SmDataUpdateCoordinator from .coordinator import SmDataUpdateCoordinator
from .entity import SmEntity from .entity import SmEntity
@ -67,6 +71,23 @@ SENSORS = [
), ),
] ]
UPTIME = [
SmSensorEntityDescription(
key="core_uptime",
translation_key="core_uptime",
device_class=SensorDeviceClass.TIMESTAMP,
entity_registry_enabled_default=False,
value_fn=lambda x: x.uptime,
),
SmSensorEntityDescription(
key="socket_uptime",
translation_key="socket_uptime",
device_class=SensorDeviceClass.TIMESTAMP,
entity_registry_enabled_default=False,
value_fn=lambda x: x.socket_uptime,
),
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -77,7 +98,10 @@ async def async_setup_entry(
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( async_add_entities(
SmSensorEntity(coordinator, description) for description in SENSORS chain(
(SmSensorEntity(coordinator, description) for description in SENSORS),
(SmUptimeSensorEntity(coordinator, description) for description in UPTIME),
)
) )
@ -98,6 +122,48 @@ class SmSensorEntity(SmEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
@property @property
def native_value(self) -> float | None: def native_value(self) -> datetime | float | None:
"""Return the sensor value.""" """Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.data.sensors) return self.entity_description.value_fn(self.coordinator.data.sensors)
class SmUptimeSensorEntity(SmSensorEntity):
"""Representation of a slzb uptime sensor."""
def __init__(
self,
coordinator: SmDataUpdateCoordinator,
description: SmSensorEntityDescription,
) -> None:
"Initialize uptime sensor instance."
super().__init__(coordinator, description)
self._last_uptime: datetime | None = None
def get_uptime(self, uptime: float | None) -> datetime | None:
"""Return device uptime or zigbee socket uptime.
Converts uptime from seconds to a datetime value, allow up to 5
seconds deviation. This avoids unnecessary updates to sensor state,
that may be caused by clock jitter.
"""
if uptime is None:
# reset to unknown state
self._last_uptime = None
return None
new_uptime = utcnow() - timedelta(seconds=uptime)
if (
not self._last_uptime
or abs(new_uptime - self._last_uptime) > UPTIME_DEVIATION
):
self._last_uptime = new_uptime
return self._last_uptime
@property
def native_value(self) -> datetime | None:
"""Return the sensor value."""
value = self.entity_description.value_fn(self.coordinator.data.sensors)
return self.get_uptime(value)

View File

@ -43,6 +43,12 @@
}, },
"ram_usage": { "ram_usage": {
"name": "RAM usage" "name": "RAM usage"
},
"core_uptime": {
"name": "Core uptime"
},
"socket_uptime": {
"name": "Zigbee uptime"
} }
}, },
"button": { "button": {

View File

@ -53,6 +53,53 @@
'state': '35.0', 'state': '35.0',
}) })
# --- # ---
# name: test_sensors[sensor.mock_title_core_uptime-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_core_uptime',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Core uptime',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'core_uptime',
'unique_id': 'aa:bb:cc:dd:ee:ff_core_uptime',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.mock_title_core_uptime-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Title Core uptime',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_core_uptime',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-06-25T02:51:15+00:00',
})
# ---
# name: test_sensors[sensor.mock_title_filesystem_usage-entry] # name: test_sensors[sensor.mock_title_filesystem_usage-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -149,6 +196,100 @@
'state': '99', 'state': '99',
}) })
# --- # ---
# name: test_sensors[sensor.mock_title_timestamp-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_timestamp',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Timestamp',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'core_uptime',
'unique_id': 'aa:bb:cc:dd:ee:ff_core_uptime',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.mock_title_timestamp-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Title Timestamp',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_timestamp',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-06-25T02:51:15+00:00',
})
# ---
# name: test_sensors[sensor.mock_title_timestamp_2-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_timestamp_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Timestamp',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'socket_uptime',
'unique_id': 'aa:bb:cc:dd:ee:ff_socket_uptime',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.mock_title_timestamp_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Title Timestamp',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_timestamp_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-06-30T23:57:53+00:00',
})
# ---
# name: test_sensors[sensor.mock_title_zigbee_chip_temp-entry] # name: test_sensors[sensor.mock_title_zigbee_chip_temp-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -203,6 +344,53 @@
'state': '32.7', 'state': '32.7',
}) })
# --- # ---
# name: test_sensors[sensor.mock_title_zigbee_uptime-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_zigbee_uptime',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Zigbee uptime',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'socket_uptime',
'unique_id': 'aa:bb:cc:dd:ee:ff_socket_uptime',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.mock_title_zigbee_uptime-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Title Zigbee uptime',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_zigbee_uptime',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-06-30T23:57:53+00:00',
})
# ---
# name: test_sensors[sensor.slzb_06_core_chip_temp-entry] # name: test_sensors[sensor.slzb_06_core_chip_temp-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -1,9 +1,12 @@
"""Tests for the SMLIGHT sensor platform.""" """Tests for the SMLIGHT sensor platform."""
from unittest.mock import MagicMock
from pysmlight import Sensors
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -25,6 +28,7 @@ def platforms() -> list[Platform]:
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.freeze_time("2024-07-01 00:00:00+00:00")
async def test_sensors( async def test_sensors(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
@ -46,9 +50,26 @@ async def test_disabled_by_default_sensors(
"""Test the disabled by default SMLIGHT sensors.""" """Test the disabled by default SMLIGHT sensors."""
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
for sensor in ("ram_usage", "filesystem_usage"): for sensor in ("core_uptime", "filesystem_usage", "ram_usage", "zigbee_uptime"):
assert not hass.states.get(f"sensor.mock_title_{sensor}") assert not hass.states.get(f"sensor.mock_title_{sensor}")
assert (entry := entity_registry.async_get(f"sensor.mock_title_{sensor}")) assert (entry := entity_registry.async_get(f"sensor.mock_title_{sensor}"))
assert entry.disabled assert entry.disabled
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_zigbee_uptime_disconnected(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock,
) -> None:
"""Test for uptime when zigbee socket is disconnected.
In this case zigbee uptime state should be unknown.
"""
mock_smlight_client.get_sensors.return_value = Sensors(socket_uptime=0)
await setup_integration(hass, mock_config_entry)
state = hass.states.get("sensor.mock_title_zigbee_uptime")
assert state.state == STATE_UNKNOWN