HomeKit Restructure (new config options) (#12997)

* 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
This commit is contained in:
cdce8p 2018-03-15 02:48:21 +01:00 committed by GitHub
parent 64f18c62f4
commit d348f09d3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1038 additions and 715 deletions

View file

@ -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()
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)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, homekit.start_driver)
return True
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)
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
def start_driver(self, event):
"""Start the accessory driver."""
from pyhap.accessory_driver import AccessoryDriver
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
self._hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
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)
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")
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."""
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)
for state in self._hass.states.all():
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)
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()

View file

@ -2,15 +2,31 @@
import logging
from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver
from .const import (
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__)
def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
acc.add_service(service)
return service
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
@ -21,36 +37,23 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def add_preload_service(acc, service, chars=None, opt_chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
if opt_chars:
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
for opt_char_name in opt_chars:
opt_char = get_char_loader().get(opt_char_name)
service.add_opt_characteristic(opt_char)
acc.add_service(service)
return service
def override_properties(char, properties=None, valid_values=None):
"""Override characteristic property values and valid values."""
if properties:
char.properties.update(properties)
def override_properties(char, new_properties):
"""Override characteristic property values."""
char.properties.update(new_properties)
if valid_values:
char.properties['ValidValues'].update(valid_values)
class HomeAccessory(Accessory):
"""Class to extend the Accessory class."""
"""Adapter class for Accessory."""
def __init__(self, display_name, model, category='OTHER', **kwargs):
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
"""Initialize a Accessory object."""
super().__init__(display_name, **kwargs)
set_accessory_info(self, display_name, model)
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
self.category = getattr(Category, category, Category.OTHER)
def _set_services(self):
@ -58,13 +61,37 @@ class HomeAccessory(Accessory):
class HomeBridge(Bridge):
"""Class to extend the Bridge class."""
"""Adapter class for Bridge."""
def __init__(self, display_name, model, pincode, **kwargs):
def __init__(self, hass, name=BRIDGE_NAME,
model=BRIDGE_MODEL, **kwargs):
"""Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode, **kwargs)
set_accessory_info(self, display_name, model)
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
self._hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
pass
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
dismiss_setup_message(self._hass)
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self, self._hass)
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""
def __init__(self, *args, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(*args, **kwargs)

View file

@ -1,7 +1,30 @@
"""Constants used be the HomeKit component."""
# #### MISC ####
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
# #### CONFIG ####
CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter'
# #### CONFIG DEFAULTS ####
DEFAULT_AUTO_START = True
DEFAULT_PORT = 51827
# #### HOMEKIT COMPONENT SERVICES ####
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# Services
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_BRIDGING_STATE = 'BridgingState'
SERV_SECURITY_SYSTEM = 'SecuritySystem'
@ -10,7 +33,8 @@ SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# Characteristics
# #### Characteristics ####
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_CATEGORY = 'Category'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
@ -33,5 +57,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# Properties
# #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}

View file

@ -0,0 +1,4 @@
# Describes the format for available HomeKit services
start:
description: Starts the HomeKit component driver.

View file

@ -14,16 +14,17 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Window')
class Window(HomeAccessory):
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name):
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
"""Initialize a Window accessory object."""
super().__init__(display_name, entity_id, 'WINDOW')
super().__init__(display_name, entity_id, 'WINDOW_COVERING',
*args, **kwargs)
self._hass = hass
self._entity_id = entity_id
@ -31,12 +32,12 @@ class Window(HomeAccessory):
self.current_position = None
self.homekit_target = None
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.serv_cover. \
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.serv_cover. \
self.char_target_position = serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.serv_cover. \
self.char_position_state = serv_cover. \
get_characteristic(CHAR_POSITION_STATE)
self.char_current_position.value = 0
self.char_target_position.value = 0
@ -55,15 +56,14 @@ class Window(HomeAccessory):
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
if value != self.current_position:
_LOGGER.debug("%s: Set position to %d", self._entity_id, value)
_LOGGER.debug('%s: Set position to %d', self._entity_id, value)
self.homekit_target = value
if value > self.current_position:
self.char_position_state.set_value(1)
elif value < self.current_position:
self.char_position_state.set_value(0)
self._hass.services.call(
'cover', 'set_cover_position',
{'entity_id': self._entity_id, 'position': value})
self._hass.components.cover.set_cover_position(
value, self._entity_id)
def update_cover_position(self, entity_id=None, old_state=None,
new_state=None):
@ -71,9 +71,10 @@ class Window(HomeAccessory):
if new_state is None:
return
current_position = new_state.attributes[ATTR_CURRENT_POSITION]
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if current_position is None:
return
self.current_position = int(current_position)
self.char_current_position.set_value(self.current_position)

View file

@ -28,9 +28,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name, alarm_code=None):
def __init__(self, hass, entity_id, display_name,
alarm_code, *args, **kwargs):
"""Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
*args, **kwargs)
self._hass = hass
self._entity_id = entity_id
@ -38,11 +40,11 @@ class SecuritySystem(HomeAccessory):
self.flag_target_state = False
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = self.service_alarm. \
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm. \
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
self.char_current_state.value = 3
self.char_target_state = self.service_alarm. \
self.char_target_state = serv_alarm. \
get_characteristic(CHAR_TARGET_SECURITY_STATE)
self.char_target_state.value = 3
@ -58,15 +60,13 @@ class SecuritySystem(HomeAccessory):
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set security state to %d",
_LOGGER.debug('%s: Set security state to %d',
self._entity_id, value)
self.flag_target_state = True
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self._entity_id}
if self._alarm_code is not None:
params[ATTR_CODE] = self._alarm_code
params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code}
self._hass.services.call('alarm_control_panel', service, params)
def update_security_state(self, entity_id=None,
@ -78,15 +78,15 @@ class SecuritySystem(HomeAccessory):
hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT:
return
current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state)
_LOGGER.debug("%s: Updated current state to %s (%d)",
self._entity_id, hass_state,
current_security_state)
self.char_current_state.set_value(current_security_state,
should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)',
self._entity_id, hass_state, current_security_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state,
should_callback=False)
elif self.char_target_state.get_value() \
== self.char_current_state.get_value():
if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False

View file

@ -36,16 +36,15 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, display_name):
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(display_name, entity_id, 'SENSOR')
super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
@ -68,5 +67,5 @@ class TemperatureSensor(HomeAccessory):
temperature = calc_temperature(new_state.state, unit)
if temperature is not None:
self.char_temp.set_value(temperature)
_LOGGER.debug("%s: Current temperature set to %d°C",
_LOGGER.debug('%s: Current temperature set to %d°C',
self._entity_id, temperature)

View file

@ -1,7 +1,8 @@
"""Class to hold all switch accessories."""
import logging
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
from homeassistant.core import split_entity_id
from homeassistant.helpers.event import async_track_state_change
@ -16,9 +17,9 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name):
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
"""Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, 'SWITCH')
super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
@ -26,8 +27,8 @@ class Switch(HomeAccessory):
self.flag_target_state = False
self.service_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
serv_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = serv_switch.get_characteristic(CHAR_ON)
self.char_on.value = False
self.char_on.setter_callback = self.set_state
@ -41,10 +42,10 @@ class Switch(HomeAccessory):
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s",
_LOGGER.debug('%s: Set switch state to %s',
self._entity_id, value)
self.flag_target_state = True
service = 'turn_on' if value else 'turn_off'
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self._hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self._entity_id})
@ -53,10 +54,10 @@ class Switch(HomeAccessory):
if new_state is None:
return
current_state = (new_state.state == 'on')
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug("%s: Set current state to %s",
_LOGGER.debug('%s: Set current state to %s',
self._entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False)
else:
self.flag_target_state = False
self.flag_target_state = False

View file

@ -7,8 +7,7 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
@ -33,9 +32,11 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, support_auto=False):
def __init__(self, hass, entity_id, display_name,
support_auto, *args, **kwargs):
"""Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT')
super().__init__(display_name, entity_id, 'THERMOSTAT',
*args, **kwargs)
self._hass = hass
self._entity_id = entity_id
@ -46,48 +47,47 @@ class Thermostat(HomeAccessory):
self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
extra_chars = None
# Add additional characteristics if auto mode is supported
if support_auto:
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE]
extra_chars = [
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
# Preload the thermostat service
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
# Current and target mode characteristics
self.char_current_heat_cool = self.service_thermostat. \
self.char_current_heat_cool = serv_thermostat. \
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
self.char_current_heat_cool.value = 0
self.char_target_heat_cool = self.service_thermostat. \
self.char_target_heat_cool = serv_thermostat. \
get_characteristic(CHAR_TARGET_HEATING_COOLING)
self.char_target_heat_cool.value = 0
self.char_target_heat_cool.setter_callback = self.set_heat_cool
# Current and target temperature characteristics
self.char_current_temp = self.service_thermostat. \
self.char_current_temp = serv_thermostat. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_current_temp.value = 21.0
self.char_target_temp = self.service_thermostat. \
self.char_target_temp = serv_thermostat. \
get_characteristic(CHAR_TARGET_TEMPERATURE)
self.char_target_temp.value = 21.0
self.char_target_temp.setter_callback = self.set_target_temperature
# Display units characteristic
self.char_display_units = self.service_thermostat. \
self.char_display_units = serv_thermostat. \
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
self.char_display_units.value = 0
# If the device supports it: high and low temperature characteristics
if support_auto:
self.char_cooling_thresh_temp = self.service_thermostat. \
self.char_cooling_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
self.char_cooling_thresh_temp.value = 23.0
self.char_cooling_thresh_temp.setter_callback = \
self.set_cooling_threshold
self.char_heating_thresh_temp = self.service_thermostat. \
self.char_heating_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
self.char_heating_thresh_temp.value = 19.0
self.char_heating_thresh_temp.setter_callback = \
@ -107,47 +107,40 @@ class Thermostat(HomeAccessory):
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value)
_LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.services.call('climate', 'set_operation_mode',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_OPERATION_MODE: hass_value})
self._hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self._entity_id)
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f",
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f',
self._entity_id, value)
self.coolingthresh_flag_target_state = True
low = self.char_heating_thresh_temp.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_HIGH: value,
ATTR_TARGET_TEMP_LOW: low})
low = self.char_heating_thresh_temp.value
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=value,
target_temp_low=low)
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heating threshold temperature to %.2f",
_LOGGER.debug('%s: Set heating threshold temperature to %.2f',
self._entity_id, value)
self.heatingthresh_flag_target_state = True
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_LOW: value,
ATTR_TARGET_TEMP_HIGH: high})
high = self.char_cooling_thresh_temp.value
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=high,
target_temp_low=value)
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.2f",
_LOGGER.debug('%s: Set target temperature to %.2f',
self._entity_id, value)
self.temperature_flag_target_state = True
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TEMPERATURE: value})
self._hass.components.climate.set_temperature(
temperature=value, entity_id=self._entity_id)
def update_thermostat(self, entity_id=None,
old_state=None, new_state=None):
@ -166,62 +159,58 @@ class Thermostat(HomeAccessory):
if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp,
should_callback=False)
else:
self.temperature_flag_target_state = False
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp is not None:
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if cooling_thresh is not None:
if cooling_thresh:
if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value(
cooling_thresh, should_callback=False)
else:
self.coolingthresh_flag_target_state = False
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp is not None:
if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if heating_thresh is not None:
if heating_thresh:
if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value(
heating_thresh, should_callback=False)
else:
self.heatingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
# Update display units
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if display_units is not None \
if display_units \
and display_units in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(
UNIT_HASS_TO_HOMEKIT[display_units])
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode is not None \
if operation_mode \
and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
else:
self.heat_cool_flag_target_state = False
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
if operation_mode == STATE_HEAT:
if current_temp < target_temp:
if isinstance(target_temp, float) and current_temp < target_temp:
current_operation_mode = STATE_HEAT
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_COOL:
if current_temp > target_temp:
if isinstance(target_temp, float) and current_temp > target_temp:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_AUTO:
# Check if auto is supported
if self.char_cooling_thresh_temp is not None:
lower_temp = self.char_heating_thresh_temp.get_value()
upper_temp = self.char_cooling_thresh_temp.get_value()
if self.char_cooling_thresh_temp:
lower_temp = self.char_heating_thresh_temp.value
upper_temp = self.char_cooling_thresh_temp.value
if current_temp < lower_temp:
current_operation_mode = STATE_HEAT
elif current_temp > upper_temp:
@ -232,9 +221,11 @@ class Thermostat(HomeAccessory):
# Check if heating or cooling are supported
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
if current_temp < target_temp and heat:
if isinstance(target_temp, float) and \
current_temp < target_temp and heat:
current_operation_mode = STATE_HEAT
elif current_temp > target_temp and cool:
elif isinstance(target_temp, float) and \
current_temp > target_temp and cool:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF

View file

@ -0,0 +1,46 @@
"""Collection of useful functions for the HomeKit component."""
import logging
import voluptuous as vol
from homeassistant.core import split_entity_id
from homeassistant.const import (
ATTR_CODE)
import homeassistant.helpers.config_validation as cv
from .const import HOMEKIT_NOTIFY_ID
_LOGGER = logging.getLogger(__name__)
def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY."""
entities = {}
for key, config in values.items():
entity = cv.entity_id(key)
params = {}
if not isinstance(config, dict):
raise vol.Invalid('The configuration for "{}" must be '
' an dictionary.'.format(entity))
domain, _ = split_entity_id(entity)
if domain == 'alarm_control_panel':
code = config.get(ATTR_CODE)
params[ATTR_CODE] = cv.string(code) if code else None
entities[entity] = params
return entities
def show_setup_message(bridge, hass):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
message = 'To setup Home Assistant in the Home App, enter the ' \
'following code:\n### {}'.format(pin)
hass.components.persistent_notification.create(
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID)
def dismiss_setup_message(hass):
"""Dismiss persistent notification and remove QR code."""
hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID)