Add some tests for Koogeek LS1 (#22141)
parent
188293770e
commit
c2aa06d0d4
|
@ -1,4 +1,5 @@
|
|||
"""Code to support homekit_controller tests."""
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
|
@ -147,6 +148,41 @@ class FakeService(AbstractService):
|
|||
return char
|
||||
|
||||
|
||||
def setup_accessories_from_file(path):
|
||||
"""Load an collection of accessory defs from JSON data."""
|
||||
with open(path, 'r') as accessories_data:
|
||||
accessories_json = json.load(accessories_data)
|
||||
|
||||
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']
|
||||
service.characteristics.append(char)
|
||||
|
||||
accessory.services.append(service)
|
||||
|
||||
accessories.append(accessory)
|
||||
|
||||
return accessories
|
||||
|
||||
|
||||
async def setup_platform(hass):
|
||||
"""Load the platform but with a fake Controller API."""
|
||||
config = {
|
||||
|
@ -161,6 +197,30 @@ async def setup_platform(hass):
|
|||
return fake_controller
|
||||
|
||||
|
||||
async def setup_test_accessories(hass, accessories, capitalize=False):
|
||||
"""Load a fake homekit accessory based on a homekit accessory model.
|
||||
|
||||
If capitalize is True, property names will be in upper case.
|
||||
"""
|
||||
fake_controller = await setup_platform(hass)
|
||||
pairing = fake_controller.add(accessories)
|
||||
|
||||
discovery_info = {
|
||||
'host': '127.0.0.1',
|
||||
'port': 8080,
|
||||
'properties': {
|
||||
('MD' if capitalize else 'md'): 'TestDevice',
|
||||
('ID' if capitalize else 'id'): '00:00:00:00:00:00',
|
||||
('C#' if capitalize else 'c#'): 1,
|
||||
}
|
||||
}
|
||||
|
||||
fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return pairing
|
||||
|
||||
|
||||
async def setup_test_component(hass, services, capitalize=False, suffix=None):
|
||||
"""Load a fake homekit accessory based on a homekit accessory model.
|
||||
|
||||
|
@ -177,24 +237,10 @@ async def setup_test_component(hass, services, capitalize=False, suffix=None):
|
|||
|
||||
assert domain, 'Cannot map test homekit services to homeassistant domain'
|
||||
|
||||
fake_controller = await setup_platform(hass)
|
||||
|
||||
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' if capitalize else 'md'): 'TestDevice',
|
||||
('ID' if capitalize else 'id'): '00:00:00:00:00:00',
|
||||
('C#' if capitalize else 'c#'): 1,
|
||||
}
|
||||
}
|
||||
|
||||
fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
|
||||
await hass.async_block_till_done()
|
||||
pairing = await setup_test_accessories(hass, [accessory], capitalize)
|
||||
|
||||
entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix)
|
||||
return Helper(hass, '.'.join((domain, entity)), pairing, accessory)
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
[
|
||||
{
|
||||
"aid": 1,
|
||||
"services": [
|
||||
{
|
||||
"characteristics": [
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 2,
|
||||
"maxLen": 64,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "23",
|
||||
"value": "Koogeek-LS1-20833F"
|
||||
},
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 3,
|
||||
"maxLen": 64,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "20",
|
||||
"value": "Koogeek"
|
||||
},
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 4,
|
||||
"maxLen": 64,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "21",
|
||||
"value": "LS1"
|
||||
},
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 5,
|
||||
"maxLen": 64,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "30",
|
||||
"value": "AAAA011111111111"
|
||||
},
|
||||
{
|
||||
"format": "bool",
|
||||
"iid": 6,
|
||||
"perms": [
|
||||
"pw"
|
||||
],
|
||||
"type": "14"
|
||||
},
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 23,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "52",
|
||||
"value": "2.2.15"
|
||||
}
|
||||
],
|
||||
"iid": 1,
|
||||
"type": "3E"
|
||||
},
|
||||
{
|
||||
"characteristics": [
|
||||
{
|
||||
"ev": false,
|
||||
"format": "bool",
|
||||
"iid": 8,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw",
|
||||
"ev"
|
||||
],
|
||||
"type": "25",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"ev": false,
|
||||
"format": "float",
|
||||
"iid": 9,
|
||||
"maxValue": 359,
|
||||
"minStep": 1,
|
||||
"minValue": 0,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw",
|
||||
"ev"
|
||||
],
|
||||
"type": "13",
|
||||
"unit": "arcdegrees",
|
||||
"value": 44
|
||||
},
|
||||
{
|
||||
"ev": false,
|
||||
"format": "float",
|
||||
"iid": 10,
|
||||
"maxValue": 100,
|
||||
"minStep": 1,
|
||||
"minValue": 0,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw",
|
||||
"ev"
|
||||
],
|
||||
"type": "2F",
|
||||
"unit": "percentage",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"ev": false,
|
||||
"format": "int",
|
||||
"iid": 11,
|
||||
"maxValue": 100,
|
||||
"minStep": 1,
|
||||
"minValue": 0,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw",
|
||||
"ev"
|
||||
],
|
||||
"type": "8",
|
||||
"unit": "percentage",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"format": "string",
|
||||
"iid": 12,
|
||||
"maxLen": 64,
|
||||
"perms": [
|
||||
"pr"
|
||||
],
|
||||
"type": "23",
|
||||
"value": "Light Strip"
|
||||
}
|
||||
],
|
||||
"iid": 7,
|
||||
"primary": true,
|
||||
"type": "43"
|
||||
},
|
||||
{
|
||||
"characteristics": [
|
||||
{
|
||||
"description": "TIMER_SETTINGS",
|
||||
"format": "tlv8",
|
||||
"iid": 14,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw"
|
||||
],
|
||||
"type": "4aaaf942-0dec-11e5-b939-0800200c9a66",
|
||||
"value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
}
|
||||
],
|
||||
"iid": 13,
|
||||
"type": "4aaaf940-0dec-11e5-b939-0800200c9a66"
|
||||
},
|
||||
{
|
||||
"characteristics": [
|
||||
{
|
||||
"description": "FW Upgrade supported types",
|
||||
"format": "string",
|
||||
"iid": 16,
|
||||
"perms": [
|
||||
"pr",
|
||||
"hd"
|
||||
],
|
||||
"type": "151909D2-3802-11E4-916C-0800200C9A66",
|
||||
"value": "url,data"
|
||||
},
|
||||
{
|
||||
"description": "FW Upgrade URL",
|
||||
"format": "string",
|
||||
"iid": 17,
|
||||
"maxLen": 256,
|
||||
"perms": [
|
||||
"pw",
|
||||
"hd"
|
||||
],
|
||||
"type": "151909D1-3802-11E4-916C-0800200C9A66"
|
||||
},
|
||||
{
|
||||
"description": "FW Upgrade Status",
|
||||
"ev": false,
|
||||
"format": "int",
|
||||
"iid": 18,
|
||||
"perms": [
|
||||
"pr",
|
||||
"ev",
|
||||
"hd"
|
||||
],
|
||||
"type": "151909D6-3802-11E4-916C-0800200C9A66",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"description": "FW Upgrade Data",
|
||||
"format": "data",
|
||||
"iid": 19,
|
||||
"perms": [
|
||||
"pw",
|
||||
"hd"
|
||||
],
|
||||
"type": "151909D7-3802-11E4-916C-0800200C9A66"
|
||||
}
|
||||
],
|
||||
"hidden": true,
|
||||
"iid": 15,
|
||||
"type": "151909D0-3802-11E4-916C-0800200C9A66"
|
||||
},
|
||||
{
|
||||
"characteristics": [
|
||||
{
|
||||
"description": "Timezone",
|
||||
"format": "int",
|
||||
"iid": 21,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw"
|
||||
],
|
||||
"type": "151909D5-3802-11E4-916C-0800200C9A66",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"description": "Time value since Epoch",
|
||||
"format": "int",
|
||||
"iid": 22,
|
||||
"perms": [
|
||||
"pr",
|
||||
"pw"
|
||||
],
|
||||
"type": "151909D4-3802-11E4-916C-0800200C9A66",
|
||||
"value": 1550348623
|
||||
}
|
||||
],
|
||||
"iid": 20,
|
||||
"type": "151909D3-3802-11E4-916C-0800200C9A66"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,85 @@
|
|||
"""Make sure that existing Koogeek LS1 support isn't broken."""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homekit.exceptions import AccessoryDisconnectedError, EncryptionError
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.components.homekit_controller.common import (
|
||||
setup_accessories_from_file, setup_test_accessories, FakePairing, Helper
|
||||
)
|
||||
|
||||
LIGHT_ON = ('lightbulb', 'on')
|
||||
|
||||
|
||||
async def test_koogeek_ls1_setup(hass):
|
||||
"""Test that a Koogeek LS1 can be correctly setup in HA."""
|
||||
profile_path = os.path.join(os.path.dirname(__file__), 'koogeek_ls1.json')
|
||||
accessories = setup_accessories_from_file(profile_path)
|
||||
pairing = await setup_test_accessories(hass, accessories)
|
||||
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
# Assert that the entity is correctly added to the entity registry
|
||||
entity = entity_registry.async_get('light.testdevice')
|
||||
assert entity.unique_id == 'homekit-AAAA011111111111-7'
|
||||
|
||||
helper = Helper(hass, 'light.testdevice', pairing, accessories[0])
|
||||
state = await helper.poll_and_get_state()
|
||||
|
||||
# Assert that the friendly name is detected correctly
|
||||
assert state.attributes['friendly_name'] == 'TestDevice'
|
||||
|
||||
# Assert that all optional features the LS1 supports are detected
|
||||
assert state.attributes['supported_features'] == (
|
||||
SUPPORT_BRIGHTNESS | SUPPORT_COLOR
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure_cls', [
|
||||
AccessoryDisconnectedError, EncryptionError
|
||||
])
|
||||
async def test_recover_from_failure(hass, utcnow, failure_cls):
|
||||
"""
|
||||
Test that entity actually recovers from a network connection drop.
|
||||
|
||||
See https://github.com/home-assistant/home-assistant/issues/18949
|
||||
"""
|
||||
profile_path = os.path.join(os.path.dirname(__file__), 'koogeek_ls1.json')
|
||||
accessories = setup_accessories_from_file(profile_path)
|
||||
pairing = await setup_test_accessories(hass, accessories)
|
||||
|
||||
helper = Helper(hass, 'light.testdevice', pairing, accessories[0])
|
||||
|
||||
# Set light state on fake device to off
|
||||
helper.characteristics[LIGHT_ON].set_value(False)
|
||||
|
||||
# Test that entity starts off in a known state
|
||||
state = await helper.poll_and_get_state()
|
||||
assert state.state == 'off'
|
||||
|
||||
# Set light state on fake device to on
|
||||
helper.characteristics[LIGHT_ON].set_value(True)
|
||||
|
||||
# Test that entity remains in the same state if there is a network error
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=60)
|
||||
with mock.patch.object(FakePairing, 'get_characteristics') as get_char:
|
||||
get_char.side_effect = failure_cls('Disconnected')
|
||||
|
||||
state = await helper.poll_and_get_state()
|
||||
assert state.state == 'off'
|
||||
|
||||
get_char.assert_called_with([(1, 8), (1, 9), (1, 10), (1, 11)])
|
||||
|
||||
# Test that entity changes state when network error goes away
|
||||
next_update += timedelta(seconds=60)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = await helper.poll_and_get_state()
|
||||
assert state.state == 'on'
|
Loading…
Reference in New Issue