Skip to content

Commit

Permalink
Resume adding Z-Wave device if the page is refreshed (home-assistant#…
Browse files Browse the repository at this point in the history
…129081)

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

* add test

* address PR comments
  • Loading branch information
MindFreeze authored and noahhusby committed Oct 25, 2024
1 parent c409d5f commit d87a0f0
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 37 deletions.
95 changes: 60 additions & 35 deletions homeassistant/components/zwave_js/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
from zwave_js_server.const import (
CommandClass,
ExclusionStrategy,
InclusionState,
InclusionStrategy,
LogLevel,
NodeStatus,
Protocols,
ProvisioningEntryStatus,
QRCodeVersion,
Expand Down Expand Up @@ -693,6 +695,30 @@ def forward_dsk(event: dict) -> None:
)
)

@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
def forward_requested_grant(event: dict) -> None:
connection.send_message(
Expand Down Expand Up @@ -727,25 +753,10 @@ def node_found(event: dict) -> None:

@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,
"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}
)
forward_node_added(
event["node"],
event["result"].get("lowSecurity", False),
event["result"].get("lowSecurityReason"),
)

@callback
Expand Down Expand Up @@ -777,25 +788,39 @@ def device_registered(device: dr.DeviceEntry) -> None:
]
msg[DATA_UNSUBSCRIBE] = unsubs

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(
if controller.inclusion_state == InclusionState.INCLUDING:
connection.send_result(
msg[ID],
ERR_INVALID_FORMAT,
err.args[0],
True, # Inclusion is already in progress
)
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(
msg[ID],
result,
)
connection.send_result(
msg[ID],
result,
)


@websocket_api.require_admin
Expand Down
39 changes: 37 additions & 2 deletions tests/components/zwave_js/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from io import BytesIO
import json
from typing import Any
from unittest.mock import patch
from unittest.mock import PropertyMock, patch

import pytest
from zwave_js_server.const import (
Expand Down Expand Up @@ -489,6 +489,7 @@ async def test_node_alerts(

async def test_add_node(
hass: HomeAssistant,
nortek_thermostat,
nortek_thermostat_added_event,
integration,
client,
Expand Down Expand Up @@ -936,12 +937,46 @@ async def test_add_node(
assert msg["error"]["code"] == "zwave_error"
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
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

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()

Expand Down

0 comments on commit d87a0f0

Please sign in to comment.