Add homekit_controller tests (#20457)

* Add a test for a homekit_controller switch

* Add a test for a homekit_controller lightbulb

* Add a test for homekit_controller thermostat

* Changes from review

* Patch utcnow to known time in HK tests

* Neater fixture use per review
pull/18738/head
Jc2k 2019-01-27 11:34:49 +00:00 committed by Martin Hjelmare
parent 7368c623d4
commit 10e3698fd7
9 changed files with 343 additions and 5 deletions

View File

@ -27,6 +27,8 @@ MODE_HOMEKIT_TO_HASS = {
# Map of hass operation modes to homekit modes
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Homekit climate."""
@ -50,10 +52,10 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
def update_characteristics(self, characteristics):
"""Synchronise device state with Home Assistant."""
# pylint: disable=import-error
from homekit.models.characteristics import CharacteristicsTypes
from homekit.model.characteristics import CharacteristicsTypes
for characteristic in characteristics:
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short_uuid(characteristic['type'])
if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT:
self._state = MODE_HOMEKIT_TO_HASS.get(
characteristic['value'])
@ -62,8 +64,11 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
self._features |= SUPPORT_OPERATION_MODE
self._current_mode = MODE_HOMEKIT_TO_HASS.get(
characteristic['value'])
valid_values = characteristic.get(
'valid-values', DEFAULT_VALID_MODES)
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
mode) for mode in characteristic['valid-values']]
mode) for mode in valid_values]
elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
self._current_temp = characteristic['value']
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:

View File

@ -529,7 +529,7 @@ home-assistant-frontend==20190121.1
homeassistant-pyozw==0.1.2
# homeassistant.components.homekit_controller
# homekit==0.12.2
homekit==0.12.2
# homeassistant.components.homematicip_cloud
homematicip==0.10.4

View File

@ -112,6 +112,9 @@ holidays==0.9.9
# homeassistant.components.frontend
home-assistant-frontend==20190121.1
# homeassistant.components.homekit_controller
homekit==0.12.2
# homeassistant.components.homematicip_cloud
homematicip==0.10.4

View File

@ -32,7 +32,6 @@ COMMENT_REQUIREMENTS = (
'i2csense',
'credstash',
'bme680',
'homekit',
'py_noaa',
)
@ -64,6 +63,7 @@ TEST_REQUIREMENTS = (
'hdate',
'holidays',
'home-assistant-frontend',
'homekit',
'homematicip',
'influxdb',
'jsonpath',

View File

@ -0,0 +1,144 @@
"""Code to support homekit_controller tests."""
from datetime import timedelta
from unittest import mock
from homeassistant.components.homekit_controller import (
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, fire_service_discovered
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.
"""
def __init__(self, accessory):
"""Create a fake pairing from an accessory model."""
self.accessory = accessory
self.pairing_data = {
'accessories': self.list_accessories_and_characteristics()
}
def list_accessories_and_characteristics(self):
"""Fake implementation of list_accessories_and_characteristics."""
return [self.accessory.to_accessory_and_service_list()]
def get_characteristics(self, characteristics):
"""Fake implementation of get_characteristics."""
results = {}
for aid, cid in characteristics:
for service in self.accessory.services:
for char in service.characteristics:
if char.iid != cid:
continue
results[(aid, cid)] = {
'value': char.get_value()
}
return results
def put_characteristics(self, characteristics):
"""Fake implementation of put_characteristics."""
for _, cid, new_val in characteristics:
for service in self.accessory.services:
for char in service.characteristics:
if char.iid != cid:
continue
char.set_value(new_val)
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 = {}
def add(self, accessory):
"""Create and register a fake pairing for a simulated accessory."""
pairing = FakePairing(accessory)
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."""
from homekit.model.services import ServicesTypes
from homekit.model.characteristics import CharacteristicsTypes
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
async def setup_test_component(hass, services):
"""Load a fake homekit accessory based on a homekit accessory model."""
from homekit.model import Accessory
from homekit.model.services import ServicesTypes
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'
config = {
'discovery': {
}
}
with mock.patch('homekit.Controller') as controller:
fake_controller = controller.return_value = FakeController()
await async_setup_component(hass, DOMAIN, config)
accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1')
accessory.services.extend(services)
pairing = fake_controller.add(accessory)
discovery_info = {
'host': '127.0.0.1',
'port': 8080,
'properties': {
'md': 'TestDevice',
'id': '00:00:00:00:00:00',
'c#': 1,
}
}
fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
await hass.async_block_till_done()
return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory)

View File

@ -0,0 +1,14 @@
"""HomeKit controller session fixtures."""
import datetime
from unittest import mock
import pytest
@pytest.fixture
def utcnow(request):
"""Freeze time at a known point."""
start_dt = datetime.datetime(2019, 1, 1, 0, 0, 0)
with mock.patch('homeassistant.util.dt.utcnow') as dt_utcnow:
dt_utcnow.return_value = start_dt
yield dt_utcnow

View File

@ -0,0 +1,77 @@
"""Basic checks for HomeKitclimate."""
from homeassistant.components.climate import (
DOMAIN, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE)
from tests.components.homekit_controller.common import (
setup_test_component)
HEATING_COOLING_TARGET = ('thermostat', 'heating-cooling.target')
HEATING_COOLING_CURRENT = ('thermostat', 'heating-cooling.current')
TEMPERATURE_TARGET = ('thermostat', 'temperature.target')
TEMPERATURE_CURRENT = ('thermostat', 'temperature.current')
async def test_climate_change_thermostat_state(hass, utcnow):
"""Test that we can turn a HomeKit thermostat on and off again."""
from homekit.model.services import ThermostatService
helper = await setup_test_component(hass, [ThermostatService()])
await hass.services.async_call(DOMAIN, SERVICE_SET_OPERATION_MODE, {
'entity_id': 'climate.testdevice',
'operation_mode': 'heat',
}, blocking=True)
assert helper.characteristics[HEATING_COOLING_TARGET].value == 1
await hass.services.async_call(DOMAIN, SERVICE_SET_OPERATION_MODE, {
'entity_id': 'climate.testdevice',
'operation_mode': 'cool',
}, blocking=True)
assert helper.characteristics[HEATING_COOLING_TARGET].value == 2
async def test_climate_change_thermostat_temperature(hass, utcnow):
"""Test that we can turn a HomeKit thermostat on and off again."""
from homekit.model.services import ThermostatService
helper = await setup_test_component(hass, [ThermostatService()])
await hass.services.async_call(DOMAIN, SERVICE_SET_TEMPERATURE, {
'entity_id': 'climate.testdevice',
'temperature': 21,
}, blocking=True)
assert helper.characteristics[TEMPERATURE_TARGET].value == 21
await hass.services.async_call(DOMAIN, SERVICE_SET_TEMPERATURE, {
'entity_id': 'climate.testdevice',
'temperature': 25,
}, blocking=True)
assert helper.characteristics[TEMPERATURE_TARGET].value == 25
async def test_climate_read_thermostat_state(hass, utcnow):
"""Test that we can read the state of a HomeKit thermostat accessory."""
from homekit.model.services import ThermostatService
helper = await setup_test_component(hass, [ThermostatService()])
# Simulate that heating is on
helper.characteristics[TEMPERATURE_CURRENT].value = 19
helper.characteristics[TEMPERATURE_TARGET].value = 21
helper.characteristics[HEATING_COOLING_CURRENT].value = 1
helper.characteristics[HEATING_COOLING_TARGET].value = 1
state = await helper.poll_and_get_state()
assert state.state == 'heat'
assert state.attributes['current_temperature'] == 19
# Simulate that cooling is on
helper.characteristics[TEMPERATURE_CURRENT].value = 21
helper.characteristics[TEMPERATURE_TARGET].value = 19
helper.characteristics[HEATING_COOLING_CURRENT].value = 2
helper.characteristics[HEATING_COOLING_TARGET].value = 2
state = await helper.poll_and_get_state()
assert state.state == 'cool'
assert state.attributes['current_temperature'] == 21

View File

@ -0,0 +1,46 @@
"""Basic checks for HomeKitSwitch."""
from tests.components.homekit_controller.common import (
setup_test_component)
async def test_switch_change_light_state(hass, utcnow):
"""Test that we can turn a HomeKit light on and off again."""
from homekit.model.services import BHSLightBulbService
helper = await setup_test_component(hass, [BHSLightBulbService()])
await hass.services.async_call('light', 'turn_on', {
'entity_id': 'light.testdevice',
'brightness': 255,
'hs_color': [4, 5],
}, blocking=True)
assert helper.characteristics[('lightbulb', 'on')].value == 1
assert helper.characteristics[('lightbulb', 'brightness')].value == 100
assert helper.characteristics[('lightbulb', 'hue')].value == 4
assert helper.characteristics[('lightbulb', 'saturation')].value == 5
await hass.services.async_call('light', 'turn_off', {
'entity_id': 'light.testdevice',
}, blocking=True)
assert helper.characteristics[('lightbulb', 'on')].value == 0
async def test_switch_read_light_state(hass, utcnow):
"""Test that we can read the state of a HomeKit light accessory."""
from homekit.model.services import BHSLightBulbService
helper = await setup_test_component(hass, [BHSLightBulbService()])
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == 'off'
# Simulate that someone switched on the device in the real world not via HA
helper.characteristics[('lightbulb', 'on')].set_value(True)
state = await helper.poll_and_get_state()
assert state.state == 'on'
# Simulate that device switched off in the real world not via HA
helper.characteristics[('lightbulb', 'on')].set_value(False)
state = await helper.poll_and_get_state()
assert state.state == 'off'

View File

@ -0,0 +1,49 @@
"""Basic checks for HomeKitSwitch."""
from tests.components.homekit_controller.common import (
setup_test_component)
async def test_switch_change_outlet_state(hass, utcnow):
"""Test that we can turn a HomeKit outlet on and off again."""
from homekit.model.services import OutletService
helper = await setup_test_component(hass, [OutletService()])
await hass.services.async_call('switch', 'turn_on', {
'entity_id': 'switch.testdevice',
}, blocking=True)
assert helper.characteristics[('outlet', 'on')].value == 1
await hass.services.async_call('switch', 'turn_off', {
'entity_id': 'switch.testdevice',
}, blocking=True)
assert helper.characteristics[('outlet', 'on')].value == 0
async def test_switch_read_outlet_state(hass, utcnow):
"""Test that we can read the state of a HomeKit outlet accessory."""
from homekit.model.services import OutletService
helper = await setup_test_component(hass, [OutletService()])
# Initial state is that the switch is off and the outlet isn't in use
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == 'off'
assert switch_1.attributes['outlet_in_use'] is False
# Simulate that someone switched on the device in the real world not via HA
helper.characteristics[('outlet', 'on')].set_value(True)
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == 'on'
assert switch_1.attributes['outlet_in_use'] is False
# Simulate that device switched off in the real world not via HA
helper.characteristics[('outlet', 'on')].set_value(False)
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == 'off'
# Simulate that someone plugged something into the device
helper.characteristics[('outlet', 'outlet-in-use')].value = True
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == 'off'
assert switch_1.attributes['outlet_in_use'] is True