diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index ceac4e5aaae..bd2f0953c27 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -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" diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 7de3a735b96..bc6c719c8fd 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -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"] diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index ecffb902928..bc1434f4bd9 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -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 + ) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index ed62649de2d..6986a26e0b0 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -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) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 99a10bc1539..488f6cb4f1e 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -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", diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 4f89148a21c..e685e80718f 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -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", diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 53aa9e84054..608abb5effb 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -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 diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index dc670393f18..1137420bd82 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -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 diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 239f5468cb3..5ccbfb96afb 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -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, diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index dcb06aa2825..99c332955c3 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -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": [ diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 5341febc62a..892b47f39ca 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -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, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 312b93aff6f..d2b19fe8893 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -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" diff --git a/homeassistant/const.py b/homeassistant/const.py index 0fe06355342..4042ccf0620 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 0562e18b5a5..2a796e8895e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/requirements_all.txt b/requirements_all.txt index 4b9242fe2a5..f299e5b6d32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 506a740b2aa..85f05c514b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index a09525d9dec..6f17f5db786 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -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 diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index f3520f835ca..26eb3dc671c 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -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" diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index 5837296d154..7db7b4a7c4a 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -110,6 +110,7 @@ { "id": "11", "isEnabled": false, + "name": null, "acState": { "on": false, "targetTemperature": 21, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 56197fa39ec..a6a533acc71 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -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: