Skip to content

Commit

Permalink
https://github.com/home-assistant/home-assistant/pull/22430
Browse files Browse the repository at this point in the history
Move HKDevice into connection
  • Loading branch information
Jc2k committed Mar 26, 2019
1 parent 4fd975b commit 769554f
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 160 deletions.
161 changes: 3 additions & 158 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
"""Support for Homekit device discovery."""
import asyncio
import json
import logging
import os

from homeassistant.components.discovery import SERVICE_HOMEKIT
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import call_later

from .connection import get_accessory_information
from .connection import get_accessory_information, HKDevice
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']

HOMEKIT_DIR = '.homekit'

HOMEKIT_IGNORE = [
'BSB002',
'Home Assistant Bridge',
Expand All @@ -27,163 +23,12 @@

_LOGGER = logging.getLogger(__name__)

RETRY_INTERVAL = 60 # seconds

PAIRING_FILE = "pairing.json"


def escape_characteristic_name(char_name):
"""Escape any dash or dots in a characteristics name."""
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):
"""Representation of a Home Assistant HomeKit device."""

Expand Down
161 changes: 161 additions & 0 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
"""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):
Expand Down Expand Up @@ -33,3 +48,149 @@ def get_accessory_name(accessory_info):
if field in accessory_info:
return accessory_info[field]
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
)
3 changes: 3 additions & 0 deletions homeassistant/components/homekit_controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
CONTROLLER = "{}-controller".format(DOMAIN)

HOMEKIT_DIR = '.homekit'
PAIRING_FILE = 'pairing.json'

# Mapping from Homekit type to component.
HOMEKIT_ACCESSORY_DISPATCH = {
'lightbulb': 'light',
Expand Down
5 changes: 3 additions & 2 deletions tests/components/homekit_controller/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes)
from homekit.model import Accessory, get_id
from homekit.exceptions import AccessoryNotFoundError
from homeassistant.components.homekit_controller import (
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT)
from homeassistant.components.homekit_controller import SERVICE_HOMEKIT
from homeassistant.components.homekit_controller.const import (
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, fire_service_discovered
Expand Down

0 comments on commit 769554f

Please sign in to comment.