Add last seen sensor for zwave_js devices (#107345)

This commit is contained in:
Raman Gupta 2024-01-31 01:17:43 -05:00 committed by GitHub
parent b629ad9c3d
commit 9ed50d8b0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 43 deletions

View File

@ -1,8 +1,10 @@
"""Representation of Z-Wave sensors.""" """Representation of Z-Wave sensors."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Callable, Mapping
from typing import cast from dataclasses import dataclass
from datetime import datetime
from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
@ -326,131 +328,166 @@ ENTITY_DESCRIPTION_KEY_MAP = {
} }
def convert_dict_of_dicts(
statistics: ControllerStatisticsDataType | NodeStatisticsDataType, key: str
) -> Any:
"""Convert a dictionary of dictionaries to a value."""
keys = key.split(".")
return statistics.get(keys[0], {}).get(keys[1], {}).get(keys[2]) # type: ignore[attr-defined]
@dataclass(frozen=True, kw_only=True)
class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription):
"""Class to represent a Z-Wave JS statistics sensor entity description."""
convert: Callable[
[ControllerStatisticsDataType | NodeStatisticsDataType, str], Any
] = lambda statistics, key: statistics.get(key)
# Controller statistics descriptions # Controller statistics descriptions
ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="messagesTX", key="messagesTX",
name="Successful messages (TX)", name="Successful messages (TX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="messagesRX", key="messagesRX",
name="Successful messages (RX)", name="Successful messages (RX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="messagesDroppedTX", key="messagesDroppedTX",
name="Messages dropped (TX)", name="Messages dropped (TX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="messagesDroppedRX", key="messagesDroppedRX",
name="Messages dropped (RX)", name="Messages dropped (RX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="NAK", key="NAK",
name="Messages not accepted", name="Messages not accepted",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="timeoutResponse", key="timeoutResponse",
name="Timed out responses", name="Timed out responses",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="timeoutCallback", key="timeoutCallback",
name="Timed out callbacks", name="Timed out callbacks",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel0.average", key="backgroundRSSI.channel0.average",
name="Average background RSSI (channel 0)", name="Average background RSSI (channel 0)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel0.current", key="backgroundRSSI.channel0.current",
name="Current background RSSI (channel 0)", name="Current background RSSI (channel 0)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
convert=convert_dict_of_dicts,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel1.average", key="backgroundRSSI.channel1.average",
name="Average background RSSI (channel 1)", name="Average background RSSI (channel 1)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel1.current", key="backgroundRSSI.channel1.current",
name="Current background RSSI (channel 1)", name="Current background RSSI (channel 1)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
convert=convert_dict_of_dicts,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel2.average", key="backgroundRSSI.channel2.average",
name="Average background RSSI (channel 2)", name="Average background RSSI (channel 2)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel2.current", key="backgroundRSSI.channel2.current",
name="Current background RSSI (channel 2)", name="Current background RSSI (channel 2)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
convert=convert_dict_of_dicts,
), ),
] ]
# Node statistics descriptions # Node statistics descriptions
ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="commandsRX", key="commandsRX",
name="Successful commands (RX)", name="Successful commands (RX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="commandsTX", key="commandsTX",
name="Successful commands (TX)", name="Successful commands (TX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="commandsDroppedRX", key="commandsDroppedRX",
name="Commands dropped (RX)", name="Commands dropped (RX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="commandsDroppedTX", key="commandsDroppedTX",
name="Commands dropped (TX)", name="Commands dropped (TX)",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="timeoutResponse", key="timeoutResponse",
name="Timed out responses", name="Timed out responses",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="rtt", key="rtt",
name="Round Trip Time", name="Round Trip Time",
native_unit_of_measurement=UnitOfTime.MILLISECONDS, native_unit_of_measurement=UnitOfTime.MILLISECONDS,
device_class=SensorDeviceClass.DURATION, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( ZWaveJSStatisticsSensorEntityDescription(
key="rssi", key="rssi",
name="RSSI", name="RSSI",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
ZWaveJSStatisticsSensorEntityDescription(
key="lastSeen",
name="Last Seen",
device_class=SensorDeviceClass.TIMESTAMP,
convert=(
lambda statistics, key: (
datetime.fromisoformat(dt) # type: ignore[arg-type]
if (dt := statistics.get(key))
else None
)
),
),
] ]
@ -890,6 +927,7 @@ class ZWaveControllerStatusSensor(SensorEntity):
class ZWaveStatisticsSensor(SensorEntity): class ZWaveStatisticsSensor(SensorEntity):
"""Representation of a node/controller statistics sensor.""" """Representation of a node/controller statistics sensor."""
entity_description: ZWaveJSStatisticsSensorEntityDescription
_attr_should_poll = False _attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
@ -900,7 +938,7 @@ class ZWaveStatisticsSensor(SensorEntity):
config_entry: ConfigEntry, config_entry: ConfigEntry,
driver: Driver, driver: Driver,
statistics_src: ZwaveNode | Controller, statistics_src: ZwaveNode | Controller,
description: SensorEntityDescription, description: ZWaveJSStatisticsSensorEntityDescription,
) -> None: ) -> None:
"""Initialize a Z-Wave statistics entity.""" """Initialize a Z-Wave statistics entity."""
self.entity_description = description self.entity_description = description
@ -929,25 +967,11 @@ class ZWaveStatisticsSensor(SensorEntity):
" service won't work for it" " service won't work for it"
) )
def _get_data_from_statistics(
self, statistics: ControllerStatisticsDataType | NodeStatisticsDataType
) -> int | None:
"""Get the data from the statistics dict."""
if "." not in self.entity_description.key:
return cast(int | None, statistics.get(self.entity_description.key))
# If key contains dots, we need to traverse the dict to get to the right value
for key in self.entity_description.key.split("."):
if key not in statistics:
return None
statistics = statistics[key] # type: ignore[literal-required]
return cast(int, statistics)
@callback @callback
def statistics_updated(self, event_data: dict) -> None: def statistics_updated(self, event_data: dict) -> None:
"""Call when statistics updated event is received.""" """Call when statistics updated event is received."""
self._attr_native_value = self._get_data_from_statistics( self._attr_native_value = self.entity_description.convert(
event_data["statistics"] event_data["statistics"], self.entity_description.key
) )
self.async_write_ha_state() self.async_write_ha_state()
@ -972,6 +996,6 @@ class ZWaveStatisticsSensor(SensorEntity):
) )
# Set initial state # Set initial state
self._attr_native_value = self._get_data_from_statistics( self._attr_native_value = self.entity_description.convert(
self.statistics_src.statistics.data self.statistics_src.statistics.data, self.entity_description.key
) )

View File

@ -731,6 +731,7 @@ NODE_STATISTICS_SUFFIXES = {
NODE_STATISTICS_SUFFIXES_UNKNOWN = { NODE_STATISTICS_SUFFIXES_UNKNOWN = {
"round_trip_time": 6, "round_trip_time": 6,
"rssi": 7, "rssi": 7,
"last_seen": "2024-01-01T00:00:00+00:00",
} }
@ -843,6 +844,7 @@ async def test_statistics_sensors(
"repeaterRSSI": [], "repeaterRSSI": [],
"routeFailedBetween": [], "routeFailedBetween": [],
}, },
"lastSeen": "2024-01-01T00:00:00+0000",
}, },
}, },
) )