mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Add last seen sensor for zwave_js devices (#107345)
This commit is contained in:
parent
b629ad9c3d
commit
9ed50d8b0c
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user