Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading