This commit is contained in:
Paulus Schoutsen 2022-09-18 14:05:34 -04:00 committed by GitHub
commit a411cd9c20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2643 additions and 145 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"],

View File

@ -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"],

View File

@ -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": [

View File

@ -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(

View File

@ -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"
}

View File

@ -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",

View File

@ -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."""

View File

@ -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)

View File

@ -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": [
{

View File

@ -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(

View File

@ -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):

View File

@ -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": [

View File

@ -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",

View File

@ -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(

View File

@ -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,

View File

@ -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

View File

@ -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": [

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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",
}

View File

@ -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
):

View File

@ -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={

View File

@ -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."""

View File

@ -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."""

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,8 @@
blueprint:
name: "Call service"
domain: script
input:
service_to_call:
sequence:
service: !input service_to_call
entity_id: light.kitchen