mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add zwave_js controller status sensor (#99252)
* Add zwave_js controller status sensor * Also update network status command * fix tests * Remove WS command since we have a sensor entity * Update sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * move driver assertion out of closures * store state in tests --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
027ce55fa6
commit
6e5f4566d5
@ -353,8 +353,13 @@ class ControllerEvents:
|
||||
)
|
||||
)
|
||||
|
||||
# No need for a ping button or node status sensor for controller nodes
|
||||
if not node.is_controller_node:
|
||||
if node.is_controller_node:
|
||||
# Create a controller status sensor for each device
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self.config_entry.entry_id}_add_controller_status_sensor",
|
||||
)
|
||||
else:
|
||||
# Create a node status sensor for each device
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
|
@ -514,6 +514,7 @@ async def websocket_network_status(
|
||||
"is_heal_network_active": controller.is_heal_network_active,
|
||||
"inclusion_state": controller.inclusion_state,
|
||||
"rf_region": controller.rf_region,
|
||||
"status": controller.status,
|
||||
"nodes": [node_status(node) for node in driver.controller.nodes.values()],
|
||||
},
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import CommandClass, NodeStatus
|
||||
from zwave_js_server.const import CommandClass, ControllerStatus, NodeStatus
|
||||
from zwave_js_server.const.command_class.meter import (
|
||||
RESET_METER_OPTION_TARGET_VALUE,
|
||||
RESET_METER_OPTION_TYPE,
|
||||
@ -91,7 +91,13 @@ from .helpers import get_device_info, get_valueless_base_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
STATUS_ICON: dict[NodeStatus, str] = {
|
||||
CONTROLLER_STATUS_ICON: dict[ControllerStatus, str] = {
|
||||
ControllerStatus.READY: "mdi:check",
|
||||
ControllerStatus.UNRESPONSIVE: "mdi:bell-off",
|
||||
ControllerStatus.JAMMED: "mdi:lock",
|
||||
}
|
||||
|
||||
NODE_STATUS_ICON: dict[NodeStatus, str] = {
|
||||
NodeStatus.ALIVE: "mdi:heart-pulse",
|
||||
NodeStatus.ASLEEP: "mdi:sleep",
|
||||
NodeStatus.AWAKE: "mdi:eye",
|
||||
@ -485,12 +491,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Z-Wave sensor from config entry."""
|
||||
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
|
||||
@callback
|
||||
def async_add_sensor(info: ZwaveDiscoveryInfo) -> None:
|
||||
"""Add Z-Wave Sensor."""
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
entities: list[ZWaveBaseEntity] = []
|
||||
|
||||
if info.platform_data:
|
||||
@ -529,18 +535,19 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@callback
|
||||
def async_add_controller_status_sensor() -> None:
|
||||
"""Add controller status sensor."""
|
||||
async_add_entities([ZWaveControllerStatusSensor(config_entry, driver)])
|
||||
|
||||
@callback
|
||||
def async_add_node_status_sensor(node: ZwaveNode) -> None:
|
||||
"""Add node status sensor."""
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
async_add_entities([ZWaveNodeStatusSensor(config_entry, driver, node)])
|
||||
|
||||
@callback
|
||||
def async_add_statistics_sensors(node: ZwaveNode) -> None:
|
||||
"""Add statistics sensors."""
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
async_add_entities(
|
||||
[
|
||||
ZWaveStatisticsSensor(
|
||||
@ -565,6 +572,14 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"{DOMAIN}_{config_entry.entry_id}_add_controller_status_sensor",
|
||||
async_add_controller_status_sensor,
|
||||
)
|
||||
)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
@ -828,7 +843,7 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Icon of the entity."""
|
||||
return STATUS_ICON[self.node.status]
|
||||
return NODE_STATUS_ICON[self.node.status]
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
@ -856,6 +871,71 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ZWaveControllerStatusSensor(SensorEntity):
|
||||
"""Representation of a controller status sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, driver: Driver) -> None:
|
||||
"""Initialize a generic Z-Wave device entity."""
|
||||
self.config_entry = config_entry
|
||||
self.controller = driver.controller
|
||||
node = self.controller.own_node
|
||||
assert node
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = "Status"
|
||||
self._base_unique_id = get_valueless_base_unique_id(driver, node)
|
||||
self._attr_unique_id = f"{self._base_unique_id}.controller_status"
|
||||
# device may not be precreated in main handler yet
|
||||
self._attr_device_info = get_device_info(driver, node)
|
||||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""Poll a value."""
|
||||
# We log an error instead of raising an exception because this service call occurs
|
||||
# in a separate task since it is called via the dispatcher and we don't want to
|
||||
# raise the exception in that separate task because it is confusing to the user.
|
||||
LOGGER.error(
|
||||
"There is no value to refresh for this entity so the zwave_js.refresh_value"
|
||||
" service won't work for it"
|
||||
)
|
||||
|
||||
@callback
|
||||
def _status_changed(self, _: dict) -> None:
|
||||
"""Call when status event is received."""
|
||||
self._attr_native_value = self.controller.status.name.lower()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Icon of the entity."""
|
||||
return CONTROLLER_STATUS_ICON[self.controller.status]
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
# Add value_changed callbacks.
|
||||
self.async_on_remove(self.controller.on("status changed", self._status_changed))
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self.unique_id}_poll_value",
|
||||
self.async_poll_value,
|
||||
)
|
||||
)
|
||||
# we don't listen for `remove_entity_on_ready_node` signal because this is not
|
||||
# a regular node
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
|
||||
self.async_remove,
|
||||
)
|
||||
)
|
||||
self._attr_native_value: str = self.controller.status.name.lower()
|
||||
|
||||
|
||||
class ZWaveStatisticsSensor(SensorEntity):
|
||||
"""Representation of a node/controller statistics sensor."""
|
||||
|
||||
|
@ -24,7 +24,8 @@
|
||||
"sucNodeId": 1,
|
||||
"supportsTimers": false,
|
||||
"isHealNetworkActive": false,
|
||||
"inclusionState": 0
|
||||
"inclusionState": 0,
|
||||
"status": 0
|
||||
},
|
||||
"nodes": []
|
||||
}
|
||||
|
@ -227,7 +227,9 @@ async def test_indicator_test(
|
||||
assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 0
|
||||
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 # only ping
|
||||
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 # include node status
|
||||
assert (
|
||||
len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
||||
) # include node + controller status
|
||||
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
|
||||
|
||||
entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor"
|
||||
|
@ -205,7 +205,7 @@ async def test_on_node_added_not_ready(
|
||||
dev_reg = dr.async_get(hass)
|
||||
device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}"
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
assert len(hass.states.async_all()) == 1
|
||||
assert len(dev_reg.devices) == 1
|
||||
|
||||
node_state = deepcopy(zp3111_not_ready_state)
|
||||
@ -224,7 +224,7 @@ async def test_on_node_added_not_ready(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# the only entities are the node status sensor and ping button
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
@ -326,7 +326,7 @@ async def test_existing_node_not_ready(
|
||||
assert not device.sw_version
|
||||
|
||||
# the only entities are the node status sensor and ping button
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
@ -964,7 +964,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) == 91
|
||||
assert len(entity_entries) == 92
|
||||
|
||||
# Remove a node and reload the entry
|
||||
old_node = driver.controller.nodes.pop(13)
|
||||
@ -976,7 +976,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) == 60
|
||||
assert len(entity_entries) == 61
|
||||
assert (
|
||||
dev_reg.async_get_device(identifiers={get_device_id(driver, old_node)}) is None
|
||||
)
|
||||
|
@ -261,6 +261,47 @@ async def test_config_parameter_sensor(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_controller_status_sensor(
|
||||
hass: HomeAssistant, client, integration
|
||||
) -> None:
|
||||
"""Test controller status sensor is created and gets updated on controller state changes."""
|
||||
entity_id = "sensor.z_stick_gen5_usb_controller_status"
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
|
||||
assert not entity_entry.disabled
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "ready"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:check"
|
||||
|
||||
event = Event(
|
||||
"status changed",
|
||||
data={"source": "controller", "event": "status changed", "status": 1},
|
||||
)
|
||||
client.driver.controller.receive_event(event)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unresponsive"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:bell-off"
|
||||
|
||||
# Test transitions work
|
||||
event = Event(
|
||||
"status changed",
|
||||
data={"source": "controller", "event": "status changed", "status": 2},
|
||||
)
|
||||
client.driver.controller.receive_event(event)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "jammed"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:lock"
|
||||
|
||||
# Disconnect the client and make sure the entity is still available
|
||||
await client.disconnect()
|
||||
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_node_status_sensor(
|
||||
hass: HomeAssistant, client, lock_id_lock_as_id150, integration
|
||||
) -> None:
|
||||
@ -325,6 +366,16 @@ async def test_node_status_sensor(
|
||||
is None
|
||||
)
|
||||
|
||||
# Assert a controller status sensor entity is not created for a node
|
||||
assert (
|
||||
ent_reg.async_get_entity_id(
|
||||
DOMAIN,
|
||||
"sensor",
|
||||
f"{get_valueless_base_unique_id(driver, node)}.controller_status",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_node_status_sensor_not_ready(
|
||||
hass: HomeAssistant,
|
||||
|
Loading…
x
Reference in New Issue
Block a user