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 reviewpull/18738/head
parent
7368c623d4
commit
10e3698fd7
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
Loading…
Reference in New Issue