From a7315477ef821c370d216faee22212d8918c3c1d Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 21 Nov 2022 08:47:38 +0100 Subject: [PATCH 01/13] Bump pyotgw to 2.1.3 (#82430) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 4b9242fe2a5..07991f71128 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 506a740b2aa..70000bc7504 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 6c88574dee64dbdfef648512027848429790592b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 21 Nov 2022 19:39:34 -0500 Subject: [PATCH 02/13] Bump ZHA dependencies (#82509) * Bump ZHA dependencies * Use the corrected `TypeValue` keyword argument name in unit tests --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- tests/components/zha/common.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 07991f71128..9af711e869e 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 @@ -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 70000bc7504..ba756cff034 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 @@ -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/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: From c91ef833f4fba4bbb36dd7fbac87434ed2416579 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Nov 2022 03:07:05 -0600 Subject: [PATCH 03/13] Ensure esphome client tasks await cancelation (#82547) --- homeassistant/components/esphome/bluetooth/client.py | 3 +++ 1 file changed, 3 insertions(+) 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" From e3d1851343414fc760d5de45467d3fe6907f129c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 24 Nov 2022 08:01:20 -0800 Subject: [PATCH 04/13] Bump gcal_sync to 4.0.3 (#82606) fixes undefined --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 9af711e869e..d80f6231a85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba756cff034..d96123352af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From b7f0cba843d0f7e97b7f56eb4e269b3a05d2651c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Nov 2022 15:20:19 -0700 Subject: [PATCH 05/13] Fix iBeacons with infrequent random mac address changes unexpectedly going unavailable (#82668) fixes https://github.com/home-assistant/core/issues/79781 --- .../components/ibeacon/coordinator.py | 20 ++- .../components/ibeacon/test_device_tracker.py | 117 +++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) 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/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" From 7a1c5066a7fe5ced4454e5411029764a1641109d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Nov 2022 09:19:06 -1000 Subject: [PATCH 06/13] Fix homekit controller triggers not attaching when integration is setup after startup (#82717) fixes https://github.com/home-assistant/core/issues/78852 --- .../homekit_controller/device_trigger.py | 86 +++++++----- .../homekit_controller/test_device_trigger.py | 127 ++++++++++++++++++ 2 files changed, 181 insertions(+), 32 deletions(-) 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/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 From d7be66629312d8acc75ff05b96f2ffdccc62a6eb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Nov 2022 16:05:54 +0100 Subject: [PATCH 07/13] Bump pysensibo to 1.0.21 (#82023) * pysensibo 1.0.21 * Add name to fixture --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/fixtures/data.json | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 4f89148a21c..af0a4b00f23 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.21"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index d80f6231a85..2b2968db891 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1864,7 +1864,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.20 +pysensibo==1.0.21 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d96123352af..7dcb0bd6eee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1314,7 +1314,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.20 +pysensibo==1.0.21 # homeassistant.components.serial # homeassistant.components.zha 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, From 8c636399059398fe961b4d3b359b89d9c3eec6b2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 26 Nov 2022 22:14:11 +0100 Subject: [PATCH 08/13] Bump pysensibo to 1.0.22 (#82738) fixes undefined --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index af0a4b00f23..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.21"], + "requirements": ["pysensibo==1.0.22"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2b2968db891..85c836c37f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1864,7 +1864,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.21 +pysensibo==1.0.22 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dcb0bd6eee..38dc6c846c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1314,7 +1314,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.21 +pysensibo==1.0.22 # homeassistant.components.serial # homeassistant.components.zha From 97a7745e0a66f1ca152a015f1a7a6b749a8348cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 27 Nov 2022 16:46:42 +0100 Subject: [PATCH 09/13] Update pyTibber to 0.26.1 (#82787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update tibber library, 0.26.0 Signed-off-by: Daniel Hjelseth Høyer * 0.26.1 Signed-off-by: Daniel Hjelseth Høyer Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 85c836c37f4..483c69d1936 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38dc6c846c9..f9ae1a84145 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 78ba1cbb329f214cad25624e8f84601c156e36f3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 27 Nov 2022 12:59:54 -0700 Subject: [PATCH 10/13] Add missing SimpliSafe keypad battery sensor (#82797) --- homeassistant/components/simplisafe/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) 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, From 688b643ed6d09d5ca71079909851085b0e794a10 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 28 Nov 2022 02:51:30 -0700 Subject: [PATCH 11/13] Add missing SimpliSafe alarm states (#82813) --- .../components/simplisafe/alarm_control_panel.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index dc670393f18..ef3e09bc70f 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: From f76e6d7ab3fad815ad165eaf20abb5403dd3ee1f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Nov 2022 18:35:35 -0700 Subject: [PATCH 12/13] Bump `simplisafe-python` to 2022.11.2 (#82943) --- homeassistant/components/simplisafe/__init__.py | 4 ++-- .../components/simplisafe/alarm_control_panel.py | 12 +++++++++--- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) 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 ef3e09bc70f..1137420bd82 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -224,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, @@ -234,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/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/requirements_all.txt b/requirements_all.txt index 483c69d1936..f299e5b6d32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9ae1a84145..85f05c514b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 20b73d87246b51d2c4043d1096c816fafd05d818 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Nov 2022 11:49:39 +0100 Subject: [PATCH 13/13] Bumped version to 2022.11.5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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"