Bump zwave_js lib to 0.43.0 and fix multi-file firmware updates (#79342)

pull/79600/head
Raman Gupta 2022-10-04 10:40:49 -04:00 committed by GitHub
parent 2b27cfdabb
commit 27413cee19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 232 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
import dataclasses
from functools import partial, wraps
from typing import Any, Literal, cast
from typing import Any, Literal
from aiohttp import web, web_exceptions, web_request
import voluptuous as vol
@ -27,7 +27,7 @@ from zwave_js_server.exceptions import (
NotFoundError,
SetValueFailed,
)
from zwave_js_server.firmware import begin_firmware_update
from zwave_js_server.firmware import update_firmware
from zwave_js_server.model.controller import (
ControllerStatistics,
InclusionGrant,
@ -36,8 +36,9 @@ from zwave_js_server.model.controller import (
)
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.firmware import (
FirmwareUpdateFinished,
FirmwareUpdateData,
FirmwareUpdateProgress,
FirmwareUpdateResult,
)
from zwave_js_server.model.log_config import LogConfig
from zwave_js_server.model.log_message import LogMessage
@ -1897,11 +1898,14 @@ async def websocket_is_node_firmware_update_in_progress(
def _get_firmware_update_progress_dict(
progress: FirmwareUpdateProgress,
) -> dict[str, int]:
) -> dict[str, int | float]:
"""Get a dictionary of firmware update progress."""
return {
"current_file": progress.current_file,
"total_files": progress.total_files,
"sent_fragments": progress.sent_fragments,
"total_fragments": progress.total_fragments,
"progress": progress.progress,
}
@ -1943,14 +1947,16 @@ async def websocket_subscribe_firmware_update_status(
@callback
def forward_finished(event: dict) -> None:
finished: FirmwareUpdateFinished = event["firmware_update_finished"]
finished: FirmwareUpdateResult = event["firmware_update_finished"]
connection.send_message(
websocket_api.event_message(
msg[ID],
{
"event": event["event"],
"status": finished.status,
"success": finished.success,
"wait_time": finished.wait_time,
"reinterview": finished.reinterview,
},
)
)
@ -2052,21 +2058,20 @@ class FirmwareUploadView(HomeAssistantView):
if "file" not in data or not isinstance(data["file"], web_request.FileField):
raise web_exceptions.HTTPBadRequest
target = None
if "target" in data:
target = int(cast(str, data["target"]))
uploaded_file: web_request.FileField = data["file"]
try:
await begin_firmware_update(
await update_firmware(
node.client.ws_server_url,
node,
uploaded_file.filename,
await hass.async_add_executor_job(uploaded_file.file.read),
[
FirmwareUpdateData(
uploaded_file.filename,
await hass.async_add_executor_job(uploaded_file.file.read),
)
],
async_get_clientsession(hass),
additional_user_agent_components=USER_AGENT,
target=target,
)
except BaseZwaveJSServerError as err:
raise web_exceptions.HTTPBadRequest(reason=str(err)) from err

View File

@ -3,7 +3,7 @@
"name": "Z-Wave",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.42.0"],
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["usb", "http", "websocket_api"],
"iot_class": "local_push",

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import datetime, timedelta
from math import floor
from typing import Any
from awesomeversion import AwesomeVersion
@ -13,10 +12,9 @@ from zwave_js_server.const import NodeStatus
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.firmware import (
FirmwareUpdateFinished,
FirmwareUpdateInfo,
FirmwareUpdateProgress,
FirmwareUpdateStatus,
FirmwareUpdateResult,
)
from zwave_js_server.model.node import Node as ZwaveNode
@ -91,9 +89,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._poll_unsub: Callable[[], None] | None = None
self._progress_unsub: Callable[[], None] | None = None
self._finished_unsub: Callable[[], None] | None = None
self._num_files_installed: int = 0
self._finished_event = asyncio.Event()
self._finished_status: FirmwareUpdateStatus | None = None
self._result: FirmwareUpdateResult | None = None
# Entity class attributes
self._attr_name = "Firmware"
@ -115,25 +112,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
progress: FirmwareUpdateProgress = event["firmware_update_progress"]
if not self._latest_version_firmware:
return
# We will assume that each file in the firmware update represents an equal
# percentage of the overall progress. This is likely not true because each file
# may be a different size, but it's the best we can do since we don't know the
# total number of fragments across all files.
self._attr_in_progress = floor(
100
* (
self._num_files_installed
+ (progress.sent_fragments / progress.total_fragments)
)
/ len(self._latest_version_firmware.files)
)
self._attr_in_progress = int(progress.progress)
self.async_write_ha_state()
@callback
def _update_finished(self, event: dict[str, Any]) -> None:
"""Update install progress on event."""
finished: FirmwareUpdateFinished = event["firmware_update_finished"]
self._finished_status = finished.status
result: FirmwareUpdateResult = event["firmware_update_finished"]
self._result = result
self._finished_event.set()
@callback
@ -149,10 +135,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._finished_unsub()
self._finished_unsub = None
self._finished_status = None
self._result = None
self._finished_event.clear()
self._num_files_installed = 0
self._attr_in_progress = 0
self._attr_in_progress = False
if write_state:
self.async_write_ha_state()
@ -235,41 +220,23 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
"firmware update finished", self._update_finished
)
for file in firmware.files:
try:
await self.driver.controller.async_begin_ota_firmware_update(
self.node, file
)
except BaseZwaveJSServerError as err:
self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(err) from err
# We need to block until we receive the `firmware update finished` event
await self._finished_event.wait()
# Clear the event so that a second firmware update blocks again
self._finished_event.clear()
assert self._finished_status is not None
# If status is not OK, we should throw an error to let the user know
if self._finished_status not in (
FirmwareUpdateStatus.OK_NO_RESTART,
FirmwareUpdateStatus.OK_RESTART_PENDING,
FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION,
):
status = self._finished_status
self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(status.name.replace("_", " ").title())
# If we get here, the firmware installation was successful and we need to
# update progress accordingly
self._num_files_installed += 1
self._attr_in_progress = floor(
100 * self._num_files_installed / len(firmware.files)
try:
await self.driver.controller.async_firmware_update_ota(
self.node, firmware.files
)
except BaseZwaveJSServerError as err:
self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(err) from err
# Clear the status so we can get a new one
self._finished_status = None
self.async_write_ha_state()
# We need to block until we receive the `firmware update finished` event
await self._finished_event.wait()
assert self._result is not None
# If the update was not successful, we should throw an error to let the user know
if not self._result.success:
error_msg = self._result.status.name.replace("_", " ").title()
self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(error_msg)
# If we get here, all files were installed successfully
self._attr_installed_version = self._attr_latest_version = firmware.version

View File

@ -2622,7 +2622,7 @@ zigpy==0.51.1
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.42.0
zwave-js-server-python==0.43.0
# homeassistant.components.zwave_me
zwave_me_ws==0.2.6

View File

@ -1814,7 +1814,7 @@ zigpy-znp==0.9.0
zigpy==0.51.1
# homeassistant.components.zwave_js
zwave-js-server-python==0.42.0
zwave-js-server-python==0.43.0
# homeassistant.components.zwave_me
zwave_me_ws==0.2.6

View File

@ -28,6 +28,7 @@ from zwave_js_server.model.controller import (
ProvisioningEntry,
QRProvisioningInformation,
)
from zwave_js_server.model.firmware import FirmwareUpdateData
from zwave_js_server.model.node import Node
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND
@ -2815,18 +2816,20 @@ async def test_firmware_upload_view(
client = await hass_client()
device = get_device(hass, multisensor_6)
with patch(
"homeassistant.components.zwave_js.api.begin_firmware_update",
"homeassistant.components.zwave_js.api.update_firmware",
) as mock_cmd, patch.dict(
"homeassistant.components.zwave_js.api.USER_AGENT",
{"HomeAssistant": "0.0.0"},
):
resp = await client.post(
f"/api/zwave_js/firmware/upload/{device.id}",
data={"file": firmware_file, "target": "15"},
data={"file": firmware_file},
)
assert mock_cmd.call_args[0][1:3] == (
multisensor_6,
[FirmwareUpdateData("file", bytes(10))],
)
assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10))
assert mock_cmd.call_args[1] == {
"target": 15,
"additional_user_agent_components": {"HomeAssistant": "0.0.0"},
}
assert json.loads(await resp.text()) is None
@ -2839,7 +2842,7 @@ async def test_firmware_upload_view_failed_command(
client = await hass_client()
device = get_device(hass, multisensor_6)
with patch(
"homeassistant.components.zwave_js.api.begin_firmware_update",
"homeassistant.components.zwave_js.api.update_firmware",
side_effect=FailedCommand("test", "test"),
):
resp = await client.post(
@ -3502,8 +3505,13 @@ async def test_subscribe_firmware_update_status(
"source": "node",
"event": "firmware update progress",
"nodeId": multisensor_6.node_id,
"sentFragments": 1,
"totalFragments": 10,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1,
"totalFragments": 10,
"progress": 10.0,
},
},
)
multisensor_6.receive_event(event)
@ -3511,8 +3519,11 @@ async def test_subscribe_firmware_update_status(
msg = await ws_client.receive_json()
assert msg["event"] == {
"event": "firmware update progress",
"current_file": 1,
"total_files": 1,
"sent_fragments": 1,
"total_fragments": 10,
"progress": 10.0,
}
event = Event(
@ -3521,8 +3532,12 @@ async def test_subscribe_firmware_update_status(
"source": "node",
"event": "firmware update finished",
"nodeId": multisensor_6.node_id,
"status": 255,
"waitTime": 10,
"result": {
"status": 255,
"success": True,
"waitTime": 10,
"reInterview": False,
},
},
)
multisensor_6.receive_event(event)
@ -3531,7 +3546,9 @@ async def test_subscribe_firmware_update_status(
assert msg["event"] == {
"event": "firmware update finished",
"status": 255,
"success": True,
"wait_time": 10,
"reinterview": False,
}
@ -3551,8 +3568,13 @@ async def test_subscribe_firmware_update_status_initial_value(
"source": "node",
"event": "firmware update progress",
"nodeId": multisensor_6.node_id,
"sentFragments": 1,
"totalFragments": 10,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1,
"totalFragments": 10,
"progress": 10.0,
},
},
)
multisensor_6.receive_event(event)
@ -3574,8 +3596,11 @@ async def test_subscribe_firmware_update_status_initial_value(
msg = await ws_client.receive_json()
assert msg["event"] == {
"event": "firmware update progress",
"current_file": 1,
"total_files": 1,
"sent_fragments": 1,
"total_fragments": 10,
"progress": 10.0,
}

View File

@ -324,7 +324,7 @@ async def test_update_entity_progress(
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
client.async_send_command.reset_mock()
client.async_send_command.return_value = None
client.async_send_command.return_value = {"success": False}
# Test successful install call without a version
install_task = hass.async_create_task(
@ -352,8 +352,13 @@ async def test_update_entity_progress(
"source": "node",
"event": "firmware update progress",
"nodeId": node.node_id,
"sentFragments": 1,
"totalFragments": 20,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1,
"totalFragments": 20,
"progress": 5.0,
},
},
)
node.receive_event(event)
@ -370,7 +375,11 @@ async def test_update_entity_progress(
"source": "node",
"event": "firmware update finished",
"nodeId": node.node_id,
"status": FirmwareUpdateStatus.OK_NO_RESTART,
"result": {
"status": FirmwareUpdateStatus.OK_NO_RESTART,
"success": True,
"reInterview": False,
},
},
)
@ -381,142 +390,7 @@ async def test_update_entity_progress(
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 0
assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
assert state.state == STATE_OFF
await install_task
async def test_update_entity_progress_multiple(
hass,
client,
climate_radio_thermostat_ct100_plus_different_endpoints,
integration,
):
"""Test update entity progress with multiple files."""
node = climate_radio_thermostat_ct100_plus_different_endpoints
client.async_send_command.return_value = FIRMWARE_UPDATE_MULTIPLE_FILES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
await hass.async_block_till_done()
state = hass.states.get(UPDATE_ENTITY)
assert state
assert state.state == STATE_ON
attrs = state.attributes
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
client.async_send_command.reset_mock()
client.async_send_command.return_value = None
# Test successful install call without a version
install_task = hass.async_create_task(
hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{
ATTR_ENTITY_ID: UPDATE_ENTITY,
},
blocking=True,
)
)
# Sleep so that task starts
await asyncio.sleep(0.1)
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] is True
node.receive_event(
Event(
type="firmware update progress",
data={
"source": "node",
"event": "firmware update progress",
"nodeId": node.node_id,
"sentFragments": 1,
"totalFragments": 20,
},
)
)
# Block so HA can do its thing
await asyncio.sleep(0)
# Validate that the progress is updated (two files means progress is 50% of 5)
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 2
node.receive_event(
Event(
type="firmware update finished",
data={
"source": "node",
"event": "firmware update finished",
"nodeId": node.node_id,
"status": FirmwareUpdateStatus.OK_NO_RESTART,
},
)
)
# Block so HA can do its thing
await asyncio.sleep(0)
# One file done, progress should be 50%
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 50
node.receive_event(
Event(
type="firmware update progress",
data={
"source": "node",
"event": "firmware update progress",
"nodeId": node.node_id,
"sentFragments": 1,
"totalFragments": 20,
},
)
)
# Block so HA can do its thing
await asyncio.sleep(0)
# Validate that the progress is updated (50% + 50% of 5)
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 52
node.receive_event(
Event(
type="firmware update finished",
data={
"source": "node",
"event": "firmware update finished",
"nodeId": node.node_id,
"status": FirmwareUpdateStatus.OK_NO_RESTART,
},
)
)
# Block so HA can do its thing
await asyncio.sleep(0)
# Validate that progress is reset and entity reflects new version
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 0
assert attrs[ATTR_IN_PROGRESS] is False
assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
assert state.state == STATE_OFF
@ -546,10 +420,11 @@ async def test_update_entity_install_failed(
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
client.async_send_command.reset_mock()
client.async_send_command.return_value = None
client.async_send_command.return_value = {"success": False}
async def call_install():
await hass.services.async_call(
# Test install call - we expect it to finish fail
install_task = hass.async_create_task(
hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{
@ -557,9 +432,7 @@ async def test_update_entity_install_failed(
},
blocking=True,
)
# Test install call - we expect it to raise
install_task = hass.async_create_task(call_install())
)
# Sleep so that task starts
await asyncio.sleep(0.1)
@ -570,8 +443,13 @@ async def test_update_entity_install_failed(
"source": "node",
"event": "firmware update progress",
"nodeId": node.node_id,
"sentFragments": 1,
"totalFragments": 20,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1,
"totalFragments": 20,
"progress": 5.0,
},
},
)
node.receive_event(event)
@ -588,7 +466,11 @@ async def test_update_entity_install_failed(
"source": "node",
"event": "firmware update finished",
"nodeId": node.node_id,
"status": FirmwareUpdateStatus.ERROR_TIMEOUT,
"result": {
"status": FirmwareUpdateStatus.ERROR_TIMEOUT,
"success": False,
"reInterview": False,
},
},
)
@ -599,7 +481,7 @@ async def test_update_entity_install_failed(
state = hass.states.get(UPDATE_ENTITY)
assert state
attrs = state.attributes
assert attrs[ATTR_IN_PROGRESS] == 0
assert attrs[ATTR_IN_PROGRESS] is False
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
assert state.state == STATE_ON