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_USB_PATH,
|
||||||
CONF_USE_ADDON,
|
CONF_USE_ADDON,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DRIVER_READY_TIMEOUT,
|
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||||
EVENT_VALUE_UPDATED,
|
EVENT_VALUE_UPDATED,
|
||||||
LIB_LOGGER,
|
LIB_LOGGER,
|
||||||
@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData
|
|||||||
from .services import async_setup_services
|
from .services import async_setup_services
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 10
|
CONNECT_TIMEOUT = 10
|
||||||
|
DRIVER_READY_TIMEOUT = 60
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
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
|
# listen for new nodes being added to the mesh
|
||||||
self.config_entry.async_on_unload(
|
self.config_entry.async_on_unload(
|
||||||
controller.on(
|
controller.on(
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import dataclasses
|
import dataclasses
|
||||||
@ -87,7 +86,6 @@ from .const import (
|
|||||||
CONF_DATA_COLLECTION_OPTED_IN,
|
CONF_DATA_COLLECTION_OPTED_IN,
|
||||||
CONF_INSTALLER_MODE,
|
CONF_INSTALLER_MODE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DRIVER_READY_TIMEOUT,
|
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
USER_AGENT,
|
USER_AGENT,
|
||||||
@ -98,6 +96,7 @@ from .helpers import (
|
|||||||
async_get_node_from_device_id,
|
async_get_node_from_device_id,
|
||||||
async_get_provisioning_entry_from_device_id,
|
async_get_provisioning_entry_from_device_id,
|
||||||
async_get_version_info,
|
async_get_version_info,
|
||||||
|
async_wait_for_driver_ready_event,
|
||||||
get_device_id,
|
get_device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller(
|
|||||||
connection.send_result(msg[ID], device.id)
|
connection.send_result(msg[ID], device.id)
|
||||||
async_cleanup()
|
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 = [
|
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added
|
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()
|
await driver.async_hard_reset()
|
||||||
|
|
||||||
with suppress(TimeoutError):
|
with suppress(TimeoutError):
|
||||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
await wait_for_driver_ready()
|
||||||
await wait_driver_ready.wait()
|
|
||||||
|
|
||||||
# When resetting the controller, the controller home id is also changed.
|
# When resetting the controller, the controller home id is also changed.
|
||||||
# The controller state in the client is stale after resetting the controller,
|
# 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.
|
# 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,
|
# The stale unique id needs to be handled by a repair flow,
|
||||||
# after the config entry has been reloaded.
|
# after the config entry has been reloaded.
|
||||||
LOGGER.error(
|
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"
|
"unique id with new home id, after controller reset"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry, unique_id=str(version_info.home_id)
|
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(
|
@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
|
# Set up subscription for progress events
|
||||||
connection.subscriptions[msg["id"]] = async_cleanup
|
connection.subscriptions[msg["id"]] = async_cleanup
|
||||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||||
controller.on("nvm convert progress", forward_progress),
|
controller.on("nvm convert progress", forward_progress),
|
||||||
controller.on("nvm restore 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})
|
await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False})
|
||||||
|
|
||||||
with suppress(TimeoutError):
|
with suppress(TimeoutError):
|
||||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
await wait_for_driver_ready()
|
||||||
await wait_driver_ready.wait()
|
|
||||||
|
|
||||||
# When restoring the NVM to the controller, the controller home id is also changed.
|
# 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,
|
# 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.
|
# 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,
|
# The stale unique id needs to be handled by a repair flow,
|
||||||
# after the config entry has been reloaded.
|
# after the config entry has been reloaded.
|
||||||
LOGGER.error(
|
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"
|
"unique id with new home id, after controller NVM restore"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry, unique_id=str(version_info.home_id)
|
entry, unique_id=str(version_info.home_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
connection.send_message(
|
connection.send_message(
|
||||||
@ -3152,3 +3134,4 @@ async def websocket_restore_nvm(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
connection.send_result(msg[ID])
|
connection.send_result(msg[ID])
|
||||||
|
async_cleanup()
|
||||||
|
@ -62,9 +62,12 @@ from .const import (
|
|||||||
CONF_USB_PATH,
|
CONF_USB_PATH,
|
||||||
CONF_USE_ADDON,
|
CONF_USE_ADDON,
|
||||||
DOMAIN,
|
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
|
from .models import ZwaveJSConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -1396,19 +1399,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
event["bytesWritten"] / event["total"] * 0.5 + 0.5
|
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()
|
driver = self._get_driver()
|
||||||
controller = driver.controller
|
controller = driver.controller
|
||||||
wait_driver_ready = asyncio.Event()
|
|
||||||
unsubs = [
|
unsubs = [
|
||||||
controller.on("nvm convert progress", forward_progress),
|
controller.on("nvm convert progress", forward_progress),
|
||||||
controller.on("nvm restore 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:
|
try:
|
||||||
await controller.async_restore_nvm(
|
await controller.async_restore_nvm(
|
||||||
self.backup_data, {"preserveRoutes": False}
|
self.backup_data, {"preserveRoutes": False}
|
||||||
@ -1417,8 +1416,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
raise AbortFlow(f"Failed to restore network: {err}") from err
|
raise AbortFlow(f"Failed to restore network: {err}") from err
|
||||||
else:
|
else:
|
||||||
with suppress(TimeoutError):
|
with suppress(TimeoutError):
|
||||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
await wait_for_driver_ready()
|
||||||
await wait_driver_ready.wait()
|
|
||||||
try:
|
try:
|
||||||
version_info = await async_get_version_info(
|
version_info = await async_get_version_info(
|
||||||
self.hass, config_entry.data[CONF_URL]
|
self.hass, config_entry.data[CONF_URL]
|
||||||
@ -1435,10 +1433,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
config_entry, unique_id=str(version_info.home_id)
|
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 config entry will be also be reloaded when the driver is ready,
|
||||||
# the stale device entry.
|
# 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,
|
# Since both the old and the new controller have the same node id,
|
||||||
# but different hardware identifiers, the integration
|
# but different hardware identifiers, the integration
|
||||||
# will create a new device for the new controller, on the first reload,
|
# 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,
|
||||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
|
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Other constants
|
|
||||||
|
|
||||||
DRIVER_READY_TIMEOUT = 60
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import astuple, dataclass
|
from dataclasses import astuple, dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
@ -56,6 +56,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .models import ZwaveJSConfigEntry
|
from .models import ZwaveJSConfigEntry
|
||||||
|
|
||||||
|
DRIVER_READY_EVENT_TIMEOUT = 60
|
||||||
SERVER_VERSION_TIMEOUT = 10
|
SERVER_VERSION_TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
|
|||||||
return version_info
|
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):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Indicate connection error."""
|
"""Indicate connection error."""
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test the Z-Wave JS Websocket API."""
|
"""Test the Z-Wave JS Websocket API."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@ -5109,17 +5110,12 @@ async def test_hard_reset_controller(
|
|||||||
ws_client = await hass_ws_client(hass)
|
ws_client = await hass_ws_client(hass)
|
||||||
assert entry.unique_id == "3245146787"
|
assert entry.unique_id == "3245146787"
|
||||||
|
|
||||||
async def async_send_command_driver_ready(
|
async def mock_driver_hard_reset() -> None:
|
||||||
message: dict[str, Any],
|
|
||||||
require_schema: int | None = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Send a command and get a response."""
|
|
||||||
client.driver.emit(
|
client.driver.emit(
|
||||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
"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(
|
await ws_client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
@ -5128,6 +5124,7 @@ async def test_hard_reset_controller(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
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 device is not None
|
||||||
assert msg["result"] == device.id
|
assert msg["result"] == device.id
|
||||||
assert msg["success"]
|
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
|
|
||||||
)
|
|
||||||
assert entry.unique_id == "1234"
|
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.
|
# 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()
|
msg = await ws_client.receive_json()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
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 device is not None
|
||||||
assert msg["result"] == device.id
|
assert msg["result"] == device.id
|
||||||
assert msg["success"]
|
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
|
|
||||||
)
|
|
||||||
assert (
|
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"
|
"unique id with new home id, after controller reset"
|
||||||
) in caplog.text
|
) 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.
|
# Test sending command with driver not ready and timeout.
|
||||||
|
|
||||||
async def async_send_command_no_driver_ready(
|
async def mock_driver_hard_reset_no_driver_ready() -> None:
|
||||||
message: dict[str, Any],
|
pass
|
||||||
require_schema: int | None = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Send a command and get a response."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
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(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
|
"homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT",
|
||||||
new=0,
|
new=0,
|
||||||
):
|
):
|
||||||
await ws_client.send_json_auto_id(
|
await ws_client.send_json_auto_id(
|
||||||
@ -5201,6 +5184,7 @@ async def test_hard_reset_controller(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
||||||
@ -5208,32 +5192,29 @@ async def test_hard_reset_controller(
|
|||||||
assert device is not None
|
assert device is not None
|
||||||
assert msg["result"] == device.id
|
assert msg["result"] == device.id
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
assert client.driver.async_hard_reset.call_count == 1
|
||||||
|
|
||||||
assert client.async_send_command.call_count == 3
|
client.driver.async_hard_reset.reset_mock()
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Test FailedZWaveCommand is caught
|
# Test FailedZWaveCommand is caught
|
||||||
with patch(
|
client.driver.async_hard_reset.side_effect = FailedZWaveCommand(
|
||||||
"zwave_js_server.model.driver.Driver.async_hard_reset",
|
"failed_command", 1, "error message"
|
||||||
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
|
)
|
||||||
):
|
|
||||||
await ws_client.send_json_auto_id(
|
|
||||||
{
|
|
||||||
TYPE: "zwave_js/hard_reset_controller",
|
|
||||||
ENTRY_ID: entry.entry_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
msg = await ws_client.receive_json()
|
|
||||||
|
|
||||||
assert not msg["success"]
|
await ws_client.send_json_auto_id(
|
||||||
assert msg["error"]["code"] == "zwave_error"
|
{
|
||||||
assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message"
|
TYPE: "zwave_js/hard_reset_controller",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
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
|
# 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)
|
||||||
@ -5578,17 +5559,24 @@ async def test_restore_nvm(
|
|||||||
# Set up mocks for the controller events
|
# Set up mocks for the controller events
|
||||||
controller = client.driver.controller
|
controller = client.driver.controller
|
||||||
|
|
||||||
async def async_send_command_driver_ready(
|
async def mock_restore_nvm_base64(
|
||||||
message: dict[str, Any],
|
self, base64_data: str, options: dict[str, bool] | None = None
|
||||||
require_schema: int | None = None,
|
) -> None:
|
||||||
) -> dict:
|
controller.emit(
|
||||||
"""Send a command and get a response."""
|
"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(
|
client.driver.emit(
|
||||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
"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
|
# Send the subscription request
|
||||||
await ws_client.send_json_auto_id(
|
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()
|
msg = await ws_client.receive_json()
|
||||||
assert msg["type"] == "event"
|
assert msg["type"] == "event"
|
||||||
assert msg["event"]["event"] == "finished"
|
assert msg["event"]["event"] == "finished"
|
||||||
@ -5609,53 +5609,18 @@ async def test_restore_nvm(
|
|||||||
assert msg["type"] == "result"
|
assert msg["type"] == "result"
|
||||||
assert msg["success"] is True
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Verify the restore was called
|
# Verify the restore was called
|
||||||
# The first call is the relevant one for nvm restore.
|
# The first call is the relevant one for nvm restore.
|
||||||
assert client.async_send_command.call_count == 3
|
assert controller.async_restore_nvm_base64.call_count == 1
|
||||||
assert client.async_send_command.call_args_list[0] == call(
|
assert controller.async_restore_nvm_base64.call_args == call(
|
||||||
{
|
"dGVzdA==",
|
||||||
"command": "controller.restore_nvm",
|
{"preserveRoutes": False},
|
||||||
"nvmData": "dGVzdA==",
|
|
||||||
"migrateOptions": {"preserveRoutes": False},
|
|
||||||
},
|
|
||||||
require_schema=42,
|
|
||||||
)
|
)
|
||||||
assert entry.unique_id == "1234"
|
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.
|
# 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()
|
msg = await ws_client.receive_json()
|
||||||
assert msg["type"] == "event"
|
assert msg["type"] == "event"
|
||||||
assert msg["event"]["event"] == "finished"
|
assert msg["event"]["event"] == "finished"
|
||||||
@ -5680,47 +5657,46 @@ async def test_restore_nvm(
|
|||||||
assert msg["type"] == "result"
|
assert msg["type"] == "result"
|
||||||
assert msg["success"] is True
|
assert msg["success"] is True
|
||||||
|
|
||||||
assert client.async_send_command.call_count == 3
|
await hass.async_block_till_done()
|
||||||
assert client.async_send_command.call_args_list[0] == call(
|
|
||||||
{
|
assert controller.async_restore_nvm_base64.call_count == 1
|
||||||
"command": "controller.restore_nvm",
|
assert controller.async_restore_nvm_base64.call_args == call(
|
||||||
"nvmData": "dGVzdA==",
|
"dGVzdA==",
|
||||||
"migrateOptions": {"preserveRoutes": False},
|
{"preserveRoutes": False},
|
||||||
},
|
|
||||||
require_schema=42,
|
|
||||||
)
|
)
|
||||||
assert (
|
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"
|
"unique id with new home id, after controller NVM restore"
|
||||||
) in caplog.text
|
) 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(
|
async def mock_restore_nvm_without_driver_ready(
|
||||||
message: dict[str, Any],
|
data: bytes, options: dict[str, bool] | None = None
|
||||||
require_schema: int | None = None,
|
):
|
||||||
) -> dict:
|
controller.data["homeId"] = 3245146787
|
||||||
"""Send a command and get a response."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
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(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
|
"homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT",
|
||||||
new=0,
|
new=0,
|
||||||
):
|
):
|
||||||
# Send the subscription request
|
# Send the subscription request
|
||||||
await ws_client.send_json_auto_id(
|
await ws_client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
"type": "zwave_js/restore_nvm",
|
"type": "zwave_js/restore_nvm",
|
||||||
"entry_id": integration.entry_id,
|
"entry_id": entry.entry_id,
|
||||||
"data": "dGVzdA==", # base64 encoded "test"
|
"data": "dGVzdA==", # base64 encoded "test"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify the finished event first
|
# Verify the finished event
|
||||||
msg = await ws_client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
assert msg["type"] == "event"
|
assert msg["type"] == "event"
|
||||||
@ -5734,37 +5710,41 @@ async def test_restore_nvm(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Verify the restore was called
|
# Verify the restore was called
|
||||||
# The first call is the relevant one for nvm restore.
|
assert controller.async_restore_nvm_base64.call_count == 1
|
||||||
assert client.async_send_command.call_count == 3
|
assert controller.async_restore_nvm_base64.call_args == call(
|
||||||
assert client.async_send_command.call_args_list[0] == call(
|
"dGVzdA==",
|
||||||
{
|
{"preserveRoutes": False},
|
||||||
"command": "controller.restore_nvm",
|
|
||||||
"nvmData": "dGVzdA==",
|
|
||||||
"migrateOptions": {"preserveRoutes": False},
|
|
||||||
},
|
|
||||||
require_schema=42,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
client.async_send_command.reset_mock()
|
controller.async_restore_nvm_base64.reset_mock()
|
||||||
|
|
||||||
# Test restore failure
|
# Test restore failure
|
||||||
with patch(
|
controller.async_restore_nvm_base64.side_effect = FailedZWaveCommand(
|
||||||
f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64",
|
"failed_command", 1, "error message"
|
||||||
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,
|
|
||||||
"data": "dGVzdA==", # base64 encoded "test"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify error response
|
# Send the subscription request
|
||||||
msg = await ws_client.receive_json()
|
await ws_client.send_json_auto_id(
|
||||||
assert not msg["success"]
|
{
|
||||||
assert msg["error"]["code"] == "zwave_error"
|
"type": "zwave_js/restore_nvm",
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"data": "dGVzdA==", # base64 encoded "test"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify error response
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
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
|
# Test entry_id not found
|
||||||
await ws_client.send_json_auto_id(
|
await ws_client.send_json_auto_id(
|
||||||
@ -5779,13 +5759,13 @@ async def test_restore_nvm(
|
|||||||
assert msg["error"]["code"] == "not_found"
|
assert msg["error"]["code"] == "not_found"
|
||||||
|
|
||||||
# Test config entry not loaded
|
# 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 hass.async_block_till_done()
|
||||||
|
|
||||||
await ws_client.send_json_auto_id(
|
await ws_client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
"type": "zwave_js/restore_nvm",
|
"type": "zwave_js/restore_nvm",
|
||||||
"entry_id": integration.entry_id,
|
"entry_id": entry.entry_id,
|
||||||
"data": "dGVzdA==", # base64 encoded "test"
|
"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")
|
assert restart_addon.call_args == call("core_zwave_js")
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"),
|
("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"),
|
||||||
new=0,
|
new=0,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
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
|
assert client.connect.call_count == 2
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
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 entry.state is config_entries.ConfigEntryState.LOADED
|
||||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||||
assert len(events) == 2
|
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")
|
assert restart_addon.call_args == call("core_zwave_js")
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"),
|
("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"),
|
||||||
new=0,
|
new=0,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
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
|
assert client.connect.call_count == 2
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
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 entry.state is config_entries.ConfigEntryState.LOADED
|
||||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||||
assert len(events) == 2
|
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)
|
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
|
||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
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