mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
2022.9.5 (#78703)
This commit is contained in:
commit
a411cd9c20
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
@ -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": [
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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": [
|
||||
{
|
||||
|
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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": [
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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": [
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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={
|
||||
|
@ -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."""
|
||||
|
@ -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
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
8
tests/testing_config/blueprints/script/test_service.yaml
Normal file
8
tests/testing_config/blueprints/script/test_service.yaml
Normal 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
|
Loading…
x
Reference in New Issue
Block a user