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
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"

View File

@ -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"]

View File

@ -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
)

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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": [

View File

@ -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,

View File

@ -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"

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

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

View File

@ -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: