2019-01-27 11:34:49 +00:00
|
|
|
"""Code to support homekit_controller tests."""
|
2019-03-19 07:58:56 +00:00
|
|
|
import json
|
2019-03-30 10:21:11 +00:00
|
|
|
import os
|
2019-01-27 11:34:49 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
from unittest import mock
|
|
|
|
|
2019-01-28 12:20:32 +00:00
|
|
|
from homekit.model.services import AbstractService, ServicesTypes
|
|
|
|
from homekit.model.characteristics import (
|
|
|
|
AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes)
|
|
|
|
from homekit.model import Accessory, get_id
|
2019-03-13 01:45:34 +00:00
|
|
|
from homekit.exceptions import AccessoryNotFoundError
|
2019-05-13 06:56:05 +00:00
|
|
|
|
|
|
|
from homeassistant import config_entries
|
2019-03-28 03:01:10 +00:00
|
|
|
from homeassistant.components.homekit_controller.const import (
|
2019-04-17 17:02:51 +00:00
|
|
|
CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH)
|
2019-05-13 06:56:05 +00:00
|
|
|
from homeassistant.components.homekit_controller import (
|
|
|
|
async_setup_entry, config_flow)
|
2019-01-27 11:34:49 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
import homeassistant.util.dt as dt_util
|
2019-05-13 06:56:05 +00:00
|
|
|
from tests.common import async_fire_time_changed, load_fixture
|
2019-01-27 11:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FakePairing:
|
|
|
|
"""
|
|
|
|
A test fake that pretends to be a paired HomeKit accessory.
|
|
|
|
|
|
|
|
This only contains methods and values that exist on the upstream Pairing
|
|
|
|
class.
|
|
|
|
"""
|
|
|
|
|
2019-03-07 03:44:52 +00:00
|
|
|
def __init__(self, accessories):
|
2019-01-27 11:34:49 +00:00
|
|
|
"""Create a fake pairing from an accessory model."""
|
2019-03-07 03:44:52 +00:00
|
|
|
self.accessories = accessories
|
|
|
|
self.pairing_data = {}
|
2019-03-13 01:45:34 +00:00
|
|
|
self.available = True
|
2019-01-27 11:34:49 +00:00
|
|
|
|
|
|
|
def list_accessories_and_characteristics(self):
|
|
|
|
"""Fake implementation of list_accessories_and_characteristics."""
|
2019-03-07 03:44:52 +00:00
|
|
|
accessories = [
|
|
|
|
a.to_accessory_and_service_list() for a in self.accessories
|
|
|
|
]
|
|
|
|
# replicate what happens upstream right now
|
|
|
|
self.pairing_data['accessories'] = accessories
|
|
|
|
return accessories
|
2019-01-27 11:34:49 +00:00
|
|
|
|
|
|
|
def get_characteristics(self, characteristics):
|
|
|
|
"""Fake implementation of get_characteristics."""
|
2019-03-13 01:45:34 +00:00
|
|
|
if not self.available:
|
|
|
|
raise AccessoryNotFoundError('Accessory not found')
|
|
|
|
|
2019-01-27 11:34:49 +00:00
|
|
|
results = {}
|
|
|
|
for aid, cid in characteristics:
|
2019-03-07 03:44:52 +00:00
|
|
|
for accessory in self.accessories:
|
|
|
|
if aid != accessory.aid:
|
|
|
|
continue
|
|
|
|
for service in accessory.services:
|
|
|
|
for char in service.characteristics:
|
|
|
|
if char.iid != cid:
|
|
|
|
continue
|
|
|
|
results[(aid, cid)] = {
|
|
|
|
'value': char.get_value()
|
|
|
|
}
|
2019-01-27 11:34:49 +00:00
|
|
|
return results
|
|
|
|
|
|
|
|
def put_characteristics(self, characteristics):
|
|
|
|
"""Fake implementation of put_characteristics."""
|
2019-03-07 03:44:52 +00:00
|
|
|
for aid, cid, new_val in characteristics:
|
|
|
|
for accessory in self.accessories:
|
|
|
|
if aid != accessory.aid:
|
|
|
|
continue
|
|
|
|
for service in accessory.services:
|
|
|
|
for char in service.characteristics:
|
|
|
|
if char.iid != cid:
|
|
|
|
continue
|
|
|
|
char.set_value(new_val)
|
2019-01-27 11:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FakeController:
|
|
|
|
"""
|
|
|
|
A test fake that pretends to be a paired HomeKit accessory.
|
|
|
|
|
|
|
|
This only contains methods and values that exist on the upstream Controller
|
|
|
|
class.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
"""Create a Fake controller with no pairings."""
|
|
|
|
self.pairings = {}
|
|
|
|
|
2019-03-07 03:44:52 +00:00
|
|
|
def add(self, accessories):
|
2019-01-27 11:34:49 +00:00
|
|
|
"""Create and register a fake pairing for a simulated accessory."""
|
2019-03-07 03:44:52 +00:00
|
|
|
pairing = FakePairing(accessories)
|
2019-01-27 11:34:49 +00:00
|
|
|
self.pairings['00:00:00:00:00:00'] = pairing
|
|
|
|
return pairing
|
|
|
|
|
|
|
|
|
|
|
|
class Helper:
|
|
|
|
"""Helper methods for interacting with HomeKit fakes."""
|
|
|
|
|
|
|
|
def __init__(self, hass, entity_id, pairing, accessory):
|
|
|
|
"""Create a helper for a given accessory/entity."""
|
|
|
|
self.hass = hass
|
|
|
|
self.entity_id = entity_id
|
|
|
|
self.pairing = pairing
|
|
|
|
self.accessory = accessory
|
|
|
|
|
|
|
|
self.characteristics = {}
|
|
|
|
for service in self.accessory.services:
|
|
|
|
service_name = ServicesTypes.get_short(service.type)
|
|
|
|
for char in service.characteristics:
|
|
|
|
char_name = CharacteristicsTypes.get_short(char.type)
|
|
|
|
self.characteristics[(service_name, char_name)] = char
|
|
|
|
|
|
|
|
async def poll_and_get_state(self):
|
|
|
|
"""Trigger a time based poll and return the current entity state."""
|
|
|
|
next_update = dt_util.utcnow() + timedelta(seconds=60)
|
|
|
|
async_fire_time_changed(self.hass, next_update)
|
|
|
|
await self.hass.async_block_till_done()
|
|
|
|
|
|
|
|
state = self.hass.states.get(self.entity_id)
|
|
|
|
assert state is not None
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
2019-01-28 12:20:32 +00:00
|
|
|
class FakeCharacteristic(AbstractCharacteristic):
|
|
|
|
"""
|
|
|
|
A model of a generic HomeKit characteristic.
|
|
|
|
|
|
|
|
Base is abstract and can't be instanced directly so this subclass is
|
|
|
|
needed even though it doesn't add any methods.
|
|
|
|
"""
|
|
|
|
|
2019-05-02 03:44:55 +00:00
|
|
|
def to_accessory_and_service_list(self):
|
|
|
|
"""Serialize the characteristic."""
|
|
|
|
# Upstream doesn't correctly serialize valid_values
|
|
|
|
# This fix will be upstreamed and this function removed when it
|
|
|
|
# is fixed.
|
|
|
|
record = super().to_accessory_and_service_list()
|
|
|
|
if self.valid_values:
|
|
|
|
record['valid-values'] = self.valid_values
|
|
|
|
return record
|
2019-01-28 12:20:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FakeService(AbstractService):
|
|
|
|
"""A model of a generic HomeKit service."""
|
|
|
|
|
|
|
|
def __init__(self, service_name):
|
|
|
|
"""Create a fake service by its short form HAP spec name."""
|
|
|
|
char_type = ServicesTypes.get_uuid(service_name)
|
|
|
|
super().__init__(char_type, get_id())
|
|
|
|
|
|
|
|
def add_characteristic(self, name):
|
|
|
|
"""Add a characteristic to this service by name."""
|
|
|
|
full_name = 'public.hap.characteristic.' + name
|
|
|
|
char = FakeCharacteristic(get_id(), full_name, None)
|
|
|
|
char.perms = [
|
|
|
|
CharacteristicPermissions.paired_read,
|
|
|
|
CharacteristicPermissions.paired_write
|
|
|
|
]
|
|
|
|
self.characteristics.append(char)
|
|
|
|
return char
|
|
|
|
|
|
|
|
|
2019-03-30 10:21:11 +00:00
|
|
|
async def setup_accessories_from_file(hass, path):
|
2019-03-19 07:58:56 +00:00
|
|
|
"""Load an collection of accessory defs from JSON data."""
|
2019-03-30 10:21:11 +00:00
|
|
|
accessories_fixture = await hass.async_add_executor_job(
|
|
|
|
load_fixture,
|
|
|
|
os.path.join('homekit_controller', path),
|
|
|
|
)
|
|
|
|
accessories_json = json.loads(accessories_fixture)
|
2019-03-19 07:58:56 +00:00
|
|
|
|
|
|
|
accessories = []
|
|
|
|
|
|
|
|
for accessory_data in accessories_json:
|
|
|
|
accessory = Accessory('Name', 'Mfr', 'Model', '0001', '0.1')
|
|
|
|
accessory.services = []
|
|
|
|
accessory.aid = accessory_data['aid']
|
|
|
|
for service_data in accessory_data['services']:
|
|
|
|
service = FakeService('public.hap.service.accessory-information')
|
|
|
|
service.type = service_data['type']
|
|
|
|
service.iid = service_data['iid']
|
|
|
|
|
|
|
|
for char_data in service_data['characteristics']:
|
|
|
|
char = FakeCharacteristic(1, '23', None)
|
|
|
|
char.type = char_data['type']
|
|
|
|
char.iid = char_data['iid']
|
|
|
|
char.perms = char_data['perms']
|
|
|
|
char.format = char_data['format']
|
|
|
|
if 'description' in char_data:
|
|
|
|
char.description = char_data['description']
|
|
|
|
if 'value' in char_data:
|
|
|
|
char.value = char_data['value']
|
2019-04-30 22:08:31 +00:00
|
|
|
if 'minValue' in char_data:
|
|
|
|
char.minValue = char_data['minValue']
|
|
|
|
if 'maxValue' in char_data:
|
|
|
|
char.maxValue = char_data['maxValue']
|
|
|
|
if 'valid-values' in char_data:
|
|
|
|
char.valid_values = char_data['valid-values']
|
2019-03-19 07:58:56 +00:00
|
|
|
service.characteristics.append(char)
|
|
|
|
|
|
|
|
accessory.services.append(service)
|
|
|
|
|
|
|
|
accessories.append(accessory)
|
|
|
|
|
|
|
|
return accessories
|
|
|
|
|
|
|
|
|
2019-03-07 03:44:52 +00:00
|
|
|
async def setup_platform(hass):
|
|
|
|
"""Load the platform but with a fake Controller API."""
|
|
|
|
config = {
|
|
|
|
'discovery': {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
with mock.patch('homekit.Controller') as controller:
|
|
|
|
fake_controller = controller.return_value = FakeController()
|
|
|
|
await async_setup_component(hass, DOMAIN, config)
|
|
|
|
|
|
|
|
return fake_controller
|
|
|
|
|
|
|
|
|
2019-05-13 06:56:05 +00:00
|
|
|
async def setup_test_accessories(hass, accessories):
|
|
|
|
"""Load a fake homekit device based on captured JSON profile."""
|
2019-03-19 07:58:56 +00:00
|
|
|
fake_controller = await setup_platform(hass)
|
|
|
|
pairing = fake_controller.add(accessories)
|
|
|
|
|
|
|
|
discovery_info = {
|
2019-05-13 06:56:05 +00:00
|
|
|
'name': 'TestDevice',
|
2019-03-19 07:58:56 +00:00
|
|
|
'host': '127.0.0.1',
|
|
|
|
'port': 8080,
|
|
|
|
'properties': {
|
2019-05-13 06:56:05 +00:00
|
|
|
'md': 'TestDevice',
|
|
|
|
'id': '00:00:00:00:00:00',
|
|
|
|
'c#': 1,
|
2019-03-19 07:58:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-13 06:56:05 +00:00
|
|
|
pairing.pairing_data.update({
|
|
|
|
'AccessoryPairingID': discovery_info['properties']['id'],
|
|
|
|
})
|
|
|
|
|
|
|
|
config_entry = config_entries.ConfigEntry(
|
|
|
|
1, 'homekit_controller', 'TestData', pairing.pairing_data,
|
|
|
|
'test', config_entries.CONN_CLASS_LOCAL_PUSH
|
|
|
|
)
|
|
|
|
|
|
|
|
pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing'
|
|
|
|
with mock.patch(pairing_cls_loc) as pairing_cls:
|
|
|
|
pairing_cls.return_value = pairing
|
|
|
|
await async_setup_entry(hass, config_entry)
|
|
|
|
await hass.async_block_till_done()
|
2019-03-19 07:58:56 +00:00
|
|
|
|
|
|
|
return pairing
|
|
|
|
|
|
|
|
|
2019-04-17 17:02:51 +00:00
|
|
|
async def device_config_changed(hass, accessories):
|
|
|
|
"""Discover new devices added to HomeAssistant at runtime."""
|
|
|
|
# Update the accessories our FakePairing knows about
|
|
|
|
controller = hass.data[CONTROLLER]
|
|
|
|
pairing = controller.pairings['00:00:00:00:00:00']
|
|
|
|
pairing.accessories = accessories
|
|
|
|
|
|
|
|
discovery_info = {
|
2019-05-13 06:56:05 +00:00
|
|
|
'name': 'TestDevice',
|
2019-04-17 17:02:51 +00:00
|
|
|
'host': '127.0.0.1',
|
|
|
|
'port': 8080,
|
|
|
|
'properties': {
|
|
|
|
'md': 'TestDevice',
|
|
|
|
'id': '00:00:00:00:00:00',
|
|
|
|
'c#': '2',
|
|
|
|
'sf': '0',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-13 06:56:05 +00:00
|
|
|
# Config Flow will abort and notify us if the discovery event is of
|
|
|
|
# interest - in this case c# has incremented
|
|
|
|
flow = config_flow.HomekitControllerFlowHandler()
|
|
|
|
flow.hass = hass
|
|
|
|
flow.context = {}
|
2019-05-29 18:20:04 +00:00
|
|
|
result = await flow.async_step_zeroconf(discovery_info)
|
2019-05-13 06:56:05 +00:00
|
|
|
assert result['type'] == 'abort'
|
|
|
|
assert result['reason'] == 'already_configured'
|
2019-04-17 17:02:51 +00:00
|
|
|
|
|
|
|
# Wait for services to reconfigure
|
|
|
|
await hass.async_block_till_done()
|
2019-04-18 15:55:34 +00:00
|
|
|
await hass.async_block_till_done()
|
2019-04-17 17:02:51 +00:00
|
|
|
|
|
|
|
|
2019-02-28 18:09:04 +00:00
|
|
|
async def setup_test_component(hass, services, capitalize=False, suffix=None):
|
2019-02-24 12:56:52 +00:00
|
|
|
"""Load a fake homekit accessory based on a homekit accessory model.
|
|
|
|
|
|
|
|
If capitalize is True, property names will be in upper case.
|
2019-02-28 18:09:04 +00:00
|
|
|
|
|
|
|
If suffix is set, entityId will include the suffix
|
2019-02-24 12:56:52 +00:00
|
|
|
"""
|
2019-01-27 11:34:49 +00:00
|
|
|
domain = None
|
|
|
|
for service in services:
|
|
|
|
service_name = ServicesTypes.get_short(service.type)
|
|
|
|
if service_name in HOMEKIT_ACCESSORY_DISPATCH:
|
|
|
|
domain = HOMEKIT_ACCESSORY_DISPATCH[service_name]
|
|
|
|
break
|
|
|
|
|
|
|
|
assert domain, 'Cannot map test homekit services to homeassistant domain'
|
|
|
|
|
|
|
|
accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1')
|
|
|
|
accessory.services.extend(services)
|
|
|
|
|
2019-05-13 06:56:05 +00:00
|
|
|
pairing = await setup_test_accessories(hass, [accessory])
|
2019-02-28 18:09:04 +00:00
|
|
|
entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix)
|
|
|
|
return Helper(hass, '.'.join((domain, entity)), pairing, accessory)
|