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

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

View File

@ -3,7 +3,7 @@
"name": "Z-Wave", "name": "Z-Wave",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js", "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"], "codeowners": ["@home-assistant/z-wave"],
"dependencies": ["usb", "http", "websocket_api"], "dependencies": ["usb", "http", "websocket_api"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
from math import floor
from typing import Any from typing import Any
from awesomeversion import AwesomeVersion 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.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
from zwave_js_server.model.driver import Driver from zwave_js_server.model.driver import Driver
from zwave_js_server.model.firmware import ( from zwave_js_server.model.firmware import (
FirmwareUpdateFinished,
FirmwareUpdateInfo, FirmwareUpdateInfo,
FirmwareUpdateProgress, FirmwareUpdateProgress,
FirmwareUpdateStatus, FirmwareUpdateResult,
) )
from zwave_js_server.model.node import Node as ZwaveNode 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._poll_unsub: Callable[[], None] | None = None
self._progress_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None
self._finished_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_event = asyncio.Event()
self._finished_status: FirmwareUpdateStatus | None = None self._result: FirmwareUpdateResult | None = None
# Entity class attributes # Entity class attributes
self._attr_name = "Firmware" self._attr_name = "Firmware"
@ -115,25 +112,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
progress: FirmwareUpdateProgress = event["firmware_update_progress"] progress: FirmwareUpdateProgress = event["firmware_update_progress"]
if not self._latest_version_firmware: if not self._latest_version_firmware:
return return
# We will assume that each file in the firmware update represents an equal self._attr_in_progress = int(progress.progress)
# 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.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _update_finished(self, event: dict[str, Any]) -> None: def _update_finished(self, event: dict[str, Any]) -> None:
"""Update install progress on event.""" """Update install progress on event."""
finished: FirmwareUpdateFinished = event["firmware_update_finished"] result: FirmwareUpdateResult = event["firmware_update_finished"]
self._finished_status = finished.status self._result = result
self._finished_event.set() self._finished_event.set()
@callback @callback
@ -149,10 +135,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._finished_unsub() self._finished_unsub()
self._finished_unsub = None self._finished_unsub = None
self._finished_status = None self._result = None
self._finished_event.clear() self._finished_event.clear()
self._num_files_installed = 0 self._attr_in_progress = False
self._attr_in_progress = 0
if write_state: if write_state:
self.async_write_ha_state() self.async_write_ha_state()
@ -235,10 +220,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
"firmware update finished", self._update_finished "firmware update finished", self._update_finished
) )
for file in firmware.files:
try: try:
await self.driver.controller.async_begin_ota_firmware_update( await self.driver.controller.async_firmware_update_ota(
self.node, file self.node, firmware.files
) )
except BaseZwaveJSServerError as err: except BaseZwaveJSServerError as err:
self._unsub_firmware_events_and_reset_progress() self._unsub_firmware_events_and_reset_progress()
@ -246,30 +230,13 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
# We need to block until we receive the `firmware update finished` event # We need to block until we receive the `firmware update finished` event
await self._finished_event.wait() await self._finished_event.wait()
# Clear the event so that a second firmware update blocks again assert self._result is not None
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 the update was not successful, we should throw an error to let the user know
if self._finished_status not in ( if not self._result.success:
FirmwareUpdateStatus.OK_NO_RESTART, error_msg = self._result.status.name.replace("_", " ").title()
FirmwareUpdateStatus.OK_RESTART_PENDING,
FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION,
):
status = self._finished_status
self._unsub_firmware_events_and_reset_progress() self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(status.name.replace("_", " ").title()) raise HomeAssistantError(error_msg)
# 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)
)
# Clear the status so we can get a new one
self._finished_status = None
self.async_write_ha_state()
# If we get here, all files were installed successfully # If we get here, all files were installed successfully
self._attr_installed_version = self._attr_latest_version = firmware.version 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 zm-py==0.5.2
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.42.0 zwave-js-server-python==0.43.0
# homeassistant.components.zwave_me # homeassistant.components.zwave_me
zwave_me_ws==0.2.6 zwave_me_ws==0.2.6

View File

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

View File

@ -28,6 +28,7 @@ from zwave_js_server.model.controller import (
ProvisioningEntry, ProvisioningEntry,
QRProvisioningInformation, QRProvisioningInformation,
) )
from zwave_js_server.model.firmware import FirmwareUpdateData
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND 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() client = await hass_client()
device = get_device(hass, multisensor_6) device = get_device(hass, multisensor_6)
with patch( with patch(
"homeassistant.components.zwave_js.api.begin_firmware_update", "homeassistant.components.zwave_js.api.update_firmware",
) as mock_cmd, patch.dict( ) as mock_cmd, patch.dict(
"homeassistant.components.zwave_js.api.USER_AGENT", "homeassistant.components.zwave_js.api.USER_AGENT",
{"HomeAssistant": "0.0.0"}, {"HomeAssistant": "0.0.0"},
): ):
resp = await client.post( resp = await client.post(
f"/api/zwave_js/firmware/upload/{device.id}", 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] == { assert mock_cmd.call_args[1] == {
"target": 15,
"additional_user_agent_components": {"HomeAssistant": "0.0.0"}, "additional_user_agent_components": {"HomeAssistant": "0.0.0"},
} }
assert json.loads(await resp.text()) is None assert json.loads(await resp.text()) is None
@ -2839,7 +2842,7 @@ async def test_firmware_upload_view_failed_command(
client = await hass_client() client = await hass_client()
device = get_device(hass, multisensor_6) device = get_device(hass, multisensor_6)
with patch( with patch(
"homeassistant.components.zwave_js.api.begin_firmware_update", "homeassistant.components.zwave_js.api.update_firmware",
side_effect=FailedCommand("test", "test"), side_effect=FailedCommand("test", "test"),
): ):
resp = await client.post( resp = await client.post(
@ -3502,8 +3505,13 @@ async def test_subscribe_firmware_update_status(
"source": "node", "source": "node",
"event": "firmware update progress", "event": "firmware update progress",
"nodeId": multisensor_6.node_id, "nodeId": multisensor_6.node_id,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1, "sentFragments": 1,
"totalFragments": 10, "totalFragments": 10,
"progress": 10.0,
},
}, },
) )
multisensor_6.receive_event(event) multisensor_6.receive_event(event)
@ -3511,8 +3519,11 @@ async def test_subscribe_firmware_update_status(
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["event"] == { assert msg["event"] == {
"event": "firmware update progress", "event": "firmware update progress",
"current_file": 1,
"total_files": 1,
"sent_fragments": 1, "sent_fragments": 1,
"total_fragments": 10, "total_fragments": 10,
"progress": 10.0,
} }
event = Event( event = Event(
@ -3521,8 +3532,12 @@ async def test_subscribe_firmware_update_status(
"source": "node", "source": "node",
"event": "firmware update finished", "event": "firmware update finished",
"nodeId": multisensor_6.node_id, "nodeId": multisensor_6.node_id,
"result": {
"status": 255, "status": 255,
"success": True,
"waitTime": 10, "waitTime": 10,
"reInterview": False,
},
}, },
) )
multisensor_6.receive_event(event) multisensor_6.receive_event(event)
@ -3531,7 +3546,9 @@ async def test_subscribe_firmware_update_status(
assert msg["event"] == { assert msg["event"] == {
"event": "firmware update finished", "event": "firmware update finished",
"status": 255, "status": 255,
"success": True,
"wait_time": 10, "wait_time": 10,
"reinterview": False,
} }
@ -3551,8 +3568,13 @@ async def test_subscribe_firmware_update_status_initial_value(
"source": "node", "source": "node",
"event": "firmware update progress", "event": "firmware update progress",
"nodeId": multisensor_6.node_id, "nodeId": multisensor_6.node_id,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1, "sentFragments": 1,
"totalFragments": 10, "totalFragments": 10,
"progress": 10.0,
},
}, },
) )
multisensor_6.receive_event(event) multisensor_6.receive_event(event)
@ -3574,8 +3596,11 @@ async def test_subscribe_firmware_update_status_initial_value(
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["event"] == { assert msg["event"] == {
"event": "firmware update progress", "event": "firmware update progress",
"current_file": 1,
"total_files": 1,
"sent_fragments": 1, "sent_fragments": 1,
"total_fragments": 10, "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" assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
client.async_send_command.reset_mock() 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 # Test successful install call without a version
install_task = hass.async_create_task( install_task = hass.async_create_task(
@ -352,8 +352,13 @@ async def test_update_entity_progress(
"source": "node", "source": "node",
"event": "firmware update progress", "event": "firmware update progress",
"nodeId": node.node_id, "nodeId": node.node_id,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1, "sentFragments": 1,
"totalFragments": 20, "totalFragments": 20,
"progress": 5.0,
},
}, },
) )
node.receive_event(event) node.receive_event(event)
@ -370,7 +375,11 @@ async def test_update_entity_progress(
"source": "node", "source": "node",
"event": "firmware update finished", "event": "firmware update finished",
"nodeId": node.node_id, "nodeId": node.node_id,
"result": {
"status": FirmwareUpdateStatus.OK_NO_RESTART, "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) state = hass.states.get(UPDATE_ENTITY)
assert state assert state
attrs = state.attributes 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
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_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
assert state.state == STATE_OFF assert state.state == STATE_OFF
@ -546,10 +420,11 @@ async def test_update_entity_install_failed(
assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
client.async_send_command.reset_mock() 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(): # Test install call - we expect it to finish fail
await hass.services.async_call( install_task = hass.async_create_task(
hass.services.async_call(
UPDATE_DOMAIN, UPDATE_DOMAIN,
SERVICE_INSTALL, SERVICE_INSTALL,
{ {
@ -557,9 +432,7 @@ async def test_update_entity_install_failed(
}, },
blocking=True, blocking=True,
) )
)
# Test install call - we expect it to raise
install_task = hass.async_create_task(call_install())
# Sleep so that task starts # Sleep so that task starts
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@ -570,8 +443,13 @@ async def test_update_entity_install_failed(
"source": "node", "source": "node",
"event": "firmware update progress", "event": "firmware update progress",
"nodeId": node.node_id, "nodeId": node.node_id,
"progress": {
"currentFile": 1,
"totalFiles": 1,
"sentFragments": 1, "sentFragments": 1,
"totalFragments": 20, "totalFragments": 20,
"progress": 5.0,
},
}, },
) )
node.receive_event(event) node.receive_event(event)
@ -588,7 +466,11 @@ async def test_update_entity_install_failed(
"source": "node", "source": "node",
"event": "firmware update finished", "event": "firmware update finished",
"nodeId": node.node_id, "nodeId": node.node_id,
"result": {
"status": FirmwareUpdateStatus.ERROR_TIMEOUT, "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) state = hass.states.get(UPDATE_ENTITY)
assert state assert state
attrs = state.attributes 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_INSTALLED_VERSION] == "10.7"
assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
assert state.state == STATE_ON assert state.state == STATE_ON