Skip to content

Commit

Permalink
HomeKit Restructure (new config options) (#12997)
Browse files Browse the repository at this point in the history
* Restructure
* Pincode will now be autogenerated and display using a persistence notification
* Added 'homekit.start' service
* Added config options
* Renamed files for types
* Improved tests
* Changes (based on feedback)
* Removed CONF_PIN_CODE
* Added services.yaml
* Service will only be registered if auto_start=False
* Bugfix names, changed default port
* Generate aids with zlib.adler32
* Added entity filter, minor changes
* Small changes
  • Loading branch information
cdce8p committed Mar 15, 2018
1 parent 64f18c6 commit d348f09
Show file tree
Hide file tree
Showing 22 changed files with 1,069 additions and 746 deletions.
205 changes: 125 additions & 80 deletions homeassistant/components/homekit/__init__.py
Expand Up @@ -3,154 +3,199 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/
"""
import asyncio
import logging
import re
from zlib import adler32

import voluptuous as vol

from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.const import (
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
from .util import (
validate_entity_config, show_setup_message)

TYPES = Registry()
_LOGGER = logging.getLogger(__name__)

_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"

DOMAIN = 'homekit'
REQUIREMENTS = ['HAP-python==1.1.7']

BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode'

HOMEKIT_FILE = '.homekit.state'


def valid_pin(value):
"""Validate pin code value."""
match = re.match(_RE_VALID_PINCODE, str(value).strip())
if not match:
raise vol.Invalid("Pin must be in the format: '123-45-678'")
return match.group(0)


CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({
vol.Optional(CONF_PORT, default=51826): vol.Coerce(int),
vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
})
}, extra=vol.ALLOW_EXTRA)


@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Setup the HomeKit component."""
_LOGGER.debug("Begin setup HomeKit")
_LOGGER.debug('Begin setup HomeKit')

conf = config[DOMAIN]
port = conf.get(CONF_PORT)
pin = str.encode(conf.get(CONF_PIN_CODE))
port = conf[CONF_PORT]
auto_start = conf[CONF_AUTO_START]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]

homekit = HomeKit(hass, port)
homekit.setup_bridge(pin)
homekit = HomeKit(hass, port, entity_filter, entity_config)
homekit.setup()

hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, homekit.start_driver)
return True
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
return True

def handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
if homekit.started:
_LOGGER.warning('HomeKit is already running')
return
homekit.start()

hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START,
handle_homekit_service_start)

def import_types():
"""Import all types from files in the HomeKit directory."""
_LOGGER.debug("Import type files.")
# pylint: disable=unused-variable
from . import ( # noqa F401
covers, security_systems, sensors, switches, thermostats)
return True


def get_accessory(hass, state):
def get_accessory(hass, state, aid, config):
"""Take state and return an accessory object if supported."""
_LOGGER.debug('%s: <aid=%d config=%s>')
if not aid:
_LOGGER.warning('The entitiy "%s" is not supported, since it '
'generates an invalid aid, please change it.',
state.entity_id)
return None

if state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug("Add \"%s\" as \"%s\"",
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
state.name)
state.name, aid=aid)

elif state.domain == 'cover':
# Only add covers that support set_cover_position
if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4:
_LOGGER.debug("Add \"%s\" as \"%s\"",
state.entity_id, 'Window')
return TYPES['Window'](hass, state.entity_id, state.name)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_SET_POSITION:
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'WindowCovering')
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
aid=aid)

elif state.domain == 'alarm_control_panel':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id,
_LOGGER.debug('Add "%s" as "%s"', state.entity_id,
'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name)
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config[ATTR_CODE], aid=aid)

elif state.domain == 'climate':
support_auto = False
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
SUPPORT_TARGET_TEMPERATURE_HIGH
# Check if climate device supports auto mode
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
support_auto = True
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
support_auto = bool(features & support_temp_range)

_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto)
state.name, support_auto, aid=aid)

elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name)
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)

_LOGGER.warning('The entity "%s" is not supported yet',
state.entity_id)
return None


def generate_aid(entity_id):
"""Generate accessory aid with zlib adler32."""
aid = adler32(entity_id.encode('utf-8'))
if aid == 0 or aid == 1:
return None
return aid


class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant."""

def __init__(self, hass, port):
def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object."""
self._hass = hass
self._port = port
self._filter = entity_filter
self._config = entity_config
self.started = False

self.bridge = None
self.driver = None

def setup_bridge(self, pin):
"""Setup the bridge component to track all accessories."""
from .accessories import HomeBridge
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
def setup(self):
"""Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver

self._hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)

def start_driver(self, event):
path = self._hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self._hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)

def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
if not state or not self._filter(state.entity_id):
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self._hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)

def start(self, *args):
"""Start the accessory driver."""
from pyhap.accessory_driver import AccessoryDriver
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
if self.started:
return
self.started = True

# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_security_systems, type_sensors,
type_switches, type_thermostats)

import_types()
_LOGGER.debug("Start adding accessories.")
for state in self._hass.states.all():
acc = get_accessory(self._hass, state)
if acc is not None:
self.bridge.add_accessory(acc)
self.add_bridge_accessory(state)
for entity_id in self._config:
_LOGGER.warning('The entity "%s" was not setup when HomeKit '
'was started', entity_id)
self.bridge.set_broker(self.driver)

ip_address = get_local_ip()
path = self._hass.config.path(HOMEKIT_FILE)
self.driver = AccessoryDriver(self.bridge, self._port,
ip_address, path)
_LOGGER.debug("Driver started")
if not self.bridge.paired:
show_setup_message(self.bridge, self._hass)

_LOGGER.debug('Driver start')
self.driver.start()

def stop_driver(self, event):
def stop(self, *args):
"""Stop the accessory driver."""
_LOGGER.debug("Driver stop")
if self.driver is not None:
if not self.started:
return

_LOGGER.debug('Driver stop')
if self.driver and self.driver.run_sentinel:
self.driver.stop()

0 comments on commit d348f09

Please sign in to comment.