Store HomeKit generated accessory id against unique_id where possible (#33109)

* HomeKit: Store generated aid against unique_id where possible

* Fix conflict

* Fix max accessories check

* homekit counts the bridge as an accessory

* Add coverage for aidmanager

* prepare for merge

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Jc2k 2020-04-19 19:51:09 +01:00 committed by GitHub
parent 3259ba6116
commit a80ce60e75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 317 additions and 40 deletions

View file

@ -0,0 +1,120 @@
"""Tests for the HomeKit AID manager."""
from asynctest import patch
import pytest
from homeassistant.components.homekit.aidmanager import (
AccessoryAidStorage,
get_system_unique_id,
)
from homeassistant.helpers import device_registry
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
@pytest.fixture
def device_reg(hass):
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
def entity_reg(hass):
"""Return an empty, loaded, registry."""
return mock_registry(hass)
async def test_aid_generation(hass, device_reg, entity_reg):
"""Test generating aids."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
light_ent = entity_reg.async_get_or_create(
"light", "device", "unique_id", device_id=device_entry.id
)
light_ent2 = entity_reg.async_get_or_create(
"light", "device", "other_unique_id", device_id=device_entry.id
)
remote_ent = entity_reg.async_get_or_create(
"remote", "device", "unique_id", device_id=device_entry.id
)
hass.states.async_set(light_ent.entity_id, "on")
hass.states.async_set(light_ent2.entity_id, "on")
hass.states.async_set(remote_ent.entity_id, "on")
hass.states.async_set("remote.has_no_unique_id", "on")
with patch(
"homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save"
):
aid_storage = AccessoryAidStorage(hass)
await aid_storage.async_initialize()
for _ in range(0, 2):
assert (
aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id)
== 1692141785
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id(light_ent2.entity_id)
== 2732133210
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id(remote_ent.entity_id)
== 1867188557
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id("remote.has_no_unique_id")
== 1872038229
)
aid_storage.delete_aid(get_system_unique_id(light_ent))
aid_storage.delete_aid(get_system_unique_id(light_ent2))
aid_storage.delete_aid(get_system_unique_id(remote_ent))
aid_storage.delete_aid("non-existant-one")
for _ in range(0, 2):
assert (
aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id)
== 1692141785
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id(light_ent2.entity_id)
== 2732133210
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id(remote_ent.entity_id)
== 1867188557
)
assert (
aid_storage.get_or_allocate_aid_for_entity_id("remote.has_no_unique_id")
== 1872038229
)
async def test_aid_adler32_collision(hass, device_reg, entity_reg):
"""Test generating aids."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
with patch(
"homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save"
):
aid_storage = AccessoryAidStorage(hass)
await aid_storage.async_initialize()
seen_aids = set()
for unique_id in range(0, 202):
ent = entity_reg.async_get_or_create(
"light", "device", unique_id, device_id=device_entry.id
)
hass.states.async_set(ent.entity_id, "on")
aid = aid_storage.get_or_allocate_aid_for_entity_id(ent.entity_id)
assert aid not in seen_aids
seen_aids.add(aid)

View file

@ -12,10 +12,10 @@ from homeassistant.components.homekit import (
STATUS_STOPPED,
STATUS_WAIT,
HomeKit,
generate_aid,
)
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import (
AID_STORAGE,
BRIDGE_NAME,
CONF_AUTO_START,
CONF_SAFE_MODE,
@ -51,17 +51,6 @@ def debounce_patcher():
patcher.stop()
def test_generate_aid():
"""Test generate aid method."""
aid = generate_aid("demo.entity")
assert isinstance(aid, int)
assert aid >= 2 and aid <= 18446744073709551615
with patch(f"{PATH_HOMEKIT}.adler32") as mock_adler32:
mock_adler32.side_effect = [0, 1]
assert generate_aid("demo.entity") is None
async def test_setup_min(hass):
"""Test async_setup with min config options."""
with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
@ -223,29 +212,31 @@ async def test_homekit_setup_safe_mode(hass, hk_driver):
assert homekit.driver.safe_mode is True
async def test_homekit_add_accessory():
async def test_homekit_add_accessory(hass):
"""Add accessory if config exists and get_acc returns an accessory."""
homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None)
homekit = HomeKit(hass, None, None, None, lambda entity_id: True, {}, None, None)
homekit.driver = "driver"
homekit.bridge = mock_bridge = Mock()
homekit.bridge.accessories = range(10)
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.side_effect = [None, "acc", None]
homekit.add_bridge_accessory(State("light.demo", "on"))
mock_get_acc.assert_called_with("hass", "driver", ANY, 363398124, {})
mock_get_acc.assert_called_with(hass, "driver", ANY, 363398124, {})
assert not mock_bridge.add_accessory.called
homekit.add_bridge_accessory(State("demo.test", "on"))
mock_get_acc.assert_called_with("hass", "driver", ANY, 294192020, {})
mock_get_acc.assert_called_with(hass, "driver", ANY, 294192020, {})
assert mock_bridge.add_accessory.called
homekit.add_bridge_accessory(State("demo.test_2", "on"))
mock_get_acc.assert_called_with("hass", "driver", ANY, 429982757, {})
mock_get_acc.assert_called_with(hass, "driver", ANY, 429982757, {})
mock_bridge.add_accessory.assert_called_with("acc")
async def test_homekit_remove_accessory():
async def test_homekit_remove_accessory(hass):
"""Remove accessory from bridge."""
homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None)
homekit.driver = "driver"
@ -259,8 +250,12 @@ async def test_homekit_remove_accessory():
async def test_homekit_entity_filter(hass):
"""Test the entity filter."""
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
entity_filter = generate_filter(["cover"], ["demo.test"], [], [])
homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None)
homekit.bridge = Mock()
homekit.bridge.accessories = {}
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.return_value = None
@ -314,6 +309,8 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p
pin = b"123-45-678"
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None)
homekit.bridge = Mock()
homekit.bridge.accessories = []
@ -366,6 +363,7 @@ async def test_homekit_reset_accessories(hass):
entity_id = "light.demo"
homekit = HomeKit(hass, None, None, None, {}, {entity_id: {}}, None)
homekit.bridge = Mock()
homekit.bridge.accessories = {}
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
f"{PATH_HOMEKIT}.HomeKit.setup"
@ -375,7 +373,7 @@ async def test_homekit_reset_accessories(hass):
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
aid = generate_aid(entity_id)
aid = hass.data[AID_STORAGE].get_or_allocate_aid_for_entity_id(entity_id)
homekit.bridge.accessories = {aid: "acc"}
homekit.status = STATUS_RUNNING
@ -394,11 +392,17 @@ async def test_homekit_reset_accessories(hass):
async def test_homekit_too_many_accessories(hass, hk_driver):
"""Test adding too many accessories to HomeKit."""
homekit = HomeKit(hass, None, None, None, None, None, None)
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None)
homekit.bridge = Mock()
homekit.bridge.accessories = range(MAX_DEVICES + 1)
# The bridge itself counts as an accessory
homekit.bridge.accessories = range(MAX_DEVICES)
homekit.driver = hk_driver
hass.states.async_set("light.demo", "on")
with patch("pyhap.accessory_driver.AccessoryDriver.start"), patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn: