mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
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:
parent
64f18c62f4
commit
d348f09d3d
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
4
homeassistant/components/homekit/services.yaml
Normal file
4
homeassistant/components/homekit/services.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
# Describes the format for available HomeKit services
|
||||
|
||||
start:
|
||||
description: Starts the HomeKit component driver.
|
@ -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)
|
||||
|
@ -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
|
@ -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)
|
@ -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
|
@ -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
|
46
homeassistant/components/homekit/util.py
Normal file
46
homeassistant/components/homekit/util.py
Normal 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)
|
@ -1 +0,0 @@
|
||||
"""The tests for the homekit component."""
|
@ -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))
|
||||
|
@ -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, {})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
104
tests/components/homekit/test_type_switches.py
Normal file
104
tests/components/homekit/test_type_switches.py
Normal 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)
|
@ -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()
|
83
tests/components/homekit/test_util.py
Normal file
83
tests/components/homekit/test_util.py
Normal 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)
|
@ -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)
|
Loading…
x
Reference in New Issue
Block a user