mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add support for HomeKit (#12488)
* Basic Homekit support * Added Temperatur Sensor * Added Window Cover * Code refactored * Added class HomeAccessory(Accessory) * Added class HomeBridge(Bridge) * Changed homekit imports to relative, to enable use in custom_components * Updated requirements * Added docs * Other smaller changes * Changed Homekit from entity to class * Changes based on feedback * Updated config schema * Add only covers that support set_cover_position * Addressed comments, updated to pyhap==1.1.5 * For lint: added files to gen_requirements_all * Added codeowner * Small change to Wrapper classes * Moved imports to import_types, small changes * Small changes, added tests * Homekit class: removed add_accessory since it's already covered by pyhap * Added test requirement: HAP-python * Added test suit for homekit setup and interaction between HA and pyhap * Added test suit for get_accessories function * Test bugfix * Added validate pincode, tests for cover and sensor types
This commit is contained in:
parent
dc21c61a44
commit
eec3bad94f
@ -77,6 +77,7 @@ homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
|
133
homeassistant/components/homekit/__init__.py
Normal file
133
homeassistant/components/homekit/__init__.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""Support for Apple Homekit.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/homekit/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
|
||||
TEMP_CELSIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
TYPES = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$")
|
||||
|
||||
DOMAIN = 'homekit'
|
||||
REQUIREMENTS = ['HAP-python==1.1.5']
|
||||
|
||||
BRIDGE_NAME = 'Home Assistant'
|
||||
CONF_PIN_CODE = 'pincode'
|
||||
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
|
||||
|
||||
def valid_pin(value):
|
||||
"""Validate pincode value."""
|
||||
match = _RE_VALID_PINCODE.findall(value.strip())
|
||||
if match == []:
|
||||
raise vol.Invalid("Pin must be in the format: '123-45-678'")
|
||||
return match[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,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the homekit component."""
|
||||
_LOGGER.debug("Begin setup homekit")
|
||||
|
||||
conf = config[DOMAIN]
|
||||
port = conf.get(CONF_PORT)
|
||||
pin = str.encode(conf.get(CONF_PIN_CODE))
|
||||
|
||||
homekit = Homekit(hass, port)
|
||||
homekit.setup_bridge(pin)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, homekit.start_driver)
|
||||
return True
|
||||
|
||||
|
||||
def import_types():
|
||||
"""Import all types from files in the homekit dir."""
|
||||
_LOGGER.debug("Import type files.")
|
||||
# pylint: disable=unused-variable
|
||||
from .covers import Window # noqa F401
|
||||
# pylint: disable=unused-variable
|
||||
from .sensors import TemperatureSensor # noqa F401
|
||||
|
||||
|
||||
def get_accessory(hass, state):
|
||||
"""Take state and return an accessory object if supported."""
|
||||
if state.domain == 'sensor':
|
||||
if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS:
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"",
|
||||
state.entity_id, 'TemperatureSensor')
|
||||
return TYPES['TemperatureSensor'](hass, state.entity_id,
|
||||
state.name)
|
||||
|
||||
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)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Homekit():
|
||||
"""Class to handle all actions between homekit and Home Assistant."""
|
||||
|
||||
def __init__(self, hass, port):
|
||||
"""Initialize a homekit object."""
|
||||
self._hass = hass
|
||||
self._port = port
|
||||
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, pincode=pin)
|
||||
self.bridge.set_accessory_info('homekit.bridge')
|
||||
|
||||
def start_driver(self, event):
|
||||
"""Start the accessory driver."""
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
self._hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
|
||||
|
||||
import_types()
|
||||
_LOGGER.debug("Start adding accessories.")
|
||||
for state in self._hass.states.all():
|
||||
acc = get_accessory(self._hass, state)
|
||||
if acc is not None:
|
||||
self.bridge.add_accessory(acc)
|
||||
|
||||
ip_address = get_local_ip()
|
||||
path = self._hass.config.path(HOMEKIT_FILE)
|
||||
self.driver = AccessoryDriver(self.bridge, self._port,
|
||||
ip_address, path)
|
||||
_LOGGER.debug("Driver started")
|
||||
self.driver.start()
|
||||
|
||||
def stop_driver(self, event):
|
||||
"""Stop the accessory driver."""
|
||||
_LOGGER.debug("Driver stop")
|
||||
if self.driver is not None:
|
||||
self.driver.stop()
|
55
homeassistant/components/homekit/accessories.py
Normal file
55
homeassistant/components/homekit/accessories.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Extend the basic Accessory and Bridge functions."""
|
||||
from pyhap.accessory import Accessory, Bridge, Category
|
||||
|
||||
from .const import (
|
||||
SERVICES_ACCESSORY_INFO, MANUFACTURER,
|
||||
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER)
|
||||
|
||||
|
||||
class HomeAccessory(Accessory):
|
||||
"""Class to extend the Accessory class."""
|
||||
|
||||
ALL_CATEGORIES = Category
|
||||
|
||||
def __init__(self, display_name):
|
||||
"""Initialize a Accessory object."""
|
||||
super().__init__(display_name)
|
||||
|
||||
def set_category(self, category):
|
||||
"""Set the category of the accessory."""
|
||||
self.category = category
|
||||
|
||||
def add_preload_service(self, service):
|
||||
"""Define the services to be available for the accessory."""
|
||||
from pyhap.loader import get_serv_loader
|
||||
self.add_service(get_serv_loader().get(service))
|
||||
|
||||
def set_accessory_info(self, model, manufacturer=MANUFACTURER,
|
||||
serial_number='0000'):
|
||||
"""Set the default accessory information."""
|
||||
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
|
||||
service_info.get_characteristic(CHAR_MODEL) \
|
||||
.set_value(model)
|
||||
service_info.get_characteristic(CHAR_MANUFACTURER) \
|
||||
.set_value(manufacturer)
|
||||
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
|
||||
.set_value(serial_number)
|
||||
|
||||
|
||||
class HomeBridge(Bridge):
|
||||
"""Class to extend the Bridge class."""
|
||||
|
||||
def __init__(self, display_name, pincode):
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(display_name, pincode=pincode)
|
||||
|
||||
def set_accessory_info(self, model, manufacturer=MANUFACTURER,
|
||||
serial_number='0000'):
|
||||
"""Set the default accessory information."""
|
||||
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
|
||||
service_info.get_characteristic(CHAR_MODEL) \
|
||||
.set_value(model)
|
||||
service_info.get_characteristic(CHAR_MANUFACTURER) \
|
||||
.set_value(manufacturer)
|
||||
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
|
||||
.set_value(serial_number)
|
18
homeassistant/components/homekit/const.py
Normal file
18
homeassistant/components/homekit/const.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Constants used be the homekit component."""
|
||||
MANUFACTURER = 'HomeAssistant'
|
||||
|
||||
# Service: AccessoryInfomation
|
||||
SERVICES_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
CHAR_MODEL = 'Model'
|
||||
CHAR_MANUFACTURER = 'Manufacturer'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
|
||||
# Service: TemperatureSensor
|
||||
SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
|
||||
# Service: WindowCovering
|
||||
SERVICES_WINDOW_COVERING = 'WindowCovering'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
84
homeassistant/components/homekit/covers.py
Normal file
84
homeassistant/components/homekit/covers.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""Class to hold all cover accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
||||
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Window')
|
||||
class Window(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):
|
||||
"""Initialize a Window accessory object."""
|
||||
super().__init__(display_name)
|
||||
self.set_category(self.ALL_CATEGORIES.WINDOW)
|
||||
self.set_accessory_info(entity_id)
|
||||
self.add_preload_service(SERVICES_WINDOW_COVERING)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
||||
self.current_position = None
|
||||
self.homekit_target = None
|
||||
|
||||
self.service_cover = self.get_service(SERVICES_WINDOW_COVERING)
|
||||
self.char_current_position = self.service_cover. \
|
||||
get_characteristic(CHAR_CURRENT_POSITION)
|
||||
self.char_target_position = self.service_cover. \
|
||||
get_characteristic(CHAR_TARGET_POSITION)
|
||||
self.char_position_state = self.service_cover. \
|
||||
get_characteristic(CHAR_POSITION_STATE)
|
||||
|
||||
self.char_target_position.setter_callback = self.move_cover
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_cover_position(new_state=state)
|
||||
|
||||
async_track_state_change(
|
||||
self._hass, self._entity_id, self.update_cover_position)
|
||||
|
||||
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)
|
||||
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})
|
||||
|
||||
def update_cover_position(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Update cover position after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_position = new_state.attributes[ATTR_CURRENT_POSITION]
|
||||
if current_position is None:
|
||||
return
|
||||
self.current_position = int(current_position)
|
||||
self.char_current_position.set_value(self.current_position)
|
||||
|
||||
if self.homekit_target is None or \
|
||||
abs(self.current_position - self.homekit_target) < 6:
|
||||
self.char_target_position.set_value(self.current_position)
|
||||
self.char_position_state.set_value(2)
|
||||
self.homekit_target = None
|
50
homeassistant/components/homekit/sensors.py
Normal file
50
homeassistant/components/homekit/sensors.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Class to hold all sensor accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('TemperatureSensor')
|
||||
class TemperatureSensor(HomeAccessory):
|
||||
"""Generate a TemperatureSensor accessory for a temperature sensor.
|
||||
|
||||
Sensor entity must return either temperature in °C or STATE_UNKNOWN.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a TemperatureSensor accessory object."""
|
||||
super().__init__(display_name)
|
||||
self.set_category(self.ALL_CATEGORIES.SENSOR)
|
||||
self.set_accessory_info(entity_id)
|
||||
self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
||||
self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR)
|
||||
self.char_temp = self.service_temp. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_temperature(new_state=state)
|
||||
|
||||
async_track_state_change(
|
||||
self._hass, self._entity_id, self.update_temperature)
|
||||
|
||||
def update_temperature(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Update temperature after state changed."""
|
||||
temperature = new_state.state
|
||||
if temperature != STATE_UNKNOWN:
|
||||
self.char_temp.set_value(float(temperature))
|
@ -23,6 +23,9 @@ attrs==17.4.0
|
||||
# homeassistant.components.doorbird
|
||||
DoorBirdPy==0.1.2
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==1.1.5
|
||||
|
||||
# homeassistant.components.isy994
|
||||
PyISY==1.1.0
|
||||
|
||||
|
@ -18,6 +18,9 @@ flake8-docstrings==1.0.3
|
||||
asynctest>=0.11.1
|
||||
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==1.1.5
|
||||
|
||||
# homeassistant.components.notify.html5
|
||||
PyJWT==1.5.3
|
||||
|
||||
|
@ -47,6 +47,7 @@ TEST_REQUIREMENTS = (
|
||||
'evohomeclient',
|
||||
'feedparser',
|
||||
'gTTS-token',
|
||||
'HAP-python',
|
||||
'ha-ffmpeg',
|
||||
'haversine',
|
||||
'hbmqtt',
|
||||
@ -92,6 +93,9 @@ TEST_REQUIREMENTS = (
|
||||
|
||||
IGNORE_PACKAGES = (
|
||||
'homeassistant.components.recorder.models',
|
||||
'homeassistant.components.homekit.accessories',
|
||||
'homeassistant.components.homekit.covers',
|
||||
'homeassistant.components.homekit.sensors'
|
||||
)
|
||||
|
||||
IGNORE_PIN = ('colorlog>2.1,<3', 'keyring>=9.3,<10.0', 'urllib3')
|
||||
|
1
tests/components/homekit/__init__.py
Normal file
1
tests/components/homekit/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The tests for the homekit component."""
|
83
tests/components/homekit/test_covers.py
Normal file
83
tests/components/homekit/test_covers.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Test different accessory types: Covers."""
|
||||
import unittest
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION, ATTR_CURRENT_POSITION)
|
||||
from homeassistant.components.homekit.covers import Window
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN, STATE_OPEN,
|
||||
ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestHomekitSensors(unittest.TestCase):
|
||||
"""Test class for all accessory types regarding covers."""
|
||||
|
||||
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 everthing that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_window_set_cover_position(self):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
window_cover = 'cover.window'
|
||||
|
||||
acc = Window(self.hass, window_cover, 'Cover')
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_current_position.value, 0)
|
||||
self.assertEqual(acc.char_target_position.value, 0)
|
||||
self.assertEqual(acc.char_position_state.value, 0)
|
||||
|
||||
self.hass.states.set(window_cover, STATE_UNKNOWN,
|
||||
{ATTR_CURRENT_POSITION: None})
|
||||
self.hass.block_till_done()
|
||||
|
||||
self.assertEqual(acc.char_current_position.value, 0)
|
||||
self.assertEqual(acc.char_target_position.value, 0)
|
||||
self.assertEqual(acc.char_position_state.value, 0)
|
||||
|
||||
self.hass.states.set(window_cover, STATE_OPEN,
|
||||
{ATTR_CURRENT_POSITION: 50})
|
||||
self.hass.block_till_done()
|
||||
|
||||
self.assertEqual(acc.char_current_position.value, 50)
|
||||
self.assertEqual(acc.char_target_position.value, 50)
|
||||
self.assertEqual(acc.char_position_state.value, 2)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_target_position.set_value(25)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'set_cover_position')
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25)
|
||||
|
||||
self.assertEqual(acc.char_current_position.value, 50)
|
||||
self.assertEqual(acc.char_target_position.value, 25)
|
||||
self.assertEqual(acc.char_position_state.value, 0)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_target_position.set_value(75)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'set_cover_position')
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
|
||||
|
||||
self.assertEqual(acc.char_current_position.value, 50)
|
||||
self.assertEqual(acc.char_target_position.value, 75)
|
||||
self.assertEqual(acc.char_position_state.value, 1)
|
46
tests/components/homekit/test_get_accessories.py
Normal file
46
tests/components/homekit/test_get_accessories.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Package to test the get_accessory method."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from homeassistant.core import State
|
||||
from homeassistant.components.homekit import (
|
||||
TYPES, get_accessory, import_types)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES,
|
||||
TEMP_CELSIUS, STATE_UNKNOWN)
|
||||
|
||||
|
||||
def test_import_types():
|
||||
"""Test if all type files are imported correctly."""
|
||||
try:
|
||||
import_types()
|
||||
assert True
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
assert False
|
||||
|
||||
|
||||
def test_component_not_supported():
|
||||
"""Test with unsupported component."""
|
||||
state = State('demo.unsupported', STATE_UNKNOWN)
|
||||
|
||||
assert True if get_accessory(None, state) is None else False
|
||||
|
||||
|
||||
def test_sensor_temperatur_celsius():
|
||||
"""Test temperature sensor with celsius as unit."""
|
||||
mock_type = MagicMock()
|
||||
with patch.dict(TYPES, {'TemperatureSensor': mock_type}):
|
||||
state = State('sensor.temperatur', '23',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
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.setposition', 'open',
|
||||
{ATTR_SUPPORTED_FEATURES: 4})
|
||||
get_accessory(None, state)
|
||||
assert len(mock_type.mock_calls) == 1
|
124
tests/components/homekit/test_homekit.py
Normal file
124
tests/components/homekit/test_homekit.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Tests for the homekit component."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.core import Event
|
||||
from homeassistant.components.homekit import (
|
||||
CONF_PIN_CODE, BRIDGE_NAME, Homekit, valid_pin)
|
||||
from homeassistant.components.homekit.covers import Window
|
||||
from homeassistant.components.homekit.sensors import TemperatureSensor
|
||||
from homeassistant.const import (
|
||||
CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
HOMEKIT_PATH = 'homeassistant.components.homekit'
|
||||
|
||||
CONFIG_MIN = {'homekit': {}}
|
||||
CONFIG = {
|
||||
'homekit': {
|
||||
CONF_PORT: 11111,
|
||||
CONF_PIN_CODE: '987-65-432',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestHomekit(unittest.TestCase):
|
||||
"""Test the Multicover component."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everthing that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@patch(HOMEKIT_PATH + '.Homekit.start_driver')
|
||||
@patch(HOMEKIT_PATH + '.Homekit.setup_bridge')
|
||||
@patch(HOMEKIT_PATH + '.Homekit.__init__')
|
||||
def test_setup_min(self, mock_homekit, mock_setup_bridge,
|
||||
mock_start_driver):
|
||||
"""Test async_setup with minimal config option."""
|
||||
mock_homekit.return_value = None
|
||||
|
||||
self.assertTrue(setup.setup_component(
|
||||
self.hass, 'homekit', CONFIG_MIN))
|
||||
|
||||
mock_homekit.assert_called_once_with(self.hass, 51826)
|
||||
mock_setup_bridge.assert_called_with(b'123-45-678')
|
||||
mock_start_driver.assert_not_called()
|
||||
|
||||
self.hass.start()
|
||||
self.hass.block_till_done()
|
||||
mock_start_driver.assert_called_once()
|
||||
|
||||
@patch(HOMEKIT_PATH + '.Homekit.start_driver')
|
||||
@patch(HOMEKIT_PATH + '.Homekit.setup_bridge')
|
||||
@patch(HOMEKIT_PATH + '.Homekit.__init__')
|
||||
def test_setup_parameters(self, mock_homekit, mock_setup_bridge,
|
||||
mock_start_driver):
|
||||
"""Test async_setup with full config option."""
|
||||
mock_homekit.return_value = None
|
||||
|
||||
self.assertTrue(setup.setup_component(
|
||||
self.hass, 'homekit', CONFIG))
|
||||
|
||||
mock_homekit.assert_called_once_with(self.hass, 11111)
|
||||
mock_setup_bridge.assert_called_with(b'987-65-432')
|
||||
|
||||
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'):
|
||||
with self.assertRaises(vol.MultipleInvalid):
|
||||
schema(value)
|
||||
|
||||
for value in ('123-45-678', '234-56-789'):
|
||||
self.assertTrue(schema(value))
|
||||
|
||||
@patch('pyhap.accessory_driver.AccessoryDriver.persist')
|
||||
@patch('pyhap.accessory_driver.AccessoryDriver.stop')
|
||||
@patch('pyhap.accessory_driver.AccessoryDriver.start')
|
||||
@patch(HOMEKIT_PATH + '.import_types')
|
||||
@patch(HOMEKIT_PATH + '.get_accessory')
|
||||
def test_homekit_pyhap_interaction(
|
||||
self, mock_get_accessory, mock_import_types,
|
||||
mock_driver_start, mock_driver_stop, mock_file_persist):
|
||||
"""Test the interaction between the homekit class and pyhap."""
|
||||
acc1 = TemperatureSensor(self.hass, 'sensor.temp', 'Temperature')
|
||||
acc2 = Window(self.hass, 'cover.hall_window', 'Cover')
|
||||
mock_get_accessory.side_effect = [acc1, acc2]
|
||||
|
||||
homekit = Homekit(self.hass, 51826)
|
||||
homekit.setup_bridge(b'123-45-678')
|
||||
|
||||
self.assertEqual(homekit.bridge.display_name, BRIDGE_NAME)
|
||||
|
||||
self.hass.states.set('demo.demo1', 'on')
|
||||
self.hass.states.set('demo.demo2', 'off')
|
||||
|
||||
self.hass.start()
|
||||
self.hass.block_till_done()
|
||||
|
||||
homekit.start_driver(Event(EVENT_HOMEASSISTANT_START))
|
||||
|
||||
self.assertEqual(mock_get_accessory.call_count, 2)
|
||||
mock_import_types.assert_called_once()
|
||||
mock_driver_start.assert_called_once()
|
||||
|
||||
accessories = homekit.bridge.accessories
|
||||
self.assertEqual(accessories[2], acc1)
|
||||
self.assertEqual(accessories[3], acc2)
|
||||
|
||||
mock_driver_stop.assert_not_called()
|
||||
|
||||
self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
||||
self.hass.block_till_done()
|
||||
|
||||
mock_driver_stop.assert_called_once()
|
37
tests/components/homekit/test_sensors.py
Normal file
37
tests/components/homekit/test_sensors.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Test different accessory types: Sensors."""
|
||||
import unittest
|
||||
|
||||
from homeassistant.components.homekit.sensors import TemperatureSensor
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestHomekitSensors(unittest.TestCase):
|
||||
"""Test class for all accessory types regarding sensors."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everthing that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_temperature_celsius(self):
|
||||
"""Test if accessory is updated after state change."""
|
||||
temperature_sensor = 'sensor.temperature'
|
||||
|
||||
self.hass.states.set(temperature_sensor, STATE_UNKNOWN,
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
|
||||
acc = TemperatureSensor(self.hass, temperature_sensor, 'Temperature')
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_temp.value, 0.0)
|
||||
|
||||
self.hass.states.set(temperature_sensor, '20')
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_temp.value, 20)
|
Loading…
x
Reference in New Issue
Block a user