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)

View File

@ -1 +0,0 @@
"""The tests for the homekit component."""

View File

@ -2,166 +2,164 @@
This includes tests for all mock object types.
"""
from unittest.mock import patch
# pylint: disable=unused-import
from pyhap.loader import get_serv_loader, get_char_loader # noqa F401
import unittest
from unittest.mock import call, patch, Mock
from homeassistant.components.homekit.accessories import (
set_accessory_info, add_preload_service, override_properties,
HomeAccessory, HomeBridge)
add_preload_service, set_accessory_info, override_properties,
HomeAccessory, HomeBridge, HomeDriver)
from homeassistant.components.homekit.const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
from tests.mock.homekit import (
get_patch_paths, mock_preload_service,
MockTypeLoader, MockAccessory, MockService, MockChar)
PATH_SERV = 'pyhap.loader.get_serv_loader'
PATH_CHAR = 'pyhap.loader.get_char_loader'
PATH_ACC, _ = get_patch_paths()
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
@patch(PATH_CHAR, return_value=MockTypeLoader('char'))
@patch(PATH_SERV, return_value=MockTypeLoader('service'))
def test_add_preload_service(mock_serv, mock_char):
"""Test method add_preload_service.
class TestAccessories(unittest.TestCase):
"""Test pyhap adapter methods."""
The methods 'get_serv_loader' and 'get_char_loader' are mocked.
"""
acc = MockAccessory('Accessory')
serv = add_preload_service(acc, 'TestService',
['TestChar', 'TestChar2'],
['TestOptChar', 'TestOptChar2'])
def test_add_preload_service(self):
"""Test add_preload_service without additional characteristics."""
acc = Mock()
serv = add_preload_service(acc, 'AirPurifier')
self.assertEqual(acc.mock_calls, [call.add_service(serv)])
with self.assertRaises(AssertionError):
serv.get_characteristic('Name')
assert serv.display_name == 'TestService'
assert len(serv.characteristics) == 2
assert len(serv.opt_characteristics) == 2
# Test with typo in service name
with self.assertRaises(KeyError):
add_preload_service(Mock(), 'AirPurifierTypo')
acc.services = []
serv = add_preload_service(acc, 'TestService')
# Test adding additional characteristic as string
serv = add_preload_service(Mock(), 'AirPurifier', 'Name')
serv.get_characteristic('Name')
assert not serv.characteristics
assert not serv.opt_characteristics
# Test adding additional characteristics as list
serv = add_preload_service(Mock(), 'AirPurifier',
['Name', 'RotationSpeed'])
serv.get_characteristic('Name')
serv.get_characteristic('RotationSpeed')
acc.services = []
serv = add_preload_service(acc, 'TestService',
'TestChar', 'TestOptChar')
# Test adding additional characteristic with typo
with self.assertRaises(KeyError):
add_preload_service(Mock(), 'AirPurifier', 'NameTypo')
assert len(serv.characteristics) == 1
assert len(serv.opt_characteristics) == 1
def test_set_accessory_info(self):
"""Test setting the basic accessory information."""
# Test HomeAccessory
acc = HomeAccessory()
set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
assert serv.characteristics[0].display_name == 'TestChar'
assert serv.opt_characteristics[0].display_name == 'TestOptChar'
serv = acc.get_service(SERV_ACCESSORY_INFO)
self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name')
self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model')
self.assertEqual(
serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer')
self.assertEqual(
serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000')
# Test HomeBridge
acc = HomeBridge(None)
set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
def test_override_properties():
"""Test override of characteristic properties with MockChar."""
char = MockChar('TestChar')
new_prop = {1: 'Test', 2: 'Demo'}
override_properties(char, new_prop)
serv = acc.get_service(SERV_ACCESSORY_INFO)
self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model')
self.assertEqual(
serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer')
self.assertEqual(
serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000')
assert char.properties == new_prop
def test_override_properties(self):
"""Test overriding property values."""
serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed')
char_active = serv.get_characteristic('Active')
char_rotation_speed = serv.get_characteristic('RotationSpeed')
def test_set_accessory_info():
"""Test setting of basic accessory information with MockAccessory."""
acc = MockAccessory('Accessory')
set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
self.assertTrue(
char_active.properties['ValidValues'].get('State') is None)
self.assertEqual(char_rotation_speed.properties['maxValue'], 100)
assert len(acc.services) == 1
serv = acc.services[0]
override_properties(char_active, valid_values={'State': 'On'})
override_properties(char_rotation_speed, properties={'maxValue': 200})
assert serv.display_name == SERV_ACCESSORY_INFO
assert len(serv.characteristics) == 4
chars = serv.characteristics
self.assertFalse(
char_active.properties['ValidValues'].get('State') is None)
self.assertEqual(char_rotation_speed.properties['maxValue'], 200)
assert chars[0].display_name == CHAR_NAME
assert chars[0].value == 'name'
assert chars[1].display_name == CHAR_MODEL
assert chars[1].value == 'model'
assert chars[2].display_name == CHAR_MANUFACTURER
assert chars[2].value == 'manufacturer'
assert chars[3].display_name == CHAR_SERIAL_NUMBER
assert chars[3].value == '0000'
def test_home_accessory(self):
"""Test HomeAccessory class."""
acc = HomeAccessory()
self.assertEqual(acc.display_name, ACCESSORY_NAME)
self.assertEqual(acc.category, 1) # Category.OTHER
self.assertEqual(len(acc.services), 1)
serv = acc.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL)
acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2)
self.assertEqual(acc.display_name, 'test_name')
self.assertEqual(acc.category, 3) # Category.FAN
self.assertEqual(acc.aid, 2)
self.assertEqual(len(acc.services), 1)
serv = acc.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, 'test_model')
@patch(PATH_ACC, side_effect=mock_preload_service)
def test_home_accessory(mock_pre_serv):
"""Test initializing a HomeAccessory object."""
acc = HomeAccessory('TestAccessory', 'test.accessory', 'WINDOW')
def test_home_bridge(self):
"""Test HomeBridge class."""
bridge = HomeBridge(None)
self.assertEqual(bridge.display_name, BRIDGE_NAME)
self.assertEqual(bridge.category, 2) # Category.BRIDGE
self.assertEqual(len(bridge.services), 2)
serv = bridge.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO)
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL)
serv = bridge.services[1] # SERV_BRIDGING_STATE
self.assertEqual(serv.display_name, SERV_BRIDGING_STATE)
assert acc.display_name == 'TestAccessory'
assert acc.category == 13 # Category.WINDOW
assert len(acc.services) == 1
bridge = HomeBridge('hass', 'test_name', 'test_model')
self.assertEqual(bridge.display_name, 'test_name')
self.assertEqual(len(bridge.services), 2)
serv = bridge.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, 'test_model')
serv = acc.services[0]
assert serv.display_name == SERV_ACCESSORY_INFO
char_model = serv.get_characteristic(CHAR_MODEL)
assert char_model.get_value() == 'test.accessory'
# setup_message
bridge.setup_message()
# add_paired_client
with patch('pyhap.accessory.Accessory.add_paired_client') \
as mock_add_paired_client, \
patch('homeassistant.components.homekit.accessories.'
'dismiss_setup_message') as mock_dissmiss_msg:
bridge.add_paired_client('client_uuid', 'client_public')
@patch(PATH_ACC, side_effect=mock_preload_service)
def test_home_bridge(mock_pre_serv):
"""Test initializing a HomeBridge object."""
bridge = HomeBridge('TestBridge', 'test.bridge', b'123-45-678')
self.assertEqual(mock_add_paired_client.call_args,
call('client_uuid', 'client_public'))
self.assertEqual(mock_dissmiss_msg.call_args, call('hass'))
assert bridge.display_name == 'TestBridge'
assert bridge.pincode == b'123-45-678'
assert len(bridge.services) == 2
# remove_paired_client
with patch('pyhap.accessory.Accessory.remove_paired_client') \
as mock_remove_paired_client, \
patch('homeassistant.components.homekit.accessories.'
'show_setup_message') as mock_show_msg:
bridge.remove_paired_client('client_uuid')
assert bridge.services[0].display_name == SERV_ACCESSORY_INFO
assert bridge.services[1].display_name == SERV_BRIDGING_STATE
self.assertEqual(
mock_remove_paired_client.call_args, call('client_uuid'))
self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass'))
char_model = bridge.services[0].get_characteristic(CHAR_MODEL)
assert char_model.get_value() == 'test.bridge'
def test_home_driver(self):
"""Test HomeDriver class."""
bridge = HomeBridge(None)
ip_adress = '127.0.0.1'
port = 51826
path = '.homekit.state'
with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \
as mock_driver:
HomeDriver(bridge, ip_adress, port, path)
def test_mock_accessory():
"""Test attributes and functions of a MockAccessory."""
acc = MockAccessory('TestAcc')
serv = MockService('TestServ')
acc.add_service(serv)
assert acc.display_name == 'TestAcc'
assert len(acc.services) == 1
assert acc.get_service('TestServ') == serv
assert acc.get_service('NewServ').display_name == 'NewServ'
assert len(acc.services) == 2
def test_mock_service():
"""Test attributes and functions of a MockService."""
serv = MockService('TestServ')
char = MockChar('TestChar')
opt_char = MockChar('TestOptChar')
serv.add_characteristic(char)
serv.add_opt_characteristic(opt_char)
assert serv.display_name == 'TestServ'
assert len(serv.characteristics) == 1
assert len(serv.opt_characteristics) == 1
assert serv.get_characteristic('TestChar') == char
assert serv.get_characteristic('TestOptChar') == opt_char
assert serv.get_characteristic('NewChar').display_name == 'NewChar'
assert len(serv.characteristics) == 2
def test_mock_char():
"""Test attributes and functions of a MockChar."""
def callback_method(value):
"""Provide a callback options for 'set_value' method."""
assert value == 'With callback'
char = MockChar('TestChar')
char.set_value('Value')
assert char.display_name == 'TestChar'
assert char.get_value() == 'Value'
char.setter_callback = callback_method
char.set_value('With callback')
self.assertEqual(
mock_driver.call_args, call(bridge, ip_adress, port, path))

View File

@ -1,57 +1,113 @@
"""Package to test the get_accessory method."""
from unittest.mock import patch, MagicMock
import logging
import unittest
from unittest.mock import patch, Mock
from homeassistant.core import State
from homeassistant.components.homekit import (
TYPES, get_accessory, import_types)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.homekit import get_accessory, TYPES
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES,
TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN)
ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
CONFIG = {}
def test_import_types():
"""Test if all type files are imported correctly."""
try:
import_types()
assert True
# pylint: disable=broad-except
except Exception:
assert False
def test_component_not_supported():
def test_get_accessory_invalid(caplog):
"""Test with unsupported component."""
state = State('demo.unsupported', STATE_UNKNOWN)
assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \
is None
assert caplog.records[1].levelname == 'WARNING'
assert True if get_accessory(None, state) is None else False
assert get_accessory(None, State('test.test', 'on'), None, None) \
is None
assert caplog.records[3].levelname == 'WARNING'
def test_sensor_temperature_celsius():
"""Test temperature sensor with Celsius as unit."""
mock_type = MagicMock()
with patch.dict(TYPES, {'TemperatureSensor': mock_type}):
state = State('sensor.temperature', '23',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
get_accessory(None, state)
assert len(mock_type.mock_calls) == 1
class TestGetAccessories(unittest.TestCase):
"""Methods to test the get_accessory method."""
def setUp(self):
"""Setup Mock type."""
self.mock_type = Mock()
# pylint: disable=invalid-name
def test_sensor_temperature_fahrenheit():
"""Test temperature sensor with Fahrenheit as unit."""
mock_type = MagicMock()
with patch.dict(TYPES, {'TemperatureSensor': mock_type}):
state = State('sensor.temperature', '74',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT})
get_accessory(None, state)
assert len(mock_type.mock_calls) == 1
def tearDown(self):
"""Test if mock type was called."""
self.assertTrue(self.mock_type.called)
def test_sensor_temperature_celsius(self):
"""Test temperature sensor with Celsius as unit."""
with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}):
state = State('sensor.temperature', '23',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
get_accessory(None, state, 2, {})
def test_cover_set_position():
"""Test cover with support for set_cover_position."""
mock_type = MagicMock()
with patch.dict(TYPES, {'Window': mock_type}):
state = State('cover.set_position', 'open',
{ATTR_SUPPORTED_FEATURES: 4})
get_accessory(None, state)
assert len(mock_type.mock_calls) == 1
# pylint: disable=invalid-name
def test_sensor_temperature_fahrenheit(self):
"""Test temperature sensor with Fahrenheit as unit."""
with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}):
state = State('sensor.temperature', '74',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT})
get_accessory(None, state, 2, {})
def test_cover_set_position(self):
"""Test cover with support for set_cover_position."""
with patch.dict(TYPES, {'WindowCovering': self.mock_type}):
state = State('cover.set_position', 'open',
{ATTR_SUPPORTED_FEATURES: 4})
get_accessory(None, state, 2, {})
def test_alarm_control_panel(self):
"""Test alarm control panel."""
config = {ATTR_CODE: '1234'}
with patch.dict(TYPES, {'SecuritySystem': self.mock_type}):
state = State('alarm_control_panel.test', 'armed')
get_accessory(None, state, 2, config)
# pylint: disable=unsubscriptable-object
self.assertEqual(
self.mock_type.call_args[1].get('alarm_code'), '1234')
def test_climate(self):
"""Test climate devices."""
with patch.dict(TYPES, {'Thermostat': self.mock_type}):
state = State('climate.test', 'auto')
get_accessory(None, state, 2, {})
# pylint: disable=unsubscriptable-object
self.assertEqual(
self.mock_type.call_args[0][-1], False) # support_auto
def test_climate_support_auto(self):
"""Test climate devices with support for auto mode."""
with patch.dict(TYPES, {'Thermostat': self.mock_type}):
state = State('climate.test', 'auto', {
ATTR_SUPPORTED_FEATURES:
SUPPORT_TARGET_TEMPERATURE_LOW |
SUPPORT_TARGET_TEMPERATURE_HIGH})
get_accessory(None, state, 2, {})
# pylint: disable=unsubscriptable-object
self.assertEqual(
self.mock_type.call_args[0][-1], True) # support_auto
def test_switch(self):
"""Test switch."""
with patch.dict(TYPES, {'Switch': self.mock_type}):
state = State('switch.test', 'on')
get_accessory(None, state, 2, {})
def test_remote(self):
"""Test remote."""
with patch.dict(TYPES, {'Switch': self.mock_type}):
state = State('remote.test', 'on')
get_accessory(None, state, 2, {})
def test_input_boolean(self):
"""Test input_boolean."""
with patch.dict(TYPES, {'Switch': self.mock_type}):
state = State('input_boolean.test', 'on')
get_accessory(None, state, 2, {})

View File

@ -1,33 +1,22 @@
"""Tests for the HomeKit component."""
import unittest
from unittest.mock import call, patch, ANY
import voluptuous as vol
# pylint: disable=unused-import
from pyhap.accessory_driver import AccessoryDriver # noqa F401
from unittest.mock import call, patch, ANY, Mock
from homeassistant import setup
from homeassistant.core import Event
from homeassistant.components.homekit import (
CONF_PIN_CODE, HOMEKIT_FILE, HomeKit, valid_pin)
from homeassistant.core import State
from homeassistant.components.homekit import HomeKit, generate_aid
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START,
DEFAULT_PORT, SERVICE_HOMEKIT_START)
from homeassistant.helpers.entityfilter import generate_filter
from homeassistant.const import (
CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, PATH_HOMEKIT
PATH_ACC, _ = get_patch_paths()
IP_ADDRESS = '127.0.0.1'
CONFIG_MIN = {'homekit': {}}
CONFIG = {
'homekit': {
CONF_PORT: 11111,
CONF_PIN_CODE: '987-65-432',
}
}
PATH_HOMEKIT = 'homeassistant.components.homekit'
class TestHomeKit(unittest.TestCase):
@ -41,75 +30,162 @@ class TestHomeKit(unittest.TestCase):
"""Stop down everything that was started."""
self.hass.stop()
def test_validate_pincode(self):
"""Test async_setup with invalid config option."""
schema = vol.Schema(valid_pin)
def test_generate_aid(self):
"""Test generate aid method."""
aid = generate_aid('demo.entity')
self.assertIsInstance(aid, int)
self.assertTrue(aid >= 2 and aid <= 18446744073709551615)
for value in ('', '123-456-78', 'a23-45-678', '12345678', 1234):
with self.assertRaises(vol.MultipleInvalid):
schema(value)
for value in ('123-45-678', '234-56-789'):
self.assertTrue(schema(value))
with patch(PATH_HOMEKIT + '.adler32') as mock_adler32:
mock_adler32.side_effect = [0, 1]
self.assertIsNone(generate_aid('demo.entity'))
@patch(PATH_HOMEKIT + '.HomeKit')
def test_setup_min(self, mock_homekit):
"""Test async_setup with minimal config option."""
"""Test async_setup with min config options."""
self.assertTrue(setup.setup_component(
self.hass, 'homekit', CONFIG_MIN))
self.hass, DOMAIN, {DOMAIN: {}}))
self.assertEqual(mock_homekit.mock_calls,
[call(self.hass, 51826),
call().setup_bridge(b'123-45-678')])
self.assertEqual(mock_homekit.mock_calls, [
call(self.hass, DEFAULT_PORT, ANY, {}),
call().setup()])
# Test auto start enabled
mock_homekit.reset_mock()
self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
self.hass.block_till_done()
self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)])
@patch(PATH_HOMEKIT + '.HomeKit')
def test_setup_auto_start_disabled(self, mock_homekit):
"""Test async_setup with auto start disabled and test service calls."""
mock_homekit.return_value = homekit = Mock()
config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}}
self.assertTrue(setup.setup_component(
self.hass, DOMAIN, config))
self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
self.hass.block_till_done()
self.assertEqual(mock_homekit.mock_calls,
[call().start_driver(ANY)])
self.assertEqual(mock_homekit.mock_calls, [
call(self.hass, 11111, ANY, {}),
call().setup()])
@patch(PATH_HOMEKIT + '.HomeKit')
def test_setup_parameters(self, mock_homekit):
"""Test async_setup with full config option."""
self.assertTrue(setup.setup_component(
self.hass, 'homekit', CONFIG))
# Test start call with driver stopped.
homekit.reset_mock()
homekit.configure_mock(**{'started': False})
self.assertEqual(mock_homekit.mock_calls,
[call(self.hass, 11111),
call().setup_bridge(b'987-65-432')])
self.hass.services.call('homekit', 'start')
self.assertEqual(homekit.mock_calls, [call.start()])
@patch('pyhap.accessory_driver.AccessoryDriver')
def test_homekit_class(self, mock_acc_driver):
"""Test interaction between the HomeKit class and pyhap."""
with patch(PATH_HOMEKIT + '.accessories.HomeBridge') as mock_bridge:
homekit = HomeKit(self.hass, 51826)
homekit.setup_bridge(b'123-45-678')
# Test start call with driver started.
homekit.reset_mock()
homekit.configure_mock(**{'started': True})
mock_bridge.reset_mock()
self.hass.states.set('demo.demo1', 'on')
self.hass.states.set('demo.demo2', 'off')
self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START)
self.assertEqual(homekit.mock_calls, [])
with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc, \
patch(PATH_HOMEKIT + '.import_types') as mock_import_types, \
def test_homekit_setup(self):
"""Test setup of bridge and driver."""
homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {})
with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \
patch('homeassistant.util.get_local_ip') as mock_ip:
mock_get_acc.side_effect = ['TempSensor', 'Window']
mock_ip.return_value = IP_ADDRESS
homekit.start_driver(Event(EVENT_HOMEASSISTANT_START))
homekit.setup()
path = self.hass.config.path(HOMEKIT_FILE)
self.assertTrue(isinstance(homekit.bridge, HomeBridge))
self.assertEqual(mock_driver.mock_calls, [
call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)])
self.assertEqual(mock_import_types.call_count, 1)
self.assertEqual(mock_get_acc.call_count, 2)
self.assertEqual(mock_bridge.mock_calls,
[call().add_accessory('TempSensor'),
call().add_accessory('Window')])
self.assertEqual(mock_acc_driver.mock_calls,
[call(homekit.bridge, 51826, IP_ADDRESS, path),
call().start()])
mock_acc_driver.reset_mock()
# Test if stop listener is setup
self.assertEqual(
self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1)
self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
self.hass.block_till_done()
def test_homekit_add_accessory(self):
"""Add accessory if config exists and get_acc returns an accessory."""
homekit = HomeKit(self.hass, None, lambda entity_id: True, {})
homekit.bridge = HomeBridge(self.hass)
self.assertEqual(mock_acc_driver.mock_calls, [call().stop()])
with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \
as mock_add_acc, \
patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc:
mock_get_acc.side_effect = [None, 'acc', None]
homekit.add_bridge_accessory(State('light.demo', 'on'))
self.assertEqual(mock_get_acc.call_args,
call(self.hass, ANY, 363398124, {}))
self.assertFalse(mock_add_acc.called)
homekit.add_bridge_accessory(State('demo.test', 'on'))
self.assertEqual(mock_get_acc.call_args,
call(self.hass, ANY, 294192020, {}))
self.assertTrue(mock_add_acc.called)
homekit.add_bridge_accessory(State('demo.test_2', 'on'))
self.assertEqual(mock_get_acc.call_args,
call(self.hass, ANY, 429982757, {}))
self.assertEqual(mock_add_acc.mock_calls, [call('acc')])
def test_homekit_entity_filter(self):
"""Test the entity filter."""
entity_filter = generate_filter(['cover'], ['demo.test'], [], [])
homekit = HomeKit(self.hass, None, entity_filter, {})
with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc:
mock_get_acc.return_value = None
homekit.add_bridge_accessory(State('cover.test', 'open'))
self.assertTrue(mock_get_acc.called)
mock_get_acc.reset_mock()
homekit.add_bridge_accessory(State('demo.test', 'on'))
self.assertTrue(mock_get_acc.called)
mock_get_acc.reset_mock()
homekit.add_bridge_accessory(State('light.demo', 'light'))
self.assertFalse(mock_get_acc.called)
@patch(PATH_HOMEKIT + '.show_setup_message')
@patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory')
def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg):
"""Test HomeKit start method."""
homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}})
homekit.bridge = HomeBridge(self.hass)
homekit.driver = Mock()
self.hass.states.set('light.demo', 'on')
state = self.hass.states.all()[0]
homekit.start()
self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)])
self.assertEqual(mock_show_setup_msg.mock_calls, [
call(homekit.bridge, self.hass)])
self.assertEqual(homekit.driver.mock_calls, [call.start()])
self.assertTrue(homekit.started)
# Test start() if already started
homekit.driver.reset_mock()
homekit.start()
self.assertEqual(homekit.driver.mock_calls, [])
def test_homekit_stop(self):
"""Test HomeKit stop method."""
homekit = HomeKit(None, None, None, None)
homekit.driver = Mock()
# Test if started = False
homekit.stop()
self.assertFalse(homekit.driver.stop.called)
# Test if driver not started
homekit.started = True
homekit.driver.configure_mock(**{'run_sentinel': None})
homekit.stop()
self.assertFalse(homekit.driver.stop.called)
# Test if driver is started
homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'})
homekit.stop()
self.assertTrue(homekit.driver.stop.called)

View File

@ -1,64 +0,0 @@
"""Test different accessory types: Switches."""
import unittest
from unittest.mock import patch
from homeassistant.core import callback
from homeassistant.components.homekit.switches import Switch
from homeassistant.const import ATTR_SERVICE, EVENT_CALL_SERVICE
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, mock_preload_service
PATH_ACC, PATH_FILE = get_patch_paths('switches')
class TestHomekitSwitches(unittest.TestCase):
"""Test class for all accessory types regarding switches."""
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.events = []
@callback
def record_event(event):
"""Track called event."""
self.events.append(event)
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
def tearDown(self):
"""Stop down everything that was started."""
self.hass.stop()
def test_switch_set_state(self):
"""Test if accessory and HA are updated accordingly."""
switch = 'switch.testswitch'
with patch(PATH_ACC, side_effect=mock_preload_service):
with patch(PATH_FILE, side_effect=mock_preload_service):
acc = Switch(self.hass, switch, 'Switch')
acc.run()
self.assertEqual(acc.char_on.value, False)
self.hass.states.set(switch, 'on')
self.hass.block_till_done()
self.assertEqual(acc.char_on.value, True)
self.hass.states.set(switch, 'off')
self.hass.block_till_done()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
acc.char_on.set_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'turn_on')
self.assertEqual(acc.char_on.value, True)
acc.char_on.set_value(False)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'turn_off')
self.assertEqual(acc.char_on.value, False)

View File

@ -1,19 +1,15 @@
"""Test different accessory types: Covers."""
import unittest
from unittest.mock import patch
from homeassistant.core import callback
from homeassistant.components.cover import (
ATTR_POSITION, ATTR_CURRENT_POSITION)
from homeassistant.components.homekit.covers import Window
from homeassistant.components.homekit.type_covers import WindowCovering
from homeassistant.const import (
STATE_UNKNOWN, STATE_OPEN,
ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, mock_preload_service
PATH_ACC, PATH_FILE = get_patch_paths('covers')
class TestHomekitSensors(unittest.TestCase):
@ -39,10 +35,11 @@ class TestHomekitSensors(unittest.TestCase):
"""Test if accessory and HA are updated accordingly."""
window_cover = 'cover.window'
with patch(PATH_ACC, side_effect=mock_preload_service):
with patch(PATH_FILE, side_effect=mock_preload_service):
acc = Window(self.hass, window_cover, 'Cover')
acc.run()
acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 14) # WindowCovering
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)

View File

@ -1,18 +1,15 @@
"""Test different accessory types: Security Systems."""
import unittest
from unittest.mock import patch
from homeassistant.core import callback
from homeassistant.components.homekit.security_systems import SecuritySystem
from homeassistant.components.homekit.type_security_systems import (
SecuritySystem)
from homeassistant.const import (
ATTR_SERVICE, EVENT_CALL_SERVICE,
ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED)
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN)
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, mock_preload_service
PATH_ACC, PATH_FILE = get_patch_paths('security_systems')
class TestHomekitSecuritySystems(unittest.TestCase):
@ -36,12 +33,14 @@ class TestHomekitSecuritySystems(unittest.TestCase):
def test_switch_set_state(self):
"""Test if accessory and HA are updated accordingly."""
acp = 'alarm_control_panel.testsecurity'
acp = 'alarm_control_panel.test'
with patch(PATH_ACC, side_effect=mock_preload_service):
with patch(PATH_FILE, side_effect=mock_preload_service):
acc = SecuritySystem(self.hass, acp, 'SecuritySystem')
acc.run()
acc = SecuritySystem(self.hass, acp, 'SecuritySystem',
alarm_code='1234', aid=2)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 11) # AlarmSystem
self.assertEqual(acc.char_current_state.value, 3)
self.assertEqual(acc.char_target_state.value, 3)
@ -66,27 +65,40 @@ class TestHomekitSecuritySystems(unittest.TestCase):
self.assertEqual(acc.char_target_state.value, 3)
self.assertEqual(acc.char_current_state.value, 3)
self.hass.states.set(acp, STATE_UNKNOWN)
self.hass.block_till_done()
self.assertEqual(acc.char_target_state.value, 3)
self.assertEqual(acc.char_current_state.value, 3)
# Set from HomeKit
acc.char_target_state.set_value(0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'alarm_arm_home')
self.assertEqual(
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 0)
acc.char_target_state.set_value(1)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'alarm_arm_away')
self.assertEqual(
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 1)
acc.char_target_state.set_value(2)
self.hass.block_till_done()
self.assertEqual(
self.events[2].data[ATTR_SERVICE], 'alarm_arm_night')
self.assertEqual(
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 2)
acc.char_target_state.set_value(3)
self.hass.block_till_done()
self.assertEqual(
self.events[3].data[ATTR_SERVICE], 'alarm_disarm')
self.assertEqual(
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 3)

View File

@ -1,17 +1,13 @@
"""Test different accessory types: Sensors."""
import unittest
from unittest.mock import patch
from homeassistant.components.homekit.const import PROP_CELSIUS
from homeassistant.components.homekit.sensors import (
from homeassistant.components.homekit.type_sensors import (
TemperatureSensor, calc_temperature)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN)
ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, mock_preload_service
PATH_ACC, PATH_FILE = get_patch_paths('sensors')
def test_calc_temperature():
@ -32,7 +28,6 @@ class TestHomekitSensors(unittest.TestCase):
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
get_patch_paths('sensors')
def tearDown(self):
"""Stop down everything that was started."""
@ -40,27 +35,28 @@ class TestHomekitSensors(unittest.TestCase):
def test_temperature(self):
"""Test if accessory is updated after state change."""
temperature_sensor = 'sensor.temperature'
entity_id = 'sensor.temperature'
with patch(PATH_ACC, side_effect=mock_preload_service):
with patch(PATH_FILE, side_effect=mock_preload_service):
acc = TemperatureSensor(self.hass, temperature_sensor,
'Temperature')
acc.run()
acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 10) # Sensor
self.assertEqual(acc.char_temp.value, 0.0)
self.assertEqual(acc.char_temp.properties, PROP_CELSIUS)
for key, value in PROP_CELSIUS.items():
self.assertEqual(acc.char_temp.properties[key], value)
self.hass.states.set(temperature_sensor, STATE_UNKNOWN,
self.hass.states.set(entity_id, STATE_UNKNOWN,
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.hass.states.set(temperature_sensor, '20',
self.hass.states.set(entity_id, '20',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_temp.value, 20)
self.hass.states.set(temperature_sensor, '75.2',
self.hass.states.set(entity_id, '75.2',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT})
self.hass.block_till_done()
self.assertEqual(acc.char_temp.value, 24)

View File

@ -0,0 +1,104 @@
"""Test different accessory types: Switches."""
import unittest
from homeassistant.core import callback, split_entity_id
from homeassistant.components.homekit.type_switches import Switch
from homeassistant.const import (
ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE,
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF)
from tests.common import get_test_home_assistant
class TestHomekitSwitches(unittest.TestCase):
"""Test class for all accessory types regarding switches."""
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.events = []
@callback
def record_event(event):
"""Track called event."""
self.events.append(event)
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
def tearDown(self):
"""Stop down everything that was started."""
self.hass.stop()
def test_switch_set_state(self):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'switch.test'
domain = split_entity_id(entity_id)[0]
acc = Switch(self.hass, entity_id, 'Switch', aid=2)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 8) # Switch
self.assertEqual(acc.char_on.value, False)
self.hass.states.set(entity_id, STATE_ON)
self.hass.block_till_done()
self.assertEqual(acc.char_on.value, True)
self.hass.states.set(entity_id, STATE_OFF)
self.hass.block_till_done()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
acc.char_on.set_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
self.assertEqual(
self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
acc.char_on.set_value(False)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_DOMAIN], domain)
self.assertEqual(
self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF)
def test_remote_set_state(self):
"""Test service call for remote as domain."""
entity_id = 'remote.test'
domain = split_entity_id(entity_id)[0]
acc = Switch(self.hass, entity_id, 'Switch', aid=2)
acc.run()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
acc.char_on.set_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
self.assertEqual(
self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
self.assertEqual(acc.char_on.value, True)
def test_input_boolean_set_state(self):
"""Test service call for remote as domain."""
entity_id = 'input_boolean.test'
domain = split_entity_id(entity_id)[0]
acc = Switch(self.hass, entity_id, 'Switch', aid=2)
acc.run()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
acc.char_on.set_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
self.assertEqual(
self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
self.assertEqual(acc.char_on.value, True)

View File

@ -1,21 +1,18 @@
"""Test different accessory types: Thermostats."""
import unittest
from unittest.mock import patch
from homeassistant.core import callback
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH,
ATTR_OPERATION_MODE, STATE_HEAT, STATE_AUTO)
from homeassistant.components.homekit.thermostats import Thermostat, STATE_OFF
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE,
ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO)
from homeassistant.components.homekit.type_thermostats import (
Thermostat, STATE_OFF)
from homeassistant.const import (
ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA,
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from tests.common import get_test_home_assistant
from tests.mock.homekit import get_patch_paths, mock_preload_service
PATH_ACC, PATH_FILE = get_patch_paths('thermostats')
class TestHomekitThermostats(unittest.TestCase):
@ -39,12 +36,13 @@ class TestHomekitThermostats(unittest.TestCase):
def test_default_thermostat(self):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.testclimate'
climate = 'climate.test'
with patch(PATH_ACC, side_effect=mock_preload_service):
with patch(PATH_FILE, side_effect=mock_preload_service):
acc = Thermostat(self.hass, climate, 'Climate', False)
acc.run()
acc = Thermostat(self.hass, climate, 'Climate', False, aid=2)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 9) # Thermostat
self.assertEqual(acc.char_current_heat_cool.value, 0)
self.assertEqual(acc.char_target_heat_cool.value, 0)
@ -78,6 +76,30 @@ class TestHomekitThermostats(unittest.TestCase):
self.assertEqual(acc.char_current_temp.value, 23.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_COOL,
{ATTR_OPERATION_MODE: STATE_COOL,
ATTR_TEMPERATURE: 20.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_target_temp.value, 20.0)
self.assertEqual(acc.char_current_heat_cool.value, 2)
self.assertEqual(acc.char_target_heat_cool.value, 2)
self.assertEqual(acc.char_current_temp.value, 25.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_COOL,
{ATTR_OPERATION_MODE: STATE_COOL,
ATTR_TEMPERATURE: 20.0,
ATTR_CURRENT_TEMPERATURE: 19.0,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_target_temp.value, 20.0)
self.assertEqual(acc.char_current_heat_cool.value, 0)
self.assertEqual(acc.char_target_heat_cool.value, 2)
self.assertEqual(acc.char_current_temp.value, 19.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_OFF,
{ATTR_OPERATION_MODE: STATE_OFF,
ATTR_TEMPERATURE: 22.0,
@ -90,6 +112,45 @@ class TestHomekitThermostats(unittest.TestCase):
self.assertEqual(acc.char_current_temp.value, 18.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_AUTO,
{ATTR_OPERATION_MODE: STATE_AUTO,
ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_target_temp.value, 22.0)
self.assertEqual(acc.char_current_heat_cool.value, 1)
self.assertEqual(acc.char_target_heat_cool.value, 3)
self.assertEqual(acc.char_current_temp.value, 18.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_AUTO,
{ATTR_OPERATION_MODE: STATE_AUTO,
ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_target_temp.value, 22.0)
self.assertEqual(acc.char_current_heat_cool.value, 2)
self.assertEqual(acc.char_target_heat_cool.value, 3)
self.assertEqual(acc.char_current_temp.value, 25.0)
self.assertEqual(acc.char_display_units.value, 0)
self.hass.states.set(climate, STATE_AUTO,
{ATTR_OPERATION_MODE: STATE_AUTO,
ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_target_temp.value, 22.0)
self.assertEqual(acc.char_current_heat_cool.value, 0)
self.assertEqual(acc.char_target_heat_cool.value, 3)
self.assertEqual(acc.char_current_temp.value, 22.0)
self.assertEqual(acc.char_display_units.value, 0)
# Set from HomeKit
acc.char_target_temp.set_value(19.0)
self.hass.block_till_done()
@ -110,7 +171,7 @@ class TestHomekitThermostats(unittest.TestCase):
def test_auto_thermostat(self):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.testclimate'
climate = 'climate.test'
acc = Thermostat(self.hass, climate, 'Climate', True)
acc.run()

View File

@ -0,0 +1,83 @@
"""Test HomeKit util module."""
import unittest
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID
from homeassistant.components.homekit.util import (
show_setup_message, dismiss_setup_message, ATTR_CODE)
from homeassistant.components.homekit.util import validate_entity_config \
as vec
from homeassistant.components.persistent_notification import (
SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID)
from homeassistant.const import (
EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA)
from tests.common import get_test_home_assistant
class TestUtil(unittest.TestCase):
"""Test all HomeKit util methods."""
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.events = []
@callback
def record_event(event):
"""Track called event."""
self.events.append(event)
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
def tearDown(self):
"""Stop down everything that was started."""
self.hass.stop()
def test_validate_entity_config(self):
"""Test validate entities."""
configs = [{'invalid_entity_id': {}}, {'demo.test': 1},
{'demo.test': 'test'}, {'demo.test': [1, 2]},
{'demo.test': None}]
for conf in configs:
with self.assertRaises(vol.Invalid):
vec(conf)
self.assertEqual(vec({}), {})
self.assertEqual(
vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}),
{'alarm_control_panel.demo': {ATTR_CODE: '1234'}})
def test_show_setup_msg(self):
"""Test show setup message as persistence notification."""
bridge = HomeBridge(self.hass)
show_setup_message(bridge, self.hass)
self.hass.block_till_done()
data = self.events[0].data
self.assertEqual(
data.get(ATTR_DOMAIN, None), 'persistent_notification')
self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE)
self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None)
self.assertEqual(
data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None),
HOMEKIT_NOTIFY_ID)
def test_dismiss_setup_msg(self):
"""Test dismiss setup message."""
dismiss_setup_message(self.hass)
self.hass.block_till_done()
data = self.events[0].data
self.assertEqual(
data.get(ATTR_DOMAIN, None), 'persistent_notification')
self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS)
self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None)
self.assertEqual(
data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None),
HOMEKIT_NOTIFY_ID)

View File

@ -1,133 +0,0 @@
"""Basic mock functions and objects related to the HomeKit component."""
PATH_HOMEKIT = 'homeassistant.components.homekit'
def get_patch_paths(name=None):
"""Return paths to mock 'add_preload_service'."""
path_acc = PATH_HOMEKIT + '.accessories.add_preload_service'
path_file = PATH_HOMEKIT + '.' + str(name) + '.add_preload_service'
return (path_acc, path_file)
def mock_preload_service(acc, service, chars=None, opt_chars=None):
"""Mock alternative for function 'add_preload_service'."""
service = MockService(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
service.add_characteristic(char_name)
if opt_chars:
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
for opt_char_name in opt_chars:
service.add_characteristic(opt_char_name)
acc.add_service(service)
return service
class MockAccessory():
"""Define all attributes and methods for a MockAccessory."""
def __init__(self, name):
"""Initialize a MockAccessory object."""
self.display_name = name
self.services = []
def __repr__(self):
"""Return a representation of a MockAccessory. Use for debugging."""
serv_list = [serv.display_name for serv in self.services]
return "<accessory \"{}\", services={}>".format(
self.display_name, serv_list)
def add_service(self, service):
"""Add service to list of services."""
self.services.append(service)
def get_service(self, name):
"""Retrieve service from service list or return new MockService."""
for serv in self.services:
if serv.display_name == name:
return serv
serv = MockService(name)
self.add_service(serv)
return serv
class MockService():
"""Define all attributes and methods for a MockService."""
def __init__(self, name):
"""Initialize a MockService object."""
self.characteristics = []
self.opt_characteristics = []
self.display_name = name
def __repr__(self):
"""Return a representation of a MockService. Use for debugging."""
char_list = [char.display_name for char in self.characteristics]
opt_char_list = [
char.display_name for char in self.opt_characteristics]
return "<service \"{}\", chars={}, opt_chars={}>".format(
self.display_name, char_list, opt_char_list)
def add_characteristic(self, char):
"""Add characteristic to char list."""
self.characteristics.append(char)
def add_opt_characteristic(self, char):
"""Add characteristic to opt_char list."""
self.opt_characteristics.append(char)
def get_characteristic(self, name):
"""Get char for char lists or return new MockChar."""
for char in self.characteristics:
if char.display_name == name:
return char
for char in self.opt_characteristics:
if char.display_name == name:
return char
char = MockChar(name)
self.add_characteristic(char)
return char
class MockChar():
"""Define all attributes and methods for a MockChar."""
def __init__(self, name):
"""Initialize a MockChar object."""
self.display_name = name
self.properties = {}
self.value = None
self.type_id = None
self.setter_callback = None
def __repr__(self):
"""Return a representation of a MockChar. Use for debugging."""
return "<char \"{}\", value={}>".format(
self.display_name, self.value)
def set_value(self, value, should_notify=True, should_callback=True):
"""Set value of char."""
self.value = value
if self.setter_callback is not None and should_callback:
# pylint: disable=not-callable
self.setter_callback(value)
def get_value(self):
"""Get char value."""
return self.value
class MockTypeLoader():
"""Define all attributes and methods for a MockTypeLoader."""
def __init__(self, class_type):
"""Initialize a MockTypeLoader object."""
self.class_type = class_type
def get(self, name):
"""Return a MockService or MockChar object."""
if self.class_type == 'service':
return MockService(name)
elif self.class_type == 'char':
return MockChar(name)