2018-02-25 09:58:13 +00:00
|
|
|
"""Support for Apple HomeKit.
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/homekit/
|
|
|
|
"""
|
|
|
|
import logging
|
2018-03-15 01:48:21 +00:00
|
|
|
from zlib import adler32
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2018-03-07 12:17:52 +00:00
|
|
|
from homeassistant.components.climate import (
|
|
|
|
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
2018-03-15 01:48:21 +00:00
|
|
|
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
|
2018-02-19 22:46:22 +00:00
|
|
|
from homeassistant.util import get_local_ip
|
|
|
|
from homeassistant.util.decorator import Registry
|
2018-03-15 01:48:21 +00:00
|
|
|
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)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
TYPES = Registry()
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-04-06 14:20:59 +00:00
|
|
|
REQUIREMENTS = ['HAP-python==1.1.9']
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.All({
|
2018-03-15 01:48:21 +00:00
|
|
|
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,
|
2018-02-19 22:46:22 +00:00
|
|
|
})
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
async def async_setup(hass, config):
|
2018-02-25 09:58:13 +00:00
|
|
|
"""Setup the HomeKit component."""
|
2018-03-15 01:48:21 +00:00
|
|
|
_LOGGER.debug('Begin setup HomeKit')
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
conf = config[DOMAIN]
|
2018-03-15 01:48:21 +00:00
|
|
|
port = conf[CONF_PORT]
|
|
|
|
auto_start = conf[CONF_AUTO_START]
|
|
|
|
entity_filter = conf[CONF_FILTER]
|
|
|
|
entity_config = conf[CONF_ENTITY_CONFIG]
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
homekit = HomeKit(hass, port, entity_filter, entity_config)
|
|
|
|
homekit.setup()
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
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()
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START,
|
|
|
|
handle_homekit_service_start)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
return True
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def get_accessory(hass, state, aid, config):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Take state and return an accessory object if supported."""
|
2018-03-15 01:48:21 +00:00
|
|
|
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
|
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
if state.domain == 'sensor':
|
2018-02-26 03:27:40 +00:00
|
|
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
|
|
|
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
|
2018-03-15 01:48:21 +00:00
|
|
|
_LOGGER.debug('Add "%s" as "%s"',
|
2018-02-19 22:46:22 +00:00
|
|
|
state.entity_id, 'TemperatureSensor')
|
|
|
|
return TYPES['TemperatureSensor'](hass, state.entity_id,
|
2018-03-15 01:48:21 +00:00
|
|
|
state.name, aid=aid)
|
2018-03-16 00:05:28 +00:00
|
|
|
elif unit == '%':
|
|
|
|
_LOGGER.debug('Add "%s" as %s"',
|
|
|
|
state.entity_id, 'HumiditySensor')
|
|
|
|
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
|
|
|
|
aid=aid)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
elif state.domain == 'cover':
|
|
|
|
# Only add covers that support set_cover_position
|
2018-03-15 01:48:21 +00:00
|
|
|
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)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-07 12:17:52 +00:00
|
|
|
elif state.domain == 'alarm_control_panel':
|
2018-04-04 22:52:25 +00:00
|
|
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
|
2018-03-15 01:48:21 +00:00
|
|
|
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
|
2018-03-21 18:06:46 +00:00
|
|
|
alarm_code=config.get(ATTR_CODE),
|
|
|
|
aid=aid)
|
2018-03-07 12:17:52 +00:00
|
|
|
|
|
|
|
elif state.domain == 'climate':
|
2018-03-15 01:48:21 +00:00
|
|
|
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
|
|
|
SUPPORT_TARGET_TEMPERATURE_HIGH
|
2018-03-07 12:17:52 +00:00
|
|
|
# Check if climate device supports auto mode
|
2018-03-15 01:48:21 +00:00
|
|
|
support_auto = bool(features & support_temp_range)
|
|
|
|
|
|
|
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
|
2018-03-07 12:17:52 +00:00
|
|
|
return TYPES['Thermostat'](hass, state.entity_id,
|
2018-03-15 01:48:21 +00:00
|
|
|
state.name, support_auto, aid=aid)
|
2018-03-07 12:17:52 +00:00
|
|
|
|
2018-03-16 00:05:28 +00:00
|
|
|
elif state.domain == 'light':
|
2018-04-04 22:52:25 +00:00
|
|
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
|
2018-03-16 00:05:28 +00:00
|
|
|
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
|
|
|
|
|
2018-03-07 12:17:52 +00:00
|
|
|
elif state.domain == 'switch' or state.domain == 'remote' \
|
2018-03-16 00:05:28 +00:00
|
|
|
or state.domain == 'input_boolean' or state.domain == 'script':
|
2018-03-15 01:48:21 +00:00
|
|
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
|
|
|
|
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
|
2018-03-07 12:17:52 +00:00
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-02-25 09:58:13 +00:00
|
|
|
class HomeKit():
|
|
|
|
"""Class to handle all actions between HomeKit and Home Assistant."""
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def __init__(self, hass, port, entity_filter, entity_config):
|
2018-02-25 09:58:13 +00:00
|
|
|
"""Initialize a HomeKit object."""
|
2018-02-19 22:46:22 +00:00
|
|
|
self._hass = hass
|
|
|
|
self._port = port
|
2018-03-15 01:48:21 +00:00
|
|
|
self._filter = entity_filter
|
|
|
|
self._config = entity_config
|
|
|
|
self.started = False
|
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
self.bridge = None
|
|
|
|
self.driver = None
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def setup(self):
|
|
|
|
"""Setup bridge and accessory driver."""
|
|
|
|
from .accessories import HomeBridge, HomeDriver
|
|
|
|
|
|
|
|
self._hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_STOP, self.stop)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
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):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Start the accessory driver."""
|
2018-03-15 01:48:21 +00:00
|
|
|
if self.started:
|
|
|
|
return
|
|
|
|
self.started = True
|
|
|
|
|
|
|
|
# pylint: disable=unused-variable
|
|
|
|
from . import ( # noqa F401
|
2018-03-16 00:05:28 +00:00
|
|
|
type_covers, type_lights, type_security_systems, type_sensors,
|
2018-03-15 01:48:21 +00:00
|
|
|
type_switches, type_thermostats)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
for state in self._hass.states.all():
|
2018-03-15 01:48:21 +00:00
|
|
|
self.add_bridge_accessory(state)
|
|
|
|
self.bridge.set_broker(self.driver)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
if not self.bridge.paired:
|
|
|
|
show_setup_message(self.bridge, self._hass)
|
|
|
|
|
|
|
|
_LOGGER.debug('Driver start')
|
2018-02-19 22:46:22 +00:00
|
|
|
self.driver.start()
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def stop(self, *args):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Stop the accessory driver."""
|
2018-03-15 01:48:21 +00:00
|
|
|
if not self.started:
|
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.debug('Driver stop')
|
|
|
|
if self.driver and self.driver.run_sentinel:
|
2018-02-19 22:46:22 +00:00
|
|
|
self.driver.stop()
|