Refactor HomeKit to allow supported features/device class to change (#101719)

This commit is contained in:
J. Nick Koston 2023-10-10 06:20:25 -10:00 committed by GitHub
parent f166e1cc1a
commit 7b4b8e7516
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 662 additions and 452 deletions

View file

@ -36,7 +36,13 @@ from homeassistant.components.homekit.const import (
)
from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory
from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id
from homeassistant.components.light import (
ATTR_COLOR_MODE,
ATTR_SUPPORTED_COLOR_MODES,
ColorMode,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@ -532,7 +538,7 @@ async def test_homekit_remove_accessory(
acc_mock.stop = AsyncMock()
homekit.bridge.accessories = {6: acc_mock}
acc = await homekit.async_remove_bridge_accessory(6)
acc = homekit.async_remove_bridge_accessory(6)
assert acc is acc_mock
assert len(homekit.bridge.accessories) == 0
@ -876,6 +882,7 @@ async def test_homekit_stop(hass: HomeAssistant) -> None:
# Test if driver is started
homekit.status = STATUS_RUNNING
homekit._cancel_reload_dispatcher = lambda: None
await homekit.async_stop()
await hass.async_block_till_done()
assert homekit.driver.async_stop.called is True
@ -919,6 +926,120 @@ async def test_homekit_reset_accessories(
await homekit.async_stop()
async def test_homekit_reload_accessory_can_change_class(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in brdige mode.
This test ensure when device class changes the HomeKit class changes.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "switch.outlet"
hass.states.async_set(entity_id, "on", {ATTR_DEVICE_CLASS: None})
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
bridge: HomeBridge = homekit.driver.accessory
await bridge.run()
switch_accessory = next(iter(bridge.accessories.values()))
assert type(switch_accessory).__name__ == "Switch"
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id, "off", {ATTR_DEVICE_CLASS: SwitchDeviceClass.OUTLET}
)
await hass.async_block_till_done()
await hass.async_block_till_done()
outlet_accessory = next(iter(bridge.accessories.values()))
assert type(outlet_accessory).__name__ == "Outlet"
await homekit.async_stop()
async def test_homekit_reload_accessory_in_accessory_mode(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in accessory mode.
This test ensure a device class changes can change the class of
the accessory.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "switch.outlet"
hass.states.async_set(entity_id, "on", {ATTR_DEVICE_CLASS: None})
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
primary_accessory = homekit.driver.accessory
await primary_accessory.run()
assert type(primary_accessory).__name__ == "Switch"
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id, "off", {ATTR_DEVICE_CLASS: SwitchDeviceClass.OUTLET}
)
await hass.async_block_till_done()
await hass.async_block_till_done()
primary_accessory = homekit.driver.accessory
assert type(primary_accessory).__name__ == "Outlet"
await homekit.async_stop()
async def test_homekit_reload_accessory_same_class(
hass: HomeAssistant, mock_async_zeroconf: None, mock_hap
) -> None:
"""Test reloading a HomeKit Accessory in bridge mode.
The class of the accessory remains the same.
"""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "light.color"
hass.states.async_set(
entity_id,
"on",
{ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_COLOR_MODE: ColorMode.HS},
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
await async_init_entry(hass, entry)
bridge: HomeBridge = homekit.driver.accessory
await bridge.run()
light_accessory_color = next(iter(bridge.accessories.values()))
assert not hasattr(light_accessory_color, "char_color_temp")
await hass.async_block_till_done()
assert homekit.status == STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
hass.states.async_set(
entity_id,
"on",
{
ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS, ColorMode.COLOR_TEMP],
ATTR_COLOR_MODE: ColorMode.COLOR_TEMP,
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
light_accessory_color_and_temp = next(iter(bridge.accessories.values()))
assert hasattr(light_accessory_color_and_temp, "char_color_temp")
await homekit.async_stop()
async def test_homekit_unpair(
hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_async_zeroconf: None
) -> None:
@ -1076,8 +1197,8 @@ async def test_homekit_reset_accessories_not_supported(
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory.Bridge.add_accessory"
) as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch.object(
homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0
@ -1101,7 +1222,7 @@ async def test_homekit_reset_accessories_not_supported(
)
await hass.async_block_till_done()
assert hk_driver_config_changed.call_count == 2
assert hk_driver_async_update_advertisement.call_count == 1
assert not mock_add_accessory.called
assert len(homekit.bridge.accessories) == 0
homekit.status = STATUS_STOPPED
@ -1165,22 +1286,25 @@ async def test_homekit_reset_accessories_not_bridged(
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory.Bridge.add_accessory"
) as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch.object(
homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0
):
await async_init_entry(hass, entry)
assert hk_driver_async_update_advertisement.call_count == 0
acc_mock = MagicMock()
acc_mock.entity_id = entity_id
acc_mock.stop = AsyncMock()
acc_mock.to_HAP = lambda: {}
aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
homekit.bridge.accessories = {aid: acc_mock}
homekit.status = STATUS_RUNNING
homekit.driver.aio_stop_event = MagicMock()
assert hk_driver_async_update_advertisement.call_count == 0
await hass.services.async_call(
DOMAIN,
@ -1190,7 +1314,7 @@ async def test_homekit_reset_accessories_not_bridged(
)
await hass.async_block_till_done()
assert hk_driver_config_changed.call_count == 0
assert hk_driver_async_update_advertisement.call_count == 0
assert not mock_add_accessory.called
homekit.status = STATUS_STOPPED
@ -1208,8 +1332,8 @@ async def test_homekit_reset_single_accessory(
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
"pyhap.accessory_driver.AccessoryDriver.async_update_advertisement"
) as hk_driver_async_update_advertisement, patch(
"pyhap.accessory_driver.AccessoryDriver.async_start"
), patch(
f"{PATH_HOMEKIT}.accessories.HomeAccessory.run"
@ -1226,7 +1350,7 @@ async def test_homekit_reset_single_accessory(
)
await hass.async_block_till_done()
assert mock_run.called
assert hk_driver_config_changed.call_count == 1
assert hk_driver_async_update_advertisement.call_count == 1
homekit.status = STATUS_READY
await homekit.async_stop()