HomeKit Controller: Adopt config entries for pairing with homekit accessories (#23825)
* Fix user initiated pairing + show more user friendly name * Add lock around async_refresh_entity_map * Migrate homekit_controller to config entries. * Improve docstring Co-Authored-By: Martin Hjelmare <marhje52@kth.se> * Add dummy async_setup_platform * add_service -> async_add_service * Add missing returns * Enable coverage checks for homekit_controller
This commit is contained in:
parent
3508622e3b
commit
b8cbd39985
19 changed files with 334 additions and 287 deletions
|
@ -250,7 +250,6 @@ omit =
|
||||||
homeassistant/components/hitron_coda/device_tracker.py
|
homeassistant/components/hitron_coda/device_tracker.py
|
||||||
homeassistant/components/hive/*
|
homeassistant/components/hive/*
|
||||||
homeassistant/components/hlk_sw16/*
|
homeassistant/components/hlk_sw16/*
|
||||||
homeassistant/components/homekit_controller/*
|
|
||||||
homeassistant/components/homematic/*
|
homeassistant/components/homematic/*
|
||||||
homeassistant/components/homematic/climate.py
|
homeassistant/components/homematic/climate.py
|
||||||
homeassistant/components/homematic/cover.py
|
homeassistant/components/homematic/cover.py
|
||||||
|
|
|
@ -62,6 +62,7 @@ CONFIG_ENTRY_HANDLERS = {
|
||||||
SERVICE_IKEA_TRADFRI: 'tradfri',
|
SERVICE_IKEA_TRADFRI: 'tradfri',
|
||||||
'sonos': 'sonos',
|
'sonos': 'sonos',
|
||||||
SERVICE_IGD: 'upnp',
|
SERVICE_IGD: 'upnp',
|
||||||
|
SERVICE_HOMEKIT: 'homekit_controller',
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_HANDLERS = {
|
SERVICE_HANDLERS = {
|
||||||
|
@ -101,7 +102,6 @@ SERVICE_HANDLERS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
OPTIONAL_SERVICE_HANDLERS = {
|
OPTIONAL_SERVICE_HANDLERS = {
|
||||||
SERVICE_HOMEKIT: ('homekit_controller', None),
|
|
||||||
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
|
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
"""Support for Homekit device discovery."""
|
"""Support for Homekit device discovery."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
|
||||||
from homeassistant.helpers import discovery
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .config_flow import load_old_pairings
|
# We need an import from .config_flow, without it .config_flow is never loaded.
|
||||||
|
from .config_flow import HomekitControllerFlowHandler # noqa: F401
|
||||||
from .connection import get_accessory_information, HKDevice
|
from .connection import get_accessory_information, HKDevice
|
||||||
from .const import (
|
from .const import (
|
||||||
CONTROLLER, ENTITY_MAP, KNOWN_DEVICES
|
CONTROLLER, ENTITY_MAP, KNOWN_DEVICES
|
||||||
|
@ -13,12 +13,6 @@ from .const import (
|
||||||
from .const import DOMAIN # noqa: pylint: disable=unused-import
|
from .const import DOMAIN # noqa: pylint: disable=unused-import
|
||||||
from .storage import EntityMapStorage
|
from .storage import EntityMapStorage
|
||||||
|
|
||||||
HOMEKIT_IGNORE = [
|
|
||||||
'BSB002',
|
|
||||||
'Home Assistant Bridge',
|
|
||||||
'TRADFRI gateway',
|
|
||||||
]
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,61 +144,29 @@ class HomeKitEntity(Entity):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a HomeKit connection on a config entry."""
|
||||||
|
conn = HKDevice(hass, entry, entry.data)
|
||||||
|
hass.data[KNOWN_DEVICES][conn.unique_id] = conn
|
||||||
|
|
||||||
|
if not await conn.async_setup():
|
||||||
|
del hass.data[KNOWN_DEVICES][conn.unique_id]
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up for Homekit devices."""
|
"""Set up for Homekit devices."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import homekit
|
import homekit
|
||||||
from homekit.controller.ip_implementation import IpPairing
|
|
||||||
|
|
||||||
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
|
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
|
||||||
await map_storage.async_initialize()
|
await map_storage.async_initialize()
|
||||||
|
|
||||||
hass.data[CONTROLLER] = controller = homekit.Controller()
|
hass.data[CONTROLLER] = homekit.Controller()
|
||||||
|
|
||||||
old_pairings = await hass.async_add_executor_job(
|
|
||||||
load_old_pairings,
|
|
||||||
hass
|
|
||||||
)
|
|
||||||
for hkid, pairing_data in old_pairings.items():
|
|
||||||
controller.pairings[hkid] = IpPairing(pairing_data)
|
|
||||||
|
|
||||||
def discovery_dispatch(service, discovery_info):
|
|
||||||
"""Dispatcher for Homekit discovery events."""
|
|
||||||
# model, id
|
|
||||||
host = discovery_info['host']
|
|
||||||
port = discovery_info['port']
|
|
||||||
|
|
||||||
# Fold property keys to lower case, making them effectively
|
|
||||||
# case-insensitive. Some HomeKit devices capitalize them.
|
|
||||||
properties = {
|
|
||||||
key.lower(): value
|
|
||||||
for (key, value) in discovery_info['properties'].items()
|
|
||||||
}
|
|
||||||
|
|
||||||
model = properties['md']
|
|
||||||
hkid = properties['id']
|
|
||||||
config_num = int(properties['c#'])
|
|
||||||
|
|
||||||
if model in HOMEKIT_IGNORE:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only register a device once, but rescan if the config has changed
|
|
||||||
if hkid in hass.data[KNOWN_DEVICES]:
|
|
||||||
device = hass.data[KNOWN_DEVICES][hkid]
|
|
||||||
if config_num > device.config_num and \
|
|
||||||
device.pairing is not None:
|
|
||||||
device.refresh_entity_map(config_num)
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.debug('Discovered unique device %s', hkid)
|
|
||||||
device = HKDevice(hass, host, port, model, hkid, config_num, config)
|
|
||||||
device.setup()
|
|
||||||
|
|
||||||
hass.data[KNOWN_DEVICES] = {}
|
hass.data[KNOWN_DEVICES] = {}
|
||||||
|
|
||||||
await hass.async_add_executor_job(
|
|
||||||
discovery.listen, hass, SERVICE_HOMEKIT, discovery_dispatch)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,25 @@ TARGET_STATE_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit Alarm Control Panel support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is None:
|
"""Legacy set up platform."""
|
||||||
return
|
pass
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
|
||||||
add_entities([HomeKitAlarmControlPanel(accessory, discovery_info)],
|
|
||||||
True)
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit alarm control panel."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] != 'security-system':
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitAlarmControlPanel(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
|
class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
|
||||||
|
|
|
@ -8,11 +8,25 @@ from . import KNOWN_DEVICES, HomeKitEntity
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit motion sensor support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is not None:
|
"""Legacy set up platform."""
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
pass
|
||||||
add_entities([HomeKitMotionSensor(accessory, discovery_info)], True)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit lighting."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] != 'motion':
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitMotionSensor(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice):
|
class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice):
|
||||||
|
|
|
@ -26,11 +26,25 @@ MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
|
||||||
DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS)
|
DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
|
"""Legacy set up platform."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up Homekit climate."""
|
"""Set up Homekit climate."""
|
||||||
if discovery_info is not None:
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
add_entities([HomeKitClimateDevice(accessory, discovery_info)], True)
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] != 'thermostat':
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitClimateDevice(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
||||||
|
|
|
@ -78,9 +78,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
key = user_input['device']
|
key = user_input['device']
|
||||||
props = self.devices[key]['properties']
|
self.hkid = self.devices[key]['id']
|
||||||
self.hkid = props['id']
|
self.model = self.devices[key]['md']
|
||||||
self.model = props['md']
|
|
||||||
return await self.async_step_pair()
|
return await self.async_step_pair()
|
||||||
|
|
||||||
controller = homekit.Controller()
|
controller = homekit.Controller()
|
||||||
|
@ -90,11 +89,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||||
|
|
||||||
self.devices = {}
|
self.devices = {}
|
||||||
for host in all_hosts:
|
for host in all_hosts:
|
||||||
status_flags = int(host['properties']['sf'])
|
status_flags = int(host['sf'])
|
||||||
paired = not status_flags & 0x01
|
paired = not status_flags & 0x01
|
||||||
if paired:
|
if paired:
|
||||||
continue
|
continue
|
||||||
self.devices[host['properties']['id']] = host
|
self.devices[host['name']] = host
|
||||||
|
|
||||||
if not self.devices:
|
if not self.devices:
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
|
@ -263,13 +262,26 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||||
|
|
||||||
async def _entry_from_accessory(self, pairing):
|
async def _entry_from_accessory(self, pairing):
|
||||||
"""Return a config entry from an initialized bridge."""
|
"""Return a config entry from an initialized bridge."""
|
||||||
accessories = await self.hass.async_add_executor_job(
|
# The bulk of the pairing record is stored on the config entry.
|
||||||
pairing.list_accessories_and_characteristics
|
# A specific exception is the 'accessories' key. This is more
|
||||||
)
|
# volatile. We do cache it, but not against the config entry.
|
||||||
|
# So copy the pairing data and mutate the copy.
|
||||||
|
pairing_data = pairing.pairing_data.copy()
|
||||||
|
|
||||||
|
# Use the accessories data from the pairing operation if it is
|
||||||
|
# available. Otherwise request a fresh copy from the API.
|
||||||
|
# This removes the 'accessories' key from pairing_data at
|
||||||
|
# the same time.
|
||||||
|
accessories = pairing_data.pop('accessories', None)
|
||||||
|
if not accessories:
|
||||||
|
accessories = await self.hass.async_add_executor_job(
|
||||||
|
pairing.list_accessories_and_characteristics
|
||||||
|
)
|
||||||
|
|
||||||
bridge_info = get_bridge_information(accessories)
|
bridge_info = get_bridge_information(accessories)
|
||||||
name = get_accessory_name(bridge_info)
|
name = get_accessory_name(bridge_info)
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=name,
|
title=name,
|
||||||
data=pairing.pairing_data,
|
data=pairing_data,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
"""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 logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from homeassistant.helpers import discovery
|
from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP
|
||||||
|
|
||||||
from .const import (
|
|
||||||
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES,
|
|
||||||
PAIRING_FILE, HOMEKIT_DIR, ENTITY_MAP
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
RETRY_INTERVAL = 60 # seconds
|
RETRY_INTERVAL = 60 # seconds
|
||||||
|
@ -53,75 +47,69 @@ def get_accessory_name(accessory_info):
|
||||||
class HKDevice():
|
class HKDevice():
|
||||||
"""HomeKit device."""
|
"""HomeKit device."""
|
||||||
|
|
||||||
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
def __init__(self, hass, config_entry, pairing_data):
|
||||||
"""Initialise a generic HomeKit device."""
|
"""Initialise a generic HomeKit device."""
|
||||||
_LOGGER.info("Setting up Homekit device %s", model)
|
from homekit.controller.ip_implementation import IpPairing
|
||||||
self.hass = hass
|
|
||||||
self.controller = hass.data[CONTROLLER]
|
self.hass = hass
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
# We copy pairing_data because homekit_python may mutate it, but we
|
||||||
|
# don't want to mutate a dict owned by a config entry.
|
||||||
|
self.pairing_data = pairing_data.copy()
|
||||||
|
|
||||||
|
self.pairing = IpPairing(self.pairing_data)
|
||||||
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.model = model
|
|
||||||
self.hkid = hkid
|
|
||||||
self.config_num = config_num
|
|
||||||
self.config = config
|
|
||||||
self.configurator = hass.components.configurator
|
|
||||||
self.accessories = {}
|
self.accessories = {}
|
||||||
|
self.config_num = 0
|
||||||
|
|
||||||
|
# A list of callbacks that turn HK service metadata into entities
|
||||||
|
self.listeners = []
|
||||||
|
|
||||||
|
# The platorms we have forwarded the config entry so far. If a new
|
||||||
|
# accessory is added to a bridge we may have to load additional
|
||||||
|
# platforms. We don't want to load all platforms up front if its just
|
||||||
|
# a lightbulb. And we dont want to forward a config entry twice
|
||||||
|
# (triggers a Config entry already set up error)
|
||||||
|
self.platforms = set()
|
||||||
|
|
||||||
# This just tracks aid/iid pairs so we know if a HK service has been
|
# This just tracks aid/iid pairs so we know if a HK service has been
|
||||||
# mapped to a HA entity.
|
# mapped to a HA entity.
|
||||||
self.entities = []
|
self.entities = []
|
||||||
|
|
||||||
|
# There are multiple entities sharing a single connection - only
|
||||||
|
# allow one entity to use pairing at once.
|
||||||
self.pairing_lock = asyncio.Lock(loop=hass.loop)
|
self.pairing_lock = asyncio.Lock(loop=hass.loop)
|
||||||
|
|
||||||
self.pairing = self.controller.pairings.get(hkid)
|
async def async_setup(self):
|
||||||
|
|
||||||
hass.data[KNOWN_DEVICES][hkid] = self
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Prepare to use a paired HomeKit device in homeassistant."""
|
"""Prepare to use a paired HomeKit device in homeassistant."""
|
||||||
if self.pairing is None:
|
|
||||||
self.configure()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.pairing.pairing_data['AccessoryIP'] = self.host
|
|
||||||
self.pairing.pairing_data['AccessoryPort'] = self.port
|
|
||||||
|
|
||||||
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 or cache['config_num'] < self.config_num:
|
if not cache:
|
||||||
return self.refresh_entity_map(self.config_num)
|
return await self.async_refresh_entity_map(self.config_num)
|
||||||
|
|
||||||
self.accessories = cache['accessories']
|
self.accessories = cache['accessories']
|
||||||
|
self.config_num = cache['config_num']
|
||||||
|
|
||||||
# Ensure the Pairing object has access to the latest version of the
|
# Ensure the Pairing object has access to the latest version of the
|
||||||
# entity map.
|
# entity map.
|
||||||
self.pairing.pairing_data['accessories'] = self.accessories
|
self.pairing.pairing_data['accessories'] = self.accessories
|
||||||
|
|
||||||
|
self.async_load_platforms()
|
||||||
|
|
||||||
self.add_entities()
|
self.add_entities()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def refresh_entity_map(self, config_num):
|
|
||||||
"""
|
|
||||||
Handle setup of a HomeKit accessory.
|
|
||||||
|
|
||||||
The sync version will be removed when homekit_controller migrates to
|
|
||||||
config flow.
|
|
||||||
"""
|
|
||||||
self.hass.add_job(
|
|
||||||
self.async_refresh_entity_map,
|
|
||||||
config_num,
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
from homekit.exceptions import AccessoryDisconnectedError
|
from homekit.exceptions import AccessoryDisconnectedError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.accessories = await self.hass.async_add_executor_job(
|
async with self.pairing_lock:
|
||||||
self.pairing.list_accessories_and_characteristics,
|
self.accessories = await self.hass.async_add_executor_job(
|
||||||
)
|
self.pairing.list_accessories_and_characteristics
|
||||||
|
)
|
||||||
except AccessoryDisconnectedError:
|
except AccessoryDisconnectedError:
|
||||||
# If we fail to refresh this data then we will naturally retry
|
# If we fail to refresh this data then we will naturally retry
|
||||||
# later when Bonjour spots c# is still not up to date.
|
# later when Bonjour spots c# is still not up to date.
|
||||||
|
@ -139,94 +127,62 @@ class HKDevice():
|
||||||
# aid/iid to GATT characteristics. So push it to there as well.
|
# aid/iid to GATT characteristics. So push it to there as well.
|
||||||
self.pairing.pairing_data['accessories'] = self.accessories
|
self.pairing.pairing_data['accessories'] = self.accessories
|
||||||
|
|
||||||
# Register add new entities that are available
|
self.async_load_platforms()
|
||||||
await self.hass.async_add_executor_job(self.add_entities)
|
|
||||||
|
# Register and add new entities that are available
|
||||||
|
self.add_entities()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def add_listener(self, add_entities_cb):
|
||||||
|
"""Add a callback to run when discovering new entities."""
|
||||||
|
self.listeners.append(add_entities_cb)
|
||||||
|
self._add_new_entities([add_entities_cb])
|
||||||
|
|
||||||
def add_entities(self):
|
def add_entities(self):
|
||||||
"""Process the entity map and create HA entities."""
|
"""Process the entity map and create HA entities."""
|
||||||
# pylint: disable=import-error
|
self._add_new_entities(self.listeners)
|
||||||
|
|
||||||
|
def _add_new_entities(self, callbacks):
|
||||||
from homekit.model.services import ServicesTypes
|
from homekit.model.services import ServicesTypes
|
||||||
|
|
||||||
for accessory in self.accessories:
|
for accessory in self.accessories:
|
||||||
aid = accessory['aid']
|
aid = accessory['aid']
|
||||||
for service in accessory['services']:
|
for service in accessory['services']:
|
||||||
iid = service['iid']
|
iid = service['iid']
|
||||||
|
stype = ServicesTypes.get_short(service['type'].upper())
|
||||||
|
service['stype'] = stype
|
||||||
|
|
||||||
if (aid, iid) in self.entities:
|
if (aid, iid) in self.entities:
|
||||||
# Don't add the same entity again
|
# Don't add the same entity again
|
||||||
continue
|
continue
|
||||||
|
|
||||||
devtype = ServicesTypes.get_short(service['type'])
|
for listener in callbacks:
|
||||||
_LOGGER.debug("Found %s", devtype)
|
if listener(aid, service):
|
||||||
service_info = {'serial': self.hkid,
|
self.entities.append((aid, iid))
|
||||||
'aid': aid,
|
break
|
||||||
'iid': service['iid'],
|
|
||||||
'model': self.model,
|
|
||||||
'device-type': devtype}
|
|
||||||
component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
|
|
||||||
if component is not None:
|
|
||||||
discovery.load_platform(self.hass, component, DOMAIN,
|
|
||||||
service_info, self.config)
|
|
||||||
self.entities.append((aid, iid))
|
|
||||||
|
|
||||||
def device_config_callback(self, callback_data):
|
def async_load_platforms(self):
|
||||||
"""Handle initial pairing."""
|
"""Load any platforms needed by this HomeKit device."""
|
||||||
import homekit # pylint: disable=import-error
|
from homekit.model.services import ServicesTypes
|
||||||
code = callback_data.get('code').strip()
|
|
||||||
try:
|
|
||||||
self.controller.perform_pairing(self.hkid, self.hkid, code)
|
|
||||||
except homekit.UnavailableError:
|
|
||||||
error_msg = "This accessory is already paired to another device. \
|
|
||||||
Please reset the accessory and try again."
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
|
||||||
return
|
|
||||||
except homekit.AuthenticationError:
|
|
||||||
error_msg = "Incorrect HomeKit code for {}. Please check it and \
|
|
||||||
try again.".format(self.model)
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
|
||||||
return
|
|
||||||
except homekit.UnknownError:
|
|
||||||
error_msg = "Received an unknown error. Please file a bug."
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.pairing = self.controller.pairings.get(self.hkid)
|
for accessory in self.accessories:
|
||||||
if self.pairing is not None:
|
for service in accessory['services']:
|
||||||
pairing_dir = os.path.join(
|
stype = ServicesTypes.get_short(service['type'].upper())
|
||||||
self.hass.config.path(),
|
if stype not in HOMEKIT_ACCESSORY_DISPATCH:
|
||||||
HOMEKIT_DIR,
|
continue
|
||||||
)
|
|
||||||
if not os.path.exists(pairing_dir):
|
|
||||||
os.makedirs(pairing_dir)
|
|
||||||
pairing_file = os.path.join(
|
|
||||||
pairing_dir,
|
|
||||||
PAIRING_FILE,
|
|
||||||
)
|
|
||||||
self.controller.save_data(pairing_file)
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.request_done(_configurator)
|
|
||||||
self.setup()
|
|
||||||
else:
|
|
||||||
error_msg = "Unable to pair, please try again"
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
|
||||||
|
|
||||||
def configure(self):
|
platform = HOMEKIT_ACCESSORY_DISPATCH[stype]
|
||||||
"""Obtain the pairing code for a HomeKit device."""
|
if platform in self.platforms:
|
||||||
description = "Please enter the HomeKit code for your {}".format(
|
continue
|
||||||
self.model)
|
|
||||||
self.hass.data[DOMAIN+self.hkid] = \
|
self.hass.async_create_task(
|
||||||
self.configurator.request_config(self.model,
|
self.hass.config_entries.async_forward_entry_setup(
|
||||||
self.device_config_callback,
|
self.config_entry,
|
||||||
description=description,
|
platform,
|
||||||
submit_caption="submit",
|
)
|
||||||
fields=[{'id': 'code',
|
)
|
||||||
'name': 'HomeKit code',
|
self.platforms.add(platform)
|
||||||
'type': 'string'}])
|
|
||||||
|
|
||||||
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."""
|
||||||
|
@ -261,4 +217,4 @@ class HKDevice():
|
||||||
|
|
||||||
This id is random and will change if a device undergoes a hard reset.
|
This id is random and will change if a device undergoes a hard reset.
|
||||||
"""
|
"""
|
||||||
return self.hkid
|
return self.pairing_data['AccessoryPairingID']
|
||||||
|
|
|
@ -35,18 +35,30 @@ CURRENT_WINDOW_STATE_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up HomeKit Cover support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is None:
|
"""Legacy set up platform."""
|
||||||
return
|
pass
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
|
||||||
|
|
||||||
if discovery_info['device-type'] == 'garage-door-opener':
|
|
||||||
add_entities([HomeKitGarageDoorCover(accessory, discovery_info)],
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
True)
|
"""Set up Homekit covers."""
|
||||||
else:
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
add_entities([HomeKitWindowCover(accessory, discovery_info)],
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
True)
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
if service['stype'] == 'garage-door-opener':
|
||||||
|
async_add_entities([HomeKitGarageDoorCover(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if service['stype'] in ('window-covering', 'window'):
|
||||||
|
async_add_entities([HomeKitWindowCover(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
|
class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
|
||||||
|
|
|
@ -10,11 +10,25 @@ from . import KNOWN_DEVICES, HomeKitEntity
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit lighting."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is not None:
|
"""Legacy set up platform."""
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
pass
|
||||||
add_entities([HomeKitLight(accessory, discovery_info)], True)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit lightbulb."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] != 'lightbulb':
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitLight(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitLight(HomeKitEntity, Light):
|
class HomeKitLight(HomeKitEntity, Light):
|
||||||
|
|
|
@ -24,12 +24,25 @@ TARGET_STATE_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit Lock support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is None:
|
"""Legacy set up platform."""
|
||||||
return
|
pass
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
|
||||||
add_entities([HomeKitLock(accessory, discovery_info)], True)
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit lock."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] != 'lock-mechanism':
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitLock(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitLock(HomeKitEntity, LockDevice):
|
class HomeKitLock(HomeKitEntity, LockDevice):
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"homekit[IP]==0.14.0"
|
"homekit[IP]==0.14.0"
|
||||||
],
|
],
|
||||||
"dependencies": ["configurator"],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@Jc2k"
|
"@Jc2k"
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,21 +11,35 @@ UNIT_PERCENT = "%"
|
||||||
UNIT_LUX = "lux"
|
UNIT_LUX = "lux"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit sensor support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is not None:
|
"""Legacy set up platform."""
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
pass
|
||||||
devtype = discovery_info['device-type']
|
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit covers."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
devtype = service['stype']
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
if devtype == 'humidity':
|
if devtype == 'humidity':
|
||||||
add_entities(
|
async_add_entities([HomeKitHumiditySensor(conn, info)], True)
|
||||||
[HomeKitHumiditySensor(accessory, discovery_info)], True)
|
return True
|
||||||
elif devtype == 'temperature':
|
|
||||||
add_entities(
|
if devtype == 'temperature':
|
||||||
[HomeKitTemperatureSensor(accessory, discovery_info)], True)
|
async_add_entities([HomeKitTemperatureSensor(conn, info)], True)
|
||||||
elif devtype == 'light':
|
return True
|
||||||
add_entities(
|
|
||||||
[HomeKitLightSensor(accessory, discovery_info)], True)
|
if devtype == 'light':
|
||||||
|
async_add_entities([HomeKitLightSensor(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitHumiditySensor(HomeKitEntity):
|
class HomeKitHumiditySensor(HomeKitEntity):
|
||||||
|
|
|
@ -10,11 +10,25 @@ OUTLET_IN_USE = "outlet_in_use"
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up Homekit switch support."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is not None:
|
"""Legacy set up platform."""
|
||||||
accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']]
|
pass
|
||||||
add_entities([HomeKitSwitch(accessory, discovery_info)], True)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up Homekit lock."""
|
||||||
|
hkid = config_entry.data['AccessoryPairingID']
|
||||||
|
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||||
|
|
||||||
|
def async_add_service(aid, service):
|
||||||
|
if service['stype'] not in ('switch', 'outlet'):
|
||||||
|
return False
|
||||||
|
info = {'aid': aid, 'iid': service['iid']}
|
||||||
|
async_add_entities([HomeKitSwitch(conn, info)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||||
|
|
|
@ -155,6 +155,7 @@ FLOWS = [
|
||||||
'gpslogger',
|
'gpslogger',
|
||||||
'hangouts',
|
'hangouts',
|
||||||
'heos',
|
'heos',
|
||||||
|
'homekit_controller',
|
||||||
'homematicip_cloud',
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
'ifttt',
|
'ifttt',
|
||||||
|
|
|
@ -9,13 +9,15 @@ from homekit.model.characteristics import (
|
||||||
AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes)
|
AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes)
|
||||||
from homekit.model import Accessory, get_id
|
from homekit.model import Accessory, get_id
|
||||||
from homekit.exceptions import AccessoryNotFoundError
|
from homekit.exceptions import AccessoryNotFoundError
|
||||||
from homeassistant.components.homekit_controller import SERVICE_HOMEKIT
|
|
||||||
|
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 (
|
||||||
|
async_setup_entry, 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 (
|
from tests.common import async_fire_time_changed, load_fixture
|
||||||
async_fire_time_changed, async_fire_service_discovered, load_fixture)
|
|
||||||
|
|
||||||
|
|
||||||
class FakePairing:
|
class FakePairing:
|
||||||
|
@ -217,26 +219,36 @@ async def setup_platform(hass):
|
||||||
return fake_controller
|
return fake_controller
|
||||||
|
|
||||||
|
|
||||||
async def setup_test_accessories(hass, accessories, capitalize=False):
|
async def setup_test_accessories(hass, accessories):
|
||||||
"""Load a fake homekit accessory based on a homekit accessory model.
|
"""Load a fake homekit device based on captured JSON profile."""
|
||||||
|
|
||||||
If capitalize is True, property names will be in upper case.
|
|
||||||
"""
|
|
||||||
fake_controller = await setup_platform(hass)
|
fake_controller = await setup_platform(hass)
|
||||||
pairing = fake_controller.add(accessories)
|
pairing = fake_controller.add(accessories)
|
||||||
|
|
||||||
discovery_info = {
|
discovery_info = {
|
||||||
|
'name': 'TestDevice',
|
||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'properties': {
|
'properties': {
|
||||||
('MD' if capitalize else 'md'): 'TestDevice',
|
'md': 'TestDevice',
|
||||||
('ID' if capitalize else 'id'): '00:00:00:00:00:00',
|
'id': '00:00:00:00:00:00',
|
||||||
('C#' if capitalize else 'c#'): 1,
|
'c#': 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
|
pairing.pairing_data.update({
|
||||||
await hass.async_block_till_done()
|
'AccessoryPairingID': discovery_info['properties']['id'],
|
||||||
|
})
|
||||||
|
|
||||||
|
config_entry = config_entries.ConfigEntry(
|
||||||
|
1, 'homekit_controller', 'TestData', pairing.pairing_data,
|
||||||
|
'test', config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
|
)
|
||||||
|
|
||||||
|
pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing'
|
||||||
|
with mock.patch(pairing_cls_loc) as pairing_cls:
|
||||||
|
pairing_cls.return_value = pairing
|
||||||
|
await async_setup_entry(hass, config_entry)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return pairing
|
return pairing
|
||||||
|
|
||||||
|
@ -249,6 +261,7 @@ async def device_config_changed(hass, accessories):
|
||||||
pairing.accessories = accessories
|
pairing.accessories = accessories
|
||||||
|
|
||||||
discovery_info = {
|
discovery_info = {
|
||||||
|
'name': 'TestDevice',
|
||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -259,7 +272,14 @@ async def device_config_changed(hass, accessories):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
|
# Config Flow will abort and notify us if the discovery event is of
|
||||||
|
# interest - in this case c# has incremented
|
||||||
|
flow = config_flow.HomekitControllerFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
flow.context = {}
|
||||||
|
result = await flow.async_step_discovery(discovery_info)
|
||||||
|
assert result['type'] == 'abort'
|
||||||
|
assert result['reason'] == 'already_configured'
|
||||||
|
|
||||||
# Wait for services to reconfigure
|
# Wait for services to reconfigure
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -285,7 +305,6 @@ 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], capitalize)
|
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)
|
||||||
|
|
|
@ -7,11 +7,15 @@ 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.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
|
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
|
||||||
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
||||||
SUPPORT_OPERATION_MODE)
|
SUPPORT_OPERATION_MODE)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -110,14 +114,19 @@ async def test_ecobee3_setup_connection_failure(hass):
|
||||||
list_accessories = 'list_accessories_and_characteristics'
|
list_accessories = 'list_accessories_and_characteristics'
|
||||||
with mock.patch.object(FakePairing, list_accessories) as laac:
|
with mock.patch.object(FakePairing, list_accessories) as laac:
|
||||||
laac.side_effect = AccessoryDisconnectedError('Connection failed')
|
laac.side_effect = AccessoryDisconnectedError('Connection failed')
|
||||||
await setup_test_accessories(hass, accessories)
|
|
||||||
|
# If there is no cached entity map and the accessory connection is
|
||||||
|
# failing then we have to fail the config entry setup.
|
||||||
|
with pytest.raises(ConfigEntryNotReady):
|
||||||
|
await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
climate = entity_registry.async_get('climate.homew')
|
climate = entity_registry.async_get('climate.homew')
|
||||||
assert climate is None
|
assert climate is None
|
||||||
|
|
||||||
# When a regular discovery event happens it should trigger another scan
|
# When accessory raises ConfigEntryNoteReady HA will retry - lets make
|
||||||
# which should cause our entities to be added.
|
# sure there is no cruft causing conflicts left behind by now doing
|
||||||
await device_config_changed(hass, accessories)
|
# a successful setup.
|
||||||
|
await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
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'
|
||||||
|
|
|
@ -627,12 +627,10 @@ async def test_user_works(hass):
|
||||||
'name': 'TestDevice',
|
'name': 'TestDevice',
|
||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'properties': {
|
'md': 'TestDevice',
|
||||||
'md': 'TestDevice',
|
'id': '00:00:00:00:00:00',
|
||||||
'id': '00:00:00:00:00:00',
|
'c#': 1,
|
||||||
'c#': 1,
|
'sf': 1,
|
||||||
'sf': 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pairing = mock.Mock(pairing_data={
|
pairing = mock.Mock(pairing_data={
|
||||||
|
@ -666,7 +664,7 @@ async def test_user_works(hass):
|
||||||
assert result['step_id'] == 'user'
|
assert result['step_id'] == 'user'
|
||||||
|
|
||||||
result = await flow.async_step_user({
|
result = await flow.async_step_user({
|
||||||
'device': '00:00:00:00:00:00',
|
'device': 'TestDevice',
|
||||||
})
|
})
|
||||||
assert result['type'] == 'form'
|
assert result['type'] == 'form'
|
||||||
assert result['step_id'] == 'pair'
|
assert result['step_id'] == 'pair'
|
||||||
|
@ -701,12 +699,10 @@ async def test_user_no_unpaired_devices(hass):
|
||||||
'name': 'TestDevice',
|
'name': 'TestDevice',
|
||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'properties': {
|
'md': 'TestDevice',
|
||||||
'md': 'TestDevice',
|
'id': '00:00:00:00:00:00',
|
||||||
'id': '00:00:00:00:00:00',
|
'c#': 1,
|
||||||
'c#': 1,
|
'sf': 0,
|
||||||
'sf': 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with mock.patch('homekit.Controller') as controller_cls:
|
with mock.patch('homekit.Controller') as controller_cls:
|
||||||
|
|
|
@ -71,20 +71,6 @@ def create_window_covering_service_with_v_tilt():
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
|
||||||
async def test_accept_capitalized_property_names(hass, utcnow):
|
|
||||||
"""Test that we can handle a device with capitalized property names."""
|
|
||||||
window_cover = create_window_covering_service()
|
|
||||||
helper = await setup_test_component(hass, [window_cover], capitalize=True)
|
|
||||||
|
|
||||||
# The specific interaction we do here doesn't matter; we just need
|
|
||||||
# to do *something* to ensure that discovery properly dealt with the
|
|
||||||
# capitalized property names.
|
|
||||||
await hass.services.async_call('cover', 'open_cover', {
|
|
||||||
'entity_id': helper.entity_id,
|
|
||||||
}, blocking=True)
|
|
||||||
assert helper.characteristics[POSITION_TARGET].value == 100
|
|
||||||
|
|
||||||
|
|
||||||
async def test_change_window_cover_state(hass, utcnow):
|
async def test_change_window_cover_state(hass, utcnow):
|
||||||
"""Test that we can turn a HomeKit alarm on and off again."""
|
"""Test that we can turn a HomeKit alarm on and off again."""
|
||||||
window_cover = create_window_covering_service()
|
window_cover = create_window_covering_service()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue