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
Franck Nijhof 2022-11-30 14:42:45 +01:00 committed by GitHub
commit 13a4541b7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 372 additions and 72 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import contextlib
import logging import logging
from typing import Any, TypeVar, cast from typing import Any, TypeVar, cast
import uuid import uuid
@ -65,6 +66,8 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
) )
if disconnected_event.is_set(): if disconnected_event.is_set():
task.cancel() task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
raise BleakError( raise BleakError(
f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access
"Disconnected during operation" "Disconnected during operation"

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"dependencies": ["application_credentials"], "dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/calendar.google/", "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"], "codeowners": ["@allenporter"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"] "loggers": ["googleapiclient"]

View File

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import InputEventValues 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 from aiohomekit.utils import clamp_enum_to_char
import voluptuous as vol import voluptuous as vol
@ -57,28 +57,41 @@ HK_TO_HA_INPUT_EVENT_VALUES = {
class TriggerSource: class TriggerSource:
"""Represents a stateless source of event data from HomeKit.""" """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]] self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]]
) -> None: ) -> None:
"""Initialize a set of triggers for a device.""" """Set up 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]]] = {}
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.""" """Process events that have been received from a HomeKit accessory."""
for event_handler in self._callbacks.get(iid, []): for trigger_key in self._iid_trigger_keys.get(iid, set()):
event_handler(value) for event_handler in self._callbacks.get(trigger_key, []):
event_handler(value)
def async_get_triggers(self) -> Generator[tuple[str, str], None, None]: 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 yield from self._triggers
async def async_attach_trigger( @callback
def async_attach_trigger(
self, self,
config: ConfigType, config: ConfigType,
action: TriggerActionType, action: TriggerActionType,
@ -86,28 +99,25 @@ class TriggerSource:
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
trigger_data = trigger_info["trigger_data"] trigger_data = trigger_info["trigger_data"]
trigger_key = (config[CONF_TYPE], config[CONF_SUBTYPE])
job = HassJob(action) job = HassJob(action)
@callback @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"]]: if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return return
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] self._callbacks.setdefault(trigger_key, []).append(event_handler)
iid = trigger["characteristic"]
await self._connection.add_watchable_characteristics([(self._aid, iid)])
self._callbacks.setdefault(iid, []).append(event_handler)
def async_remove_handler(): def async_remove_handler():
if iid in self._callbacks: if trigger_key in self._callbacks:
self._callbacks[iid].remove(event_handler) self._callbacks[trigger_key].remove(event_handler)
return async_remove_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.""" """Enumerate a stateless switch, like a single button."""
# A stateless switch that has a SERVICE_LABEL_INDEX is part of a group # 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.""" """Enumerate a group of stateless switches, like a remote control."""
switches = list( switches = list(
service.accessory.services.filter( service.accessory.services.filter(
@ -165,7 +175,7 @@ def enumerate_stateless_switch_group(service):
return results return results
def enumerate_doorbell(service): def enumerate_doorbell(service: Service) -> list[dict[str, Any]]:
"""Enumerate doorbell buttons.""" """Enumerate doorbell buttons."""
input_event = service[CharacteristicsTypes.INPUT_EVENT] input_event = service[CharacteristicsTypes.INPUT_EVENT]
@ -217,21 +227,32 @@ async def async_setup_triggers_for_entry(
if device_id in hass.data[TRIGGERS]: if device_id in hass.data[TRIGGERS]:
return False 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 # extract any triggers - so only proceed if we can
triggers = TRIGGER_FINDERS[service_type](service) triggers = TRIGGER_FINDERS[service_type](service)
if len(triggers) == 0: if len(triggers) == 0:
return False return False
trigger = TriggerSource(conn, aid, triggers) trigger = async_get_or_create_trigger_source(conn.hass, device_id)
hass.data[TRIGGERS][device_id] = trigger hass.async_create_task(trigger.async_setup(conn, aid, triggers))
return True return True
conn.add_listener(async_add_service) 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.""" """Process events generated by a HomeKit accessory into automation triggers."""
trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS] trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS]
for (aid, iid), ev in events.items(): for (aid, iid), ev in events.items():
@ -271,5 +292,6 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
device_id = config[CONF_DEVICE_ID] device_id = config[CONF_DEVICE_ID]
device = hass.data[TRIGGERS][device_id] return async_get_or_create_trigger_source(hass, device_id).async_attach_trigger(
return await device.async_attach_trigger(config, action, trigger_info) config, action, trigger_info
)

View File

@ -354,7 +354,25 @@ class IBeaconCoordinator:
for group_id in self._group_ids_random_macs for group_id in self._group_ids_random_macs
if group_id not in self._unavailable_group_ids if group_id not in self._unavailable_group_ids
and (service_info := self._last_seen_by_group_id.get(group_id)) 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: for group_id in gone_unavailable:
self._unavailable_group_ids.add(group_id) self._unavailable_group_ids.add(group_id)

View File

@ -2,7 +2,7 @@
"domain": "opentherm_gw", "domain": "opentherm_gw",
"name": "OpenTherm Gateway", "name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": ["pyotgw==2.1.1"], "requirements": ["pyotgw==2.1.3"],
"codeowners": ["@mvn23"], "codeowners": ["@mvn23"],
"config_flow": true, "config_flow": true,
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -2,7 +2,7 @@
"domain": "sensibo", "domain": "sensibo",
"name": "Sensibo", "name": "Sensibo",
"documentation": "https://www.home-assistant.io/integrations/sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo",
"requirements": ["pysensibo==1.0.20"], "requirements": ["pysensibo==1.0.22"],
"config_flow": true, "config_flow": true,
"codeowners": ["@andrey-git", "@gjohansson-ST"], "codeowners": ["@andrey-git", "@gjohansson-ST"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -306,7 +306,7 @@ def _async_register_base_station(
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, system.system_id)}, identifiers={(DOMAIN, str(system.system_id))},
manufacturer="SimpliSafe", manufacturer="SimpliSafe",
model=system.version, model=system.version,
name=system.address, name=system.address,
@ -757,7 +757,7 @@ class SimpliSafeEntity(CoordinatorEntity):
manufacturer="SimpliSafe", manufacturer="SimpliSafe",
model=model, model=model,
name=device_name, name=device_name,
via_device=(DOMAIN, system.system_id), via_device=(DOMAIN, str(system.system_id)),
) )
self._attr_unique_id = serial self._attr_unique_id = serial

View File

@ -15,7 +15,10 @@ from simplipy.websocket import (
EVENT_AWAY_EXIT_DELAY_BY_REMOTE, EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
EVENT_DISARMED_BY_MASTER_PIN, EVENT_DISARMED_BY_MASTER_PIN,
EVENT_DISARMED_BY_REMOTE, EVENT_DISARMED_BY_REMOTE,
EVENT_ENTRY_DELAY,
EVENT_HOME_EXIT_DELAY, EVENT_HOME_EXIT_DELAY,
EVENT_SECRET_ALERT_TRIGGERED,
EVENT_USER_INITIATED_TEST,
WebsocketEvent, WebsocketEvent,
) )
@ -66,9 +69,12 @@ STATE_MAP_FROM_REST_API = {
SystemStates.ALARM_COUNT: STATE_ALARM_PENDING, SystemStates.ALARM_COUNT: STATE_ALARM_PENDING,
SystemStates.AWAY: STATE_ALARM_ARMED_AWAY, SystemStates.AWAY: STATE_ALARM_ARMED_AWAY,
SystemStates.AWAY_COUNT: STATE_ALARM_ARMING, SystemStates.AWAY_COUNT: STATE_ALARM_ARMING,
SystemStates.ENTRY_DELAY: STATE_ALARM_PENDING,
SystemStates.EXIT_DELAY: STATE_ALARM_ARMING, SystemStates.EXIT_DELAY: STATE_ALARM_ARMING,
SystemStates.HOME: STATE_ALARM_ARMED_HOME, SystemStates.HOME: STATE_ALARM_ARMED_HOME,
SystemStates.HOME_COUNT: STATE_ALARM_ARMING,
SystemStates.OFF: STATE_ALARM_DISARMED, SystemStates.OFF: STATE_ALARM_DISARMED,
SystemStates.TEST: STATE_ALARM_DISARMED,
} }
STATE_MAP_FROM_WEBSOCKET_EVENT = { STATE_MAP_FROM_WEBSOCKET_EVENT = {
@ -82,7 +88,10 @@ STATE_MAP_FROM_WEBSOCKET_EVENT = {
EVENT_AWAY_EXIT_DELAY_BY_REMOTE: STATE_ALARM_ARMING, EVENT_AWAY_EXIT_DELAY_BY_REMOTE: STATE_ALARM_ARMING,
EVENT_DISARMED_BY_MASTER_PIN: STATE_ALARM_DISARMED, EVENT_DISARMED_BY_MASTER_PIN: STATE_ALARM_DISARMED,
EVENT_DISARMED_BY_REMOTE: STATE_ALARM_DISARMED, EVENT_DISARMED_BY_REMOTE: STATE_ALARM_DISARMED,
EVENT_ENTRY_DELAY: STATE_ALARM_PENDING,
EVENT_HOME_EXIT_DELAY: STATE_ALARM_ARMING, 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 = ( WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
@ -156,13 +165,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
"""Set the state based on the latest REST API data.""" """Set the state based on the latest REST API data."""
if self._system.alarm_going_off: if self._system.alarm_going_off:
self._attr_state = STATE_ALARM_TRIGGERED 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): elif state := STATE_MAP_FROM_REST_API.get(self._system.state):
self._attr_state = state self._attr_state = state
self.async_reset_error_count() self.async_reset_error_count()
else: 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() self.async_increment_error_count()
async def async_alarm_disarm(self, code: str | None = None) -> None: 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( self._attr_extra_state_attributes.update(
{ {
ATTR_ALARM_DURATION: self._system.alarm_duration, 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_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_AWAY: self._system.entry_delay_away,
ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home,
ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, 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_GSM_STRENGTH: self._system.gsm_strength,
ATTR_LIGHT: self._system.light, ATTR_LIGHT: self._system.light,
ATTR_RF_JAMMING: self._system.rf_jamming, 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_WALL_POWER_LEVEL: self._system.wall_power_level,
ATTR_WIFI_STRENGTH: self._system.wifi_strength, 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() self._set_state_from_system_data()
@callback @callback

View File

@ -21,6 +21,7 @@ SUPPORTED_BATTERY_SENSOR_TYPES = [
DeviceTypes.CARBON_MONOXIDE, DeviceTypes.CARBON_MONOXIDE,
DeviceTypes.ENTRY, DeviceTypes.ENTRY,
DeviceTypes.GLASS_BREAK, DeviceTypes.GLASS_BREAK,
DeviceTypes.KEYPAD,
DeviceTypes.LEAK, DeviceTypes.LEAK,
DeviceTypes.LOCK_KEYPAD, DeviceTypes.LOCK_KEYPAD,
DeviceTypes.MOTION, DeviceTypes.MOTION,

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe", "name": "SimpliSafe",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe", "documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2022.07.1"], "requirements": ["simplisafe-python==2022.11.2"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"dhcp": [ "dhcp": [

View File

@ -3,7 +3,7 @@
"domain": "tibber", "domain": "tibber",
"name": "Tibber", "name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber", "documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.25.6"], "requirements": ["pyTibber==0.26.1"],
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen"],
"quality_scale": "silver", "quality_scale": "silver",
"config_flow": true, "config_flow": true,

View File

@ -4,12 +4,12 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows==0.34.2", "bellows==0.34.4",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.86", "zha-quirks==0.0.87",
"zigpy-deconz==0.19.0", "zigpy-deconz==0.19.1",
"zigpy==0.51.5", "zigpy==0.51.6",
"zigpy-xbee==0.16.2", "zigpy-xbee==0.16.2",
"zigpy-zigate==0.10.3", "zigpy-zigate==0.10.3",
"zigpy-znp==0.9.1" "zigpy-znp==0.9.1"

View File

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

View File

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

View File

@ -404,7 +404,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10 # beewi_smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.34.2 bellows==0.34.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4 bimmer_connected==0.10.4
@ -725,7 +725,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0 garages-amsterdam==3.0.0
# homeassistant.components.google # homeassistant.components.google
gcal-sync==4.0.2 gcal-sync==4.0.3
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.6.30 geniushub-client==0.6.30
@ -1412,7 +1412,7 @@ pyRFXtrx==0.30.0
pySwitchmate==0.5.1 pySwitchmate==0.5.1
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.25.6 pyTibber==0.26.1
# homeassistant.components.dlink # homeassistant.components.dlink
pyW215==0.7.0 pyW215==0.7.0
@ -1784,7 +1784,7 @@ pyopnsense==0.2.0
pyoppleio==1.0.5 pyoppleio==1.0.5
# homeassistant.components.opentherm_gw # homeassistant.components.opentherm_gw
pyotgw==2.1.1 pyotgw==2.1.3
# homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
@ -1864,7 +1864,7 @@ pysaj==0.0.16
pysdcp==1 pysdcp==1
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.20 pysensibo==1.0.22
# homeassistant.components.serial # homeassistant.components.serial
# homeassistant.components.zha # homeassistant.components.zha
@ -2262,7 +2262,7 @@ simplehound==0.3
simplepush==2.1.1 simplepush==2.1.1
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==2022.07.1 simplisafe-python==2022.11.2
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==3.1.2 sisyphus-control==3.1.2
@ -2610,7 +2610,7 @@ zengge==0.2
zeroconf==0.39.4 zeroconf==0.39.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.86 zha-quirks==0.0.87
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9 zhong_hong_hvac==1.0.9
@ -2619,7 +2619,7 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0 ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.19.0 zigpy-deconz==0.19.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee==0.16.2 zigpy-xbee==0.16.2
@ -2631,7 +2631,7 @@ zigpy-zigate==0.10.3
zigpy-znp==0.9.1 zigpy-znp==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.51.5 zigpy==0.51.6
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@ -331,7 +331,7 @@ base36==0.1.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.34.2 bellows==0.34.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4 bimmer_connected==0.10.4
@ -541,7 +541,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0 garages-amsterdam==3.0.0
# homeassistant.components.google # homeassistant.components.google
gcal-sync==4.0.2 gcal-sync==4.0.3
# homeassistant.components.geocaching # homeassistant.components.geocaching
geocachingapi==0.2.1 geocachingapi==0.2.1
@ -1012,7 +1012,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.0 pyRFXtrx==0.30.0
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.25.6 pyTibber==0.26.1
# homeassistant.components.nextbus # homeassistant.components.nextbus
py_nextbusnext==0.1.5 py_nextbusnext==0.1.5
@ -1261,7 +1261,7 @@ pyopenuv==2022.04.0
pyopnsense==0.2.0 pyopnsense==0.2.0
# homeassistant.components.opentherm_gw # homeassistant.components.opentherm_gw
pyotgw==2.1.1 pyotgw==2.1.3
# homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
@ -1314,7 +1314,7 @@ pyruckus==0.16
pysabnzbd==1.1.1 pysabnzbd==1.1.1
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.20 pysensibo==1.0.22
# homeassistant.components.serial # homeassistant.components.serial
# homeassistant.components.zha # homeassistant.components.zha
@ -1559,7 +1559,7 @@ simplehound==0.3
simplepush==2.1.1 simplepush==2.1.1
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==2022.07.1 simplisafe-python==2022.11.2
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
@ -1811,10 +1811,10 @@ zamg==0.1.1
zeroconf==0.39.4 zeroconf==0.39.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.86 zha-quirks==0.0.87
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.19.0 zigpy-deconz==0.19.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee==0.16.2 zigpy-xbee==0.16.2
@ -1826,7 +1826,7 @@ zigpy-zigate==0.10.3
zigpy-znp==0.9.1 zigpy-znp==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.51.5 zigpy==0.51.6
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.43.0 zwave-js-server-python==0.43.0

View File

@ -6,6 +6,7 @@ import pytest
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.homekit_controller.const import DOMAIN 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.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component 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() await hass.async_block_till_done()
assert len(calls) == 2 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

View File

@ -8,8 +8,17 @@ from unittest.mock import patch
import pytest 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.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 ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
STATE_HOME, STATE_HOME,
@ -27,6 +36,7 @@ from . import (
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import ( from tests.components.bluetooth import (
inject_bluetooth_service_info, inject_bluetooth_service_info,
inject_bluetooth_service_info_bleak,
patch_all_discovered_devices, patch_all_discovered_devices,
) )
@ -130,3 +140,108 @@ async def test_device_tracker_random_address(hass):
tracker_attributes = tracker.attributes tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" 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"

View File

@ -110,6 +110,7 @@
{ {
"id": "11", "id": "11",
"isEnabled": false, "isEnabled": false,
"name": null,
"acState": { "acState": {
"on": false, "on": false,
"targetTemperature": 21, "targetTemperature": 21,

View File

@ -35,7 +35,7 @@ def patch_cluster(cluster):
zcl_f.ReadAttributeRecord( zcl_f.ReadAttributeRecord(
attr_id, attr_id,
zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS,
zcl_f.TypeValue(python_type=None, value=value), zcl_f.TypeValue(type=None, value=value),
) )
) )
else: else: