"""Tests for the HomeKit component.""" import os from typing import Dict from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyhap.accessory import Accessory import pytest from homeassistant import config as hass_config from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_MOTION, ) from homeassistant.components.homekit import ( MAX_DEVICES, STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT, HomeKit, ) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AID_STORAGE, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, CONF_ENTRY_INDEX, CONF_SAFE_MODE, DEFAULT_PORT, DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT, HOMEKIT_FILE, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODE_BRIDGE, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, ) from homeassistant.components.homekit.util import ( get_aid_storage_fullpath_for_entry_id, get_persist_fullpath_for_entry_id, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, SERVICE_RELOAD, STATE_ON, ) from homeassistant.core import State from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.setup import async_setup_component from homeassistant.util import json as json_util from .util import PATH_HOMEKIT, async_init_entry, async_init_integration from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce IP_ADDRESS = "127.0.0.1" @pytest.fixture(autouse=True) def always_patch_driver(hk_driver): """Load the hk_driver fixture.""" @pytest.fixture(name="device_reg") def device_reg_fixture(hass): """Return an empty, loaded, registry.""" return mock_device_registry(hass) @pytest.fixture(name="entity_reg") def entity_reg_fixture(hass): """Return an empty, loaded, registry.""" return mock_registry(hass) @pytest.fixture(name="debounce_patcher", scope="module") def debounce_patcher_fixture(): """Patch debounce method.""" patcher = patch_debounce() yield patcher.start() patcher.stop() async def test_setup_min(hass): """Test async_setup with min config options.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, options={}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_homekit.assert_any_call( hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit().setup.called is True # Test auto start enabled mock_homekit.reset_mock() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() mock_homekit().async_start.assert_called() async def test_setup_auto_start_disabled(hass): """Test async_setup with auto start disabled and test service calls.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, options={CONF_AUTO_START: False, CONF_SAFE_MODE: DEFAULT_SAFE_MODE}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_homekit.assert_any_call( hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit().setup.called is True # Test auto_start disabled homekit.reset_mock() homekit.async_start.reset_mock() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert homekit.async_start.called is False # Test start call with driver is ready homekit.reset_mock() homekit.async_start.reset_mock() homekit.status = STATUS_READY await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) await hass.async_block_till_done() assert homekit.async_start.called is True # Test start call with driver started homekit.reset_mock() homekit.async_start.reset_mock() homekit.status = STATUS_STOPPED await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) await hass.async_block_till_done() assert homekit.async_start.called is False async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, source=SOURCE_IMPORT, ) homekit = HomeKit( hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) hass.states.async_set("light.demo", "on") hass.states.async_set("light.demo2", "on") zeroconf_mock = MagicMock() with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_executor_job(homekit.setup, zeroconf_mock) path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) mock_driver.assert_called_with( hass, entry.entry_id, BRIDGE_NAME, loop=hass.loop, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path, advertised_address=None, zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, source=SOURCE_IMPORT, ) homekit = HomeKit( hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None, HOMEKIT_MODE_BRIDGE, None, entry_id=entry.entry_id, ) mock_zeroconf = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup, mock_zeroconf) mock_driver.assert_called_with( hass, entry.entry_id, BRIDGE_NAME, loop=hass.loop, address="172.0.0.0", port=DEFAULT_PORT, persist_file=path, advertised_address=None, zeroconf_instance=mock_zeroconf, ) async def test_homekit_setup_advertise_ip(hass, hk_driver): """Test setup with given IP address to advertise.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, source=SOURCE_IMPORT, ) homekit = HomeKit( hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, HOMEKIT_MODE_BRIDGE, "192.168.1.100", entry_id=entry.entry_id, ) zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup, zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, BRIDGE_NAME, loop=hass.loop, address="0.0.0.0", port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", zeroconf_instance=zeroconf_instance, ) async def test_homekit_setup_safe_mode(hass, hk_driver): """Test if safe_mode flag is set.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, source=SOURCE_IMPORT, ) homekit = HomeKit( hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): await hass.async_add_executor_job(homekit.setup, MagicMock()) assert homekit.driver.safe_mode is True async def test_homekit_add_accessory(hass): """Add accessory if config exists and get_acc returns an accessory.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, lambda entity_id: True, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) await async_init_integration(hass) 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, 1403373688, {}) assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State("demo.test", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 600325356, {}) assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State("demo.test_2", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {}) mock_bridge.add_accessory.assert_called_with("acc") async def test_homekit_remove_accessory(hass): """Remove accessory from bridge.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, lambda entity_id: True, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() mock_bridge.accessories = {"light.demo": "acc"} acc = homekit.remove_bridge_accessory("light.demo") assert acc == "acc" assert len(mock_bridge.accessories) == 0 async def test_homekit_entity_filter(hass): """Test the entity filter.""" entry = await async_init_integration(hass) entity_filter = generate_filter(["cover"], ["demo.test"], [], []) homekit = HomeKit( hass, None, None, None, entity_filter, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = {} with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.return_value = None homekit.add_bridge_accessory(State("cover.test", "open")) assert mock_get_acc.called is True mock_get_acc.reset_mock() homekit.add_bridge_accessory(State("demo.test", "on")) assert mock_get_acc.called is True mock_get_acc.reset_mock() homekit.add_bridge_accessory(State("light.demo", "light")) assert mock_get_acc.called is False async def test_homekit_entity_glob_filter(hass): """Test the entity filter.""" entry = await async_init_integration(hass) entity_filter = generate_filter( ["cover"], ["demo.test"], [], [], ["*.included_*"], ["*.excluded_*"] ) homekit = HomeKit( hass, None, None, None, entity_filter, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = {} with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.return_value = None homekit.add_bridge_accessory(State("cover.test", "open")) assert mock_get_acc.called is True mock_get_acc.reset_mock() homekit.add_bridge_accessory(State("demo.test", "on")) assert mock_get_acc.called is True mock_get_acc.reset_mock() homekit.add_bridge_accessory(State("cover.excluded_test", "open")) assert mock_get_acc.called is False mock_get_acc.reset_mock() homekit.add_bridge_accessory(State("light.included_test", "light")) assert mock_get_acc.called is True mock_get_acc.reset_mock() async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): """Test HomeKit start method.""" entry = await async_init_integration(hass) pin = b"123-45-678" homekit = HomeKit( hass, None, None, None, {}, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.driver.accessory = Accessory(hk_driver, "any") connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF") bridge_with_wrong_mac = device_reg.async_get_or_create( config_entry_id=entry.entry_id, connections={connection}, manufacturer="Any", name="Any", model="Home Assistant HomeKit Bridge", ) hass.states.async_set("light.demo", "on") hass.states.async_set("light.demo2", "on") state = hass.states.async_all()[0] with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( f"{PATH_HOMEKIT}.show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory" ) as hk_driver_add_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ) as hk_driver_start: await homekit.async_start() await hass.async_block_till_done() mock_add_acc.assert_any_call(state) mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started hk_driver_start.reset_mock() await homekit.async_start() await hass.async_block_till_done() assert not hk_driver_start.called assert device_reg.async_get(bridge_with_wrong_mac.id) is None device = device_reg.async_get_device( {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections # Start again to make sure the registry entry is kept homekit.status = STATUS_READY with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( f"{PATH_HOMEKIT}.show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory" ) as hk_driver_add_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ) as hk_driver_start: await homekit.async_start() device = device_reg.async_get_device( {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections assert len(device_reg.devices) == 1 async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b"123-45-678" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) await async_init_entry(hass, entry) homekit = HomeKit( hass, None, None, None, entity_filter, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver homekit.driver.accessory = Accessory(hk_driver, "any") hass.states.async_set("light.demo", "on") hass.states.async_set("light.broken", "on") with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch( f"{PATH_HOMEKIT}.show_setup_message" ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory", ) as hk_driver_add_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ) as hk_driver_start: await homekit.async_start() await hass.async_block_till_done() mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started hk_driver_start.reset_mock() await homekit.async_start() await hass.async_block_till_done() assert not hk_driver_start.called async def test_homekit_stop(hass): """Test HomeKit stop method.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = Mock() homekit.driver.async_stop = AsyncMock() homekit.bridge = Mock() homekit.bridge.accessories = {} assert homekit.status == STATUS_READY await homekit.async_stop() await hass.async_block_till_done() homekit.status = STATUS_WAIT await homekit.async_stop() await hass.async_block_till_done() homekit.status = STATUS_STOPPED await homekit.async_stop() await hass.async_block_till_done() assert homekit.driver.async_stop.called is False # Test if driver is started homekit.status = STATUS_RUNNING await homekit.async_stop() await hass.async_block_till_done() assert homekit.driver.async_stop.called is True async def test_homekit_reset_accessories(hass): """Test adding too many accessories to HomeKit.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entity_id = "light.demo" homekit = HomeKit( hass, None, None, None, {}, {entity_id: {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = {} with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( f"{PATH_HOMEKIT}.HomeKit.setup" ), 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.start_service" ): await async_init_entry(hass, entry) aid = hass.data[DOMAIN][entry.entry_id][ AID_STORAGE ].get_or_allocate_aid_for_entity_id(entity_id) homekit.bridge.accessories = {aid: "acc"} homekit.status = STATUS_RUNNING await hass.services.async_call( DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) await hass.async_block_till_done() assert hk_driver_config_changed.call_count == 2 assert mock_add_accessory.called homekit.status = STATUS_READY async def test_homekit_too_many_accessories(hass, hk_driver, caplog): """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) homekit = HomeKit( hass, None, None, None, entity_filter, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) def _mock_bridge(*_): mock_bridge = HomeBridge(hass, hk_driver, "mock_bridge") # The bridge itself counts as an accessory mock_bridge.accessories = range(MAX_DEVICES) return mock_bridge homekit.driver = hk_driver homekit.driver.accessory = Accessory(hk_driver, "any") hass.states.async_set("light.demo", "on") hass.states.async_set("light.demo2", "on") hass.states.async_set("light.demo3", "on") with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory" ), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( f"{PATH_HOMEKIT}.accessories.HomeBridge", _mock_bridge ): await homekit.async_start() await hass.async_block_till_done() assert "would exceed" in caplog.text async def test_homekit_finds_linked_batteries( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit start method.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {"light.demo": {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") 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, sw_version="0.16.0", model="Powerwall 2", manufacturer="Tesla", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) binary_charging_sensor = entity_reg.async_get_or_create( "binary_sensor", "powerwall", "battery_charging", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id ) hass.states.async_set( binary_charging_sensor.entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, ) hass.states.async_set( battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} ) hass.states.async_set(light.entity_id, STATE_ON) def _mock_get_accessory(*args, **kwargs): return [None, "acc", None] with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, hk_driver, ANY, ANY, { "manufacturer": "Tesla", "model": "Powerwall 2", "sw_version": "0.16.0", "linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging", "linked_battery_sensor": "sensor.powerwall_battery", }, ) async def test_homekit_async_get_integration_fails( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {"light.demo": {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") 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, sw_version="0.16.0", model="Powerwall 2", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) binary_charging_sensor = entity_reg.async_get_or_create( "binary_sensor", "invalid_integration_does_not_exist", "battery_charging", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "invalid_integration_does_not_exist", "battery", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id ) hass.states.async_set( binary_charging_sensor.entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, ) hass.states.async_set( battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} ) hass.states.async_set(light.entity_id, STATE_ON) def _mock_get_accessory(*args, **kwargs): return [None, "acc", None] with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, hk_driver, ANY, ANY, { "model": "Powerwall 2", "sw_version": "0.16.0", "platform": "invalid_integration_does_not_exist", "linked_battery_charging_sensor": "binary_sensor.invalid_integration_does_not_exist_battery_charging", "linked_battery_sensor": "sensor.invalid_integration_does_not_exist_battery", }, ) async def test_setup_imported(hass): """Test async_setup with imported config options.""" legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") legacy_homekit_state_contents = {"homekit.state": 1} legacy_homekit_aids_contents = {"homekit.aids": 1} await hass.async_add_executor_job( _write_data, legacy_persist_file_path, legacy_homekit_state_contents ) await hass.async_add_executor_job( _write_data, legacy_aid_storage_path, legacy_homekit_aids_contents ) entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT, CONF_ENTRY_INDEX: 0}, options={}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_homekit.assert_any_call( hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit().setup.called is True # Test auto start enabled mock_homekit.reset_mock() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() mock_homekit().async_start.assert_called() migrated_persist_file_path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) assert ( await hass.async_add_executor_job( json_util.load_json, migrated_persist_file_path ) == legacy_homekit_state_contents ) os.unlink(migrated_persist_file_path) migrated_aid_file_path = get_aid_storage_fullpath_for_entry_id(hass, entry.entry_id) assert ( await hass.async_add_executor_job(json_util.load_json, migrated_aid_file_path) == legacy_homekit_aids_contents ) os.unlink(migrated_aid_file_path) async def test_yaml_updates_update_config_entry_for_name(hass): """Test async_setup with imported config.""" entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, options={}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await async_setup_component( hass, "homekit", {"homekit": {CONF_NAME: BRIDGE_NAME, CONF_PORT: 12345}} ) await hass.async_block_till_done() mock_homekit.assert_any_call( hass, BRIDGE_NAME, 12345, None, ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit().setup.called is True # Test auto start enabled mock_homekit.reset_mock() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() mock_homekit().async_start.assert_called() async def test_raise_config_entry_not_ready(hass): """Test async_setup when the port is not available.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, options={}, ) entry.add_to_hass(hass) with patch( "homeassistant.components.homekit.port_is_available", return_value=False, ): assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, options={}, ) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) system_zc = await zeroconf.async_get_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() def _write_data(path: str, data: Dict) -> None: """Write the data.""" if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) json_util.save_json(path, data) async def test_homekit_ignored_missing_devices( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {"light.demo": {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") 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, sw_version="0.16.0", model="Powerwall 2", manufacturer="Tesla", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( "binary_sensor", "powerwall", "battery_charging", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY_CHARGING, ) entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id ) # Delete the device to make sure we fallback # to using the platform device_reg.async_remove_device(device_entry.id) hass.states.async_set(light.entity_id, STATE_ON) hass.states.async_set("light.two", STATE_ON) def _mock_get_accessory(*args, **kwargs): return [None, "acc", None] with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_any_call( hass, hk_driver, ANY, ANY, { "platform": "Tesla Powerwall", "linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging", "linked_battery_sensor": "sensor.powerwall_battery", }, ) async def test_homekit_finds_linked_motion_sensors( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit start method.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {"camera.camera_demo": {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") 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, sw_version="0.16.0", model="Camera Server", manufacturer="Ubq", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) binary_motion_sensor = entity_reg.async_get_or_create( "binary_sensor", "camera", "motion_sensor", device_id=device_entry.id, device_class=DEVICE_CLASS_MOTION, ) camera = entity_reg.async_get_or_create( "camera", "camera", "demo", device_id=device_entry.id ) hass.states.async_set( binary_motion_sensor.entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION}, ) hass.states.async_set(camera.entity_id, STATE_ON) def _mock_get_accessory(*args, **kwargs): return [None, "acc", None] with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, hk_driver, ANY, ANY, { "manufacturer": "Ubq", "model": "Camera Server", "sw_version": "0.16.0", "linked_motion_sensor": "binary_sensor.camera_motion_sensor", }, ) async def test_homekit_finds_linked_humidity_sensors( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit start method.""" entry = await async_init_integration(hass) homekit = HomeKit( hass, None, None, None, {}, {"humidifier.humidifier": {}}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") 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, sw_version="0.16.1", model="Smart Brainy Clever Humidifier", manufacturer="Home Assistant", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) humidity_sensor = entity_reg.async_get_or_create( "sensor", "humidifier", "humidity_sensor", device_id=device_entry.id, device_class=DEVICE_CLASS_HUMIDITY, ) humidifier = entity_reg.async_get_or_create( "humidifier", "humidifier", "demo", device_id=device_entry.id ) hass.states.async_set( humidity_sensor.entity_id, "42", { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) hass.states.async_set(humidifier.entity_id, STATE_ON) def _mock_get_accessory(*args, **kwargs): return [None, "acc", None] with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, hk_driver, ANY, ANY, { "manufacturer": "Home Assistant", "model": "Smart Brainy Clever Humidifier", "sw_version": "0.16.1", "linked_humidity_sensor": "sensor.humidifier_humidity_sensor", }, ) async def test_reload(hass): """Test we can reload from yaml.""" entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, data={CONF_NAME: "reloadable", CONF_PORT: 12345}, options={}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() assert await async_setup_component( hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}} ) await hass.async_block_till_done() mock_homekit.assert_any_call( hass, "reloadable", 12345, None, ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit().setup.called is True yaml_path = os.path.join( _get_fixtures_base_path(), "fixtures", "homekit/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( f"{PATH_HOMEKIT}.HomeKit" ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch( f"{PATH_HOMEKIT}.get_accessory" ), patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ): mock_homekit2.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() await hass.services.async_call( "homekit", SERVICE_RELOAD, {}, blocking=True, ) await hass.async_block_till_done() mock_homekit2.assert_any_call( hass, "reloadable", 45678, None, ANY, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, ) assert mock_homekit2().setup.called is True def _get_fixtures_base_path(): return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) async def test_homekit_start_in_accessory_mode( hass, hk_driver, device_reg, debounce_patcher ): """Test HomeKit start method in accessory mode.""" entry = await async_init_integration(hass) pin = b"123-45-678" homekit = HomeKit( hass, None, None, None, {}, {}, DEFAULT_SAFE_MODE, HOMEKIT_MODE_ACCESSORY, advertise_ip=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) homekit.driver.accessory = Accessory(hk_driver, "any") hass.states.async_set("light.demo", "on") with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory" ), patch(f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.start_service" ) as hk_driver_start: await homekit.async_start() await hass.async_block_till_done() mock_add_acc.assert_not_called() mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING