Resume adding Z-Wave device if the page is refreshed (#129081)

* ZwaveJS: Resume adding a device if the page is refreshed

* add test

* address PR comments
This commit is contained in:
Petar Petrov 2024-10-25 12:08:07 +03:00 committed by GitHub
parent 0acb95bbd5
commit 47bf0ebb47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 37 deletions

View File

@ -13,8 +13,10 @@ from zwave_js_server.client import Client
from zwave_js_server.const import ( from zwave_js_server.const import (
CommandClass, CommandClass,
ExclusionStrategy, ExclusionStrategy,
InclusionState,
InclusionStrategy, InclusionStrategy,
LogLevel, LogLevel,
NodeStatus,
Protocols, Protocols,
ProvisioningEntryStatus, ProvisioningEntryStatus,
QRCodeVersion, QRCodeVersion,
@ -693,6 +695,30 @@ async def websocket_add_node(
) )
) )
@callback
def forward_node_added(
node: Node, low_security: bool, low_security_reason: str | None
) -> None:
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,
"low_security": low_security,
"low_security_reason": low_security_reason,
}
connection.send_message(
websocket_api.event_message(
msg[ID], {"event": "node added", "node": node_details}
)
)
@callback @callback
def forward_requested_grant(event: dict) -> None: def forward_requested_grant(event: dict) -> None:
connection.send_message( connection.send_message(
@ -727,25 +753,10 @@ async def websocket_add_node(
@callback @callback
def node_added(event: dict) -> None: def node_added(event: dict) -> None:
node = event["node"] forward_node_added(
interview_unsubs = [ event["node"],
node.on("interview started", forward_event), event["result"].get("lowSecurity", False),
node.on("interview completed", forward_event), event["result"].get("lowSecurityReason"),
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,
"low_security": event["result"].get("lowSecurity", False),
"low_security_reason": event["result"].get("lowSecurityReason"),
}
connection.send_message(
websocket_api.event_message(
msg[ID], {"event": "node added", "node": node_details}
)
) )
@callback @callback
@ -777,25 +788,39 @@ async def websocket_add_node(
] ]
msg[DATA_UNSUBSCRIBE] = unsubs msg[DATA_UNSUBSCRIBE] = unsubs
try: if controller.inclusion_state == InclusionState.INCLUDING:
result = await controller.async_begin_inclusion( connection.send_result(
INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value],
force_security=force_security,
provisioning=provisioning,
dsk=dsk,
)
except ValueError as err:
connection.send_error(
msg[ID], msg[ID],
ERR_INVALID_FORMAT, True, # Inclusion is already in progress
err.args[0],
) )
return # Check for nodes that have been added but not fully included
for node in controller.nodes.values():
if node.status != NodeStatus.DEAD and not node.ready:
forward_node_added(
node,
not node.is_secure,
None,
)
else:
try:
result = await controller.async_begin_inclusion(
INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value],
force_security=force_security,
provisioning=provisioning,
dsk=dsk,
)
except ValueError as err:
connection.send_error(
msg[ID],
ERR_INVALID_FORMAT,
err.args[0],
)
return
connection.send_result( connection.send_result(
msg[ID], msg[ID],
result, result,
) )
@websocket_api.require_admin @websocket_api.require_admin

View File

@ -5,7 +5,7 @@ from http import HTTPStatus
from io import BytesIO from io import BytesIO
import json import json
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import PropertyMock, patch
import pytest import pytest
from zwave_js_server.const import ( from zwave_js_server.const import (
@ -489,6 +489,7 @@ async def test_node_alerts(
async def test_add_node( async def test_add_node(
hass: HomeAssistant, hass: HomeAssistant,
nortek_thermostat,
nortek_thermostat_added_event, nortek_thermostat_added_event,
integration, integration,
client, client,
@ -936,12 +937,46 @@ async def test_add_node(
assert msg["error"]["code"] == "zwave_error" assert msg["error"]["code"] == "zwave_error"
assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message"
# Test inclusion already in progress
client.async_send_command.reset_mock()
type(client.driver.controller).inclusion_state = PropertyMock(
return_value=InclusionState.INCLUDING
)
# Create a node that's not ready
node_data = deepcopy(nortek_thermostat.data) # Copy to allow modification in tests.
node_data["ready"] = False
node_data["values"] = {}
node_data["endpoints"] = {}
node = Node(client, node_data)
client.driver.controller.nodes[node.node_id] = node
await ws_client.send_json(
{
ID: 11,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
# Verify no command was sent since inclusion is already in progress
assert len(client.async_send_command.call_args_list) == 0
# Verify we got a node added event
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "node added"
assert msg["event"]["node"]["node_id"] == node.node_id
# Test sending command with not loaded entry fails # Test sending command with not loaded entry fails
await hass.config_entries.async_unload(entry.entry_id) await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
await ws_client.send_json( await ws_client.send_json(
{ID: 11, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} {ID: 12, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id}
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()