Add zwave_js node statistics sensors (#91714)

* Add node statistics sensors

* fix tests and don't let controller state leak across tests

* Add background RSSI

* Remove extra logging statement

* fix test

* comments

* setup platform once

* Add static properties to entity description

* Update homeassistant/components/zwave_js/sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* don't dupe attribute values in entity description

* fix exception

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta
2023-05-30 20:44:32 +02:00
committed by GitHub
co-authored by Martin Hjelmare
parent 1e0770ff8a
commit 05c3d8bb37
6 changed files with 430 additions and 6 deletions
+3 -1
View File
@@ -653,7 +653,9 @@ def mock_client_fixture(
client.connect = AsyncMock(side_effect=connect)
client.listen = AsyncMock(side_effect=listen)
client.disconnect = AsyncMock(side_effect=disconnect)
client.driver = Driver(client, controller_state, log_config_state)
client.driver = Driver(
client, copy.deepcopy(controller_state), copy.deepcopy(log_config_state)
)
node = Node(client, copy.deepcopy(controller_node_state))
client.driver.controller.nodes[node.node_id] = node
@@ -690,8 +690,8 @@
"interviewStage": "Complete",
"deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1",
"statistics": {
"commandsTX": 39,
"commandsRX": 38,
"commandsTX": 0,
"commandsRX": 0,
"commandsDroppedRX": 0,
"commandsDroppedTX": 0,
"timeoutResponse": 0
+2 -2
View File
@@ -963,7 +963,7 @@ async def test_removed_device(
# Check how many entities there are
ent_reg = er.async_get(hass)
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
assert len(entity_entries) == 62
assert len(entity_entries) == 91
# Remove a node and reload the entry
old_node = driver.controller.nodes.pop(13)
@@ -975,7 +975,7 @@ async def test_removed_device(
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
assert len(device_entries) == 2
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
assert len(entity_entries) == 38
assert len(entity_entries) == 60
assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None
+165
View File
@@ -565,3 +565,168 @@ async def test_unit_change(hass: HomeAssistant, zp3111, client, integration) ->
assert state.state == "100.0"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
CONTROLLER_STATISTICS_ENTITY_PREFIX = "sensor.z_stick_gen5_usb_controller_"
# controller statistics with initial state of 0
CONTROLLER_STATISTICS_SUFFIXES = {
"successful_messages_tx": 1,
"successful_messages_rx": 2,
"messages_dropped_tx": 3,
"messages_dropped_rx": 4,
"messages_not_accepted": 5,
"collisions": 6,
"missing_acks": 7,
"timed_out_responses": 8,
"timed_out_callbacks": 9,
}
# controller statistics with initial state of unknown
CONTROLLER_STATISTICS_SUFFIXES_UNKNOWN = {
"current_background_rssi_channel_0": -1,
"average_background_rssi_channel_0": -2,
"current_background_rssi_channel_1": -3,
"average_background_rssi_channel_1": -4,
"current_background_rssi_channel_2": STATE_UNKNOWN,
"average_background_rssi_channel_2": STATE_UNKNOWN,
}
NODE_STATISTICS_ENTITY_PREFIX = "sensor.4_in_1_sensor_"
# node statistics with initial state of 0
NODE_STATISTICS_SUFFIXES = {
"successful_commands_tx": 1,
"successful_commands_rx": 2,
"commands_dropped_tx": 3,
"commands_dropped_rx": 4,
"timed_out_responses": 5,
}
# node statistics with initial state of unknown
NODE_STATISTICS_SUFFIXES_UNKNOWN = {
"round_trip_time": 6,
"rssi": 7,
}
async def test_statistics_sensors(
hass: HomeAssistant, zp3111, client, integration
) -> None:
"""Test statistics sensors."""
ent_reg = er.async_get(hass)
for prefix, suffixes in (
(CONTROLLER_STATISTICS_ENTITY_PREFIX, CONTROLLER_STATISTICS_SUFFIXES),
(CONTROLLER_STATISTICS_ENTITY_PREFIX, CONTROLLER_STATISTICS_SUFFIXES_UNKNOWN),
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES),
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES_UNKNOWN),
):
for suffix_key in suffixes:
entry = ent_reg.async_get(f"{prefix}{suffix_key}")
assert entry
assert entry.disabled
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
ent_reg.async_update_entity(entry.entity_id, **{"disabled_by": None})
# reload integration and check if entity is correctly there
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
for prefix, suffixes, initial_state in (
(CONTROLLER_STATISTICS_ENTITY_PREFIX, CONTROLLER_STATISTICS_SUFFIXES, "0"),
(
CONTROLLER_STATISTICS_ENTITY_PREFIX,
CONTROLLER_STATISTICS_SUFFIXES_UNKNOWN,
STATE_UNKNOWN,
),
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES, "0"),
(
NODE_STATISTICS_ENTITY_PREFIX,
NODE_STATISTICS_SUFFIXES_UNKNOWN,
STATE_UNKNOWN,
),
):
for suffix_key in suffixes:
entry = ent_reg.async_get(f"{prefix}{suffix_key}")
assert entry
assert not entry.disabled
assert entry.disabled_by is None
state = hass.states.get(entry.entity_id)
assert state
assert state.state == initial_state
# Fire statistics updated for controller
event = Event(
"statistics updated",
{
"source": "controller",
"event": "statistics updated",
"statistics": {
"messagesTX": 1,
"messagesRX": 2,
"messagesDroppedTX": 3,
"messagesDroppedRX": 4,
"NAK": 5,
"CAN": 6,
"timeoutACK": 7,
"timeoutResponse": 8,
"timeoutCallback": 9,
"backgroundRSSI": {
"channel0": {
"current": -1,
"average": -2,
},
"channel1": {
"current": -3,
"average": -4,
},
"timestamp": 1681967176510,
},
},
},
)
client.driver.controller.receive_event(event)
# Fire statistics updated event for node
event = Event(
"statistics updated",
{
"source": "node",
"event": "statistics updated",
"nodeId": zp3111.node_id,
"statistics": {
"commandsTX": 1,
"commandsRX": 2,
"commandsDroppedTX": 3,
"commandsDroppedRX": 4,
"timeoutResponse": 5,
"rtt": 6,
"rssi": 7,
"lwr": {
"protocolDataRate": 1,
"rssi": 1,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"nlwr": {
"protocolDataRate": 2,
"rssi": 2,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
},
},
)
zp3111.receive_event(event)
# Check that states match the statistics from the updates
for prefix, suffixes in (
(CONTROLLER_STATISTICS_ENTITY_PREFIX, CONTROLLER_STATISTICS_SUFFIXES),
(CONTROLLER_STATISTICS_ENTITY_PREFIX, CONTROLLER_STATISTICS_SUFFIXES_UNKNOWN),
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES),
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES_UNKNOWN),
):
for suffix_key, val in suffixes.items():
state = hass.states.get(f"{prefix}{suffix_key}")
assert state
assert state.state == str(val)