From d348f09d3d42c6bd65727a471cf363cbca19105f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 15 Mar 2018 02:48:21 +0100 Subject: [PATCH] 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 --- homeassistant/components/homekit/__init__.py | 205 +++++++----- .../components/homekit/accessories.py | 87 +++-- homeassistant/components/homekit/const.py | 30 +- .../components/homekit/services.yaml | 4 + .../homekit/{covers.py => type_covers.py} | 27 +- ...ty_systems.py => type_security_systems.py} | 30 +- .../homekit/{sensors.py => type_sensors.py} | 11 +- .../homekit/{switches.py => type_switches.py} | 23 +- .../{thermostats.py => type_thermostats.py} | 113 +++---- homeassistant/components/homekit/util.py | 46 +++ tests/components/homekit/__init__.py | 1 - tests/components/homekit/test_accessories.py | 316 +++++++++--------- .../homekit/test_get_accessories.py | 152 ++++++--- tests/components/homekit/test_homekit.py | 218 ++++++++---- tests/components/homekit/test_switches.py | 64 ---- .../{test_covers.py => test_type_covers.py} | 15 +- ...stems.py => test_type_security_systems.py} | 36 +- .../{test_sensors.py => test_type_sensors.py} | 30 +- .../components/homekit/test_type_switches.py | 104 ++++++ ...hermostats.py => test_type_thermostats.py} | 87 ++++- tests/components/homekit/test_util.py | 83 +++++ tests/mock/homekit.py | 133 -------- 22 files changed, 1069 insertions(+), 746 deletions(-) create mode 100644 homeassistant/components/homekit/services.yaml rename homeassistant/components/homekit/{covers.py => type_covers.py} (77%) rename homeassistant/components/homekit/{security_systems.py => type_security_systems.py} (80%) rename homeassistant/components/homekit/{sensors.py => type_sensors.py} (84%) rename homeassistant/components/homekit/{switches.py => type_switches.py} (72%) rename homeassistant/components/homekit/{thermostats.py => type_thermostats.py} (71%) create mode 100644 homeassistant/components/homekit/util.py delete mode 100644 tests/components/homekit/__init__.py delete mode 100644 tests/components/homekit/test_switches.py rename tests/components/homekit/{test_covers.py => test_type_covers.py} (87%) rename tests/components/homekit/{test_security_systems.py => test_type_security_systems.py} (72%) rename tests/components/homekit/{test_sensors.py => test_type_sensors.py} (64%) create mode 100644 tests/components/homekit/test_type_switches.py rename tests/components/homekit/{test_thermostats.py => test_type_thermostats.py} (65%) create mode 100644 tests/components/homekit/test_util.py delete mode 100644 tests/mock/homekit.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ad70740536e3b6..63013bd8fc9a22 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,154 +3,199 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ -import asyncio import logging -import re +from zlib import adler32 import voluptuous as vol -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, - TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.cover import SUPPORT_SET_POSITION +from homeassistant.const import ( + ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry +from .const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, + DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START) +from .util import ( + validate_entity_config, show_setup_message) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$" - -DOMAIN = 'homekit' REQUIREMENTS = ['HAP-python==1.1.7'] -BRIDGE_NAME = 'Home Assistant' -CONF_PIN_CODE = 'pincode' - -HOMEKIT_FILE = '.homekit.state' - - -def valid_pin(value): - """Validate pin code value.""" - match = re.match(_RE_VALID_PINCODE, str(value).strip()) - if not match: - raise vol.Invalid("Pin must be in the format: '123-45-678'") - return match.group(0) - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ - vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), - vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, }) }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Setup the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") + _LOGGER.debug('Begin setup HomeKit') conf = config[DOMAIN] - port = conf.get(CONF_PORT) - pin = str.encode(conf.get(CONF_PIN_CODE)) + port = conf[CONF_PORT] + auto_start = conf[CONF_AUTO_START] + entity_filter = conf[CONF_FILTER] + entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port) - homekit.setup_bridge(pin) + homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit.setup() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, homekit.start_driver) - return True + if auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) + return True + + def handle_homekit_service_start(service): + """Handle start HomeKit service call.""" + if homekit.started: + _LOGGER.warning('HomeKit is already running') + return + homekit.start() + hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START, + handle_homekit_service_start) -def import_types(): - """Import all types from files in the HomeKit directory.""" - _LOGGER.debug("Import type files.") - # pylint: disable=unused-variable - from . import ( # noqa F401 - covers, security_systems, sensors, switches, thermostats) + return True -def get_accessory(hass, state): +def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" + _LOGGER.debug('%s: ') + if not aid: + _LOGGER.warning('The entitiy "%s" is not supported, since it ' + 'generates an invalid aid, please change it.', + state.entity_id) + return None + if state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: - _LOGGER.debug("Add \"%s\" as \"%s\"", + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, - state.name) + state.name, aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position - if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: - _LOGGER.debug("Add \"%s\" as \"%s\"", - state.entity_id, 'Window') - return TYPES['Window'](hass, state.entity_id, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_SET_POSITION: + _LOGGER.debug('Add "%s" as "%s"', + state.entity_id, 'WindowCovering') + return TYPES['WindowCovering'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') - return TYPES['SecuritySystem'](hass, state.entity_id, state.name) + return TYPES['SecuritySystem'](hass, state.entity_id, state.name, + alarm_code=config[ATTR_CODE], aid=aid) elif state.domain == 'climate': - support_auto = False - features = state.attributes.get(ATTR_SUPPORTED_FEATURES) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE_HIGH # Check if climate device supports auto mode - if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \ - and (features & SUPPORT_TARGET_TEMPERATURE_LOW): - support_auto = True - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat') + support_auto = bool(features & support_temp_range) + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') return TYPES['Thermostat'](hass, state.entity_id, - state.name, support_auto) + state.name, support_auto, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch') - return TYPES['Switch'](hass, state.entity_id, state.name) + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') + return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) + _LOGGER.warning('The entity "%s" is not supported yet', + state.entity_id) return None +def generate_aid(entity_id): + """Generate accessory aid with zlib adler32.""" + aid = adler32(entity_id.encode('utf-8')) + if aid == 0 or aid == 1: + return None + return aid + + class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port): + def __init__(self, hass, port, entity_filter, entity_config): """Initialize a HomeKit object.""" self._hass = hass self._port = port + self._filter = entity_filter + self._config = entity_config + self.started = False + self.bridge = None self.driver = None - def setup_bridge(self, pin): - """Setup the bridge component to track all accessories.""" - from .accessories import HomeBridge - self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin) + def setup(self): + """Setup bridge and accessory driver.""" + from .accessories import HomeBridge, HomeDriver + + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.stop) - def start_driver(self, event): + path = self._hass.config.path(HOMEKIT_FILE) + self.bridge = HomeBridge(self._hass) + self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + + def add_bridge_accessory(self, state): + """Try adding accessory to bridge if configured beforehand.""" + if not state or not self._filter(state.entity_id): + return + aid = generate_aid(state.entity_id) + conf = self._config.pop(state.entity_id, {}) + acc = get_accessory(self._hass, state, aid, conf) + if acc is not None: + self.bridge.add_accessory(acc) + + def start(self, *args): """Start the accessory driver.""" - from pyhap.accessory_driver import AccessoryDriver - self._hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, self.stop_driver) + if self.started: + return + self.started = True + + # pylint: disable=unused-variable + from . import ( # noqa F401 + type_covers, type_security_systems, type_sensors, + type_switches, type_thermostats) - import_types() - _LOGGER.debug("Start adding accessories.") for state in self._hass.states.all(): - acc = get_accessory(self._hass, state) - if acc is not None: - self.bridge.add_accessory(acc) + self.add_bridge_accessory(state) + for entity_id in self._config: + _LOGGER.warning('The entity "%s" was not setup when HomeKit ' + 'was started', entity_id) + self.bridge.set_broker(self.driver) - ip_address = get_local_ip() - path = self._hass.config.path(HOMEKIT_FILE) - self.driver = AccessoryDriver(self.bridge, self._port, - ip_address, path) - _LOGGER.debug("Driver started") + if not self.bridge.paired: + show_setup_message(self.bridge, self._hass) + + _LOGGER.debug('Driver start') self.driver.start() - def stop_driver(self, event): + def stop(self, *args): """Stop the accessory driver.""" - _LOGGER.debug("Driver stop") - if self.driver is not None: + if not self.started: + return + + _LOGGER.debug('Driver stop') + if self.driver and self.driver.run_sentinel: self.driver.stop() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1cd94070289b00..0af25bc4453836 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -2,26 +2,19 @@ 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 set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, - serial_number='0000'): - """Set the default accessory information.""" - service = acc.get_service(SERV_ACCESSORY_INFO) - service.get_characteristic(CHAR_NAME).set_value(name) - service.get_characteristic(CHAR_MODEL).set_value(model) - service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) - service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) - - -def add_preload_service(acc, service, chars=None, opt_chars=None): +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) @@ -30,27 +23,37 @@ def add_preload_service(acc, service, chars=None, opt_chars=None): 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, new_properties): - """Override characteristic property values.""" - char.properties.update(new_properties) +def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, + serial_number='0000'): + """Set the default accessory information.""" + service = acc.get_service(SERV_ACCESSORY_INFO) + service.get_characteristic(CHAR_NAME).set_value(name) + service.get_characteristic(CHAR_MODEL).set_value(model) + service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) + service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) + + +def override_properties(char, properties=None, valid_values=None): + """Override characteristic property values and valid values.""" + if properties: + char.properties.update(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 @@ def _set_services(self): 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) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 73dfbf6904951a..d2b1caffe5326d 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -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_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_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' -# Properties +# #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml new file mode 100644 index 00000000000000..e30e71301b3e03 --- /dev/null +++ b/homeassistant/components/homekit/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available HomeKit services + +start: + description: Starts the HomeKit component driver. diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/type_covers.py similarity index 77% rename from homeassistant/components/homekit/covers.py rename to homeassistant/components/homekit/type_covers.py index 47713f6c6304fe..0110bff318558d 100644 --- a/homeassistant/components/homekit/covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -14,16 +14,17 @@ _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 @@ def __init__(self, hass, entity_id, display_name): 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 @@ def run(self): 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 @@ def update_cover_position(self, entity_id=None, old_state=None, 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) diff --git a/homeassistant/components/homekit/security_systems.py b/homeassistant/components/homekit/type_security_systems.py similarity index 80% rename from homeassistant/components/homekit/security_systems.py rename to homeassistant/components/homekit/type_security_systems.py index 1b8f0a6820bbe7..02742acb75d83e 100644 --- a/homeassistant/components/homekit/security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -28,9 +28,11 @@ 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 @@ def __init__(self, hass, entity_id, display_name, alarm_code=None): 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 @@ def run(self): 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 @@ def update_security_state(self, entity_id=None, 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 diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/type_sensors.py similarity index 84% rename from homeassistant/components/homekit/sensors.py rename to homeassistant/components/homekit/type_sensors.py index 40f97ae3ef70f9..286862343f4d69 100644 --- a/homeassistant/components/homekit/sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -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 @@ def update_temperature(self, entity_id=None, old_state=None, 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) diff --git a/homeassistant/components/homekit/switches.py b/homeassistant/components/homekit/type_switches.py similarity index 72% rename from homeassistant/components/homekit/switches.py rename to homeassistant/components/homekit/type_switches.py index 876b3406d28588..989bf4e19f51b2 100644 --- a/homeassistant/components/homekit/switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -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 @@ 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 @@ def __init__(self, hass, entity_id, display_name): 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 @@ def run(self): 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 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): 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 diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/type_thermostats.py similarity index 71% rename from homeassistant/components/homekit/thermostats.py rename to homeassistant/components/homekit/type_thermostats.py index 6d342273e8dc28..6e720c2214e397 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,8 +7,7 @@ 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 @@ 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 @@ def __init__(self, hass, entity_id, display_name, support_auto=False): 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 @@ def run(self): 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 @@ def update_thermostat(self, entity_id=None, 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 @@ def update_thermostat(self, entity_id=None, # 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 diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py new file mode 100644 index 00000000000000..f18eb2273db2a6 --- /dev/null +++ b/homeassistant/components/homekit/util.py @@ -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) diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py deleted file mode 100644 index 61a60cee2acf85..00000000000000 --- a/tests/components/homekit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for the homekit component.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6f39a8c792b3fa..4d230b81686485 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -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() - - -@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. - - The methods 'get_serv_loader' and 'get_char_loader' are mocked. - """ - acc = MockAccessory('Accessory') - serv = add_preload_service(acc, 'TestService', - ['TestChar', 'TestChar2'], - ['TestOptChar', 'TestOptChar2']) - - assert serv.display_name == 'TestService' - assert len(serv.characteristics) == 2 - assert len(serv.opt_characteristics) == 2 - - acc.services = [] - serv = add_preload_service(acc, 'TestService') - - assert not serv.characteristics - assert not serv.opt_characteristics - - acc.services = [] - serv = add_preload_service(acc, 'TestService', - 'TestChar', 'TestOptChar') - - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 - - assert serv.characteristics[0].display_name == 'TestChar' - assert serv.opt_characteristics[0].display_name == 'TestOptChar' - - -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) - - assert char.properties == new_prop - - -def test_set_accessory_info(): - """Test setting of basic accessory information with MockAccessory.""" - acc = MockAccessory('Accessory') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - - assert len(acc.services) == 1 - serv = acc.services[0] - - assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 4 - chars = serv.characteristics - - 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' - - -@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') - - assert acc.display_name == 'TestAccessory' - assert acc.category == 13 # Category.WINDOW - assert len(acc.services) == 1 - - 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' - - -@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') - - assert bridge.display_name == 'TestBridge' - assert bridge.pincode == b'123-45-678' - assert len(bridge.services) == 2 - - assert bridge.services[0].display_name == SERV_ACCESSORY_INFO - assert bridge.services[1].display_name == SERV_BRIDGING_STATE - - char_model = bridge.services[0].get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.bridge' - - -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') + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + + +class TestAccessories(unittest.TestCase): + """Test pyhap adapter methods.""" + + 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') + + # Test with typo in service name + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifierTypo') + + # Test adding additional characteristic as string + serv = add_preload_service(Mock(), 'AirPurifier', 'Name') + serv.get_characteristic('Name') + + # Test adding additional characteristics as list + serv = add_preload_service(Mock(), 'AirPurifier', + ['Name', 'RotationSpeed']) + serv.get_characteristic('Name') + serv.get_characteristic('RotationSpeed') + + # Test adding additional characteristic with typo + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifier', 'NameTypo') + + def test_set_accessory_info(self): + """Test setting the basic accessory information.""" + # Test HomeAccessory + acc = HomeAccessory() + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + + 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') + + 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') + + 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') + + self.assertTrue( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 100) + + override_properties(char_active, valid_values={'State': 'On'}) + override_properties(char_rotation_speed, properties={'maxValue': 200}) + + self.assertFalse( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 200) + + 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') + + 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) + + 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') + + # 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') + + self.assertEqual(mock_add_paired_client.call_args, + call('client_uuid', 'client_public')) + self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) + + # 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') + + self.assertEqual( + mock_remove_paired_client.call_args, call('client_uuid')) + self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass')) + + 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) + + self.assertEqual( + mock_driver.call_args, call(bridge, ip_adress, port, path)) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e49674a7b90a9..6e1c67cf2823f7 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -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__) -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 +CONFIG = {} -def test_component_not_supported(): +def test_get_accessory_invalid(caplog): """Test with unsupported component.""" - state = State('demo.unsupported', STATE_UNKNOWN) - - assert True if get_accessory(None, state) is None else False - - -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 - - -# 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 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 + assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ + is None + assert caplog.records[1].levelname == 'WARNING' + + assert get_accessory(None, State('test.test', 'on'), None, None) \ + is None + assert caplog.records[3].levelname == 'WARNING' + + +class TestGetAccessories(unittest.TestCase): + """Methods to test the get_accessory method.""" + + def setUp(self): + """Setup Mock type.""" + self.mock_type = Mock() + + 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, {}) + + # 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, {}) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 58c197e69ec5fe..c6d79545487356 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -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 @@ def tearDown(self): """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) - - for value in ('', '123-456-78', 'a23-45-678', '12345678', 1234): - with self.assertRaises(vol.MultipleInvalid): - schema(value) + 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-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')]) - mock_homekit.reset_mock() + 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_driver(ANY)]) + self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_parameters(self, mock_homekit): - """Test async_setup with full config option.""" + 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, 'homekit', CONFIG)) + self.hass, DOMAIN, config)) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 11111), - call().setup_bridge(b'987-65-432')]) + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, 11111, ANY, {}), + call().setup()]) + + # Test start call with driver stopped. + homekit.reset_mock() + homekit.configure_mock(**{'started': False}) + + 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.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() - - self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - self.hass.block_till_done() - - self.assertEqual(mock_acc_driver.mock_calls, [call().stop()]) + self.assertTrue(isinstance(homekit.bridge, HomeBridge)) + self.assertEqual(mock_driver.mock_calls, [ + call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) + + # Test if stop listener is setup + self.assertEqual( + self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + + 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) + + 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) diff --git a/tests/components/homekit/test_switches.py b/tests/components/homekit/test_switches.py deleted file mode 100644 index d9f2d6c1d1a040..00000000000000 --- a/tests/components/homekit/test_switches.py +++ /dev/null @@ -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) diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_type_covers.py similarity index 87% rename from tests/components/homekit/test_covers.py rename to tests/components/homekit/test_type_covers.py index fe0ede5d8fb319..45631a76c98acd 100644 --- a/tests/components/homekit/test_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -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 @@ def test_window_set_cover_position(self): """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) diff --git a/tests/components/homekit/test_security_systems.py b/tests/components/homekit/test_type_security_systems.py similarity index 72% rename from tests/components/homekit/test_security_systems.py rename to tests/components/homekit/test_type_security_systems.py index 4753e86c084f20..4d61fc4a44cbcf 100644 --- a/tests/components/homekit/test_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -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 @@ def tearDown(self): def test_switch_set_state(self): """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.testsecurity' + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code='1234', aid=2) + acc.run() - 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() + 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 @@ def test_switch_set_state(self): 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) diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_type_sensors.py similarity index 64% rename from tests/components/homekit/test_sensors.py rename to tests/components/homekit/test_type_sensors.py index 4698c3635032d0..f9a14f6b8cf139 100644 --- a/tests/components/homekit/test_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -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 @@ def tearDown(self): def test_temperature(self): """Test if accessory is updated after state change.""" - temperature_sensor = 'sensor.temperature' + entity_id = 'sensor.temperature' + + acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2) + acc.run() - 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() + 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) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py new file mode 100644 index 00000000000000..21d7583152e8e3 --- /dev/null +++ b/tests/components/homekit/test_type_switches.py @@ -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) diff --git a/tests/components/homekit/test_thermostats.py b/tests/components/homekit/test_type_thermostats.py similarity index 65% rename from tests/components/homekit/test_thermostats.py rename to tests/components/homekit/test_type_thermostats.py index fabffe881bb708..6505bf72efb3fd 100644 --- a/tests/components/homekit/test_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -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 @@ def tearDown(self): def test_default_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc.run() - 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() + 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 @@ def test_default_thermostat(self): 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 @@ def test_default_thermostat(self): 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 @@ def test_default_thermostat(self): 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() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py new file mode 100644 index 00000000000000..f95db9a4a137ab --- /dev/null +++ b/tests/components/homekit/test_util.py @@ -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) diff --git a/tests/mock/homekit.py b/tests/mock/homekit.py deleted file mode 100644 index 2872fa59f197b9..00000000000000 --- a/tests/mock/homekit.py +++ /dev/null @@ -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 "".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 "".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 "".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)