mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 18:48:22 +00:00
Fix Z-Wave handling of driver ready event (#149879)
This commit is contained in:
parent
b2349ac2bd
commit
fea5c63bba
@ -105,7 +105,6 @@ from .const import (
|
||||
CONF_USB_PATH,
|
||||
CONF_USE_ADDON,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
EVENT_VALUE_UPDATED,
|
||||
LIB_LOGGER,
|
||||
@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData
|
||||
from .services import async_setup_services
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
DRIVER_READY_TIMEOUT = 60
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@ -368,6 +368,16 @@ class DriverEvents:
|
||||
)
|
||||
)
|
||||
|
||||
# listen for driver ready event to reload the config entry
|
||||
self.config_entry.async_on_unload(
|
||||
driver.on(
|
||||
"driver ready",
|
||||
lambda _: self.hass.config_entries.async_schedule_reload(
|
||||
self.config_entry.entry_id
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# listen for new nodes being added to the mesh
|
||||
self.config_entry.async_on_unload(
|
||||
controller.on(
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from contextlib import suppress
|
||||
import dataclasses
|
||||
@ -87,7 +86,6 @@ from .const import (
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
LOGGER,
|
||||
USER_AGENT,
|
||||
@ -98,6 +96,7 @@ from .helpers import (
|
||||
async_get_node_from_device_id,
|
||||
async_get_provisioning_entry_from_device_id,
|
||||
async_get_version_info,
|
||||
async_wait_for_driver_ready_event,
|
||||
get_device_id,
|
||||
)
|
||||
|
||||
@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller(
|
||||
connection.send_result(msg[ID], device.id)
|
||||
async_cleanup()
|
||||
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
wait_driver_ready = asyncio.Event()
|
||||
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
async_dispatcher_connect(
|
||||
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added
|
||||
),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
|
||||
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
|
||||
|
||||
await driver.async_hard_reset()
|
||||
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
|
||||
await wait_for_driver_ready()
|
||||
# When resetting the controller, the controller home id is also changed.
|
||||
# The controller state in the client is stale after resetting the controller,
|
||||
# so get the new home id with a new client using the helper function.
|
||||
@ -2886,14 +2877,14 @@ async def websocket_hard_reset_controller(
|
||||
# The stale unique id needs to be handled by a repair flow,
|
||||
# after the config entry has been reloaded.
|
||||
LOGGER.error(
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller reset"
|
||||
)
|
||||
else:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
@ -3100,27 +3091,19 @@ async def websocket_restore_nvm(
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
wait_driver_ready = asyncio.Event()
|
||||
|
||||
# Set up subscription for progress events
|
||||
connection.subscriptions[msg["id"]] = async_cleanup
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
controller.on("nvm convert progress", forward_progress),
|
||||
controller.on("nvm restore progress", forward_progress),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
|
||||
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
|
||||
|
||||
await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False})
|
||||
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
|
||||
await wait_for_driver_ready()
|
||||
# When restoring the NVM to the controller, the controller home id is also changed.
|
||||
# The controller state in the client is stale after restoring the NVM,
|
||||
# so get the new home id with a new client using the helper function.
|
||||
@ -3133,14 +3116,13 @@ async def websocket_restore_nvm(
|
||||
# The stale unique id needs to be handled by a repair flow,
|
||||
# after the config entry has been reloaded.
|
||||
LOGGER.error(
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller NVM restore"
|
||||
)
|
||||
else:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
connection.send_message(
|
||||
@ -3152,3 +3134,4 @@ async def websocket_restore_nvm(
|
||||
)
|
||||
)
|
||||
connection.send_result(msg[ID])
|
||||
async_cleanup()
|
||||
|
@ -62,9 +62,12 @@ from .const import (
|
||||
CONF_USB_PATH,
|
||||
CONF_USE_ADDON,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
)
|
||||
from .helpers import CannotConnect, async_get_version_info
|
||||
from .helpers import (
|
||||
CannotConnect,
|
||||
async_get_version_info,
|
||||
async_wait_for_driver_ready_event,
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -1396,19 +1399,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
event["bytesWritten"] / event["total"] * 0.5 + 0.5
|
||||
)
|
||||
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
driver = self._get_driver()
|
||||
controller = driver.controller
|
||||
wait_driver_ready = asyncio.Event()
|
||||
unsubs = [
|
||||
controller.on("nvm convert progress", forward_progress),
|
||||
controller.on("nvm restore progress", forward_progress),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
|
||||
wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver)
|
||||
|
||||
try:
|
||||
await controller.async_restore_nvm(
|
||||
self.backup_data, {"preserveRoutes": False}
|
||||
@ -1417,8 +1416,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
raise AbortFlow(f"Failed to restore network: {err}") from err
|
||||
else:
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
await wait_for_driver_ready()
|
||||
try:
|
||||
version_info = await async_get_version_info(
|
||||
self.hass, config_entry.data[CONF_URL]
|
||||
@ -1435,10 +1433,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
await self.hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
# Reload the config entry two times to clean up
|
||||
# the stale device entry.
|
||||
# The config entry will be also be reloaded when the driver is ready,
|
||||
# by the listener in the package module,
|
||||
# and two reloads are needed to clean up the stale controller device entry.
|
||||
# Since both the old and the new controller have the same node id,
|
||||
# but different hardware identifiers, the integration
|
||||
# will create a new device for the new controller, on the first reload,
|
||||
|
@ -201,7 +201,3 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE,
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
|
||||
}
|
||||
|
||||
# Other constants
|
||||
|
||||
DRIVER_READY_TIMEOUT = 60
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import astuple, dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@ -56,6 +56,7 @@ from .const import (
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
DRIVER_READY_EVENT_TIMEOUT = 60
|
||||
SERVER_VERSION_TIMEOUT = 10
|
||||
|
||||
|
||||
@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
|
||||
return version_info
|
||||
|
||||
|
||||
@callback
|
||||
def async_wait_for_driver_ready_event(
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
driver: Driver,
|
||||
) -> Callable[[], Coroutine[Any, Any, None]]:
|
||||
"""Wait for the driver ready event and the config entry reload.
|
||||
|
||||
When the driver ready event is received
|
||||
the config entry will be reloaded by the integration.
|
||||
This function helps wait for that to happen
|
||||
before proceeding with further actions.
|
||||
|
||||
If the config entry is reloaded for another reason,
|
||||
this function will not wait for it to be reloaded again.
|
||||
|
||||
Raises TimeoutError if the driver ready event and reload
|
||||
is not received within the specified timeout.
|
||||
"""
|
||||
driver_ready_event_received = asyncio.Event()
|
||||
config_entry_reloaded = asyncio.Event()
|
||||
unsubscribers: list[Callable[[], None]] = []
|
||||
|
||||
@callback
|
||||
def driver_ready_received(event: dict) -> None:
|
||||
"""Receive the driver ready event."""
|
||||
driver_ready_event_received.set()
|
||||
|
||||
unsubscribers.append(driver.once("driver ready", driver_ready_received))
|
||||
|
||||
@callback
|
||||
def on_config_entry_state_change() -> None:
|
||||
"""Check config entry was loaded after driver ready event."""
|
||||
if config_entry.state is ConfigEntryState.LOADED:
|
||||
config_entry_reloaded.set()
|
||||
|
||||
unsubscribers.append(
|
||||
config_entry.async_on_state_change(on_config_entry_state_change)
|
||||
)
|
||||
|
||||
async def wait_for_events() -> None:
|
||||
try:
|
||||
async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT):
|
||||
await asyncio.gather(
|
||||
driver_ready_event_received.wait(), config_entry_reloaded.wait()
|
||||
)
|
||||
finally:
|
||||
for unsubscribe in unsubscribers:
|
||||
unsubscribe()
|
||||
|
||||
return wait_for_events
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Indicate connection error."""
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test the Z-Wave JS Websocket API."""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
@ -5109,17 +5110,12 @@ async def test_hard_reset_controller(
|
||||
ws_client = await hass_ws_client(hass)
|
||||
assert entry.unique_id == "3245146787"
|
||||
|
||||
async def async_send_command_driver_ready(
|
||||
message: dict[str, Any],
|
||||
require_schema: int | None = None,
|
||||
) -> dict:
|
||||
"""Send a command and get a response."""
|
||||
async def mock_driver_hard_reset() -> None:
|
||||
client.driver.emit(
|
||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
||||
)
|
||||
return {}
|
||||
|
||||
client.async_send_command.side_effect = async_send_command_driver_ready
|
||||
client.driver.async_hard_reset = AsyncMock(side_effect=mock_driver_hard_reset)
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
@ -5128,6 +5124,7 @@ async def test_hard_reset_controller(
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
||||
@ -5135,16 +5132,10 @@ async def test_hard_reset_controller(
|
||||
assert device is not None
|
||||
assert msg["result"] == device.id
|
||||
assert msg["success"]
|
||||
|
||||
assert client.async_send_command.call_count == 3
|
||||
# The first call is the relevant hard reset command.
|
||||
# 25 is the require_schema parameter.
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{"command": "driver.hard_reset"}, 25
|
||||
)
|
||||
assert client.driver.async_hard_reset.call_count == 1
|
||||
assert entry.unique_id == "1234"
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.driver.async_hard_reset.reset_mock()
|
||||
|
||||
# Test client connect error when getting the server version.
|
||||
|
||||
@ -5158,6 +5149,7 @@ async def test_hard_reset_controller(
|
||||
)
|
||||
|
||||
msg = await ws_client.receive_json()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
||||
@ -5165,33 +5157,24 @@ async def test_hard_reset_controller(
|
||||
assert device is not None
|
||||
assert msg["result"] == device.id
|
||||
assert msg["success"]
|
||||
|
||||
assert client.async_send_command.call_count == 3
|
||||
# The first call is the relevant hard reset command.
|
||||
# 25 is the require_schema parameter.
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{"command": "driver.hard_reset"}, 25
|
||||
)
|
||||
assert client.driver.async_hard_reset.call_count == 1
|
||||
assert (
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller reset"
|
||||
) in caplog.text
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.driver.async_hard_reset.reset_mock()
|
||||
get_server_version.side_effect = None
|
||||
|
||||
# Test sending command with driver not ready and timeout.
|
||||
|
||||
async def async_send_command_no_driver_ready(
|
||||
message: dict[str, Any],
|
||||
require_schema: int | None = None,
|
||||
) -> dict:
|
||||
"""Send a command and get a response."""
|
||||
return {}
|
||||
async def mock_driver_hard_reset_no_driver_ready() -> None:
|
||||
pass
|
||||
|
||||
client.async_send_command.side_effect = async_send_command_no_driver_ready
|
||||
client.driver.async_hard_reset.side_effect = mock_driver_hard_reset_no_driver_ready
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
|
||||
"homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT",
|
||||
new=0,
|
||||
):
|
||||
await ws_client.send_json_auto_id(
|
||||
@ -5201,6 +5184,7 @@ async def test_hard_reset_controller(
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
||||
@ -5208,21 +5192,15 @@ async def test_hard_reset_controller(
|
||||
assert device is not None
|
||||
assert msg["result"] == device.id
|
||||
assert msg["success"]
|
||||
assert client.driver.async_hard_reset.call_count == 1
|
||||
|
||||
assert client.async_send_command.call_count == 3
|
||||
# The first call is the relevant hard reset command.
|
||||
# 25 is the require_schema parameter.
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{"command": "driver.hard_reset"}, 25
|
||||
)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.driver.async_hard_reset.reset_mock()
|
||||
|
||||
# Test FailedZWaveCommand is caught
|
||||
with patch(
|
||||
"zwave_js_server.model.driver.Driver.async_hard_reset",
|
||||
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
|
||||
):
|
||||
client.driver.async_hard_reset.side_effect = FailedZWaveCommand(
|
||||
"failed_command", 1, "error message"
|
||||
)
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/hard_reset_controller",
|
||||
@ -5234,6 +5212,9 @@ async def test_hard_reset_controller(
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "zwave_error"
|
||||
assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message"
|
||||
assert client.driver.async_hard_reset.call_count == 1
|
||||
|
||||
client.driver.async_hard_reset.side_effect = None
|
||||
|
||||
# Test sending command with not loaded entry fails
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
@ -5578,17 +5559,24 @@ async def test_restore_nvm(
|
||||
# Set up mocks for the controller events
|
||||
controller = client.driver.controller
|
||||
|
||||
async def async_send_command_driver_ready(
|
||||
message: dict[str, Any],
|
||||
require_schema: int | None = None,
|
||||
) -> dict:
|
||||
"""Send a command and get a response."""
|
||||
async def mock_restore_nvm_base64(
|
||||
self, base64_data: str, options: dict[str, bool] | None = None
|
||||
) -> None:
|
||||
controller.emit(
|
||||
"nvm convert progress",
|
||||
{"event": "nvm convert progress", "bytesRead": 100, "total": 200},
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
controller.emit(
|
||||
"nvm restore progress",
|
||||
{"event": "nvm restore progress", "bytesWritten": 150, "total": 200},
|
||||
)
|
||||
controller.data["homeId"] = 3245146787
|
||||
client.driver.emit(
|
||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
||||
)
|
||||
return {}
|
||||
|
||||
client.async_send_command.side_effect = async_send_command_driver_ready
|
||||
controller.async_restore_nvm_base64 = AsyncMock(side_effect=mock_restore_nvm_base64)
|
||||
|
||||
# Send the subscription request
|
||||
await ws_client.send_json_auto_id(
|
||||
@ -5599,7 +5587,19 @@ async def test_restore_nvm(
|
||||
}
|
||||
)
|
||||
|
||||
# Verify the finished event first
|
||||
# Verify the convert progress event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm convert progress"
|
||||
assert msg["event"]["bytesRead"] == 100
|
||||
assert msg["event"]["total"] == 200
|
||||
|
||||
# Verify the restore progress event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm restore progress"
|
||||
assert msg["event"]["bytesWritten"] == 150
|
||||
assert msg["event"]["total"] == 200
|
||||
|
||||
# Verify the finished event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["type"] == "event"
|
||||
assert msg["event"]["event"] == "finished"
|
||||
@ -5609,53 +5609,18 @@ async def test_restore_nvm(
|
||||
assert msg["type"] == "result"
|
||||
assert msg["success"] is True
|
||||
|
||||
# Simulate progress events
|
||||
event = Event(
|
||||
"nvm restore progress",
|
||||
{
|
||||
"source": "controller",
|
||||
"event": "nvm restore progress",
|
||||
"bytesWritten": 25,
|
||||
"total": 100,
|
||||
},
|
||||
)
|
||||
controller.receive_event(event)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm restore progress"
|
||||
assert msg["event"]["bytesWritten"] == 25
|
||||
assert msg["event"]["total"] == 100
|
||||
|
||||
event = Event(
|
||||
"nvm restore progress",
|
||||
{
|
||||
"source": "controller",
|
||||
"event": "nvm restore progress",
|
||||
"bytesWritten": 50,
|
||||
"total": 100,
|
||||
},
|
||||
)
|
||||
controller.receive_event(event)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm restore progress"
|
||||
assert msg["event"]["bytesWritten"] == 50
|
||||
assert msg["event"]["total"] == 100
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the restore was called
|
||||
# The first call is the relevant one for nvm restore.
|
||||
assert client.async_send_command.call_count == 3
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{
|
||||
"command": "controller.restore_nvm",
|
||||
"nvmData": "dGVzdA==",
|
||||
"migrateOptions": {"preserveRoutes": False},
|
||||
},
|
||||
require_schema=42,
|
||||
assert controller.async_restore_nvm_base64.call_count == 1
|
||||
assert controller.async_restore_nvm_base64.call_args == call(
|
||||
"dGVzdA==",
|
||||
{"preserveRoutes": False},
|
||||
)
|
||||
assert entry.unique_id == "1234"
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
controller.async_restore_nvm_base64.reset_mock()
|
||||
|
||||
# Test client connect error when getting the server version.
|
||||
|
||||
@ -5670,7 +5635,19 @@ async def test_restore_nvm(
|
||||
}
|
||||
)
|
||||
|
||||
# Verify the finished event first
|
||||
# Verify the convert progress event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm convert progress"
|
||||
assert msg["event"]["bytesRead"] == 100
|
||||
assert msg["event"]["total"] == 200
|
||||
|
||||
# Verify the restore progress event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["event"]["event"] == "nvm restore progress"
|
||||
assert msg["event"]["bytesWritten"] == 150
|
||||
assert msg["event"]["total"] == 200
|
||||
|
||||
# Verify the finished event
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["type"] == "event"
|
||||
assert msg["event"]["event"] == "finished"
|
||||
@ -5680,47 +5657,46 @@ async def test_restore_nvm(
|
||||
assert msg["type"] == "result"
|
||||
assert msg["success"] is True
|
||||
|
||||
assert client.async_send_command.call_count == 3
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{
|
||||
"command": "controller.restore_nvm",
|
||||
"nvmData": "dGVzdA==",
|
||||
"migrateOptions": {"preserveRoutes": False},
|
||||
},
|
||||
require_schema=42,
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert controller.async_restore_nvm_base64.call_count == 1
|
||||
assert controller.async_restore_nvm_base64.call_args == call(
|
||||
"dGVzdA==",
|
||||
{"preserveRoutes": False},
|
||||
)
|
||||
assert (
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller NVM restore"
|
||||
) in caplog.text
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
controller.async_restore_nvm_base64.reset_mock()
|
||||
get_server_version.side_effect = None
|
||||
|
||||
# Test sending command with driver not ready and timeout.
|
||||
# Test sending command without driver ready event causing timeout.
|
||||
|
||||
async def async_send_command_no_driver_ready(
|
||||
message: dict[str, Any],
|
||||
require_schema: int | None = None,
|
||||
) -> dict:
|
||||
"""Send a command and get a response."""
|
||||
return {}
|
||||
async def mock_restore_nvm_without_driver_ready(
|
||||
data: bytes, options: dict[str, bool] | None = None
|
||||
):
|
||||
controller.data["homeId"] = 3245146787
|
||||
|
||||
client.async_send_command.side_effect = async_send_command_no_driver_ready
|
||||
controller.async_restore_nvm_base64.side_effect = (
|
||||
mock_restore_nvm_without_driver_ready
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
|
||||
"homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT",
|
||||
new=0,
|
||||
):
|
||||
# Send the subscription request
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "zwave_js/restore_nvm",
|
||||
"entry_id": integration.entry_id,
|
||||
"entry_id": entry.entry_id,
|
||||
"data": "dGVzdA==", # base64 encoded "test"
|
||||
}
|
||||
)
|
||||
|
||||
# Verify the finished event first
|
||||
# Verify the finished event
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["type"] == "event"
|
||||
@ -5734,29 +5710,24 @@ async def test_restore_nvm(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the restore was called
|
||||
# The first call is the relevant one for nvm restore.
|
||||
assert client.async_send_command.call_count == 3
|
||||
assert client.async_send_command.call_args_list[0] == call(
|
||||
{
|
||||
"command": "controller.restore_nvm",
|
||||
"nvmData": "dGVzdA==",
|
||||
"migrateOptions": {"preserveRoutes": False},
|
||||
},
|
||||
require_schema=42,
|
||||
assert controller.async_restore_nvm_base64.call_count == 1
|
||||
assert controller.async_restore_nvm_base64.call_args == call(
|
||||
"dGVzdA==",
|
||||
{"preserveRoutes": False},
|
||||
)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
controller.async_restore_nvm_base64.reset_mock()
|
||||
|
||||
# Test restore failure
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64",
|
||||
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
|
||||
):
|
||||
controller.async_restore_nvm_base64.side_effect = FailedZWaveCommand(
|
||||
"failed_command", 1, "error message"
|
||||
)
|
||||
|
||||
# Send the subscription request
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "zwave_js/restore_nvm",
|
||||
"entry_id": integration.entry_id,
|
||||
"entry_id": entry.entry_id,
|
||||
"data": "dGVzdA==", # base64 encoded "test"
|
||||
}
|
||||
)
|
||||
@ -5766,6 +5737,15 @@ async def test_restore_nvm(
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "zwave_error"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the restore was called
|
||||
assert controller.async_restore_nvm_base64.call_count == 1
|
||||
assert controller.async_restore_nvm_base64.call_args == call(
|
||||
"dGVzdA==",
|
||||
{"preserveRoutes": False},
|
||||
)
|
||||
|
||||
# Test entry_id not found
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
@ -5779,13 +5759,13 @@ async def test_restore_nvm(
|
||||
assert msg["error"]["code"] == "not_found"
|
||||
|
||||
# Test config entry not loaded
|
||||
await hass.config_entries.async_unload(integration.entry_id)
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "zwave_js/restore_nvm",
|
||||
"entry_id": integration.entry_id,
|
||||
"entry_id": entry.entry_id,
|
||||
"data": "dGVzdA==", # base64 encoded "test"
|
||||
}
|
||||
)
|
||||
|
@ -1101,7 +1101,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout(
|
||||
assert restart_addon.call_args == call("core_zwave_js")
|
||||
|
||||
with patch(
|
||||
("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"),
|
||||
("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"),
|
||||
new=0,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
@ -1111,7 +1111,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout(
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 4
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
@ -3897,7 +3897,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout(
|
||||
assert restart_addon.call_args == call("core_zwave_js")
|
||||
|
||||
with patch(
|
||||
("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"),
|
||||
("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"),
|
||||
new=0,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
@ -3907,7 +3907,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout(
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 4
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
|
@ -2262,3 +2262,38 @@ async def test_entity_available_when_node_dead(
|
||||
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_driver_ready_event(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test receiving a driver ready event."""
|
||||
config_entry = integration
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
config_entry_state_changes: list[ConfigEntryState] = []
|
||||
|
||||
def on_config_entry_state_change() -> None:
|
||||
"""Collect config entry state changes."""
|
||||
config_entry_state_changes.append(config_entry.state)
|
||||
|
||||
config_entry.async_on_state_change(on_config_entry_state_change)
|
||||
|
||||
driver_ready = Event(
|
||||
type="driver ready",
|
||||
data={
|
||||
"source": "driver",
|
||||
"event": "driver ready",
|
||||
},
|
||||
)
|
||||
|
||||
client.driver.receive_event(driver_ready)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(config_entry_state_changes) == 4
|
||||
assert config_entry_state_changes[0] == ConfigEntryState.UNLOAD_IN_PROGRESS
|
||||
assert config_entry_state_changes[1] == ConfigEntryState.NOT_LOADED
|
||||
assert config_entry_state_changes[2] == ConfigEntryState.SETUP_IN_PROGRESS
|
||||
assert config_entry_state_changes[3] == ConfigEntryState.LOADED
|
||||
|
Loading…
x
Reference in New Issue
Block a user