2022.11.5 (#82980)
Co-authored-by: mvn23 <schopdiedwaas@gmail.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Allen Porter <allen@thebends.org> Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: Daniel Hjelseth Høyer <github@dahoiv.net> Co-authored-by: Aaron Bach <bachya1208@gmail.com>pull/83023/head 2022.11.5
commit
13a4541b7b
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any, TypeVar, cast
|
||||
import uuid
|
||||
|
@ -65,6 +66,8 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
|
|||
)
|
||||
if disconnected_event.is_set():
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
raise BleakError(
|
||||
f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access
|
||||
"Disconnected during operation"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==4.0.2", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.0.3", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.characteristics.const import InputEventValues
|
||||
from aiohomekit.model.services import ServicesTypes
|
||||
from aiohomekit.model.services import Service, ServicesTypes
|
||||
from aiohomekit.utils import clamp_enum_to_char
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -57,28 +57,41 @@ HK_TO_HA_INPUT_EVENT_VALUES = {
|
|||
class TriggerSource:
|
||||
"""Represents a stateless source of event data from HomeKit."""
|
||||
|
||||
def __init__(
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize a set of triggers for a device."""
|
||||
self._hass = hass
|
||||
self._triggers: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
self._callbacks: dict[tuple[str, str], list[Callable[[Any], None]]] = {}
|
||||
self._iid_trigger_keys: dict[int, set[tuple[str, str]]] = {}
|
||||
|
||||
async def async_setup(
|
||||
self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]]
|
||||
) -> None:
|
||||
"""Initialize a set of triggers for a device."""
|
||||
self._hass = connection.hass
|
||||
self._connection = connection
|
||||
self._aid = aid
|
||||
self._triggers: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for trigger in triggers:
|
||||
self._triggers[(trigger["type"], trigger["subtype"])] = trigger
|
||||
self._callbacks: dict[int, list[Callable[[Any], None]]] = {}
|
||||
"""Set up a set of triggers for a device.
|
||||
|
||||
def fire(self, iid, value):
|
||||
This function must be re-entrant since
|
||||
it is called when the device is first added and
|
||||
when the config entry is reloaded.
|
||||
"""
|
||||
for trigger_data in triggers:
|
||||
trigger_key = (trigger_data[CONF_TYPE], trigger_data[CONF_SUBTYPE])
|
||||
self._triggers[trigger_key] = trigger_data
|
||||
iid = trigger_data["characteristic"]
|
||||
self._iid_trigger_keys.setdefault(iid, set()).add(trigger_key)
|
||||
await connection.add_watchable_characteristics([(aid, iid)])
|
||||
|
||||
def fire(self, iid: int, value: dict[str, Any]) -> None:
|
||||
"""Process events that have been received from a HomeKit accessory."""
|
||||
for event_handler in self._callbacks.get(iid, []):
|
||||
event_handler(value)
|
||||
for trigger_key in self._iid_trigger_keys.get(iid, set()):
|
||||
for event_handler in self._callbacks.get(trigger_key, []):
|
||||
event_handler(value)
|
||||
|
||||
def async_get_triggers(self) -> Generator[tuple[str, str], None, None]:
|
||||
"""List device triggers for homekit devices."""
|
||||
"""List device triggers for HomeKit devices."""
|
||||
yield from self._triggers
|
||||
|
||||
async def async_attach_trigger(
|
||||
@callback
|
||||
def async_attach_trigger(
|
||||
self,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
|
@ -86,28 +99,25 @@ class TriggerSource:
|
|||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
trigger_key = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||
job = HassJob(action)
|
||||
|
||||
@callback
|
||||
def event_handler(char):
|
||||
def event_handler(char: dict[str, Any]) -> None:
|
||||
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
|
||||
return
|
||||
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
|
||||
|
||||
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
|
||||
iid = trigger["characteristic"]
|
||||
|
||||
await self._connection.add_watchable_characteristics([(self._aid, iid)])
|
||||
self._callbacks.setdefault(iid, []).append(event_handler)
|
||||
self._callbacks.setdefault(trigger_key, []).append(event_handler)
|
||||
|
||||
def async_remove_handler():
|
||||
if iid in self._callbacks:
|
||||
self._callbacks[iid].remove(event_handler)
|
||||
if trigger_key in self._callbacks:
|
||||
self._callbacks[trigger_key].remove(event_handler)
|
||||
|
||||
return async_remove_handler
|
||||
|
||||
|
||||
def enumerate_stateless_switch(service):
|
||||
def enumerate_stateless_switch(service: Service) -> list[dict[str, Any]]:
|
||||
"""Enumerate a stateless switch, like a single button."""
|
||||
|
||||
# A stateless switch that has a SERVICE_LABEL_INDEX is part of a group
|
||||
|
@ -135,7 +145,7 @@ def enumerate_stateless_switch(service):
|
|||
]
|
||||
|
||||
|
||||
def enumerate_stateless_switch_group(service):
|
||||
def enumerate_stateless_switch_group(service: Service) -> list[dict[str, Any]]:
|
||||
"""Enumerate a group of stateless switches, like a remote control."""
|
||||
switches = list(
|
||||
service.accessory.services.filter(
|
||||
|
@ -165,7 +175,7 @@ def enumerate_stateless_switch_group(service):
|
|||
return results
|
||||
|
||||
|
||||
def enumerate_doorbell(service):
|
||||
def enumerate_doorbell(service: Service) -> list[dict[str, Any]]:
|
||||
"""Enumerate doorbell buttons."""
|
||||
input_event = service[CharacteristicsTypes.INPUT_EVENT]
|
||||
|
||||
|
@ -217,21 +227,32 @@ async def async_setup_triggers_for_entry(
|
|||
if device_id in hass.data[TRIGGERS]:
|
||||
return False
|
||||
|
||||
# Just because we recognise the service type doesn't mean we can actually
|
||||
# Just because we recognize the service type doesn't mean we can actually
|
||||
# extract any triggers - so only proceed if we can
|
||||
triggers = TRIGGER_FINDERS[service_type](service)
|
||||
if len(triggers) == 0:
|
||||
return False
|
||||
|
||||
trigger = TriggerSource(conn, aid, triggers)
|
||||
hass.data[TRIGGERS][device_id] = trigger
|
||||
trigger = async_get_or_create_trigger_source(conn.hass, device_id)
|
||||
hass.async_create_task(trigger.async_setup(conn, aid, triggers))
|
||||
|
||||
return True
|
||||
|
||||
conn.add_listener(async_add_service)
|
||||
|
||||
|
||||
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
|
||||
@callback
|
||||
def async_get_or_create_trigger_source(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> TriggerSource:
|
||||
"""Get or create a trigger source for a device id."""
|
||||
if not (source := hass.data[TRIGGERS].get(device_id)):
|
||||
source = TriggerSource(hass)
|
||||
hass.data[TRIGGERS][device_id] = source
|
||||
return source
|
||||
|
||||
|
||||
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], dict[str, Any]]):
|
||||
"""Process events generated by a HomeKit accessory into automation triggers."""
|
||||
trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS]
|
||||
for (aid, iid), ev in events.items():
|
||||
|
@ -271,5 +292,6 @@ async def async_attach_trigger(
|
|||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
device_id = config[CONF_DEVICE_ID]
|
||||
device = hass.data[TRIGGERS][device_id]
|
||||
return await device.async_attach_trigger(config, action, trigger_info)
|
||||
return async_get_or_create_trigger_source(hass, device_id).async_attach_trigger(
|
||||
config, action, trigger_info
|
||||
)
|
||||
|
|
|
@ -354,7 +354,25 @@ class IBeaconCoordinator:
|
|||
for group_id in self._group_ids_random_macs
|
||||
if group_id not in self._unavailable_group_ids
|
||||
and (service_info := self._last_seen_by_group_id.get(group_id))
|
||||
and now - service_info.time > UNAVAILABLE_TIMEOUT
|
||||
and (
|
||||
# We will not be callbacks for iBeacons with random macs
|
||||
# that rotate infrequently since their advertisement data is
|
||||
# does not change as the bluetooth.async_register_callback API
|
||||
# suppresses callbacks for duplicate advertisements to avoid
|
||||
# exposing integrations to the firehose of bluetooth advertisements.
|
||||
#
|
||||
# To solve this we need to ask for the latest service info for
|
||||
# the address we last saw to get the latest timestamp.
|
||||
#
|
||||
# If there is no last service info for the address we know that
|
||||
# the device is no longer advertising.
|
||||
not (
|
||||
latest_service_info := bluetooth.async_last_service_info(
|
||||
self.hass, service_info.address, connectable=False
|
||||
)
|
||||
)
|
||||
or now - latest_service_info.time > UNAVAILABLE_TIMEOUT
|
||||
)
|
||||
]
|
||||
for group_id in gone_unavailable:
|
||||
self._unavailable_group_ids.add(group_id)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "opentherm_gw",
|
||||
"name": "OpenTherm Gateway",
|
||||
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
||||
"requirements": ["pyotgw==2.1.1"],
|
||||
"requirements": ["pyotgw==2.1.3"],
|
||||
"codeowners": ["@mvn23"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "sensibo",
|
||||
"name": "Sensibo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
||||
"requirements": ["pysensibo==1.0.20"],
|
||||
"requirements": ["pysensibo==1.0.22"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
|
|
@ -306,7 +306,7 @@ def _async_register_base_station(
|
|||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, system.system_id)},
|
||||
identifiers={(DOMAIN, str(system.system_id))},
|
||||
manufacturer="SimpliSafe",
|
||||
model=system.version,
|
||||
name=system.address,
|
||||
|
@ -757,7 +757,7 @@ class SimpliSafeEntity(CoordinatorEntity):
|
|||
manufacturer="SimpliSafe",
|
||||
model=model,
|
||||
name=device_name,
|
||||
via_device=(DOMAIN, system.system_id),
|
||||
via_device=(DOMAIN, str(system.system_id)),
|
||||
)
|
||||
|
||||
self._attr_unique_id = serial
|
||||
|
|
|
@ -15,7 +15,10 @@ from simplipy.websocket import (
|
|||
EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
|
||||
EVENT_DISARMED_BY_MASTER_PIN,
|
||||
EVENT_DISARMED_BY_REMOTE,
|
||||
EVENT_ENTRY_DELAY,
|
||||
EVENT_HOME_EXIT_DELAY,
|
||||
EVENT_SECRET_ALERT_TRIGGERED,
|
||||
EVENT_USER_INITIATED_TEST,
|
||||
WebsocketEvent,
|
||||
)
|
||||
|
||||
|
@ -66,9 +69,12 @@ STATE_MAP_FROM_REST_API = {
|
|||
SystemStates.ALARM_COUNT: STATE_ALARM_PENDING,
|
||||
SystemStates.AWAY: STATE_ALARM_ARMED_AWAY,
|
||||
SystemStates.AWAY_COUNT: STATE_ALARM_ARMING,
|
||||
SystemStates.ENTRY_DELAY: STATE_ALARM_PENDING,
|
||||
SystemStates.EXIT_DELAY: STATE_ALARM_ARMING,
|
||||
SystemStates.HOME: STATE_ALARM_ARMED_HOME,
|
||||
SystemStates.HOME_COUNT: STATE_ALARM_ARMING,
|
||||
SystemStates.OFF: STATE_ALARM_DISARMED,
|
||||
SystemStates.TEST: STATE_ALARM_DISARMED,
|
||||
}
|
||||
|
||||
STATE_MAP_FROM_WEBSOCKET_EVENT = {
|
||||
|
@ -82,7 +88,10 @@ STATE_MAP_FROM_WEBSOCKET_EVENT = {
|
|||
EVENT_AWAY_EXIT_DELAY_BY_REMOTE: STATE_ALARM_ARMING,
|
||||
EVENT_DISARMED_BY_MASTER_PIN: STATE_ALARM_DISARMED,
|
||||
EVENT_DISARMED_BY_REMOTE: STATE_ALARM_DISARMED,
|
||||
EVENT_ENTRY_DELAY: STATE_ALARM_PENDING,
|
||||
EVENT_HOME_EXIT_DELAY: STATE_ALARM_ARMING,
|
||||
EVENT_SECRET_ALERT_TRIGGERED: STATE_ALARM_TRIGGERED,
|
||||
EVENT_USER_INITIATED_TEST: STATE_ALARM_DISARMED,
|
||||
}
|
||||
|
||||
WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
|
||||
|
@ -156,13 +165,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
"""Set the state based on the latest REST API data."""
|
||||
if self._system.alarm_going_off:
|
||||
self._attr_state = STATE_ALARM_TRIGGERED
|
||||
elif self._system.state == SystemStates.ERROR:
|
||||
self.async_increment_error_count()
|
||||
elif state := STATE_MAP_FROM_REST_API.get(self._system.state):
|
||||
self._attr_state = state
|
||||
self.async_reset_error_count()
|
||||
else:
|
||||
LOGGER.error("Unknown system state (REST API): %s", self._system.state)
|
||||
LOGGER.warning("Unexpected system state (REST API): %s", self._system.state)
|
||||
self.async_increment_error_count()
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
|
@ -217,9 +224,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
ATTR_ALARM_DURATION: self._system.alarm_duration,
|
||||
ATTR_ALARM_VOLUME: self._system.alarm_volume.name.lower(),
|
||||
ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level,
|
||||
ATTR_CHIME_VOLUME: self._system.chime_volume.name.lower(),
|
||||
ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away,
|
||||
ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home,
|
||||
ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away,
|
||||
|
@ -227,12 +232,20 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
|
|||
ATTR_GSM_STRENGTH: self._system.gsm_strength,
|
||||
ATTR_LIGHT: self._system.light,
|
||||
ATTR_RF_JAMMING: self._system.rf_jamming,
|
||||
ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name.lower(),
|
||||
ATTR_WALL_POWER_LEVEL: self._system.wall_power_level,
|
||||
ATTR_WIFI_STRENGTH: self._system.wifi_strength,
|
||||
}
|
||||
)
|
||||
|
||||
for key, volume_prop in (
|
||||
(ATTR_ALARM_VOLUME, self._system.alarm_volume),
|
||||
(ATTR_CHIME_VOLUME, self._system.chime_volume),
|
||||
(ATTR_VOICE_PROMPT_VOLUME, self._system.voice_prompt_volume),
|
||||
):
|
||||
if not volume_prop:
|
||||
continue
|
||||
self._attr_extra_state_attributes[key] = volume_prop.name.lower()
|
||||
|
||||
self._set_state_from_system_data()
|
||||
|
||||
@callback
|
||||
|
|
|
@ -21,6 +21,7 @@ SUPPORTED_BATTERY_SENSOR_TYPES = [
|
|||
DeviceTypes.CARBON_MONOXIDE,
|
||||
DeviceTypes.ENTRY,
|
||||
DeviceTypes.GLASS_BREAK,
|
||||
DeviceTypes.KEYPAD,
|
||||
DeviceTypes.LEAK,
|
||||
DeviceTypes.LOCK_KEYPAD,
|
||||
DeviceTypes.MOTION,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==2022.07.1"],
|
||||
"requirements": ["simplisafe-python==2022.11.2"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"dhcp": [
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"domain": "tibber",
|
||||
"name": "Tibber",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"requirements": ["pyTibber==0.25.6"],
|
||||
"requirements": ["pyTibber==0.26.1"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"quality_scale": "silver",
|
||||
"config_flow": true,
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"requirements": [
|
||||
"bellows==0.34.2",
|
||||
"bellows==0.34.4",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.86",
|
||||
"zigpy-deconz==0.19.0",
|
||||
"zigpy==0.51.5",
|
||||
"zha-quirks==0.0.87",
|
||||
"zigpy-deconz==0.19.1",
|
||||
"zigpy==0.51.6",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.1"
|
||||
|
|
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
|||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 11
|
||||
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)
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.11.4"
|
||||
version = "2022.11.5"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
|
|
@ -404,7 +404,7 @@ beautifulsoup4==4.11.1
|
|||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.2
|
||||
bellows==0.34.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
|
@ -725,7 +725,7 @@ gTTS==2.2.4
|
|||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.0.2
|
||||
gcal-sync==4.0.3
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
|
@ -1412,7 +1412,7 @@ pyRFXtrx==0.30.0
|
|||
pySwitchmate==0.5.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.25.6
|
||||
pyTibber==0.26.1
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
|
@ -1784,7 +1784,7 @@ pyopnsense==0.2.0
|
|||
pyoppleio==1.0.5
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.1.1
|
||||
pyotgw==2.1.3
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
|
@ -1864,7 +1864,7 @@ pysaj==0.0.16
|
|||
pysdcp==1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.20
|
||||
pysensibo==1.0.22
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
|
@ -2262,7 +2262,7 @@ simplehound==0.3
|
|||
simplepush==2.1.1
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.07.1
|
||||
simplisafe-python==2022.11.2
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==3.1.2
|
||||
|
@ -2610,7 +2610,7 @@ zengge==0.2
|
|||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.86
|
||||
zha-quirks==0.0.87
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
|
@ -2619,7 +2619,7 @@ zhong_hong_hvac==1.0.9
|
|||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.0
|
||||
zigpy-deconz==0.19.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.16.2
|
||||
|
@ -2631,7 +2631,7 @@ zigpy-zigate==0.10.3
|
|||
zigpy-znp==0.9.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.51.5
|
||||
zigpy==0.51.6
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
|
|
@ -331,7 +331,7 @@ base36==0.1.1
|
|||
beautifulsoup4==4.11.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.2
|
||||
bellows==0.34.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
|
@ -541,7 +541,7 @@ gTTS==2.2.4
|
|||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.0.2
|
||||
gcal-sync==4.0.3
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
|
@ -1012,7 +1012,7 @@ pyMetno==0.9.0
|
|||
pyRFXtrx==0.30.0
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.25.6
|
||||
pyTibber==0.26.1
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py_nextbusnext==0.1.5
|
||||
|
@ -1261,7 +1261,7 @@ pyopenuv==2022.04.0
|
|||
pyopnsense==0.2.0
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.1.1
|
||||
pyotgw==2.1.3
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
|
@ -1314,7 +1314,7 @@ pyruckus==0.16
|
|||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.20
|
||||
pysensibo==1.0.22
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
|
@ -1559,7 +1559,7 @@ simplehound==0.3
|
|||
simplepush==2.1.1
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.07.1
|
||||
simplisafe-python==2022.11.2
|
||||
|
||||
# homeassistant.components.slack
|
||||
slackclient==2.5.0
|
||||
|
@ -1811,10 +1811,10 @@ zamg==0.1.1
|
|||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.86
|
||||
zha-quirks==0.0.87
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.0
|
||||
zigpy-deconz==0.19.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.16.2
|
||||
|
@ -1826,7 +1826,7 @@ zigpy-zigate==0.10.3
|
|||
zigpy-znp==0.9.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.51.5
|
||||
zigpy==0.51.6
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.43.0
|
||||
|
|
|
@ -6,6 +6,7 @@ import pytest
|
|||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.device_automation import DeviceAutomationType
|
||||
from homeassistant.components.homekit_controller.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
@ -338,3 +339,129 @@ async def test_handle_events(hass, utcnow, calls):
|
|||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
async def test_handle_events_late_setup(hass, utcnow, calls):
|
||||
"""Test that events are handled when setup happens after startup."""
|
||||
helper = await setup_test_component(hass, create_remote)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entry = entity_registry.async_get("sensor.testdevice_battery")
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(entry.device_id)
|
||||
|
||||
await hass.config_entries.async_unload(helper.config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert helper.config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"alias": "single_press",
|
||||
"trigger": {
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "button1",
|
||||
"subtype": "single_press",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"{{ trigger.platform}} - "
|
||||
"{{ trigger.type }} - {{ trigger.subtype }} - "
|
||||
"{{ trigger.id }}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"alias": "long_press",
|
||||
"trigger": {
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "button2",
|
||||
"subtype": "long_press",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"{{ trigger.platform}} - "
|
||||
"{{ trigger.type }} - {{ trigger.subtype }} - "
|
||||
"{{ trigger.id }}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.config_entries.async_setup(helper.config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert helper.config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Make sure first automation (only) fires for single press
|
||||
helper.pairing.testing.update_named_service(
|
||||
"Button 1", {CharacteristicsTypes.INPUT_EVENT: 0}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["some"] == "device - button1 - single_press - 0"
|
||||
|
||||
# Make sure automation doesn't trigger for long press
|
||||
helper.pairing.testing.update_named_service(
|
||||
"Button 1", {CharacteristicsTypes.INPUT_EVENT: 1}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
# Make sure automation doesn't trigger for double press
|
||||
helper.pairing.testing.update_named_service(
|
||||
"Button 1", {CharacteristicsTypes.INPUT_EVENT: 2}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
# Make sure second automation fires for long press
|
||||
helper.pairing.testing.update_named_service(
|
||||
"Button 2", {CharacteristicsTypes.INPUT_EVENT: 2}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
assert calls[1].data["some"] == "device - button2 - long_press - 0"
|
||||
|
||||
# Turn the automations off
|
||||
await hass.services.async_call(
|
||||
"automation",
|
||||
"turn_off",
|
||||
{"entity_id": "automation.long_press"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"automation",
|
||||
"turn_off",
|
||||
{"entity_id": "automation.single_press"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Make sure event no longer fires
|
||||
helper.pairing.testing.update_named_service(
|
||||
"Button 2", {CharacteristicsTypes.INPUT_EVENT: 2}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
|
|
|
@ -8,8 +8,17 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_ble_device_from_address,
|
||||
async_last_service_info,
|
||||
)
|
||||
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
|
||||
from homeassistant.components.ibeacon.const import DOMAIN, UNAVAILABLE_TIMEOUT
|
||||
from homeassistant.components.ibeacon.const import (
|
||||
DOMAIN,
|
||||
UNAVAILABLE_TIMEOUT,
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
STATE_HOME,
|
||||
|
@ -27,6 +36,7 @@ from . import (
|
|||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.bluetooth import (
|
||||
inject_bluetooth_service_info,
|
||||
inject_bluetooth_service_info_bleak,
|
||||
patch_all_discovered_devices,
|
||||
)
|
||||
|
||||
|
@ -130,3 +140,108 @@ async def test_device_tracker_random_address(hass):
|
|||
tracker_attributes = tracker.attributes
|
||||
assert tracker.state == STATE_HOME
|
||||
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
|
||||
|
||||
|
||||
async def test_device_tracker_random_address_infrequent_changes(hass):
|
||||
"""Test creating and updating device_tracker with a random mac that only changes once per day."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
start_time = time.monotonic()
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for i in range(20):
|
||||
inject_bluetooth_service_info(
|
||||
hass,
|
||||
replace(
|
||||
BEACON_RANDOM_ADDRESS_SERVICE_INFO, address=f"AA:BB:CC:DD:EE:{i:02X}"
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tracker = hass.states.get("device_tracker.randomaddress_1234")
|
||||
tracker_attributes = tracker.attributes
|
||||
assert tracker.state == STATE_HOME
|
||||
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
with patch_all_discovered_devices([]), patch(
|
||||
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
|
||||
return_value=start_time + UNAVAILABLE_TIMEOUT + 1,
|
||||
):
|
||||
async_fire_time_changed(
|
||||
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tracker = hass.states.get("device_tracker.randomaddress_1234")
|
||||
assert tracker.state == STATE_NOT_HOME
|
||||
|
||||
inject_bluetooth_service_info(
|
||||
hass, replace(BEACON_RANDOM_ADDRESS_SERVICE_INFO, address="AA:BB:CC:DD:EE:14")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tracker = hass.states.get("device_tracker.randomaddress_1234")
|
||||
tracker_attributes = tracker.attributes
|
||||
assert tracker.state == STATE_HOME
|
||||
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
|
||||
|
||||
inject_bluetooth_service_info(
|
||||
hass, replace(BEACON_RANDOM_ADDRESS_SERVICE_INFO, address="AA:BB:CC:DD:EE:14")
|
||||
)
|
||||
device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False)
|
||||
|
||||
with patch_all_discovered_devices([device]), patch(
|
||||
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
|
||||
return_value=start_time + UPDATE_INTERVAL.total_seconds() + 1,
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tracker = hass.states.get("device_tracker.randomaddress_1234")
|
||||
tracker_attributes = tracker.attributes
|
||||
assert tracker.state == STATE_HOME
|
||||
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
|
||||
|
||||
one_day_future = start_time + 86400
|
||||
previous_service_info = async_last_service_info(
|
||||
hass, "AA:BB:CC:DD:EE:14", connectable=False
|
||||
)
|
||||
inject_bluetooth_service_info_bleak(
|
||||
hass,
|
||||
BluetoothServiceInfoBleak(
|
||||
name="RandomAddress_1234",
|
||||
address="AA:BB:CC:DD:EE:14",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={76: b"\x02\x15RandCharmBeacons\x0e\xfe\x13U\xc5"},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
time=one_day_future,
|
||||
connectable=False,
|
||||
device=device,
|
||||
advertisement=previous_service_info.advertisement,
|
||||
),
|
||||
)
|
||||
device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False)
|
||||
assert (
|
||||
async_last_service_info(hass, "AA:BB:CC:DD:EE:14", connectable=False).time
|
||||
== one_day_future
|
||||
)
|
||||
|
||||
with patch_all_discovered_devices([device]), patch(
|
||||
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
|
||||
return_value=start_time + UNAVAILABLE_TIMEOUT + 1,
|
||||
):
|
||||
async_fire_time_changed(
|
||||
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT + 1)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tracker = hass.states.get("device_tracker.randomaddress_1234")
|
||||
tracker_attributes = tracker.attributes
|
||||
assert tracker.state == STATE_HOME
|
||||
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
{
|
||||
"id": "11",
|
||||
"isEnabled": false,
|
||||
"name": null,
|
||||
"acState": {
|
||||
"on": false,
|
||||
"targetTemperature": 21,
|
||||
|
|
|
@ -35,7 +35,7 @@ def patch_cluster(cluster):
|
|||
zcl_f.ReadAttributeRecord(
|
||||
attr_id,
|
||||
zcl_f.Status.SUCCESS,
|
||||
zcl_f.TypeValue(python_type=None, value=value),
|
||||
zcl_f.TypeValue(type=None, value=value),
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue