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