This commit is contained in:
Paulus Schoutsen 2022-09-08 21:58:18 -04:00 committed by GitHub
commit 0a7f3f6ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 262 additions and 63 deletions

View File

@ -6,8 +6,8 @@
"quality_scale": "internal",
"requirements": [
"bleak==0.16.0",
"bluetooth-adapters==0.3.4",
"bluetooth-auto-recovery==0.3.1"
"bluetooth-adapters==0.3.5",
"bluetooth-auto-recovery==0.3.2"
],
"codeowners": ["@bdraco"],
"config_flow": true,

View File

@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
class EcobeeSensorEntityDescriptionMixin:
"""Represent the required ecobee entity description attributes."""
runtime_key: str
runtime_key: str | None
@dataclass
@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=TEMP_FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualTemperature",
runtime_key=None,
),
EcobeeSensorEntityDescription(
key="humidity",
@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualHumidity",
runtime_key=None,
),
EcobeeSensorEntityDescription(
key="co2PPM",
@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity):
for item in sensor["capability"]:
if item["type"] != self.entity_description.key:
continue
thermostat = self.data.ecobee.get_thermostat(self.index)
self._state = thermostat["runtime"][self.entity_description.runtime_key]
if self.entity_description.runtime_key is None:
self._state = item["value"]
else:
thermostat = self.data.ecobee.get_thermostat(self.index)
self._state = thermostat["runtime"][
self.entity_description.runtime_key
]
break

View File

@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator
async def async_get_service(
@ -31,8 +32,10 @@ async def async_get_service(
"""Get the LaMetric notification service."""
if discovery_info is None:
return None
lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]]
return LaMetricNotificationService(lametric)
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
discovery_info["entry_id"]
]
return LaMetricNotificationService(coordinator.lametric)
class LaMetricNotificationService(BaseNotificationService):

View File

@ -3,7 +3,7 @@
"name": "Litter-Robot",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.8.2"],
"requirements": ["pylitterbot==2022.9.1"],
"codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_polling",

View File

@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
async def _is_owm_api_online(hass, api_key, lat, lon):
owm = OWM(api_key).weather_manager()
return await hass.async_add_executor_job(owm.one_call, lat, lon)
return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon)

View File

@ -373,7 +373,7 @@ class Luminary(LightEntity):
self._max_mireds = color_util.color_temperature_kelvin_to_mired(
self._luminary.min_temp() or DEFAULT_KELVIN
)
if len(self._attr_supported_color_modes == 1):
if len(self._attr_supported_color_modes) == 1:
# The light supports only a single color mode
self._attr_color_mode = list(self._attr_supported_color_modes)[0]
@ -392,7 +392,7 @@ class Luminary(LightEntity):
if ColorMode.HS in self._attr_supported_color_modes:
self._rgb_color = self._luminary.rgb()
if len(self._attr_supported_color_modes > 1):
if len(self._attr_supported_color_modes) > 1:
# The light supports hs + color temp, determine which one it is
if self._rgb_color == (0, 0, 0):
self._attr_color_mode = ColorMode.COLOR_TEMP

View File

@ -9,7 +9,7 @@ from typing import Any
from regenmaschine import Client
from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError
from regenmaschine.errors import RainMachineError, UnknownAPICallError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@ -190,7 +190,9 @@ async def async_update_programs_and_zones(
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Set up RainMachine as config entry."""
websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession)
@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data = await controller.restrictions.universal()
else:
data = await controller.zones.all(details=True, include_inactive=True)
except UnknownAPICallError:
LOGGER.info(
"Skipping unsupported API call for controller %s: %s",
controller.name,
api_category,
)
except RainMachineError as err:
raise UpdateFailed(err) from err

View File

@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
def update_from_latest_data(self) -> None:
"""Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR:
self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor")
self._attr_is_on = self.coordinator.data.get("system", {}).get(
"useFlowSensor"
)
class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):

View File

@ -3,7 +3,7 @@
"name": "RainMachine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.08.0"],
"requirements": ["regenmaschine==2022.09.0"],
"codeowners": ["@bachya"],
"iot_class": "local_polling",
"homekit": {

View File

@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
def update_from_latest_data(self) -> None:
"""Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter"
)
elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
clicks_per_m3 = self.coordinator.data["system"].get(
clicks = self.coordinator.data.get("system", {}).get(
"flowSensorWateringClicks"
)
clicks_per_m3 = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter"
)
@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
else:
self._attr_native_value = None
elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorStartIndex"
)
elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorWateringClicks"
)

View File

@ -2,7 +2,7 @@
"domain": "velbus",
"name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus",
"requirements": ["velbus-aio==2022.6.2"],
"requirements": ["velbus-aio==2022.9.1"],
"config_flow": true,
"codeowners": ["@Cereal2nd", "@brefra"],
"dependencies": ["usb"],

View File

@ -313,19 +313,24 @@ class ControllerEvents:
node,
)
LOGGER.debug("Node added: %s", node.node_id)
# Listen for ready node events, both new and re-interview.
self.config_entry.async_on_unload(
node.on(
"ready",
lambda event: self.hass.async_create_task(
self.node_events.async_on_node_ready(event["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:
await self.node_events.async_on_node_ready(node)
return
# if node is not yet ready, register one-time callback for ready state
LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id)
node.once(
"ready",
lambda event: self.hass.async_create_task(
self.node_events.async_on_node_ready(event["node"])
),
)
# we do submit the node to device registry so user has
# some visual feedback that something is (in the process of) being added
self.register_node_in_dev_reg(node)
@ -414,12 +419,25 @@ class NodeEvents:
async def async_on_node_ready(self, node: ZwaveNode) -> None:
"""Handle node ready event."""
LOGGER.debug("Processing node %s", node)
driver = self.controller_events.driver_events.driver
# register (or update) node in device registry
device = self.controller_events.register_node_in_dev_reg(node)
# We only want to create the defaultdict once, even on reinterviews
if device.id not in self.controller_events.registered_unique_ids:
self.controller_events.registered_unique_ids[device.id] = defaultdict(set)
# Remove any old value ids if this is a reinterview.
self.controller_events.discovered_value_ids.pop(device.id, None)
# Remove stale entities that may exist from a previous interview.
async_dispatcher_send(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(driver, node)}_"
"remove_entity_on_ready_node"
),
)
value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {}
# run discovery on all node values and create/update entities

View File

@ -792,7 +792,9 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key,
CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key,
CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL],
CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE],
CONF_ADDON_EMULATE_HARDWARE: user_input.get(
CONF_EMULATE_HARDWARE, False
),
}
if new_addon_config != addon_config:

View File

@ -123,5 +123,5 @@ 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 = (
"55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70"
"2e39d98fc56386389fbb35e5a98fa1b44b9fdd8f971460303587cff408430d4cfcde6134"
)

View File

@ -123,6 +123,7 @@ def get_device_entities(
"entity_category": entry.entity_category,
"supported_features": entry.supported_features,
"unit_of_measurement": entry.unit_of_measurement,
"value_id": value_id,
"primary_value": primary_value_data,
}
entities.append(entity)

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN, LOGGER
from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id, get_unique_id
from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
EVENT_VALUE_UPDATED = "value updated"
EVENT_VALUE_REMOVED = "value removed"
@ -96,6 +96,17 @@ class ZWaveBaseEntity(Entity):
self.async_on_remove(
self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
"remove_entity_on_ready_node"
),
self.async_remove,
)
)
for status_event in (EVENT_ALIVE, EVENT_DEAD):
self.async_on_remove(

View File

@ -4,6 +4,7 @@ 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
@ -11,7 +12,7 @@ from zwave_js_server.client import Client as ZwaveClient
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 FirmwareUpdateInfo
from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress
from zwave_js_server.model.node import Node as ZwaveNode
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
@ -63,7 +64,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
_attr_entity_category = EntityCategory.CONFIG
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.RELEASE_NOTES
| UpdateEntityFeature.PROGRESS
)
_attr_has_entity_name = True
_attr_should_poll = False
@ -78,6 +81,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._latest_version_firmware: FirmwareUpdateInfo | None = None
self._status_unsub: Callable[[], None] | None = None
self._poll_unsub: Callable[[], None] | None = None
self._progress_unsub: Callable[[], None] | None = None
self._num_files_installed: int = 0
# Entity class attributes
self._attr_name = "Firmware"
@ -93,6 +98,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._status_unsub = None
self.hass.async_create_task(self._async_update())
@callback
def _update_progress(self, event: dict[str, Any]) -> None:
"""Update install progress on event."""
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.async_write_ha_state()
@callback
def _reset_progress(self) -> None:
"""Reset update install progress."""
if self._progress_unsub:
self._progress_unsub()
self._progress_unsub = None
self._num_files_installed = 0
self._attr_in_progress = False
self.async_write_ha_state()
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
"""Update the entity."""
self._poll_unsub = None
@ -152,18 +187,29 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
"""Install an update."""
firmware = self._latest_version_firmware
assert firmware
try:
for file in firmware.files:
self._attr_in_progress = 0
self.async_write_ha_state()
self._progress_unsub = self.node.on(
"firmware update progress", self._update_progress
)
for file in firmware.files:
try:
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 = self._attr_latest_version = firmware.version
self._latest_version_firmware = None
except BaseZwaveJSServerError as err:
self._reset_progress()
raise HomeAssistantError(err) from err
self._num_files_installed += 1
self._attr_in_progress = floor(
100 * self._num_files_installed / len(firmware.files)
)
self.async_write_ha_state()
self._attr_installed_version = self._attr_latest_version = firmware.version
self._latest_version_firmware = None
self._reset_progress()
async def async_poll_value(self, _: bool) -> None:
"""Poll a value."""
LOGGER.error(
@ -189,6 +235,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_ready_node",
self.async_remove,
)
)
self.async_on_remove(async_at_start(self.hass, self._async_update))
async def async_will_remove_from_hass(self) -> None:
@ -200,3 +254,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
if self._poll_unsub:
self._poll_unsub()
self._poll_unsub = None
if self._progress_unsub:
self._progress_unsub()
self._progress_unsub = None

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -11,8 +11,8 @@ attrs==21.2.0
awesomeversion==22.8.0
bcrypt==3.1.7
bleak==0.16.0
bluetooth-adapters==0.3.4
bluetooth-auto-recovery==0.3.1
bluetooth-adapters==0.3.5
bluetooth-auto-recovery==0.3.2
certifi>=2021.5.30
ciso8601==2.2.0
cryptography==37.0.4

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2022.9.0"
version = "2022.9.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -430,10 +430,10 @@ bluemaestro-ble==0.2.0
# bluepy==1.3.0
# homeassistant.components.bluetooth
bluetooth-adapters==0.3.4
bluetooth-adapters==0.3.5
# homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.1
bluetooth-auto-recovery==0.3.2
# homeassistant.components.bond
bond-async==0.1.22
@ -1668,7 +1668,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0
# homeassistant.components.litterrobot
pylitterbot==2022.8.2
pylitterbot==2022.9.1
# homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1
@ -2118,7 +2118,7 @@ raincloudy==0.0.7
raspyrfm-client==1.2.8
# homeassistant.components.rainmachine
regenmaschine==2022.08.0
regenmaschine==2022.09.0
# homeassistant.components.renault
renault-api==0.1.11
@ -2449,7 +2449,7 @@ vallox-websocket-api==2.12.0
vehicle==0.4.0
# homeassistant.components.velbus
velbus-aio==2022.6.2
velbus-aio==2022.9.1
# homeassistant.components.venstar
venstarcolortouch==0.18

View File

@ -341,10 +341,10 @@ blinkpy==0.19.0
bluemaestro-ble==0.2.0
# homeassistant.components.bluetooth
bluetooth-adapters==0.3.4
bluetooth-adapters==0.3.5
# homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.1
bluetooth-auto-recovery==0.3.2
# homeassistant.components.bond
bond-async==0.1.22
@ -1166,7 +1166,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0
# homeassistant.components.litterrobot
pylitterbot==2022.8.2
pylitterbot==2022.9.1
# homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1
@ -1451,7 +1451,7 @@ radios==0.1.1
radiotherm==2.1.0
# homeassistant.components.rainmachine
regenmaschine==2022.08.0
regenmaschine==2022.09.0
# homeassistant.components.renault
renault-api==0.1.11
@ -1677,7 +1677,7 @@ vallox-websocket-api==2.12.0
vehicle==0.4.0
# homeassistant.components.velbus
velbus-aio==2022.6.2
velbus-aio==2022.9.1
# homeassistant.components.venstar
venstarcolortouch==0.18

View File

@ -17,13 +17,13 @@ from tests.common import MockConfigEntry
def create_mock_robot(
robot_data: dict | None = None, side_effect: Any | None = None
robot_data: dict | None, account: Account, side_effect: Any | None = None
) -> Robot:
"""Create a mock Litter-Robot device."""
if not robot_data:
robot_data = {}
robot = LitterRobot3(data={**ROBOT_DATA, **robot_data})
robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account)
robot.start_cleaning = AsyncMock(side_effect=side_effect)
robot.set_power_status = AsyncMock(side_effect=side_effect)
robot.reset_waste_drawer = AsyncMock(side_effect=side_effect)
@ -44,7 +44,9 @@ def create_mock_account(
account = MagicMock(spec=Account)
account.connect = AsyncMock()
account.refresh_robots = AsyncMock()
account.robots = [] if skip_robots else [create_mock_robot(robot_data, side_effect)]
account.robots = (
[] if skip_robots else [create_mock_robot(robot_data, account, side_effect)]
)
return account

View File

@ -208,6 +208,8 @@ def _create_mocked_owm(is_api_online: bool):
mocked_owm.one_call.return_value = one_call
mocked_owm.weather_manager.return_value.one_call.return_value = is_api_online
mocked_owm.weather_manager.return_value.weather_at_coords.return_value = (
is_api_online
)
return mocked_owm

View File

@ -1951,6 +1951,30 @@ async def different_device_server_version(*args):
0,
different_device_server_version,
),
(
{"config": ADDON_DISCOVERY_INFO},
{},
{
"device": "/test",
"network_key": "old123",
"s0_legacy_key": "old123",
"s2_access_control_key": "old456",
"s2_authenticated_key": "old789",
"s2_unauthenticated_key": "old987",
"log_level": "info",
},
{
"usb_path": "/new",
"s0_legacy_key": "new123",
"s2_access_control_key": "new456",
"s2_authenticated_key": "new789",
"s2_unauthenticated_key": "new987",
"log_level": "info",
"emulate_hardware": False,
},
0,
different_device_server_version,
),
],
)
async def test_options_different_device(
@ -2018,14 +2042,16 @@ async def test_options_different_device(
result = await hass.config_entries.options.async_configure(result["flow_id"])
await hass.async_block_till_done()
# Default emulate_hardware is False.
addon_options = {"emulate_hardware": False} | old_addon_options
# Legacy network key is not reset.
old_addon_options.pop("network_key")
addon_options.pop("network_key")
assert set_addon_options.call_count == 2
assert set_addon_options.call_args == call(
hass,
"core_zwave_js",
{"options": old_addon_options},
{"options": addon_options},
)
assert result["type"] == "progress"
assert result["step_id"] == "start_addon"

View File

@ -152,6 +152,7 @@ async def test_device_diagnostics_missing_primary_value(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
)
assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] == {
"command_class": value.command_class,
"command_class_name": value.command_class_name,
@ -189,4 +190,5 @@ async def test_device_diagnostics_missing_primary_value(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
)
assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] is None

View File

@ -3,6 +3,7 @@ from copy import deepcopy
from unittest.mock import call, patch
import pytest
from zwave_js_server.client import Client
from zwave_js_server.event import Event
from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
from zwave_js_server.model.node import Node
@ -12,6 +13,7 @@ from homeassistant.components.zwave_js.const import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
@ -242,6 +244,61 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration):
)
async def test_existing_node_reinterview(
hass: HomeAssistant,
client: Client,
multisensor_6_state: dict,
multisensor_6: Node,
integration: MockConfigEntry,
) -> None:
"""Test we handle a node re-interview firing a node ready event."""
dev_reg = dr.async_get(hass)
node = multisensor_6
assert client.driver is not None
air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}"
air_temperature_device_id_ext = (
f"{air_temperature_device_id}-{node.manufacturer_id}:"
f"{node.product_type}:{node.product_id}"
)
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
assert state # entity and device added
assert state.state != STATE_UNAVAILABLE
device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
assert device
assert device == dev_reg.async_get_device(
identifiers={(DOMAIN, air_temperature_device_id_ext)}
)
assert device.sw_version == "1.12"
node_state = deepcopy(multisensor_6_state)
node_state["firmwareVersion"] = "1.13"
event = Event(
type="ready",
data={
"source": "node",
"event": "ready",
"nodeId": node.node_id,
"nodeState": node_state,
},
)
client.driver.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
assert state
assert state.state != STATE_UNAVAILABLE
device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
assert device
assert device == dev_reg.async_get_device(
identifiers={(DOMAIN, air_temperature_device_id_ext)}
)
assert device.sw_version == "1.13"
async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration):
"""Test we handle a non-ready node that exists during integration setup."""
dev_reg = dr.async_get(hass)