diff --git a/CODEOWNERS b/CODEOWNERS index 4609e6330d6..c4706cee1b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -867,8 +867,8 @@ build.json @home-assistant/supervisor /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse -/homeassistant/components/qingping/ @bdraco -/tests/components/qingping/ @bdraco +/homeassistant/components/qingping/ @bdraco @skgsergio +/tests/components/qingping/ @bdraco @skgsergio /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap_qsw/ @Noltari diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 154c443e799..685edc3b6d7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import blueprint +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -20,6 +21,7 @@ from homeassistant.const import ( CONF_EVENT_DATA, CONF_ID, CONF_MODE, + CONF_PATH, CONF_PLATFORM, CONF_VARIABLES, CONF_ZONE, @@ -224,6 +226,21 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: return list(automation_entity.referenced_areas) +@callback +def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all automations that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if automation_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all automations.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -346,7 +363,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return self.action_script.referenced_areas @property - def referenced_devices(self): + def referenced_blueprint(self) -> str | None: + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]) + + @property + def referenced_devices(self) -> set[str]: """Return a set of referenced devices.""" if self._referenced_devices is not None: return self._referenced_devices diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 3be11afe18b..7c2efc17bf4 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "automation_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any automation references the blueprint.""" + from . import automations_with_blueprint # pylint: disable=import-outside-toplevel + + return len(automations_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 23ab6398333..3087309f36a 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from . import websocket_api -from .const import DOMAIN # noqa: F401 +from .const import CONF_USE_BLUEPRINT, DOMAIN # noqa: F401 from .errors import ( # noqa: F401 BlueprintException, BlueprintWithNameException, diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index aceca533d23..fe714542e0f 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -91,3 +91,11 @@ class FileAlreadyExists(BlueprintWithNameException): def __init__(self, domain: str, blueprint_name: str) -> None: """Initialize blueprint exception.""" super().__init__(domain, blueprint_name, "Blueprint already exists") + + +class BlueprintInUse(BlueprintWithNameException): + """Error when a blueprint is in use.""" + + def __init__(self, domain: str, blueprint_name: str) -> None: + """Initialize blueprint exception.""" + super().__init__(domain, blueprint_name, "Blueprint in use") diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 0d90c663b4f..f77a2bed9a4 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging import pathlib import shutil @@ -35,6 +36,7 @@ from .const import ( ) from .errors import ( BlueprintException, + BlueprintInUse, FailedToLoad, FileAlreadyExists, InvalidBlueprint, @@ -183,11 +185,13 @@ class DomainBlueprints: hass: HomeAssistant, domain: str, logger: logging.Logger, + blueprint_in_use: Callable[[HomeAssistant, str], bool], ) -> None: """Initialize a domain blueprints instance.""" self.hass = hass self.domain = domain self.logger = logger + self._blueprint_in_use = blueprint_in_use self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() @@ -302,6 +306,8 @@ class DomainBlueprints: async def async_remove_blueprint(self, blueprint_path: str) -> None: """Remove a blueprint file.""" + if self._blueprint_in_use(self.hass, blueprint_path): + raise BlueprintInUse(self.domain, blueprint_path) path = self.blueprint_folder / blueprint_path await self.hass.async_add_executor_job(path.unlink) self._blueprints[blueprint_path] = None diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 80817deb2a1..014da818e12 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -384,11 +384,11 @@ class BluetoothManager: callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True) connectable = callback_matcher[CONNECTABLE] - self._callback_index.add_with_address(callback_matcher) + self._callback_index.add_callback_matcher(callback_matcher) @hass_callback def _async_remove_callback() -> None: - self._callback_index.remove_with_address(callback_matcher) + self._callback_index.remove_callback_matcher(callback_matcher) # If we have history for the subscriber, we can trigger the callback # immediately with the last packet so the subscriber can see the diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index acc8a6977e3..1b1ec016e82 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.17.0", - "bleak-retry-connector==1.15.1", + "bleak-retry-connector==1.17.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.4.0" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index dd1c9c1fa3c..1a59ee6fe4c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -173,7 +173,7 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid_set: set[str] = set() self.manufacturer_id_set: set[int] = set() - def add(self, matcher: _T) -> None: + def add(self, matcher: _T) -> bool: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -185,26 +185,28 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) - return + return True # Manufacturer data is 2nd cheapest since its all ints if MANUFACTURER_ID in matcher: self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( matcher ) - return + return True if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append( matcher ) - return + return True - def remove(self, matcher: _T) -> None: + return False + + def remove(self, matcher: _T) -> bool: """Remove a matcher from the index. Matchers only end up in one bucket, so once we have @@ -214,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove( matcher ) - return + return True if MANUFACTURER_ID in matcher: self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return + return True if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) - return + return True + + return False def build(self) -> None: """Rebuild the index sets.""" @@ -284,8 +288,11 @@ class BluetoothCallbackMatcherIndex( """Initialize the matcher index.""" super().__init__() self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {} + self.connectable: list[BluetoothCallbackMatcherWithCallback] = [] - def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None: + def add_callback_matcher( + self, matcher: BluetoothCallbackMatcherWithCallback + ) -> None: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -296,10 +303,15 @@ class BluetoothCallbackMatcherIndex( self.address.setdefault(matcher[ADDRESS], []).append(matcher) return - super().add(matcher) - self.build() + if super().add(matcher): + self.build() + return - def remove_with_address( + if CONNECTABLE in matcher: + self.connectable.append(matcher) + return + + def remove_callback_matcher( self, matcher: BluetoothCallbackMatcherWithCallback ) -> None: """Remove a matcher from the index. @@ -311,8 +323,13 @@ class BluetoothCallbackMatcherIndex( self.address[matcher[ADDRESS]].remove(matcher) return - super().remove(matcher) - self.build() + if super().remove(matcher): + self.build() + return + + if CONNECTABLE in matcher: + self.connectable.remove(matcher) + return def match_callbacks( self, service_info: BluetoothServiceInfoBleak @@ -322,6 +339,9 @@ class BluetoothCallbackMatcherIndex( for matcher in self.address.get(service_info.address, []): if ble_device_matches(matcher, service_info): matches.append(matcher) + for matcher in self.connectable: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -355,7 +375,6 @@ def ble_device_matches( # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. - if matcher.get(CONNECTABLE, True) and not service_info.connectable: return False diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b4aaab5acf0..8afbe6a70e4 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -423,7 +423,7 @@ class HKDevice: if self._polling_interval_remover: self._polling_interval_remover() - await self.pairing.close() + await self.pairing.shutdown() await self.hass.config_entries.async_unload_platforms( self.config_entry, self.platforms diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 0b67f80bac5..d9e5bfb854b 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.5.7"], + "requirements": ["aiohomekit==1.5.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 735c05e659c..9fb39f9fb47 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -2,7 +2,7 @@ "domain": "lametric", "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", - "requirements": ["demetriek==0.2.2"], + "requirements": ["demetriek==0.2.4"], "codeowners": ["@robbiet480", "@frenck"], "iot_class": "local_polling", "dependencies": ["application_credentials"], diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 89b4fdb26af..65725ed482a 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ble_ble", - "requirements": ["led-ble==0.10.0"], + "requirements": ["led-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 3fee1445758..8cf2456c0b4 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -91,7 +91,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = discovery_info _properties = discovery_info.properties - unique_id = discovery_info.hostname.split(".")[0] + unique_id = discovery_info.hostname.split(".")[0].split("-")[0] if config_entry := await self.async_set_unique_id(unique_id): try: await validate_gw_input( diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 4e1189f3782..85df751bfc7 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -11,8 +11,8 @@ "connectable": false } ], - "requirements": ["qingping-ble==0.6.0"], + "requirements": ["qingping-ble==0.7.0"], "dependencies": ["bluetooth"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@skgsergio"], "iot_class": "local_push" } diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 9703b5775bc..bd8bbfd715f 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.4"], + "requirements": ["pyrisco==0.5.5"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index efad242fbd0..53bd256c624 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -8,7 +8,7 @@ from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.components.blueprint import BlueprintInputs +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_ICON, CONF_MODE, CONF_NAME, + CONF_PATH, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -165,6 +166,21 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: return list(script_entity.script.referenced_areas) +@callback +def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all scripts that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + script_entity.entity_id + for script_entity in component.entities + if script_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Load the scripts from the configuration.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -372,6 +388,13 @@ class ScriptEntity(ToggleEntity, RestoreEntity): """Return true if script is on.""" return self.script.is_running + @property + def referenced_blueprint(self): + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH] + @callback def async_change_listener(self): """Update state.""" diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index 3c78138a4ec..9f0d4399d3d 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "script_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any script references the blueprint.""" + from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel + + return len(scripts_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints: """Get script blueprints.""" - return DomainBlueprints(hass, DOMAIN, LOGGER) + return DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2aa58b16a7e..1fb61547445 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.15"], + "requirements": ["python-songpal==0.15.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7307187bf54..58e77dfe1bf 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): - raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") + raise ConfigEntryNotReady(f"{address} is not advertising state") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) await hass.config_entries.async_forward_entry_setups( diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 94018c1b46b..ee93c74af37 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -73,7 +73,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): - if "modelName" in self.data: + if "modelName" in adv.data: self._ready_event.set() _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) if not self.device.advertisement_changed(adv): diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 41b3f5aa61b..a321c964edc 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.19.8"], + "requirements": ["PySwitchbot==0.19.9"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index eb13b68a7e2..639d2362026 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -24,7 +24,7 @@ }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.3.1"], + "requirements": ["thermobeacon-ble==0.3.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d4596619241..c42d48a604e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -616,8 +616,7 @@ async def handle_test_condition( from homeassistant.helpers import condition # Do static + dynamic validation of the condition - config = cv.CONDITION_SCHEMA(msg["condition"]) - config = await condition.async_validate_condition_config(hass, config) + config = await condition.async_validate_condition_config(hass, msg["condition"]) # Test the condition check_condition = await condition.async_from_config(hass, config) connection.send_result( diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 11922956c25..c0711a02a36 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,15 +112,6 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] -# number of speed levels each fan has -SPEEDS_FAN_MIOT = { - MODEL_FAN_1C: 3, - MODEL_FAN_P10: 4, - MODEL_FAN_P11: 4, - MODEL_FAN_P9: 4, - MODEL_FAN_ZA5: 4, -} - MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 901211d1d2d..ddbd45bff08 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,7 +85,6 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, - SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -235,13 +234,11 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + elif model == MODEL_FAN_1C: + entity = XiaomiFan1C(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanMiot( - device, config_entry, unique_id, coordinator, speed_count - ) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -1049,11 +1046,6 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" - def __init__(self, device, entry, unique_id, coordinator, speed_count): - """Initialize MIOT fan with speed count.""" - super().__init__(device, entry, unique_id, coordinator) - self._speed_count = speed_count - @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1071,9 +1063,7 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = ranged_value_to_percentage( - (1, self._speed_count), self.coordinator.data.speed - ) + self._percentage = self.coordinator.data.speed else: self._percentage = 0 @@ -1092,6 +1082,59 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = preset_mode self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan.""" + if percentage == 0: + self._percentage = 0 + await self.async_turn_off() + return + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + percentage, + ) + if result: + self._percentage = percentage + + if not self.is_on: + await self.async_turn_on() + elif result: + self.async_write_ha_state() + + +class XiaomiFanZA5(XiaomiFanMiot): + """Representation of a Xiaomi Fan ZA5.""" + + @property + def operation_mode_class(self): + """Hold operation mode class.""" + return FanZA5OperationMode + + +class XiaomiFan1C(XiaomiFanMiot): + """Representation of a Xiaomi Fan 1C (Standing Fan 2 Lite).""" + + def __init__(self, device, entry, unique_id, coordinator): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = 3 + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._state = self.coordinator.data.is_on + self._preset_mode = self.coordinator.data.mode.name + self._oscillating = self.coordinator.data.oscillate + if self.coordinator.data.is_on: + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) + else: + self._percentage = 0 + + self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan.""" if percentage == 0: @@ -1116,12 +1159,3 @@ class XiaomiFanMiot(XiaomiGenericFan): if result: self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() - - -class XiaomiFanZA5(XiaomiFanMiot): - """Representation of a Xiaomi Fan ZA5.""" - - @property - def operation_mode_class(self): - """Hold operation mode class.""" - return FanZA5OperationMode diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 673521f9e06..c70669f7cc6 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.9.0"], + "requirements": ["yalexs-ble==1.9.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 33d32e96fe0..c915d2a3e63 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -51,6 +51,9 @@ VALUES_TO_REDACT = ( def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: """Redact value of a Z-Wave value.""" + # If the value has no value, there is nothing to redact + if zwave_value.get("value") in (None, ""): + return zwave_value for value_to_redact in VALUES_TO_REDACT: command_class = None if "commandClass" in zwave_value: diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 932ed46a0fc..cb52259b654 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -96,7 +96,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): 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" - self._attr_installed_version = self._attr_latest_version = node.firmware_version + self._attr_installed_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) @@ -135,7 +135,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): @callback def _unsub_firmware_events_and_reset_progress( - self, write_state: bool = False + self, write_state: bool = True ) -> None: """Unsubscribe from firmware events and reset update install progress.""" if self._progress_unsub: @@ -184,20 +184,26 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): err, ) else: - if available_firmware_updates: - self._latest_version_firmware = latest_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise the installed + # version is the latest. + if ( + available_firmware_updates + and ( + latest_firmware := max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) ) - - # If we have an available firmware update that is a higher version than - # what's on the node, we should advertise it, otherwise there is - # nothing to do. - new_version = latest_firmware.version - current_version = self.node.firmware_version - if AwesomeVersion(new_version) > AwesomeVersion(current_version): - self._attr_latest_version = new_version - self.async_write_ha_state() + and AwesomeVersion(latest_firmware.version) + > AwesomeVersion(self.node.firmware_version) + ): + self._latest_version_firmware = latest_firmware + self._attr_latest_version = latest_firmware.version + self.async_write_ha_state() + elif self._attr_latest_version != self._attr_installed_version: + self._attr_latest_version = self._attr_installed_version + self.async_write_ha_state() finally: self._poll_unsub = async_call_later( self.hass, timedelta(days=1), self._async_update @@ -215,12 +221,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._unsub_firmware_events_and_reset_progress(True) + self._unsub_firmware_events_and_reset_progress(False) + self._attr_in_progress = True + self.async_write_ha_state() self._progress_unsub = self.node.on( "firmware update progress", self._update_progress ) - self._finished_unsub = self.node.once( + self._finished_unsub = self.node.on( "firmware update finished", self._update_finished ) @@ -235,6 +243,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): # 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 @@ -253,8 +263,12 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): 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 self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None self._unsub_firmware_events_and_reset_progress() @@ -304,4 +318,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub() self._poll_unsub = None - self._unsub_firmware_events_and_reset_progress() + self._unsub_firmware_events_and_reset_progress(False) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f37efb6c627..7e9f3b1248d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1024,7 +1024,10 @@ class ConfigEntries: raise UnknownEntry if entry.state is not ConfigEntryState.NOT_LOADED: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id {entry.entry_id}" + f" cannot be setup because is already loaded in the {entry.state} state" + ) # Setup Component if not set up yet if entry.domain in self.hass.config.components: @@ -1046,7 +1049,10 @@ class ConfigEntries: raise UnknownEntry if not entry.state.recoverable: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id " + f"{entry.entry_id} cannot be unloaded because it is not in a recoverable state ({entry.state})" + ) return await entry.async_unload(self.hass) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5b17e764b8a..c11f9a9e054 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a86c2b07bf7..3ee61afca23 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 bleak==0.17.0 bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.3 diff --git a/pyproject.toml b/pyproject.toml index 316c0eedda6..2d1213ea199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.4" +version = "2022.9.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index ecba6634ed6..91a8c24ad1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.7 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -408,7 +408,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 @@ -558,7 +558,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 @@ -974,7 +974,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 @@ -1826,7 +1826,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -2005,7 +2005,7 @@ python-ripple-api==0.0.3 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 @@ -2100,7 +2100,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.qnap qnapstats==0.4.0 @@ -2369,7 +2369,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 @@ -2548,7 +2548,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3588ff16c2..8c0e0039851 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.5.7 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -329,7 +329,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 @@ -429,7 +429,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 @@ -712,7 +712,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 @@ -1279,7 +1279,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1377,7 +1377,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 @@ -1445,7 +1445,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.rachio rachiopy==1.0.3 @@ -1618,7 +1618,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 @@ -1752,7 +1752,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 497e8b36e99..02ed94709db 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -47,7 +47,9 @@ def blueprint_2(): @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" - return models.DomainBlueprints(hass, "automation", logging.getLogger(__name__)) + return models.DomainBlueprints( + hass, "automation", logging.getLogger(__name__), None + ) def test_blueprint_model_init(): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index eb2d12f5081..05c0e4adc4c 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -8,13 +8,26 @@ from homeassistant.setup import async_setup_component from homeassistant.util.yaml import parse_yaml +@pytest.fixture +def automation_config(): + """Automation config.""" + return {} + + +@pytest.fixture +def script_config(): + """Script config.""" + return {} + + @pytest.fixture(autouse=True) -async def setup_bp(hass): +async def setup_bp(hass, automation_config, script_config): """Fixture to set up the blueprint component.""" assert await async_setup_component(hass, "blueprint", {}) - # Trigger registration of automation blueprints - await async_setup_component(hass, "automation", {}) + # Trigger registration of automation and script blueprints + await async_setup_component(hass, "automation", automation_config) + await async_setup_component(hass, "script", script_config) async def test_list_blueprints(hass, hass_ws_client): @@ -251,3 +264,89 @@ async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_cli assert msg["id"] == 9 assert not msg["success"] + + +@pytest.mark.parametrize( + "automation_config", + ( + { + "automation": { + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_automation( + hass, aioclient_mock, hass_ws_client +): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_event_service.yaml", + "domain": "automation", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } + + +@pytest.mark.parametrize( + "script_config", + ( + { + "script": { + "test_script": { + "use_blueprint": { + "path": "test_service.yaml", + "input": { + "service_to_call": "test.automation", + }, + } + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_script(hass, aioclient_mock, hass_ws_client): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_service.yaml", + "domain": "script", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e4b84b943b4..dcbf51a1dbd 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1321,6 +1321,61 @@ async def test_register_callback_by_manufacturer_id( assert service_info.manufacturer_id == 21 +async def test_register_callback_by_connectable( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by connectable.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {CONNECTABLE: False}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 2 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "rtx" + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + + async def test_filtering_noisy_apple_devices( hass, mock_bleak_scanner_start, enable_bluetooth ): diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 4dbe1d2615f..84d2335b16f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -36,7 +36,8 @@ TEST_USERNAME2 = "stretch" TEST_DISCOVERY = ZeroconfServiceInfo( host=TEST_HOST, addresses=[TEST_HOST], - hostname=f"{TEST_HOSTNAME}.local.", + # The added `-2` is to simulate mDNS collision + hostname=f"{TEST_HOSTNAME}-2.local.", name="mock_name", port=DEFAULT_PORT, properties={ diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 354a4edeb0d..a5905a809a9 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1603,6 +1603,42 @@ async def test_test_condition(hass, websocket_client): assert msg["success"] assert msg["result"]["result"] is True + await websocket_client.send_json( + { + "id": 6, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'paulus') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is True + + await websocket_client.send_json( + { + "id": 7, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'frenck') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is False + async def test_execute_script(hass, websocket_client): """Test testing a condition.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 04a9c5671f9..075ac76ede8 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -264,6 +264,12 @@ def config_entry_diagnostics_fixture(): return json.loads(load_fixture("zwave_js/config_entry_diagnostics.json")) +@pytest.fixture(name="config_entry_diagnostics_redacted", scope="session") +def config_entry_diagnostics_redacted_fixture(): + """Load the redacted config entry diagnostics fixture data.""" + return json.loads(load_fixture("zwave_js/config_entry_diagnostics_redacted.json")) + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json new file mode 100644 index 00000000000..1e68d82d586 --- /dev/null +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json @@ -0,0 +1,1936 @@ +[ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": "**REDACTED**", + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true + }, + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": "**REDACTED**", + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", + "manufacturerId": 134, + "productType": 1, + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, + 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, + 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, + 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, + 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, + 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false + }, + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "**REDACTED**", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } + } + } +] diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 41505364111..64f27805243 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from zwave_js_server.event import Event -from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.zwave_js.diagnostics import ( ZwaveValueMatcher, async_get_device_diagnostics, @@ -26,7 +25,11 @@ from tests.components.diagnostics import ( async def test_config_entry_diagnostics( - hass, hass_client, integration, config_entry_diagnostics + hass, + hass_client, + integration, + config_entry_diagnostics, + config_entry_diagnostics_redacted, ): """Test the config entry level diagnostics data dump.""" with patch( @@ -36,16 +39,7 @@ async def test_config_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, integration ) - assert len(diagnostics) == 3 - assert diagnostics[0]["homeId"] == REDACTED - nodes = diagnostics[2]["result"]["state"]["nodes"] - for node in nodes: - assert "location" not in node or node["location"] == REDACTED - for value in node["values"]: - if value["commandClass"] == 99 and value["property"] == "userCode": - assert value["value"] == REDACTED - else: - assert value.get("value") != REDACTED + assert diagnostics == config_entry_diagnostics_redacted async def test_device_diagnostics( diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 0b567d93106..b2517c3dd34 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -7,14 +7,16 @@ from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand from zwave_js_server.model.firmware import FirmwareUpdateStatus -from homeassistant.components.update.const import ( +from homeassistant.components.update import ( ATTR_AUTO_UPDATE, ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, + SERVICE_SKIP, ) from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id @@ -52,6 +54,19 @@ FIRMWARE_UPDATES = { ] } +FIRMWARE_UPDATE_MULTIPLE_FILES = { + "updates": [ + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"}, + {"target": 1, "url": "https://example4.com", "integrity": "sha4"}, + ], + }, + ] +} + async def test_update_entity_states( hass, @@ -64,7 +79,6 @@ async def test_update_entity_states( ): """Test update entity states.""" ws_client = await hass_ws_client(hass) - await hass.async_block_till_done() assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF @@ -327,6 +341,11 @@ async def test_update_entity_progress( # 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 + event = Event( type="firmware update progress", data={ @@ -362,7 +381,142 @@ async def test_update_entity_progress( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + 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_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_OFF @@ -445,7 +599,7 @@ async def test_update_entity_install_failed( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_IN_PROGRESS] == 0 assert attrs[ATTR_INSTALLED_VERSION] == "10.7" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON @@ -453,3 +607,63 @@ async def test_update_entity_install_failed( # validate that the install task failed with pytest.raises(HomeAssistantError): await install_task + + +async def test_update_entity_reload( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity maintains state after reload.""" + 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 + + client.async_send_command.return_value = FIRMWARE_UPDATES + + 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 hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_SKIP, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Trigger another update and make sure the skipped version is still skipped + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=4)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b923e37b636..d2d4ffe1134 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1141,7 +1141,7 @@ async def test_entry_setup_invalid_state(hass, manager, state): MockModule("comp", async_setup=mock_setup, async_setup_entry=mock_setup_entry), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 0 @@ -1201,7 +1201,7 @@ async def test_entry_unload_invalid_state(hass, manager, state): mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1296,7 +1296,7 @@ async def test_entry_reload_error(hass, manager, state): ), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1370,7 +1370,10 @@ async def test_entry_disable_without_reload_support(hass, manager): assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD # Enable - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.FAILED_UNLOAD), + ): await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 @@ -3270,7 +3273,10 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): ) entry.add_to_hass(hass) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.SETUP_IN_PROGRESS), + ): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS diff --git a/tests/testing_config/blueprints/script/test_service.yaml b/tests/testing_config/blueprints/script/test_service.yaml new file mode 100644 index 00000000000..4de991e90dc --- /dev/null +++ b/tests/testing_config/blueprints/script/test_service.yaml @@ -0,0 +1,8 @@ +blueprint: + name: "Call service" + domain: script + input: + service_to_call: +sequence: + service: !input service_to_call + entity_id: light.kitchen