mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Move HKDevice into connection (#22430)
This commit is contained in:
parent
14ceb8472f
commit
8bf5e57b7f
@ -1,24 +1,20 @@
|
|||||||
"""Support for Homekit device discovery."""
|
"""Support for Homekit device discovery."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import call_later
|
|
||||||
|
|
||||||
from .config_flow import load_old_pairings
|
from .config_flow import load_old_pairings
|
||||||
from .connection import get_accessory_information
|
from .connection import get_accessory_information HKDevice
|
||||||
from .const import (
|
from .const import (
|
||||||
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES
|
CONTROLLER, HOMEKIT_DIR, KNOWN_DEVICES, PAIRING_FILE
|
||||||
)
|
)
|
||||||
|
from .const import DOMAIN # noqa: pylint: disable=unused-import
|
||||||
|
|
||||||
REQUIREMENTS = ['homekit[IP]==0.13.0']
|
REQUIREMENTS = ['homekit[IP]==0.13.0']
|
||||||
|
|
||||||
HOMEKIT_DIR = '.homekit'
|
|
||||||
|
|
||||||
HOMEKIT_IGNORE = [
|
HOMEKIT_IGNORE = [
|
||||||
'BSB002',
|
'BSB002',
|
||||||
'Home Assistant Bridge',
|
'Home Assistant Bridge',
|
||||||
@ -27,163 +23,12 @@ HOMEKIT_IGNORE = [
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
RETRY_INTERVAL = 60 # seconds
|
|
||||||
|
|
||||||
PAIRING_FILE = "pairing.json"
|
|
||||||
|
|
||||||
|
|
||||||
def escape_characteristic_name(char_name):
|
def escape_characteristic_name(char_name):
|
||||||
"""Escape any dash or dots in a characteristics name."""
|
"""Escape any dash or dots in a characteristics name."""
|
||||||
return char_name.replace('-', '_').replace('.', '_')
|
return char_name.replace('-', '_').replace('.', '_')
|
||||||
|
|
||||||
|
|
||||||
class HKDevice():
|
|
||||||
"""HomeKit device."""
|
|
||||||
|
|
||||||
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
|
||||||
"""Initialise a generic HomeKit device."""
|
|
||||||
_LOGGER.info("Setting up Homekit device %s", model)
|
|
||||||
self.hass = hass
|
|
||||||
self.controller = hass.data[CONTROLLER]
|
|
||||||
|
|
||||||
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._connection_warning_logged = False
|
|
||||||
|
|
||||||
# This just tracks aid/iid pairs so we know if a HK service has been
|
|
||||||
# mapped to a HA entity.
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
self.pairing_lock = asyncio.Lock(loop=hass.loop)
|
|
||||||
|
|
||||||
self.pairing = self.controller.pairings.get(hkid)
|
|
||||||
|
|
||||||
if self.pairing is not None:
|
|
||||||
self.accessory_setup()
|
|
||||||
else:
|
|
||||||
self.configure()
|
|
||||||
|
|
||||||
def accessory_setup(self):
|
|
||||||
"""Handle setup of a HomeKit accessory."""
|
|
||||||
# pylint: disable=import-error
|
|
||||||
from homekit.model.services import ServicesTypes
|
|
||||||
from homekit.exceptions import AccessoryDisconnectedError
|
|
||||||
|
|
||||||
self.pairing.pairing_data['AccessoryIP'] = self.host
|
|
||||||
self.pairing.pairing_data['AccessoryPort'] = self.port
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = self.pairing.list_accessories_and_characteristics()
|
|
||||||
except AccessoryDisconnectedError:
|
|
||||||
call_later(
|
|
||||||
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
|
||||||
return
|
|
||||||
for accessory in data:
|
|
||||||
aid = accessory['aid']
|
|
||||||
for service in accessory['services']:
|
|
||||||
iid = service['iid']
|
|
||||||
if (aid, iid) in self.entities:
|
|
||||||
# Don't add the same entity again
|
|
||||||
continue
|
|
||||||
|
|
||||||
devtype = ServicesTypes.get_short(service['type'])
|
|
||||||
_LOGGER.debug("Found %s", devtype)
|
|
||||||
service_info = {'serial': self.hkid,
|
|
||||||
'aid': aid,
|
|
||||||
'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):
|
|
||||||
"""Handle initial pairing."""
|
|
||||||
import homekit # pylint: disable=import-error
|
|
||||||
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)
|
|
||||||
if self.pairing is not None:
|
|
||||||
pairing_file = os.path.join(
|
|
||||||
self.hass.config.path(),
|
|
||||||
HOMEKIT_DIR,
|
|
||||||
PAIRING_FILE,
|
|
||||||
)
|
|
||||||
self.controller.save_data(pairing_file)
|
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
|
||||||
self.configurator.request_done(_configurator)
|
|
||||||
self.accessory_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):
|
|
||||||
"""Obtain the pairing code for a HomeKit device."""
|
|
||||||
description = "Please enter the HomeKit code for your {}".format(
|
|
||||||
self.model)
|
|
||||||
self.hass.data[DOMAIN+self.hkid] = \
|
|
||||||
self.configurator.request_config(self.model,
|
|
||||||
self.device_config_callback,
|
|
||||||
description=description,
|
|
||||||
submit_caption="submit",
|
|
||||||
fields=[{'id': 'code',
|
|
||||||
'name': 'HomeKit code',
|
|
||||||
'type': 'string'}])
|
|
||||||
|
|
||||||
async def get_characteristics(self, *args, **kwargs):
|
|
||||||
"""Read latest state from homekit accessory."""
|
|
||||||
async with self.pairing_lock:
|
|
||||||
chars = await self.hass.async_add_executor_job(
|
|
||||||
self.pairing.get_characteristics,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
return chars
|
|
||||||
|
|
||||||
async def put_characteristics(self, characteristics):
|
|
||||||
"""Control a HomeKit device state from Home Assistant."""
|
|
||||||
chars = []
|
|
||||||
for row in characteristics:
|
|
||||||
chars.append((
|
|
||||||
row['aid'],
|
|
||||||
row['iid'],
|
|
||||||
row['value'],
|
|
||||||
))
|
|
||||||
|
|
||||||
async with self.pairing_lock:
|
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
self.pairing.put_characteristics,
|
|
||||||
chars
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeKitEntity(Entity):
|
class HomeKitEntity(Entity):
|
||||||
"""Representation of a Home Assistant HomeKit device."""
|
"""Representation of a Home Assistant HomeKit device."""
|
||||||
|
|
||||||
|
@ -1,4 +1,19 @@
|
|||||||
"""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 logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.helpers.event import call_later
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, PAIRING_FILE, HOMEKIT_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
RETRY_INTERVAL = 60 # seconds
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_accessory_information(accessory):
|
def get_accessory_information(accessory):
|
||||||
@ -33,3 +48,149 @@ def get_accessory_name(accessory_info):
|
|||||||
if field in accessory_info:
|
if field in accessory_info:
|
||||||
return accessory_info[field]
|
return accessory_info[field]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class HKDevice():
|
||||||
|
"""HomeKit device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
||||||
|
"""Initialise a generic HomeKit device."""
|
||||||
|
_LOGGER.info("Setting up Homekit device %s", model)
|
||||||
|
self.hass = hass
|
||||||
|
self.controller = hass.data[CONTROLLER]
|
||||||
|
|
||||||
|
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._connection_warning_logged = False
|
||||||
|
|
||||||
|
# This just tracks aid/iid pairs so we know if a HK service has been
|
||||||
|
# mapped to a HA entity.
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
self.pairing_lock = asyncio.Lock(loop=hass.loop)
|
||||||
|
|
||||||
|
self.pairing = self.controller.pairings.get(hkid)
|
||||||
|
|
||||||
|
if self.pairing is not None:
|
||||||
|
self.accessory_setup()
|
||||||
|
else:
|
||||||
|
self.configure()
|
||||||
|
|
||||||
|
def accessory_setup(self):
|
||||||
|
"""Handle setup of a HomeKit accessory."""
|
||||||
|
# pylint: disable=import-error
|
||||||
|
from homekit.model.services import ServicesTypes
|
||||||
|
from homekit.exceptions import AccessoryDisconnectedError
|
||||||
|
|
||||||
|
self.pairing.pairing_data['AccessoryIP'] = self.host
|
||||||
|
self.pairing.pairing_data['AccessoryPort'] = self.port
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.pairing.list_accessories_and_characteristics()
|
||||||
|
except AccessoryDisconnectedError:
|
||||||
|
call_later(
|
||||||
|
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
||||||
|
return
|
||||||
|
for accessory in data:
|
||||||
|
aid = accessory['aid']
|
||||||
|
for service in accessory['services']:
|
||||||
|
iid = service['iid']
|
||||||
|
if (aid, iid) in self.entities:
|
||||||
|
# Don't add the same entity again
|
||||||
|
continue
|
||||||
|
|
||||||
|
devtype = ServicesTypes.get_short(service['type'])
|
||||||
|
_LOGGER.debug("Found %s", devtype)
|
||||||
|
service_info = {'serial': self.hkid,
|
||||||
|
'aid': aid,
|
||||||
|
'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)
|
||||||
|
|
||||||
|
def device_config_callback(self, callback_data):
|
||||||
|
"""Handle initial pairing."""
|
||||||
|
import homekit # pylint: disable=import-error
|
||||||
|
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)
|
||||||
|
if self.pairing is not None:
|
||||||
|
pairing_file = os.path.join(
|
||||||
|
self.hass.config.path(),
|
||||||
|
HOMEKIT_DIR,
|
||||||
|
PAIRING_FILE,
|
||||||
|
)
|
||||||
|
self.controller.save_data(pairing_file)
|
||||||
|
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||||
|
self.configurator.request_done(_configurator)
|
||||||
|
self.accessory_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):
|
||||||
|
"""Obtain the pairing code for a HomeKit device."""
|
||||||
|
description = "Please enter the HomeKit code for your {}".format(
|
||||||
|
self.model)
|
||||||
|
self.hass.data[DOMAIN+self.hkid] = \
|
||||||
|
self.configurator.request_config(self.model,
|
||||||
|
self.device_config_callback,
|
||||||
|
description=description,
|
||||||
|
submit_caption="submit",
|
||||||
|
fields=[{'id': 'code',
|
||||||
|
'name': 'HomeKit code',
|
||||||
|
'type': 'string'}])
|
||||||
|
|
||||||
|
async def get_characteristics(self, *args, **kwargs):
|
||||||
|
"""Read latest state from homekit accessory."""
|
||||||
|
async with self.pairing_lock:
|
||||||
|
chars = await self.hass.async_add_executor_job(
|
||||||
|
self.pairing.get_characteristics,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return chars
|
||||||
|
|
||||||
|
async def put_characteristics(self, characteristics):
|
||||||
|
"""Control a HomeKit device state from Home Assistant."""
|
||||||
|
chars = []
|
||||||
|
for row in characteristics:
|
||||||
|
chars.append((
|
||||||
|
row['aid'],
|
||||||
|
row['iid'],
|
||||||
|
row['value'],
|
||||||
|
))
|
||||||
|
|
||||||
|
async with self.pairing_lock:
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self.pairing.put_characteristics,
|
||||||
|
chars
|
||||||
|
)
|
||||||
|
@ -4,6 +4,9 @@ DOMAIN = 'homekit_controller'
|
|||||||
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
||||||
CONTROLLER = "{}-controller".format(DOMAIN)
|
CONTROLLER = "{}-controller".format(DOMAIN)
|
||||||
|
|
||||||
|
HOMEKIT_DIR = '.homekit'
|
||||||
|
PAIRING_FILE = 'pairing.json'
|
||||||
|
|
||||||
# Mapping from Homekit type to component.
|
# Mapping from Homekit type to component.
|
||||||
HOMEKIT_ACCESSORY_DISPATCH = {
|
HOMEKIT_ACCESSORY_DISPATCH = {
|
||||||
'lightbulb': 'light',
|
'lightbulb': 'light',
|
||||||
|
@ -8,8 +8,9 @@ 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 (
|
from homeassistant.components.homekit_controller import SERVICE_HOMEKIT
|
||||||
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT)
|
from homeassistant.components.homekit_controller.const import (
|
||||||
|
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH)
|
||||||
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, fire_service_discovered
|
from tests.common import async_fire_time_changed, fire_service_discovered
|
||||||
|
Loading…
x
Reference in New Issue
Block a user