Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 deletions homeassistant/components/zwave_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def register_node_in_dev_reg(
dev_reg: device_registry.DeviceRegistry,
client: ZwaveClient,
node: ZwaveNode,
remove_device_func: Callable[[device_registry.DeviceEntry], None],
remove_device_func: Callable[[device_registry.DeviceEntry], None] | None = None,
) -> device_registry.DeviceEntry:
"""Register node in dev reg."""
device_id = get_device_id(client, node)
Expand All @@ -141,7 +141,8 @@ def register_node_in_dev_reg(
# Replace the device if it can be determined that this node is not the
# same product as it was previously.
if (
device_id_ext
remove_device_func
Comment thread
raman325 marked this conversation as resolved.
Outdated
and device_id_ext
and device
and len(device.identifiers) == 2
and device_id_ext not in device.identifiers
Expand Down
148 changes: 55 additions & 93 deletions homeassistant/components/zwave_js/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@
EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER,
)
from .helpers import async_enable_statistics, update_data_collection_preference
from .helpers import (
async_enable_statistics,
async_get_node_from_device_id,
update_data_collection_preference,
)
from .migrate import (
ZWaveMigrationData,
async_get_migration_data,
Expand All @@ -83,6 +87,7 @@
ENTRY_ID = "entry_id"
ERR_NOT_LOADED = "not_loaded"
NODE_ID = "node_id"
DEVICE_ID = "device_id"
COMMAND_CLASS_ID = "command_class_id"
TYPE = "type"
PROPERTY = "property"
Expand Down Expand Up @@ -254,21 +259,20 @@ async def async_get_entry_func(
def async_get_node(orig_func: Callable) -> Callable:
"""Decorate async function to get node."""

@async_get_entry
@wraps(orig_func)
async def async_get_node_func(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
entry: ConfigEntry,
client: Client,
hass: HomeAssistant, connection: ActiveConnection, msg: dict
) -> None:
"""Provide user specific data and store to function."""
node_id = msg[NODE_ID]
node = client.driver.controller.nodes.get(node_id)
device_id = msg[DEVICE_ID]

if node is None:
connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found")
try:
node = async_get_node_from_device_id(hass, device_id)
except ValueError as err:
error_code = ERR_NOT_FOUND
if "loaded" in err.args[0]:
error_code = ERR_NOT_LOADED
connection.send_error(msg[ID], error_code, err.args[0])
return
await orig_func(hass, connection, msg, node)

Expand Down Expand Up @@ -299,13 +303,26 @@ async def async_handle_failed_command_func(
return async_handle_failed_command_func


def node_status(node: Node) -> dict[str, Any]:
"""Get node status."""
return {
"node_id": node.node_id,
"is_routing": node.is_routing,
"status": node.status,
"is_secure": node.is_secure,
"ready": node.ready,
"zwave_plus_version": node.zwave_plus_version,
"highest_security_class": node.highest_security_class,
"is_controller_node": node.is_controller_node,
}


@callback
def async_register_api(hass: HomeAssistant) -> None:
"""Register all of our api endpoints."""
websocket_api.async_register_command(hass, websocket_network_status)
websocket_api.async_register_command(hass, websocket_node_status)
websocket_api.async_register_command(hass, websocket_node_metadata)
websocket_api.async_register_command(hass, websocket_ping_node)
websocket_api.async_register_command(hass, websocket_add_node)
websocket_api.async_register_command(hass, websocket_grant_security_classes)
websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin)
Expand Down Expand Up @@ -395,7 +412,9 @@ async def websocket_network_status(
"supports_timers": controller.supports_timers,
"is_heal_network_active": controller.is_heal_network_active,
"inclusion_state": controller.inclusion_state,
"nodes": list(client.driver.controller.nodes),
"nodes": [
node_status(node) for node in client.driver.controller.nodes.values()
],
},
}
connection.send_result(
Expand All @@ -407,8 +426,7 @@ async def websocket_network_status(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/node_ready",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down Expand Up @@ -443,8 +461,7 @@ def async_cleanup() -> None:
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/node_status",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand All @@ -456,27 +473,13 @@ async def websocket_node_status(
node: Node,
) -> None:
"""Get the status of a Z-Wave JS node."""
data = {
"node_id": node.node_id,
"is_routing": node.is_routing,
"status": node.status,
"is_secure": node.is_secure,
"ready": node.ready,
"zwave_plus_version": node.zwave_plus_version,
"highest_security_class": node.highest_security_class,
"is_controller_node": node.is_controller_node,
}
connection.send_result(
msg[ID],
data,
)
connection.send_result(msg[ID], node_status(node))


@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/node_metadata",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down Expand Up @@ -504,30 +507,6 @@ async def websocket_node_metadata(
)


@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/ping_node",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
}
)
@websocket_api.async_response
@async_handle_failed_command
@async_get_node
async def websocket_ping_node(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
node: Node,
) -> None:
"""Ping a Z-Wave JS node."""
result = await node.async_ping()
connection.send_result(
msg[ID],
result,
)


@websocket_api.require_admin
@websocket_api.websocket_command(
{
Expand Down Expand Up @@ -1189,23 +1168,20 @@ def device_registered(device: DeviceEntry) -> None:
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/remove_failed_node",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
@async_handle_failed_command
@async_get_entry
@async_get_node
Comment thread
MartinHjelmare marked this conversation as resolved.
async def websocket_remove_failed_node(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
entry: ConfigEntry,
client: Client,
node: Node,
) -> None:
"""Remove a failed node from the Z-Wave network."""
controller = client.driver.controller
node_id = msg[NODE_ID]
controller = node.client.driver.controller

@callback
def async_cleanup() -> None:
Expand All @@ -1215,10 +1191,7 @@ def async_cleanup() -> None:

@callback
def node_removed(event: dict) -> None:
node = event["node"]
node_details = {
"node_id": node.node_id,
}
node_details = {"node_id": event["node"].node_id}

connection.send_message(
websocket_api.event_message(
Expand All @@ -1229,7 +1202,7 @@ def node_removed(event: dict) -> None:
connection.subscriptions[msg["id"]] = async_cleanup
msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)]

result = await controller.async_remove_failed_node(node_id)
result = await controller.async_remove_failed_node(node.node_id)
connection.send_result(
msg[ID],
result,
Expand Down Expand Up @@ -1335,24 +1308,21 @@ async def websocket_stop_healing_network(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/heal_node",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
@async_handle_failed_command
@async_get_entry
@async_get_node
Comment thread
MartinHjelmare marked this conversation as resolved.
async def websocket_heal_node(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
entry: ConfigEntry,
client: Client,
node: Node,
) -> None:
"""Heal a node on the Z-Wave network."""
controller = client.driver.controller
node_id = msg[NODE_ID]
result = await controller.async_heal_node(node_id)
controller = node.client.driver.controller
result = await controller.async_heal_node(node.node_id)
connection.send_result(
msg[ID],
result,
Expand All @@ -1363,8 +1333,7 @@ async def websocket_heal_node(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/refresh_node_info",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
},
)
@websocket_api.async_response
Expand Down Expand Up @@ -1414,8 +1383,7 @@ def forward_stage(event: dict) -> None:
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/refresh_node_values",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
},
)
@websocket_api.async_response
Expand All @@ -1436,8 +1404,7 @@ async def websocket_refresh_node_values(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/refresh_node_cc_values",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
vol.Required(COMMAND_CLASS_ID): int,
},
)
Expand Down Expand Up @@ -1469,8 +1436,7 @@ async def websocket_refresh_node_cc_values(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/set_config_parameter",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
vol.Required(PROPERTY): int,
vol.Optional(PROPERTY_KEY): int,
vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA),
Expand Down Expand Up @@ -1521,8 +1487,7 @@ async def websocket_set_config_parameter(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/get_config_parameters",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down Expand Up @@ -1762,8 +1727,7 @@ async def websocket_data_collection_status(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/abort_firmware_update",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down Expand Up @@ -1794,8 +1758,7 @@ def _get_firmware_update_progress_dict(
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/subscribe_firmware_update_status",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down Expand Up @@ -2031,8 +1994,7 @@ def _get_node_statistics_dict(statistics: NodeStatistics) -> dict[str, int]:
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/subscribe_node_statistics",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
vol.Required(DEVICE_ID): str,
}
)
@websocket_api.async_response
Expand Down
19 changes: 9 additions & 10 deletions homeassistant/components/zwave_js/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,24 +178,22 @@ def async_get_node_from_device_id(
# Use device config entry ID's to validate that this is a valid zwave_js device
# and to get the client
config_entry_ids = device_entry.config_entries
config_entry_id = next(
entry = next(
(
config_entry_id
for config_entry_id in config_entry_ids
if cast(
ConfigEntry,
hass.config_entries.async_get_entry(config_entry_id),
).domain
== DOMAIN
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in config_entry_ids
),
None,
)
if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]:
if entry and entry.state != ConfigEntryState.LOADED:
raise ValueError(f"Device {device_id} config entry is not loaded")
if entry is None or entry.entry_id not in hass.data[DOMAIN]:
raise ValueError(
f"Device {device_id} is not from an existing zwave_js config entry"
)

client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]
client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]

# Get node ID from device identifier, perform some validation, and then get the
# node
Expand Down Expand Up @@ -399,6 +397,7 @@ def async_is_device_config_entry_not_loaded(
raise ValueError(f"Device {device_id} not found")
return any(
(entry := hass.config_entries.async_get_entry(entry_id))
and entry.domain == DOMAIN
Comment thread
raman325 marked this conversation as resolved.
Outdated
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
and entry.state != ConfigEntryState.LOADED
for entry_id in device.config_entries
)
Expand Down
Loading