Add support for zwave_js firmware update service (#77401)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>pull/77572/head
parent
f78b39bdbf
commit
df214c2d26
|
@ -19,8 +19,6 @@ from zwave_js_server.model.notification import (
|
|||
)
|
||||
from zwave_js_server.model.value import Value, ValueNotification
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
|
@ -28,6 +26,7 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID,
|
||||
CONF_URL,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
@ -244,7 +243,7 @@ async def setup_driver( # noqa: C901
|
|||
registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict)
|
||||
discovered_value_ids: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
async def async_setup_platform(platform: str) -> None:
|
||||
async def async_setup_platform(platform: Platform) -> None:
|
||||
"""Set up platform if needed."""
|
||||
if platform not in platform_setup_tasks:
|
||||
platform_setup_tasks[platform] = hass.async_create_task(
|
||||
|
@ -353,17 +352,23 @@ async def setup_driver( # noqa: C901
|
|||
# No need for a ping button or node status sensor for controller nodes
|
||||
if not node.is_controller_node:
|
||||
# Create a node status sensor for each device
|
||||
await async_setup_platform(SENSOR_DOMAIN)
|
||||
await async_setup_platform(Platform.SENSOR)
|
||||
async_dispatcher_send(
|
||||
hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node
|
||||
)
|
||||
|
||||
# Create a ping button for each device
|
||||
await async_setup_platform(BUTTON_DOMAIN)
|
||||
await async_setup_platform(Platform.BUTTON)
|
||||
async_dispatcher_send(
|
||||
hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node
|
||||
)
|
||||
|
||||
# Create a firmware update entity for each device
|
||||
await async_setup_platform(Platform.UPDATE)
|
||||
async_dispatcher_send(
|
||||
hass, f"{DOMAIN}_{entry.entry_id}_add_firmware_update_entity", node
|
||||
)
|
||||
|
||||
# we only want to run discovery when the node has reached ready state,
|
||||
# otherwise we'll have all kinds of missing info issues.
|
||||
if node.ready:
|
||||
|
|
|
@ -120,3 +120,7 @@ ENTITY_DESC_KEY_TEMPERATURE = "temperature"
|
|||
ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature"
|
||||
ENTITY_DESC_KEY_MEASUREMENT = "measurement"
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing"
|
||||
|
||||
# This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for
|
||||
# your own (https://github.com/zwave-js/firmware-updates/).
|
||||
API_KEY_FIRMWARE_UPDATE_SERVICE = "b48e74337db217f44e1e003abb1e9144007d260a17e2b2422e0a45d0eaf6f4ad86f2a9943f17fee6dde343941f238a64"
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
"""Representation of Z-Wave updates."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import NodeStatus
|
||||
from zwave_js_server.exceptions import BaseZwaveJSServerError
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.firmware import FirmwareUpdateInfo
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
from homeassistant.components.update.const import UpdateEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER
|
||||
from .helpers import get_device_id, get_valueless_base_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(days=1)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Z-Wave button from config entry."""
|
||||
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
|
||||
@callback
|
||||
def async_add_firmware_update_entity(node: ZwaveNode) -> None:
|
||||
"""Add firmware update entity."""
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
async_add_entities([ZWaveNodeFirmwareUpdate(driver, node)], True)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"{DOMAIN}_{config_entry.entry_id}_add_firmware_update_entity",
|
||||
async_add_firmware_update_entity,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
"""Representation of a firmware update entity."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, driver: Driver, node: ZwaveNode) -> None:
|
||||
"""Initialize a Z-Wave device firmware update entity."""
|
||||
self.driver = driver
|
||||
self.node = node
|
||||
self.available_firmware_updates: list[FirmwareUpdateInfo] = []
|
||||
self._latest_version_firmware: FirmwareUpdateInfo | None = None
|
||||
self._status_unsub: Callable[[], None] | None = None
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = "Firmware"
|
||||
self._base_unique_id = get_valueless_base_unique_id(driver, node)
|
||||
self._attr_unique_id = f"{self._base_unique_id}.firmware_update"
|
||||
# device may not be precreated in main handler yet
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={get_device_id(driver, node)},
|
||||
sw_version=node.firmware_version,
|
||||
name=node.name or node.device_config.description or f"Node {node.node_id}",
|
||||
model=node.device_config.label,
|
||||
manufacturer=node.device_config.manufacturer,
|
||||
suggested_area=node.location if node.location else None,
|
||||
)
|
||||
|
||||
self._attr_installed_version = self._attr_latest_version = node.firmware_version
|
||||
|
||||
def _update_on_wake_up(self, _: dict[str, Any]) -> None:
|
||||
"""Update the entity when node is awake."""
|
||||
self._status_unsub = None
|
||||
self.hass.async_create_task(self.async_update(True))
|
||||
|
||||
async def async_update(self, write_state: bool = False) -> None:
|
||||
"""Update the entity."""
|
||||
if self.node.status == NodeStatus.ASLEEP:
|
||||
if not self._status_unsub:
|
||||
self._status_unsub = self.node.once("wake up", self._update_on_wake_up)
|
||||
return
|
||||
self.available_firmware_updates = (
|
||||
await self.driver.controller.async_get_available_firmware_updates(
|
||||
self.node, API_KEY_FIRMWARE_UPDATE_SERVICE
|
||||
)
|
||||
)
|
||||
self._async_process_available_updates(write_state)
|
||||
|
||||
@callback
|
||||
def _async_process_available_updates(self, write_state: bool = True) -> None:
|
||||
"""
|
||||
Process available firmware updates.
|
||||
|
||||
Sets latest version attribute and FirmwareUpdateInfo instance.
|
||||
"""
|
||||
# If we have an available firmware update that is a higher version than what's
|
||||
# on the node, we should advertise it, otherwise we are on the latest version
|
||||
if self.available_firmware_updates and AwesomeVersion(
|
||||
(
|
||||
firmware := max(
|
||||
self.available_firmware_updates,
|
||||
key=lambda x: AwesomeVersion(x.version),
|
||||
)
|
||||
).version
|
||||
) > AwesomeVersion(self.node.firmware_version):
|
||||
self._latest_version_firmware = firmware
|
||||
self._attr_latest_version = firmware.version
|
||||
else:
|
||||
self._latest_version_firmware = None
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_release_notes(self) -> str | None:
|
||||
"""Get release notes."""
|
||||
if self._latest_version_firmware is None:
|
||||
return None
|
||||
return self._latest_version_firmware.changelog
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
"""Install an update."""
|
||||
firmware = self._latest_version_firmware
|
||||
assert firmware
|
||||
self._attr_in_progress = True
|
||||
self.async_write_ha_state()
|
||||
try:
|
||||
for file in firmware.files:
|
||||
await self.driver.controller.async_begin_ota_firmware_update(
|
||||
self.node, file
|
||||
)
|
||||
except BaseZwaveJSServerError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
else:
|
||||
self._attr_installed_version = firmware.version
|
||||
self.available_firmware_updates.remove(firmware)
|
||||
self._async_process_available_updates()
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""Poll a value."""
|
||||
LOGGER.error(
|
||||
"There is no value to refresh for this entity so the zwave_js.refresh_value "
|
||||
"service won't work for it"
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self.unique_id}_poll_value",
|
||||
self.async_poll_value,
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
|
||||
self.async_remove,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed."""
|
||||
if self._status_unsub:
|
||||
self._status_unsub()
|
||||
self._status_unsub = None
|
|
@ -844,6 +844,8 @@ async def integration_fixture(hass, client):
|
|||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
|
|
|
@ -513,6 +513,8 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration):
|
|||
await hass.config_entries.async_reload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
@ -774,6 +776,8 @@ async def test_thermostat_fan_without_off(
|
|||
await hass.config_entries.async_reload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
|
|
@ -211,8 +211,8 @@ async def test_on_node_added_not_ready(
|
|||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# the only entities are the node status sensor and ping button
|
||||
assert len(hass.states.async_all()) == 2
|
||||
# the only entities are the node status sensor, ping button, and firmware update
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
|
@ -254,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati
|
|||
assert not device.model
|
||||
assert not device.sw_version
|
||||
|
||||
# the only entities are the node status sensor and ping button
|
||||
assert len(hass.states.async_all()) == 2
|
||||
# the only entities are the node status sensor, ping button, and firmware update
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
|
@ -817,7 +817,7 @@ async def test_removed_device(
|
|||
# Check how many entities there are
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||
assert len(entity_entries) == 29
|
||||
assert len(entity_entries) == 31
|
||||
|
||||
# Remove a node and reload the entry
|
||||
old_node = driver.controller.nodes.pop(13)
|
||||
|
@ -829,7 +829,7 @@ async def test_removed_device(
|
|||
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
|
||||
assert len(device_entries) == 1
|
||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||
assert len(entity_entries) == 17
|
||||
assert len(entity_entries) == 18
|
||||
assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
"""Test the Z-Wave JS update entities."""
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
|
||||
from homeassistant.components.update.const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_IN_PROGRESS,
|
||||
ATTR_INSTALLED_VERSION,
|
||||
ATTR_LATEST_VERSION,
|
||||
ATTR_RELEASE_URL,
|
||||
DOMAIN as UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
)
|
||||
from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE
|
||||
from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_registry import async_get
|
||||
from homeassistant.util import datetime as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
UPDATE_ENTITY = "update.z_wave_thermostat_firmware"
|
||||
|
||||
|
||||
async def test_update_entity_success(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
controller_node,
|
||||
integration,
|
||||
caplog,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test update entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF
|
||||
|
||||
client.async_send_command.return_value = {"updates": []}
|
||||
|
||||
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_OFF
|
||||
|
||||
await ws_client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "update/release_notes",
|
||||
"entity_id": UPDATE_ENTITY,
|
||||
}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["result"] is None
|
||||
|
||||
client.async_send_command.return_value = {
|
||||
"updates": [
|
||||
{
|
||||
"version": "10.11.1",
|
||||
"changelog": "blah 1",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example1.com", "integrity": "sha1"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.2.4",
|
||||
"changelog": "blah 2",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example2.com", "integrity": "sha2"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.1.5",
|
||||
"changelog": "blah 3",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example3.com", "integrity": "sha3"}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
attrs = state.attributes
|
||||
assert not attrs[ATTR_AUTO_UPDATE]
|
||||
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
|
||||
assert not attrs[ATTR_IN_PROGRESS]
|
||||
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
|
||||
assert attrs[ATTR_RELEASE_URL] is None
|
||||
|
||||
await ws_client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "update/release_notes",
|
||||
"entity_id": UPDATE_ENTITY,
|
||||
}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["result"] == "blah 2"
|
||||
|
||||
# Refresh value should not be supported by this entity
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_REFRESH_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert "There is no value to refresh for this entity" in caplog.text
|
||||
|
||||
# Assert a node firmware update entity is not created for the controller
|
||||
driver = client.driver
|
||||
node = driver.controller.nodes[1]
|
||||
assert node.is_controller_node
|
||||
assert (
|
||||
async_get(hass).async_get_entity_id(
|
||||
DOMAIN,
|
||||
"sensor",
|
||||
f"{get_valueless_base_unique_id(driver, node)}.firmware_update",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test successful install call without a version
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
args = client.async_send_command.call_args_list[0][0][0]
|
||||
assert args["command"] == "controller.begin_ota_firmware_update"
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
assert args["update"] == {
|
||||
"target": 0,
|
||||
"url": "https://example2.com",
|
||||
"integrity": "sha2",
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_update_entity_failure(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
controller_node,
|
||||
integration,
|
||||
caplog,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test update entity failed install."""
|
||||
client.async_send_command.return_value = {
|
||||
"updates": [
|
||||
{
|
||||
"version": "10.11.1",
|
||||
"changelog": "blah 1",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example1.com", "integrity": "sha1"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.2.4",
|
||||
"changelog": "blah 2",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example2.com", "integrity": "sha2"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.1.5",
|
||||
"changelog": "blah 3",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example3.com", "integrity": "sha3"}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test failed installation by driver
|
||||
client.async_send_command.side_effect = FailedZWaveCommand("test", 12, "test")
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_update_entity_sleep(
|
||||
hass,
|
||||
client,
|
||||
multisensor_6,
|
||||
integration,
|
||||
):
|
||||
"""Test update occurs when device is asleep after it wakes up."""
|
||||
event = Event(
|
||||
"sleep",
|
||||
data={"source": "node", "event": "sleep", "nodeId": multisensor_6.node_id},
|
||||
)
|
||||
multisensor_6.receive_event(event)
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
client.async_send_command.return_value = {
|
||||
"updates": [
|
||||
{
|
||||
"version": "10.11.1",
|
||||
"changelog": "blah 1",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example1.com", "integrity": "sha1"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.2.4",
|
||||
"changelog": "blah 2",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example2.com", "integrity": "sha2"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "11.1.5",
|
||||
"changelog": "blah 3",
|
||||
"files": [
|
||||
{"target": 0, "url": "https://example3.com", "integrity": "sha3"}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Because node is asleep we shouldn't attempt to check for firmware updates
|
||||
assert len(client.async_send_command.call_args_list) == 0
|
||||
|
||||
event = Event(
|
||||
"wake up",
|
||||
data={"source": "node", "event": "wake up", "nodeId": multisensor_6.node_id},
|
||||
)
|
||||
multisensor_6.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Now that the node is up we can check for updates
|
||||
assert len(client.async_send_command.call_args_list) > 0
|
||||
|
||||
args = client.async_send_command.call_args_list[0][0][0]
|
||||
assert args["command"] == "controller.get_available_firmware_updates"
|
||||
assert args["nodeId"] == multisensor_6.node_id
|
Loading…
Reference in New Issue