mirror of
https://github.com/home-assistant/core.git
synced 2025-04-29 11:47:50 +00:00
Only poll HomeKit connection once for all entities on a single bridge/pairing (#25249)
* Stub for polling from a central location * Allow connection to know the entity objects attached to it * Move polling logic to connection * Don't poll if no characteristics selected * Loosen coupling between entity and HKDevice * Disable track_time_interval when removing entry * Revert self.entities changes * Use @callback for async_state_changed * Split out unload and remove and add a test * Test that entity is gone and fix docstring
This commit is contained in:
parent
58f946e452
commit
8c69fd91ff
@ -1,6 +1,7 @@
|
|||||||
"""Support for Homekit device discovery."""
|
"""Support for Homekit device discovery."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
@ -27,7 +28,6 @@ class HomeKitEntity(Entity):
|
|||||||
|
|
||||||
def __init__(self, accessory, devinfo):
|
def __init__(self, accessory, devinfo):
|
||||||
"""Initialise a generic HomeKit device."""
|
"""Initialise a generic HomeKit device."""
|
||||||
self._available = True
|
|
||||||
self._accessory = accessory
|
self._accessory = accessory
|
||||||
self._aid = devinfo['aid']
|
self._aid = devinfo['aid']
|
||||||
self._iid = devinfo['iid']
|
self._iid = devinfo['iid']
|
||||||
@ -35,6 +35,39 @@ class HomeKitEntity(Entity):
|
|||||||
self._chars = {}
|
self._chars = {}
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
|
self._signals = []
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Entity added to hass."""
|
||||||
|
self._signals.append(
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
|
self._accessory.signal_state_updated,
|
||||||
|
self.async_state_changed,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._accessory.add_pollable_characteristics(
|
||||||
|
self.pollable_characteristics,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Prepare to be removed from hass."""
|
||||||
|
self._accessory.remove_pollable_characteristics(
|
||||||
|
self._aid,
|
||||||
|
)
|
||||||
|
|
||||||
|
for signal_remove in self._signals:
|
||||||
|
signal_remove()
|
||||||
|
self._signals.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return False.
|
||||||
|
|
||||||
|
Data update is triggered from HKDevice.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Configure an entity baed on its HomeKit characterstics metadata."""
|
"""Configure an entity baed on its HomeKit characterstics metadata."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
@ -47,7 +80,7 @@ class HomeKitEntity(Entity):
|
|||||||
get_uuid(c) for c in self.get_characteristic_types()
|
get_uuid(c) for c in self.get_characteristic_types()
|
||||||
]
|
]
|
||||||
|
|
||||||
self._chars_to_poll = []
|
self.pollable_characteristics = []
|
||||||
self._chars = {}
|
self._chars = {}
|
||||||
self._char_names = {}
|
self._char_names = {}
|
||||||
|
|
||||||
@ -75,7 +108,7 @@ class HomeKitEntity(Entity):
|
|||||||
from homekit.model.characteristics import CharacteristicsTypes
|
from homekit.model.characteristics import CharacteristicsTypes
|
||||||
|
|
||||||
# Build up a list of (aid, iid) tuples to poll on update()
|
# Build up a list of (aid, iid) tuples to poll on update()
|
||||||
self._chars_to_poll.append((self._aid, char['iid']))
|
self.pollable_characteristics.append((self._aid, char['iid']))
|
||||||
|
|
||||||
# Build a map of ctype -> iid
|
# Build a map of ctype -> iid
|
||||||
short_name = CharacteristicsTypes.get_short(char['type'])
|
short_name = CharacteristicsTypes.get_short(char['type'])
|
||||||
@ -91,30 +124,11 @@ class HomeKitEntity(Entity):
|
|||||||
# pylint: disable=not-callable
|
# pylint: disable=not-callable
|
||||||
setup_fn(char)
|
setup_fn(char)
|
||||||
|
|
||||||
async def async_update(self):
|
@callback
|
||||||
"""Obtain a HomeKit device's state."""
|
def async_state_changed(self):
|
||||||
# pylint: disable=import-error
|
"""Collect new data from bridge and update the entity state in hass."""
|
||||||
from homekit.exceptions import (
|
accessory_state = self._accessory.current_state.get(self._aid, {})
|
||||||
AccessoryDisconnectedError, AccessoryNotFoundError,
|
for iid, result in accessory_state.items():
|
||||||
EncryptionError)
|
|
||||||
|
|
||||||
try:
|
|
||||||
new_values_dict = await self._accessory.get_characteristics(
|
|
||||||
self._chars_to_poll
|
|
||||||
)
|
|
||||||
except AccessoryNotFoundError:
|
|
||||||
# Not only did the connection fail, but also the accessory is not
|
|
||||||
# visible on the network.
|
|
||||||
self._available = False
|
|
||||||
return
|
|
||||||
except (AccessoryDisconnectedError, EncryptionError):
|
|
||||||
# Temporary connection failure. Device is still available but our
|
|
||||||
# connection was dropped.
|
|
||||||
return
|
|
||||||
|
|
||||||
self._available = True
|
|
||||||
|
|
||||||
for (_, iid), result in new_values_dict.items():
|
|
||||||
if 'value' not in result:
|
if 'value' not in result:
|
||||||
continue
|
continue
|
||||||
# Callback to update the entity with this characteristic value
|
# Callback to update the entity with this characteristic value
|
||||||
@ -125,6 +139,8 @@ class HomeKitEntity(Entity):
|
|||||||
# pylint: disable=not-callable
|
# pylint: disable=not-callable
|
||||||
update_fn(result['value'])
|
update_fn(result['value'])
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the ID of this device."""
|
"""Return the ID of this device."""
|
||||||
@ -139,7 +155,7 @@ class HomeKitEntity(Entity):
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._accessory.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
@ -211,6 +227,17 @@ async def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Disconnect from HomeKit devices before unloading entry."""
|
||||||
|
hkid = entry.data['AccessoryPairingID']
|
||||||
|
|
||||||
|
if hkid in hass.data[KNOWN_DEVICES]:
|
||||||
|
connection = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
await connection.async_unload()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_remove_entry(hass, entry):
|
async def async_remove_entry(hass, entry):
|
||||||
"""Cleanup caches before removing config entry."""
|
"""Cleanup caches before removing config entry."""
|
||||||
hkid = entry.data['AccessoryPairingID']
|
hkid = entry.data['AccessoryPairingID']
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
"""Helpers for managing a pairing with a HomeKit accessory or bridge."""
|
"""Helpers for managing a pairing with a HomeKit accessory or bridge."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
|
from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
|
||||||
RETRY_INTERVAL = 60 # seconds
|
RETRY_INTERVAL = 60 # seconds
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -81,11 +85,54 @@ class HKDevice():
|
|||||||
# allow one entity to use pairing at once.
|
# allow one entity to use pairing at once.
|
||||||
self.pairing_lock = asyncio.Lock()
|
self.pairing_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
self.available = True
|
||||||
|
|
||||||
|
self.signal_state_updated = '_'.join((
|
||||||
|
DOMAIN,
|
||||||
|
self.unique_id,
|
||||||
|
'state_updated',
|
||||||
|
))
|
||||||
|
|
||||||
|
# Current values of all characteristics homekit_controller is tracking.
|
||||||
|
# Key is a (accessory_id, characteristic_id) tuple.
|
||||||
|
self.current_state = {}
|
||||||
|
|
||||||
|
self.pollable_characteristics = []
|
||||||
|
|
||||||
|
# If this is set polling is active and can be disabled by calling
|
||||||
|
# this method.
|
||||||
|
self._polling_interval_remover = None
|
||||||
|
|
||||||
|
def add_pollable_characteristics(self, characteristics):
|
||||||
|
"""Add (aid, iid) pairs that we need to poll."""
|
||||||
|
self.pollable_characteristics.extend(characteristics)
|
||||||
|
|
||||||
|
def remove_pollable_characteristics(self, accessory_id):
|
||||||
|
"""Remove all pollable characteristics by accessory id."""
|
||||||
|
self.pollable_characteristics = [
|
||||||
|
char for char in self.pollable_characteristics
|
||||||
|
if char[0] != accessory_id
|
||||||
|
]
|
||||||
|
|
||||||
|
def async_set_unavailable(self):
|
||||||
|
"""Mark state of all entities on this connection as unavailable."""
|
||||||
|
self.available = False
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_send(
|
||||||
|
self.signal_state_updated,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_setup(self):
|
async def async_setup(self):
|
||||||
"""Prepare to use a paired HomeKit device in homeassistant."""
|
"""Prepare to use a paired HomeKit device in homeassistant."""
|
||||||
cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
|
cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
|
||||||
if not cache:
|
if not cache:
|
||||||
return await self.async_refresh_entity_map(self.config_num)
|
if await self.async_refresh_entity_map(self.config_num):
|
||||||
|
self._polling_interval_remover = async_track_time_interval(
|
||||||
|
self.hass,
|
||||||
|
self.async_update,
|
||||||
|
DEFAULT_SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
self.accessories = cache['accessories']
|
self.accessories = cache['accessories']
|
||||||
self.config_num = cache['config_num']
|
self.config_num = cache['config_num']
|
||||||
@ -98,8 +145,34 @@ class HKDevice():
|
|||||||
|
|
||||||
self.add_entities()
|
self.add_entities()
|
||||||
|
|
||||||
|
await self.async_update()
|
||||||
|
|
||||||
|
self._polling_interval_remover = async_track_time_interval(
|
||||||
|
self.hass,
|
||||||
|
self.async_update,
|
||||||
|
DEFAULT_SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def async_unload(self):
|
||||||
|
"""Stop interacting with device and prepare for removal from hass."""
|
||||||
|
if self._polling_interval_remover:
|
||||||
|
self._polling_interval_remover()
|
||||||
|
|
||||||
|
unloads = []
|
||||||
|
for platform in self.platforms:
|
||||||
|
unloads.append(
|
||||||
|
self.hass.config_entries.async_forward_entry_unload(
|
||||||
|
self.config_entry,
|
||||||
|
platform
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*unloads)
|
||||||
|
|
||||||
|
return False not in results
|
||||||
|
|
||||||
async def async_refresh_entity_map(self, config_num):
|
async def async_refresh_entity_map(self, config_num):
|
||||||
"""Handle setup of a HomeKit accessory."""
|
"""Handle setup of a HomeKit accessory."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
@ -132,6 +205,8 @@ class HKDevice():
|
|||||||
# Register and add new entities that are available
|
# Register and add new entities that are available
|
||||||
self.add_entities()
|
self.add_entities()
|
||||||
|
|
||||||
|
await self.async_update()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_listener(self, add_entities_cb):
|
def add_listener(self, add_entities_cb):
|
||||||
@ -184,6 +259,47 @@ class HKDevice():
|
|||||||
)
|
)
|
||||||
self.platforms.add(platform)
|
self.platforms.add(platform)
|
||||||
|
|
||||||
|
async def async_update(self, now=None):
|
||||||
|
"""Poll state of all entities attached to this bridge/accessory."""
|
||||||
|
# pylint: disable=import-error
|
||||||
|
from homekit.exceptions import (
|
||||||
|
AccessoryDisconnectedError, AccessoryNotFoundError,
|
||||||
|
EncryptionError)
|
||||||
|
|
||||||
|
if not self.pollable_characteristics:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"HomeKit connection not polling any characteristics."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("Starting HomeKit controller update")
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_values_dict = await self.get_characteristics(
|
||||||
|
self.pollable_characteristics,
|
||||||
|
)
|
||||||
|
except AccessoryNotFoundError:
|
||||||
|
# Not only did the connection fail, but also the accessory is not
|
||||||
|
# visible on the network.
|
||||||
|
self.async_set_unavailable()
|
||||||
|
return
|
||||||
|
except (AccessoryDisconnectedError, EncryptionError):
|
||||||
|
# Temporary connection failure. Device is still available but our
|
||||||
|
# connection was dropped.
|
||||||
|
return
|
||||||
|
|
||||||
|
self.available = True
|
||||||
|
|
||||||
|
for (aid, cid), value in new_values_dict.items():
|
||||||
|
accessory = self.current_state.setdefault(aid, {})
|
||||||
|
accessory[cid] = value
|
||||||
|
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_send(
|
||||||
|
self.signal_state_updated,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Finished HomeKit controller update")
|
||||||
|
|
||||||
async def get_characteristics(self, *args, **kwargs):
|
async def get_characteristics(self, *args, **kwargs):
|
||||||
"""Read latest state from homekit accessory."""
|
"""Read latest state from homekit accessory."""
|
||||||
async with self.pairing_lock:
|
async with self.pairing_lock:
|
||||||
|
@ -14,10 +14,12 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components.homekit_controller.const import (
|
from homeassistant.components.homekit_controller.const import (
|
||||||
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH)
|
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH)
|
||||||
from homeassistant.components.homekit_controller import (
|
from homeassistant.components.homekit_controller import (
|
||||||
async_setup_entry, config_flow)
|
config_flow)
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from tests.common import async_fire_time_changed, load_fixture
|
from tests.common import (
|
||||||
|
async_fire_time_changed, load_fixture, MockConfigEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakePairing:
|
class FakePairing:
|
||||||
@ -97,12 +99,13 @@ class FakeController:
|
|||||||
class Helper:
|
class Helper:
|
||||||
"""Helper methods for interacting with HomeKit fakes."""
|
"""Helper methods for interacting with HomeKit fakes."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, pairing, accessory):
|
def __init__(self, hass, entity_id, pairing, accessory, config_entry):
|
||||||
"""Create a helper for a given accessory/entity."""
|
"""Create a helper for a given accessory/entity."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self.pairing = pairing
|
self.pairing = pairing
|
||||||
self.accessory = accessory
|
self.accessory = accessory
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
self.characteristics = {}
|
self.characteristics = {}
|
||||||
for service in self.accessory.services:
|
for service in self.accessory.services:
|
||||||
@ -113,9 +116,7 @@ class Helper:
|
|||||||
|
|
||||||
async def poll_and_get_state(self):
|
async def poll_and_get_state(self):
|
||||||
"""Trigger a time based poll and return the current entity state."""
|
"""Trigger a time based poll and return the current entity state."""
|
||||||
next_update = dt_util.utcnow() + timedelta(seconds=60)
|
await time_changed(self.hass, 60)
|
||||||
async_fire_time_changed(self.hass, next_update)
|
|
||||||
await self.hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = self.hass.states.get(self.entity_id)
|
state = self.hass.states.get(self.entity_id)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
@ -161,6 +162,13 @@ class FakeService(AbstractService):
|
|||||||
return char
|
return char
|
||||||
|
|
||||||
|
|
||||||
|
async def time_changed(hass, seconds):
|
||||||
|
"""Trigger time changed."""
|
||||||
|
next_update = dt_util.utcnow() + timedelta(seconds)
|
||||||
|
async_fire_time_changed(hass, next_update)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def setup_accessories_from_file(hass, path):
|
async def setup_accessories_from_file(hass, path):
|
||||||
"""Load an collection of accessory defs from JSON data."""
|
"""Load an collection of accessory defs from JSON data."""
|
||||||
accessories_fixture = await hass.async_add_executor_job(
|
accessories_fixture = await hass.async_add_executor_job(
|
||||||
@ -239,18 +247,24 @@ async def setup_test_accessories(hass, accessories):
|
|||||||
'AccessoryPairingID': discovery_info['properties']['id'],
|
'AccessoryPairingID': discovery_info['properties']['id'],
|
||||||
})
|
})
|
||||||
|
|
||||||
config_entry = config_entries.ConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
1, 'homekit_controller', 'TestData', pairing.pairing_data,
|
version=1,
|
||||||
'test', config_entries.CONN_CLASS_LOCAL_PUSH
|
domain='homekit_controller',
|
||||||
|
entry_id='TestData',
|
||||||
|
data=pairing.pairing_data,
|
||||||
|
title='test',
|
||||||
|
connection_class=config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
)
|
)
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing'
|
pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing'
|
||||||
with mock.patch(pairing_cls_loc) as pairing_cls:
|
with mock.patch(pairing_cls_loc) as pairing_cls:
|
||||||
pairing_cls.return_value = pairing
|
pairing_cls.return_value = pairing
|
||||||
await async_setup_entry(hass, config_entry)
|
await config_entry.async_setup(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return pairing
|
return config_entry, pairing
|
||||||
|
|
||||||
|
|
||||||
async def device_config_changed(hass, accessories):
|
async def device_config_changed(hass, accessories):
|
||||||
@ -305,6 +319,8 @@ async def setup_test_component(hass, services, capitalize=False, suffix=None):
|
|||||||
accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1')
|
accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1')
|
||||||
accessory.services.extend(services)
|
accessory.services.extend(services)
|
||||||
|
|
||||||
pairing = await setup_test_accessories(hass, [accessory])
|
config_entry, pairing = await setup_test_accessories(hass, [accessory])
|
||||||
entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix)
|
entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix)
|
||||||
return Helper(hass, '.'.join((domain, entity)), pairing, accessory)
|
return Helper(
|
||||||
|
hass, '.'.join((domain, entity)), pairing, accessory, config_entry
|
||||||
|
)
|
||||||
|
@ -14,7 +14,7 @@ async def test_aqara_gateway_setup(hass):
|
|||||||
"""Test that a Aqara Gateway can be correctly setup in HA."""
|
"""Test that a Aqara Gateway can be correctly setup in HA."""
|
||||||
accessories = await setup_accessories_from_file(
|
accessories = await setup_accessories_from_file(
|
||||||
hass, 'aqara_gateway.json')
|
hass, 'aqara_gateway.json')
|
||||||
pairing = await setup_test_accessories(hass, accessories)
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
@ -24,7 +24,9 @@ async def test_aqara_gateway_setup(hass):
|
|||||||
assert alarm.unique_id == 'homekit-0000000123456789-66304'
|
assert alarm.unique_id == 'homekit-0000000123456789-66304'
|
||||||
|
|
||||||
alarm_helper = Helper(
|
alarm_helper = Helper(
|
||||||
hass, 'alarm_control_panel.aqara_hub_1563', pairing, accessories[0])
|
hass, 'alarm_control_panel.aqara_hub_1563', pairing, accessories[0],
|
||||||
|
config_entry
|
||||||
|
)
|
||||||
alarm_state = await alarm_helper.poll_and_get_state()
|
alarm_state = await alarm_helper.poll_and_get_state()
|
||||||
assert alarm_state.attributes['friendly_name'] == 'Aqara Hub-1563'
|
assert alarm_state.attributes['friendly_name'] == 'Aqara Hub-1563'
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ async def test_aqara_gateway_setup(hass):
|
|||||||
assert light.unique_id == 'homekit-0000000123456789-65792'
|
assert light.unique_id == 'homekit-0000000123456789-65792'
|
||||||
|
|
||||||
light_helper = Helper(
|
light_helper = Helper(
|
||||||
hass, 'light.aqara_hub_1563', pairing, accessories[0])
|
hass, 'light.aqara_hub_1563', pairing, accessories[0], config_entry)
|
||||||
light_state = await light_helper.poll_and_get_state()
|
light_state = await light_helper.poll_and_get_state()
|
||||||
assert light_state.attributes['friendly_name'] == 'Aqara Hub-1563'
|
assert light_state.attributes['friendly_name'] == 'Aqara Hub-1563'
|
||||||
assert light_state.attributes['supported_features'] == (
|
assert light_state.attributes['supported_features'] == (
|
||||||
|
@ -7,30 +7,31 @@ https://github.com/home-assistant/home-assistant/issues/15336
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from homekit import AccessoryDisconnectedError
|
from homekit import AccessoryDisconnectedError
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY)
|
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY)
|
||||||
|
|
||||||
|
|
||||||
from tests.components.homekit_controller.common import (
|
from tests.components.homekit_controller.common import (
|
||||||
FakePairing, device_config_changed, setup_accessories_from_file,
|
FakePairing, device_config_changed, setup_accessories_from_file,
|
||||||
setup_test_accessories, Helper
|
setup_test_accessories, Helper, time_changed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_ecobee3_setup(hass):
|
async def test_ecobee3_setup(hass):
|
||||||
"""Test that a Ecbobee 3 can be correctly setup in HA."""
|
"""Test that a Ecbobee 3 can be correctly setup in HA."""
|
||||||
accessories = await setup_accessories_from_file(hass, 'ecobee3.json')
|
accessories = await setup_accessories_from_file(hass, 'ecobee3.json')
|
||||||
pairing = await setup_test_accessories(hass, accessories)
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
climate = entity_registry.async_get('climate.homew')
|
climate = entity_registry.async_get('climate.homew')
|
||||||
assert climate.unique_id == 'homekit-123456789012-16'
|
assert climate.unique_id == 'homekit-123456789012-16'
|
||||||
|
|
||||||
climate_helper = Helper(hass, 'climate.homew', pairing, accessories[0])
|
climate_helper = Helper(
|
||||||
|
hass, 'climate.homew', pairing, accessories[0], config_entry
|
||||||
|
)
|
||||||
climate_state = await climate_helper.poll_and_get_state()
|
climate_state = await climate_helper.poll_and_get_state()
|
||||||
assert climate_state.attributes['friendly_name'] == 'HomeW'
|
assert climate_state.attributes['friendly_name'] == 'HomeW'
|
||||||
assert climate_state.attributes['supported_features'] == (
|
assert climate_state.attributes['supported_features'] == (
|
||||||
@ -53,7 +54,7 @@ async def test_ecobee3_setup(hass):
|
|||||||
assert occ1.unique_id == 'homekit-AB1C-56'
|
assert occ1.unique_id == 'homekit-AB1C-56'
|
||||||
|
|
||||||
occ1_helper = Helper(
|
occ1_helper = Helper(
|
||||||
hass, 'binary_sensor.kitchen', pairing, accessories[0])
|
hass, 'binary_sensor.kitchen', pairing, accessories[0], config_entry)
|
||||||
occ1_state = await occ1_helper.poll_and_get_state()
|
occ1_state = await occ1_helper.poll_and_get_state()
|
||||||
assert occ1_state.attributes['friendly_name'] == 'Kitchen'
|
assert occ1_state.attributes['friendly_name'] == 'Kitchen'
|
||||||
|
|
||||||
@ -131,8 +132,8 @@ async def test_ecobee3_setup_connection_failure(hass):
|
|||||||
|
|
||||||
# If there is no cached entity map and the accessory connection is
|
# If there is no cached entity map and the accessory connection is
|
||||||
# failing then we have to fail the config entry setup.
|
# failing then we have to fail the config entry setup.
|
||||||
with pytest.raises(ConfigEntryNotReady):
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
await setup_test_accessories(hass, accessories)
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||||
|
|
||||||
climate = entity_registry.async_get('climate.homew')
|
climate = entity_registry.async_get('climate.homew')
|
||||||
assert climate is None
|
assert climate is None
|
||||||
@ -140,7 +141,16 @@ async def test_ecobee3_setup_connection_failure(hass):
|
|||||||
# When accessory raises ConfigEntryNoteReady HA will retry - lets make
|
# When accessory raises ConfigEntryNoteReady HA will retry - lets make
|
||||||
# sure there is no cruft causing conflicts left behind by now doing
|
# sure there is no cruft causing conflicts left behind by now doing
|
||||||
# a successful setup.
|
# a successful setup.
|
||||||
await setup_test_accessories(hass, accessories)
|
|
||||||
|
# We just advance time by 5 minutes so that the retry happens, rather
|
||||||
|
# than manually invoking async_setup_entry - this means we need to
|
||||||
|
# make sure the IpPairing mock is in place or we'll try to connect to
|
||||||
|
# a real device. Normally this mocking is done by the helper in
|
||||||
|
# setup_test_accessories.
|
||||||
|
pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing'
|
||||||
|
with mock.patch(pairing_cls_loc) as pairing_cls:
|
||||||
|
pairing_cls.return_value = pairing
|
||||||
|
await time_changed(hass, 5 * 60)
|
||||||
|
|
||||||
climate = entity_registry.async_get('climate.homew')
|
climate = entity_registry.async_get('climate.homew')
|
||||||
assert climate.unique_id == 'homekit-123456789012-16'
|
assert climate.unique_id == 'homekit-123456789012-16'
|
||||||
|
@ -19,7 +19,7 @@ LIGHT_ON = ('lightbulb', 'on')
|
|||||||
async def test_koogeek_ls1_setup(hass):
|
async def test_koogeek_ls1_setup(hass):
|
||||||
"""Test that a Koogeek LS1 can be correctly setup in HA."""
|
"""Test that a Koogeek LS1 can be correctly setup in HA."""
|
||||||
accessories = await setup_accessories_from_file(hass, 'koogeek_ls1.json')
|
accessories = await setup_accessories_from_file(hass, 'koogeek_ls1.json')
|
||||||
pairing = await setup_test_accessories(hass, accessories)
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
@ -27,7 +27,13 @@ async def test_koogeek_ls1_setup(hass):
|
|||||||
entry = entity_registry.async_get('light.koogeek_ls1_20833f')
|
entry = entity_registry.async_get('light.koogeek_ls1_20833f')
|
||||||
assert entry.unique_id == 'homekit-AAAA011111111111-7'
|
assert entry.unique_id == 'homekit-AAAA011111111111-7'
|
||||||
|
|
||||||
helper = Helper(hass, 'light.koogeek_ls1_20833f', pairing, accessories[0])
|
helper = Helper(
|
||||||
|
hass,
|
||||||
|
'light.koogeek_ls1_20833f',
|
||||||
|
pairing,
|
||||||
|
accessories[0],
|
||||||
|
config_entry
|
||||||
|
)
|
||||||
state = await helper.poll_and_get_state()
|
state = await helper.poll_and_get_state()
|
||||||
|
|
||||||
# Assert that the friendly name is detected correctly
|
# Assert that the friendly name is detected correctly
|
||||||
@ -58,9 +64,11 @@ async def test_recover_from_failure(hass, utcnow, failure_cls):
|
|||||||
See https://github.com/home-assistant/home-assistant/issues/18949
|
See https://github.com/home-assistant/home-assistant/issues/18949
|
||||||
"""
|
"""
|
||||||
accessories = await setup_accessories_from_file(hass, 'koogeek_ls1.json')
|
accessories = await setup_accessories_from_file(hass, 'koogeek_ls1.json')
|
||||||
pairing = await setup_test_accessories(hass, accessories)
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
helper = Helper(hass, 'light.koogeek_ls1_20833f', pairing, accessories[0])
|
helper = Helper(
|
||||||
|
hass, 'light.koogeek_ls1_20833f', pairing, accessories[0], config_entry
|
||||||
|
)
|
||||||
|
|
||||||
# Set light state on fake device to off
|
# Set light state on fake device to off
|
||||||
helper.characteristics[LIGHT_ON].set_value(False)
|
helper.characteristics[LIGHT_ON].set_value(False)
|
||||||
@ -80,7 +88,8 @@ async def test_recover_from_failure(hass, utcnow, failure_cls):
|
|||||||
state = await helper.poll_and_get_state()
|
state = await helper.poll_and_get_state()
|
||||||
assert state.state == 'off'
|
assert state.state == 'off'
|
||||||
|
|
||||||
get_char.assert_called_with([(1, 8), (1, 9), (1, 10), (1, 11)])
|
chars = get_char.call_args[0][0]
|
||||||
|
assert set(chars) == {(1, 8), (1, 9), (1, 10), (1, 11)}
|
||||||
|
|
||||||
# Test that entity changes state when network error goes away
|
# Test that entity changes state when network error goes away
|
||||||
next_update += timedelta(seconds=60)
|
next_update += timedelta(seconds=60)
|
||||||
|
@ -14,14 +14,16 @@ from tests.components.homekit_controller.common import (
|
|||||||
async def test_lennox_e30_setup(hass):
|
async def test_lennox_e30_setup(hass):
|
||||||
"""Test that a Lennox E30 can be correctly setup in HA."""
|
"""Test that a Lennox E30 can be correctly setup in HA."""
|
||||||
accessories = await setup_accessories_from_file(hass, 'lennox_e30.json')
|
accessories = await setup_accessories_from_file(hass, 'lennox_e30.json')
|
||||||
pairing = await setup_test_accessories(hass, accessories)
|
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
climate = entity_registry.async_get('climate.lennox')
|
climate = entity_registry.async_get('climate.lennox')
|
||||||
assert climate.unique_id == 'homekit-XXXXXXXX-100'
|
assert climate.unique_id == 'homekit-XXXXXXXX-100'
|
||||||
|
|
||||||
climate_helper = Helper(hass, 'climate.lennox', pairing, accessories[0])
|
climate_helper = Helper(
|
||||||
|
hass, 'climate.lennox', pairing, accessories[0], config_entry
|
||||||
|
)
|
||||||
climate_state = await climate_helper.poll_and_get_state()
|
climate_state = await climate_helper.poll_and_get_state()
|
||||||
assert climate_state.attributes['friendly_name'] == 'Lennox'
|
assert climate_state.attributes['friendly_name'] == 'Lennox'
|
||||||
assert climate_state.attributes['supported_features'] == (
|
assert climate_state.attributes['supported_features'] == (
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Basic checks for HomeKitSwitch."""
|
"""Basic checks for HomeKitSwitch."""
|
||||||
|
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
|
||||||
|
|
||||||
from tests.components.homekit_controller.common import (
|
from tests.components.homekit_controller.common import (
|
||||||
FakeService, setup_test_component)
|
FakeService, setup_test_component)
|
||||||
|
|
||||||
@ -152,3 +154,23 @@ async def test_light_becomes_unavailable_but_recovers(hass, utcnow):
|
|||||||
assert state.state == 'on'
|
assert state.state == 'on'
|
||||||
assert state.attributes['brightness'] == 255
|
assert state.attributes['brightness'] == 255
|
||||||
assert state.attributes['color_temp'] == 400
|
assert state.attributes['color_temp'] == 400
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_unloaded(hass, utcnow):
|
||||||
|
"""Test entity and HKDevice are correctly unloaded."""
|
||||||
|
bulb = create_lightbulb_service_with_color_temp()
|
||||||
|
helper = await setup_test_component(hass, [bulb])
|
||||||
|
|
||||||
|
# Initial state is that the light is off
|
||||||
|
state = await helper.poll_and_get_state()
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
unload_result = await helper.config_entry.async_unload(hass)
|
||||||
|
assert unload_result is True
|
||||||
|
|
||||||
|
# Make sure entity is unloaded
|
||||||
|
assert hass.states.get(helper.entity_id) is None
|
||||||
|
|
||||||
|
# Make sure HKDevice is no longer set to poll this accessory
|
||||||
|
conn = hass.data[KNOWN_DEVICES]['00:00:00:00:00:00']
|
||||||
|
assert not conn.pollable_characteristics
|
||||||
|
Loading…
x
Reference in New Issue
Block a user