mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add zwave_js WS API commands to replace and remove failed nodes (#51018)
* Add zwave_js WS API commands to replace and remove failed nodes * no need to manually add node to driver in test
This commit is contained in:
parent
6a7968593d
commit
ebf6e3d985
@ -51,6 +51,7 @@ TYPE = "type"
|
|||||||
PROPERTY = "property"
|
PROPERTY = "property"
|
||||||
PROPERTY_KEY = "property_key"
|
PROPERTY_KEY = "property_key"
|
||||||
VALUE = "value"
|
VALUE = "value"
|
||||||
|
SECURE = "secure"
|
||||||
|
|
||||||
# constants for log config commands
|
# constants for log config commands
|
||||||
CONFIG = "config"
|
CONFIG = "config"
|
||||||
@ -129,6 +130,8 @@ def async_register_api(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, websocket_add_node)
|
websocket_api.async_register_command(hass, websocket_add_node)
|
||||||
websocket_api.async_register_command(hass, websocket_stop_inclusion)
|
websocket_api.async_register_command(hass, websocket_stop_inclusion)
|
||||||
websocket_api.async_register_command(hass, websocket_remove_node)
|
websocket_api.async_register_command(hass, websocket_remove_node)
|
||||||
|
websocket_api.async_register_command(hass, websocket_remove_failed_node)
|
||||||
|
websocket_api.async_register_command(hass, websocket_replace_failed_node)
|
||||||
websocket_api.async_register_command(hass, websocket_stop_exclusion)
|
websocket_api.async_register_command(hass, websocket_stop_exclusion)
|
||||||
websocket_api.async_register_command(hass, websocket_refresh_node_info)
|
websocket_api.async_register_command(hass, websocket_refresh_node_info)
|
||||||
websocket_api.async_register_command(hass, websocket_refresh_node_values)
|
websocket_api.async_register_command(hass, websocket_refresh_node_values)
|
||||||
@ -211,7 +214,7 @@ async def websocket_node_status(
|
|||||||
{
|
{
|
||||||
vol.Required(TYPE): "zwave_js/add_node",
|
vol.Required(TYPE): "zwave_js/add_node",
|
||||||
vol.Required(ENTRY_ID): str,
|
vol.Required(ENTRY_ID): str,
|
||||||
vol.Optional("secure", default=False): bool,
|
vol.Optional(SECURE, default=False): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@ -225,7 +228,7 @@ async def websocket_add_node(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Add a node to the Z-Wave network."""
|
"""Add a node to the Z-Wave network."""
|
||||||
controller = client.driver.controller
|
controller = client.driver.controller
|
||||||
include_non_secure = not msg["secure"]
|
include_non_secure = not msg[SECURE]
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_cleanup() -> None:
|
def async_cleanup() -> None:
|
||||||
@ -409,6 +412,165 @@ async def websocket_remove_node(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zwave_js/replace_failed_node",
|
||||||
|
vol.Required(ENTRY_ID): str,
|
||||||
|
vol.Required(NODE_ID): int,
|
||||||
|
vol.Optional(SECURE, default=False): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
@async_get_entry
|
||||||
|
async def websocket_replace_failed_node(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
client: Client,
|
||||||
|
) -> None:
|
||||||
|
"""Replace a failed node with a new node."""
|
||||||
|
controller = client.driver.controller
|
||||||
|
include_non_secure = not msg[SECURE]
|
||||||
|
node_id = msg[NODE_ID]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_cleanup() -> None:
|
||||||
|
"""Remove signal listeners."""
|
||||||
|
for unsub in unsubs:
|
||||||
|
unsub()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def forward_event(event: dict) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(msg[ID], {"event": event["event"]})
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def forward_stage(event: dict) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg[ID], {"event": event["event"], "stage": event["stageName"]}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def node_added(event: dict) -> None:
|
||||||
|
node = event["node"]
|
||||||
|
interview_unsubs = [
|
||||||
|
node.on("interview started", forward_event),
|
||||||
|
node.on("interview completed", forward_event),
|
||||||
|
node.on("interview stage completed", forward_stage),
|
||||||
|
node.on("interview failed", forward_event),
|
||||||
|
]
|
||||||
|
unsubs.extend(interview_unsubs)
|
||||||
|
node_details = {
|
||||||
|
"node_id": node.node_id,
|
||||||
|
"status": node.status,
|
||||||
|
"ready": node.ready,
|
||||||
|
}
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg[ID], {"event": "node added", "node": node_details}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def node_removed(event: dict) -> None:
|
||||||
|
node = event["node"]
|
||||||
|
node_details = {
|
||||||
|
"node_id": node.node_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg[ID], {"event": "node removed", "node": node_details}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def device_registered(device: DeviceEntry) -> None:
|
||||||
|
device_details = {
|
||||||
|
"name": device.name,
|
||||||
|
"id": device.id,
|
||||||
|
"manufacturer": device.manufacturer,
|
||||||
|
"model": device.model,
|
||||||
|
}
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg[ID], {"event": "device registered", "device": device_details}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.subscriptions[msg["id"]] = async_cleanup
|
||||||
|
unsubs = [
|
||||||
|
controller.on("inclusion started", forward_event),
|
||||||
|
controller.on("inclusion failed", forward_event),
|
||||||
|
controller.on("inclusion stopped", forward_event),
|
||||||
|
controller.on("node removed", node_removed),
|
||||||
|
controller.on("node added", node_added),
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await controller.async_replace_failed_node(node_id, include_non_secure)
|
||||||
|
connection.send_result(
|
||||||
|
msg[ID],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zwave_js/remove_failed_node",
|
||||||
|
vol.Required(ENTRY_ID): str,
|
||||||
|
vol.Required(NODE_ID): int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
@async_get_entry
|
||||||
|
async def websocket_remove_failed_node(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
client: Client,
|
||||||
|
) -> None:
|
||||||
|
"""Remove a failed node from the Z-Wave network."""
|
||||||
|
controller = client.driver.controller
|
||||||
|
node_id = msg[NODE_ID]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_cleanup() -> None:
|
||||||
|
"""Remove signal listeners."""
|
||||||
|
unsub()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def node_removed(event: dict) -> None:
|
||||||
|
node = event["node"]
|
||||||
|
node_details = {
|
||||||
|
"node_id": node.node_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg[ID], {"event": "node removed", "node": node_details}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.subscriptions[msg["id"]] = async_cleanup
|
||||||
|
unsub = controller.on("node removed", node_removed)
|
||||||
|
|
||||||
|
result = await controller.async_remove_failed_node(node_id)
|
||||||
|
connection.send_result(
|
||||||
|
msg[ID],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
|
@ -146,7 +146,7 @@ async def test_add_node(
|
|||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
assert msg["event"]["event"] == "node added"
|
assert msg["event"]["event"] == "node added"
|
||||||
node_details = {
|
node_details = {
|
||||||
"node_id": 53,
|
"node_id": 67,
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"ready": False,
|
"ready": False,
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ async def test_add_node(
|
|||||||
# Test receiving interview events
|
# Test receiving interview events
|
||||||
event = Event(
|
event = Event(
|
||||||
type="interview started",
|
type="interview started",
|
||||||
data={"source": "node", "event": "interview started", "nodeId": 53},
|
data={"source": "node", "event": "interview started", "nodeId": 67},
|
||||||
)
|
)
|
||||||
client.driver.receive_event(event)
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ async def test_add_node(
|
|||||||
"source": "node",
|
"source": "node",
|
||||||
"event": "interview stage completed",
|
"event": "interview stage completed",
|
||||||
"stageName": "NodeInfo",
|
"stageName": "NodeInfo",
|
||||||
"nodeId": 53,
|
"nodeId": 67,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
client.driver.receive_event(event)
|
client.driver.receive_event(event)
|
||||||
@ -184,7 +184,7 @@ async def test_add_node(
|
|||||||
|
|
||||||
event = Event(
|
event = Event(
|
||||||
type="interview completed",
|
type="interview completed",
|
||||||
data={"source": "node", "event": "interview completed", "nodeId": 53},
|
data={"source": "node", "event": "interview completed", "nodeId": 67},
|
||||||
)
|
)
|
||||||
client.driver.receive_event(event)
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
@ -193,7 +193,7 @@ async def test_add_node(
|
|||||||
|
|
||||||
event = Event(
|
event = Event(
|
||||||
type="interview failed",
|
type="interview failed",
|
||||||
data={"source": "node", "event": "interview failed", "nodeId": 53},
|
data={"source": "node", "event": "interview failed", "nodeId": 67},
|
||||||
)
|
)
|
||||||
client.driver.receive_event(event)
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
@ -289,9 +289,6 @@ async def test_remove_node(
|
|||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
assert msg["event"]["event"] == "exclusion started"
|
assert msg["event"]["event"] == "exclusion started"
|
||||||
|
|
||||||
# Add mock node to controller
|
|
||||||
client.driver.controller.nodes[67] = nortek_thermostat
|
|
||||||
|
|
||||||
dev_reg = dr.async_get(hass)
|
dev_reg = dr.async_get(hass)
|
||||||
|
|
||||||
# Create device registry entry for mock node
|
# Create device registry entry for mock node
|
||||||
@ -325,6 +322,221 @@ async def test_remove_node(
|
|||||||
assert msg["error"]["code"] == ERR_NOT_LOADED
|
assert msg["error"]["code"] == ERR_NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_replace_failed_node(
|
||||||
|
hass,
|
||||||
|
integration,
|
||||||
|
client,
|
||||||
|
hass_ws_client,
|
||||||
|
nortek_thermostat,
|
||||||
|
nortek_thermostat_added_event,
|
||||||
|
nortek_thermostat_removed_event,
|
||||||
|
):
|
||||||
|
"""Test the replace_failed_node websocket command."""
|
||||||
|
entry = integration
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
|
||||||
|
# Create device registry entry for mock node
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, "3245146787-67")},
|
||||||
|
name="Node 67",
|
||||||
|
)
|
||||||
|
|
||||||
|
client.async_send_command.return_value = {"success": True}
|
||||||
|
|
||||||
|
# Order of events we receive for a successful replacement is `inclusion started`,
|
||||||
|
# `inclusion stopped`, `node removed`, `node added`, then interview stages.
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
TYPE: "zwave_js/replace_failed_node",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
NODE_ID: 67,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
type="inclusion started",
|
||||||
|
data={
|
||||||
|
"source": "controller",
|
||||||
|
"event": "inclusion started",
|
||||||
|
"secure": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "inclusion started"
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
type="inclusion stopped",
|
||||||
|
data={
|
||||||
|
"source": "controller",
|
||||||
|
"event": "inclusion stopped",
|
||||||
|
"secure": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "inclusion stopped"
|
||||||
|
|
||||||
|
# Fire node removed event
|
||||||
|
client.driver.receive_event(nortek_thermostat_removed_event)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "node removed"
|
||||||
|
|
||||||
|
# Verify device was removed from device registry
|
||||||
|
device = dev_reg.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "3245146787-67")},
|
||||||
|
)
|
||||||
|
assert device is None
|
||||||
|
|
||||||
|
client.driver.receive_event(nortek_thermostat_added_event)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "node added"
|
||||||
|
node_details = {
|
||||||
|
"node_id": 67,
|
||||||
|
"status": 0,
|
||||||
|
"ready": False,
|
||||||
|
}
|
||||||
|
assert msg["event"]["node"] == node_details
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "device registered"
|
||||||
|
# Check the keys of the device item
|
||||||
|
assert list(msg["event"]["device"]) == ["name", "id", "manufacturer", "model"]
|
||||||
|
|
||||||
|
# Test receiving interview events
|
||||||
|
event = Event(
|
||||||
|
type="interview started",
|
||||||
|
data={"source": "node", "event": "interview started", "nodeId": 67},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "interview started"
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
type="interview stage completed",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "interview stage completed",
|
||||||
|
"stageName": "NodeInfo",
|
||||||
|
"nodeId": 67,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "interview stage completed"
|
||||||
|
assert msg["event"]["stage"] == "NodeInfo"
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
type="interview completed",
|
||||||
|
data={"source": "node", "event": "interview completed", "nodeId": 67},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "interview completed"
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
type="interview failed",
|
||||||
|
data={"source": "node", "event": "interview failed", "nodeId": 67},
|
||||||
|
)
|
||||||
|
client.driver.receive_event(event)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "interview failed"
|
||||||
|
|
||||||
|
# Test sending command with not loaded entry fails
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
TYPE: "zwave_js/replace_failed_node",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
NODE_ID: 67,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == ERR_NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_failed_node(
|
||||||
|
hass,
|
||||||
|
integration,
|
||||||
|
client,
|
||||||
|
hass_ws_client,
|
||||||
|
nortek_thermostat,
|
||||||
|
nortek_thermostat_removed_event,
|
||||||
|
):
|
||||||
|
"""Test the remove_failed_node websocket command."""
|
||||||
|
entry = integration
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
client.async_send_command.return_value = {"success": True}
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
TYPE: "zwave_js/remove_failed_node",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
NODE_ID: 67,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
|
||||||
|
# Create device registry entry for mock node
|
||||||
|
device = dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, "3245146787-67")},
|
||||||
|
name="Node 67",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fire node removed event
|
||||||
|
client.driver.receive_event(nortek_thermostat_removed_event)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["event"]["event"] == "node removed"
|
||||||
|
|
||||||
|
# Verify device was removed from device registry
|
||||||
|
device = dev_reg.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "3245146787-67")},
|
||||||
|
)
|
||||||
|
assert device is None
|
||||||
|
|
||||||
|
# Test sending command with not loaded entry fails
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
TYPE: "zwave_js/remove_failed_node",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
NODE_ID: 67,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == ERR_NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_node_info(
|
async def test_refresh_node_info(
|
||||||
hass, client, integration, hass_ws_client, multisensor_6
|
hass, client, integration, hass_ws_client, multisensor_6
|
||||||
):
|
):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"source": "controller",
|
"source": "controller",
|
||||||
"event": "node added",
|
"event": "node added",
|
||||||
"node": {
|
"node": {
|
||||||
"nodeId": 53,
|
"nodeId": 67,
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"ready": false,
|
"ready": false,
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"interviewAttempts": 1,
|
"interviewAttempts": 1,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
{
|
{
|
||||||
"nodeId": 53,
|
"nodeId": 67,
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user