diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 23492b12ccc..7c787c7e7be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -34,7 +34,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized +from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA @@ -58,7 +58,6 @@ from . import ( # noqa: F401 from .accessories import HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( - AID_STORAGE, ATTR_INTERGRATION, ATTR_MANUFACTURER, ATTR_MODEL, @@ -241,9 +240,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port = conf[CONF_PORT] _LOGGER.debug("Begin setup HomeKit for %s", name) - aid_storage = AccessoryAidStorage(hass, entry.entry_id) - - await aid_storage.async_initialize() # ip_address and advertise_ip are yaml only ip_address = conf.get(CONF_IP_ADDRESS) advertise_ip = conf.get(CONF_ADVERTISE_IP) @@ -276,26 +272,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.entry_id, entry.title, ) - zeroconf_instance = await zeroconf.async_get_instance(hass) - - # If the previous instance hasn't cleaned up yet - # we need to wait a bit - try: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) - except (OSError, AttributeError) as ex: - _LOGGER.warning( - "%s could not be setup because the local port %s is in use", name, port - ) - raise ConfigEntryNotReady from ex - - undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { - AID_STORAGE: aid_storage, HOMEKIT: homekit, - UNDO_UPDATE_LISTENER: undo_listener, + UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), } + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + if hass.state == CoreState.running: await homekit.async_start() elif auto_start: @@ -463,6 +447,7 @@ class HomeKit: self._entry_id = entry_id self._entry_title = entry_title self._homekit_mode = homekit_mode + self.aid_storage = None self.status = STATUS_READY self.bridge = None @@ -470,7 +455,6 @@ class HomeKit: def setup(self, zeroconf_instance): """Set up bridge and accessory driver.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -503,10 +487,9 @@ class HomeKit: self.driver.config_changed() return - aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE] removed = [] for entity_id in entity_ids: - aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid not in self.bridge.accessories: continue @@ -531,9 +514,6 @@ class HomeKit: def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" - if not self._filter(state.entity_id): - return - # The bridge itself counts as an accessory if len(self.bridge.accessories) + 1 >= MAX_DEVICES: _LOGGER.warning( @@ -555,9 +535,7 @@ class HomeKit: state.entity_id, ) - aid = self.hass.data[DOMAIN][self._entry_id][ - AID_STORAGE - ].get_or_allocate_aid_for_entity_id(state.entity_id) + aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id) conf = self._config.pop(state.entity_id, {}) # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent @@ -578,15 +556,10 @@ class HomeKit: acc = self.bridge.accessories.pop(aid) return acc - async def async_start(self, *args): - """Start the accessory driver.""" - if self.status != STATUS_READY: - return - self.status = STATUS_WAIT - - ent_reg = await entity_registry.async_get_registry(self.hass) - dev_reg = await device_registry.async_get_registry(self.hass) - + async def async_configure_accessories(self): + """Configure accessories for the included states.""" + dev_reg = device_registry.async_get(self.hass) + ent_reg = entity_registry.async_get(self.hass) device_lookup = ent_reg.async_get_device_class_lookup( { (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING), @@ -597,10 +570,9 @@ class HomeKit: } ) - bridged_states = [] + entity_states = [] for state in self.hass.states.async_all(): entity_id = state.entity_id - if not self._filter(entity_id): continue @@ -611,17 +583,40 @@ class HomeKit: ) self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state) - bridged_states.append(state) + entity_states.append(state) - self._async_register_bridge(dev_reg) - await self._async_start(bridged_states) + return entity_states + + async def async_start(self, *args): + """Load storage and start.""" + if self.status != STATUS_READY: + return + self.status = STATUS_WAIT + zc_instance = await zeroconf.async_get_instance(self.hass) + await self.hass.async_add_executor_job(self.setup, zc_instance) + self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) + await self.aid_storage.async_initialize() + await self._async_create_accessories() + self._async_register_bridge() _LOGGER.debug("Driver start for %s", self._name) await self.driver.async_start() self.status = STATUS_RUNNING + if self.driver.state.paired: + return + + show_setup_message( + self.hass, + self._entry_id, + accessory_friendly_name(self._entry_title, self.driver.accessory), + self.driver.state.pincode, + self.driver.accessory.xhm_uri(), + ) + @callback - def _async_register_bridge(self, dev_reg): + def _async_register_bridge(self): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + dev_reg = device_registry.async_get(self.hass) formatted_mac = device_registry.format_mac(self.driver.state.mac) # Connections and identifiers are both used here. # @@ -645,8 +640,9 @@ class HomeKit: identifiers={identifier}, connections={connection}, manufacturer=MANUFACTURER, - name=self._name, - model=f"Home Assistant HomeKit {hk_mode_name}", + name=accessory_friendly_name(self._entry_title, self.driver.accessory), + model=f"HomeKit {hk_mode_name}", + entry_type="service", ) @callback @@ -663,14 +659,13 @@ class HomeKit: for device_id in devices_to_purge: dev_reg.async_remove_device(device_id) - async def _async_start(self, entity_states): - """Start the accessory.""" + async def _async_create_accessories(self): + """Create the accessories.""" + entity_states = await self.async_configure_accessories() if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) - - self.driver.add_accessory(acc) else: self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: @@ -679,15 +674,6 @@ class HomeKit: await self.hass.async_add_executor_job(self.driver.add_accessory, acc) - if not self.driver.state.paired: - show_setup_message( - self.hass, - self._entry_id, - accessory_friendly_name(self._entry_title, self.driver.accessory), - self.driver.state.pincode, - self.driver.accessory.xhm_uri(), - ) - async def async_stop(self, *args): """Stop the accessory driver.""" if self.status != STATUS_RUNNING: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 840e9ebe607..ff45306b351 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -5,7 +5,6 @@ DEBOUNCE_TIMEOUT = 0.5 DEVICE_PRECISION_LEEWAY = 6 DOMAIN = "homekit" HOMEKIT_FILE = ".homekit.state" -AID_STORAGE = "homekit-aid-allocations" HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" HOMEKIT = "homekit" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 46b893bb96d..10550cf4a11 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -487,8 +487,10 @@ def accessory_friendly_name(hass_name, accessory): see both to identify the accessory. """ accessory_mdns_name = accessory.display_name - if hass_name.startswith(accessory_mdns_name): + if hass_name.casefold().startswith(accessory_mdns_name.casefold()): return hass_name + if accessory_mdns_name.casefold().startswith(hass_name.casefold()): + return accessory_mdns_name return f"{hass_name} ({accessory_mdns_name})" diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 228b5f07837..469a0a7deb7 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -14,7 +14,9 @@ def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.Zeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" - ), patch("pyhap.accessory_driver.HAPServer"), patch( + ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( + "pyhap.accessory_driver.HAPServer.async_start" + ), patch( "pyhap.accessory_driver.AccessoryDriver.publish" ), patch( "pyhap.accessory_driver.AccessoryDriver.persist" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 4d2fbfe951d..6d5f3faae95 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,4 +1,5 @@ """Tests for the HomeKit component.""" +import asyncio import os from typing import Dict from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch @@ -23,7 +24,6 @@ from homeassistant.components.homekit import ( ) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - AID_STORAGE, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, @@ -47,7 +47,6 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, PERCENTAGE, SERVICE_RELOAD, STATE_ON, @@ -98,8 +97,28 @@ def _mock_homekit(hass, entry, homekit_mode, entity_filter=None): ) +def _mock_homekit_bridge(hass, entry): + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.driver = MagicMock() + return homekit + + +def _mock_accessories(accessory_count): + accessories = {} + for idx in range(accessory_count + 1): + accessories[idx + 1000] = MagicMock(async_stop=AsyncMock()) + return accessories + + +def _mock_pyhap_bridge(): + return MagicMock( + aid=1, accessories=_mock_accessories(10), display_name="HomeKit Bridge" + ) + + async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, @@ -126,18 +145,16 @@ async def test_setup_min(hass, mock_zeroconf): entry.entry_id, entry.title, ) - assert mock_homekit().setup.called is True # Test auto start enabled - mock_homekit.reset_mock() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - - mock_homekit().async_start.assert_called() + assert mock_homekit().async_start.called is True async def test_setup_auto_start_disabled(hass, mock_zeroconf): """Test async_setup with auto start disabled and test service calls.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, @@ -164,7 +181,6 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf): entry.entry_id, entry.title, ) - assert mock_homekit().setup.called is True # Test auto_start disabled homekit.reset_mock() @@ -237,9 +253,6 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): ) 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, mock_zeroconf): """Test setup with given IP address.""" @@ -321,40 +334,37 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" - + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entry.add_to_hass(hass) - homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - homekit.driver = "driver" - homekit.bridge = mock_bridge = Mock() - homekit.bridge.accessories = range(10) - homekit.async_start = AsyncMock() + homekit = _mock_homekit_bridge(hass, entry) + mock_acc = Mock(category="any") with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mock_acc = Mock(category="any") + homekit.bridge = _mock_pyhap_bridge() with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, mock_acc, None] state = State("light.demo", "on") homekit.add_bridge_accessory(state) mock_get_acc.assert_called_with(hass, ANY, ANY, 1403373688, {}) - assert not mock_bridge.add_accessory.called + assert not homekit.bridge.add_accessory.called state = State("demo.test", "on") homekit.add_bridge_accessory(state) mock_get_acc.assert_called_with(hass, ANY, ANY, 600325356, {}) - assert mock_bridge.add_accessory.called + assert homekit.bridge.add_accessory.called state = State("demo.test_2", "on") homekit.add_bridge_accessory(state) mock_get_acc.assert_called_with(hass, ANY, ANY, 1467253281, {}) - assert mock_bridge.add_accessory.called + assert homekit.bridge.add_accessory.called @pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) @@ -362,29 +372,27 @@ async def test_homekit_warn_add_accessory_bridge( hass, acc_category, mock_zeroconf, caplog ): """Test we warn when adding cameras or tvs to a bridge.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entry.add_to_hass(hass) - homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - homekit.driver = "driver" - homekit.bridge = mock_bridge = Mock() - homekit.bridge.accessories = range(10) - homekit.async_start = AsyncMock() + homekit = _mock_homekit_bridge(hass, entry) with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_camera_acc = Mock(category=acc_category) + homekit.bridge = _mock_pyhap_bridge() with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, mock_camera_acc, None] state = State("camera.test", "on") homekit.add_bridge_accessory(state) mock_get_acc.assert_called_with(hass, ANY, ANY, 1508819236, {}) - assert not mock_bridge.add_accessory.called + assert not homekit.bridge.add_accessory.called assert "accessory mode" in caplog.text @@ -396,12 +404,12 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = "driver" - homekit.bridge = mock_bridge = Mock() - mock_bridge.accessories = {"light.demo": "acc"} + homekit.bridge = _mock_pyhap_bridge() + homekit.bridge.accessories = {"light.demo": "acc"} acc = homekit.remove_bridge_accessory("light.demo") assert acc == "acc" - assert len(mock_bridge.accessories) == 0 + assert len(homekit.bridge.accessories) == 0 async def test_homekit_entity_filter(hass, mock_zeroconf): @@ -413,20 +421,14 @@ async def test_homekit_entity_filter(hass, mock_zeroconf): homekit.bridge = Mock() homekit.bridge.accessories = {} + hass.states.async_set("cover.test", "open") + hass.states.async_set("demo.test", "on") + hass.states.async_set("light.demo", "on") - 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 + filtered_states = await homekit.async_configure_accessories() + assert hass.states.get("cover.test") in filtered_states + assert hass.states.get("demo.test") in filtered_states + assert hass.states.get("light.demo") not in filtered_states async def test_homekit_entity_glob_filter(hass, mock_zeroconf): @@ -441,39 +443,29 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf): homekit.bridge = Mock() homekit.bridge.accessories = {} - with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: - mock_get_acc.return_value = None + hass.states.async_set("cover.test", "open") + hass.states.async_set("demo.test", "on") + hass.states.async_set("cover.excluded_test", "open") + hass.states.async_set("light.included_test", "on") - 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() + filtered_states = await homekit.async_configure_accessories() + assert hass.states.get("cover.test") in filtered_states + assert hass.states.get("demo.test") in filtered_states + assert hass.states.get("cover.excluded_test") not in filtered_states + assert hass.states.get("light.included_test") in filtered_states -async def test_homekit_start(hass, hk_driver, device_reg): +async def test_homekit_start(hass, hk_driver, mock_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass) - pin = b"123-45-678" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) 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") + acc = Accessory(hk_driver, "any") + homekit.driver.accessory = acc connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF") bridge_with_wrong_mac = device_reg.async_get_or_create( @@ -491,8 +483,6 @@ async def test_homekit_start(hass, hk_driver, device_reg): 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.async_start" ) as hk_driver_start: await homekit.async_start() @@ -500,9 +490,8 @@ async def test_homekit_start(hass, hk_driver, device_reg): await hass.async_block_till_done() mock_add_acc.assert_any_call(state) mock_setup_msg.assert_called_with( - hass, entry.entry_id, "Mock Title (any)", pin, ANY + hass, entry.entry_id, "Mock Title (Home Assistant Bridge)", ANY, ANY ) - hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -526,8 +515,6 @@ async def test_homekit_start(hass, hk_driver, device_reg): 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.async_start" ) as hk_driver_start: await homekit.async_start() @@ -545,7 +532,6 @@ async def test_homekit_start(hass, hk_driver, device_reg): async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroconf): """Test HomeKit start method.""" - pin = b"123-45-678" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -565,17 +551,14 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc 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.async_start" ) as hk_driver_start: await homekit.async_start() await hass.async_block_till_done() mock_setup_msg.assert_called_with( - hass, entry.entry_id, "Mock Title (any)", pin, ANY + hass, entry.entry_id, "Mock Title (Home Assistant Bridge)", ANY, ANY ) - hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -616,27 +599,23 @@ async def test_homekit_stop(hass): async def test_homekit_reset_accessories(hass, mock_zeroconf): """Test adding too many accessories to HomeKit.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entity_id = "light.demo" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - 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.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_start" ): await async_init_entry(hass, entry) - aid = hass.data[DOMAIN][entry.entry_id][ - AID_STORAGE - ].get_or_allocate_aid_for_entity_id(entity_id) + aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) homekit.bridge.accessories = {aid: "acc"} homekit.status = STATUS_RUNNING @@ -675,10 +654,8 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco hass.states.async_set("light.demo3", "on") with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - "pyhap.accessory_driver.AccessoryDriver.add_accessory" - ), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( - f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge - ): + f"{PATH_HOMEKIT}.show_setup_message" + ), patch(f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge): await homekit.async_start() await hass.async_block_till_done() assert "would exceed" in caplog.text @@ -693,9 +670,7 @@ async def test_homekit_finds_linked_batteries( homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = hk_driver - # pylint: disable=protected-access - homekit._filter = Mock(return_value=True) - homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + homekit.bridge = MagicMock() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -735,17 +710,15 @@ async def test_homekit_finds_linked_batteries( ) hass.states.async_set(light.entity_id, STATE_ON) - 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.async_start" - ): + with patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( + f"{PATH_HOMEKIT}.get_accessory" + ) as mock_get_acc, patch("pyhap.accessory_driver.AccessoryDriver.async_start"): await homekit.async_start() await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, - hk_driver, + ANY, ANY, ANY, { @@ -766,8 +739,6 @@ async def test_homekit_async_get_integration_fails( homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) 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={}) @@ -817,7 +788,7 @@ async def test_homekit_async_get_integration_fails( mock_get_acc.assert_called_with( hass, - hk_driver, + ANY, ANY, ANY, { @@ -832,6 +803,7 @@ async def test_homekit_async_get_integration_fails( async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): """Test async_setup with imported config.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, @@ -861,7 +833,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): entry.entry_id, entry.title, ) - assert mock_homekit().setup.called is True # Test auto start enabled mock_homekit.reset_mock() @@ -871,20 +842,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): mock_homekit().async_start.assert_called() -async def test_raise_config_entry_not_ready(hass, mock_zeroconf): - """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(f"{PATH_HOMEKIT}.HomeKit.setup", side_effect=OSError): - 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( @@ -917,13 +874,12 @@ async def test_homekit_ignored_missing_devices( hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" + await async_setup_component(hass, "persistent_notification", {}) entry = await async_init_integration(hass) homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = hk_driver - # pylint: disable=protected-access - homekit._filter = Mock(return_value=True) - homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + homekit.bridge = _mock_pyhap_bridge() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -952,25 +908,28 @@ async def test_homekit_ignored_missing_devices( light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id ) - + before_removal = entity_reg.entities.copy() # Delete the device to make sure we fallback # to using the platform device_reg.async_remove_device(device_entry.id) + # Wait for the entities to be removed + await asyncio.sleep(0) + await asyncio.sleep(0) + # Restore the registry + entity_reg.entities = before_removal hass.states.async_set(light.entity_id, STATE_ON) hass.states.async_set("light.two", STATE_ON) - 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.async_start" - ): + with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( + f"{PATH_HOMEKIT}.HomeBridge", return_value=homekit.bridge + ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"): await homekit.async_start() - await hass.async_block_till_done() + await hass.async_block_till_done() mock_get_acc.assert_any_call( hass, - hk_driver, + ANY, ANY, ANY, { @@ -990,8 +949,6 @@ async def test_homekit_finds_linked_motion_sensors( homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) 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={}) @@ -1032,7 +989,7 @@ async def test_homekit_finds_linked_motion_sensors( mock_get_acc.assert_called_with( hass, - hk_driver, + ANY, ANY, ANY, { @@ -1053,7 +1010,6 @@ async def test_homekit_finds_linked_humidity_sensors( homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = hk_driver - homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") config_entry = MockConfigEntry(domain="test", data={}) @@ -1097,7 +1053,7 @@ async def test_homekit_finds_linked_humidity_sensors( mock_get_acc.assert_called_with( hass, - hk_driver, + ANY, ANY, ANY, { @@ -1111,6 +1067,7 @@ async def test_homekit_finds_linked_humidity_sensors( async def test_reload(hass, mock_zeroconf): """Test we can reload from yaml.""" + await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, @@ -1121,7 +1078,6 @@ async def test_reload(hass, mock_zeroconf): 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}} ) @@ -1140,7 +1096,6 @@ async def test_reload(hass, mock_zeroconf): entry.entry_id, entry.title, ) - assert mock_homekit().setup.called is True yaml_path = os.path.join( _get_fixtures_base_path(), "fixtures", @@ -1156,7 +1111,6 @@ async def test_reload(hass, mock_zeroconf): "pyhap.accessory_driver.AccessoryDriver.async_start" ): mock_homekit2.return_value = homekit = Mock() - type(homekit).async_start = AsyncMock() await hass.services.async_call( "homekit", SERVICE_RELOAD, @@ -1178,33 +1132,30 @@ async def test_reload(hass, mock_zeroconf): entry.entry_id, entry.title, ) - 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): +async def test_homekit_start_in_accessory_mode( + hass, hk_driver, mock_zeroconf, device_reg +): """Test HomeKit start method in accessory mode.""" entry = await async_init_integration(hass) - pin = b"123-45-678" - homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) 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( + f"{PATH_HOMEKIT}.show_setup_message" + ) as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: await homekit.async_start() @@ -1212,7 +1163,7 @@ async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg): await hass.async_block_till_done() mock_add_acc.assert_not_called() mock_setup_msg.assert_called_with( - hass, entry.entry_id, "Mock Title (any)", pin, ANY + hass, entry.entry_id, "Mock Title (demo)", ANY, ANY ) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 9b03d616002..c458d1dc4ef 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -294,5 +294,7 @@ async def test_accessory_friendly_name(): accessory = Mock() accessory.display_name = "same" - assert accessory_friendly_name("same", accessory) == "same" + assert accessory_friendly_name("Same", accessory) == "Same" assert accessory_friendly_name("hass title", accessory) == "hass title (same)" + accessory.display_name = "Hass title 123" + assert accessory_friendly_name("hass title", accessory) == "Hass title 123"