Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recreate HomeKit accessories when calling the reset_accessory service #53199

Merged
merged 6 commits into from Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 80 additions & 39 deletions homeassistant/components/homekit/__init__.py
Expand Up @@ -120,6 +120,10 @@

MDNS_TARGET_IP = "224.0.0.251"

_HOMEKIT_CONFIG_UPDATE_TIME = (
5 # number of seconds to wait for homekit to see the c# change
)


def _has_all_unique_names_and_ports(bridges):
"""Validate that each homekit bridge configured has a unique name."""
Expand Down Expand Up @@ -351,7 +355,7 @@ def _async_register_events_and_services(hass: HomeAssistant):
"""Register events and services for HomeKit."""
hass.http.register_view(HomeKitPairingQRView)

def handle_homekit_reset_accessory(service):
async def async_handle_homekit_reset_accessory(service):
"""Handle start HomeKit service call."""
for entry_id in hass.data[DOMAIN]:
if HOMEKIT not in hass.data[DOMAIN][entry_id]:
Expand All @@ -365,12 +369,12 @@ def handle_homekit_reset_accessory(service):
continue

entity_ids = service.data.get("entity_id")
homekit.reset_accessories(entity_ids)
await homekit.async_reset_accessories(entity_ids)

hass.services.async_register(
DOMAIN,
SERVICE_HOMEKIT_RESET_ACCESSORY,
handle_homekit_reset_accessory,
async_handle_homekit_reset_accessory,
schema=RESET_ACCESSORY_SERVICE_SCHEMA,
)

Expand Down Expand Up @@ -486,36 +490,61 @@ def setup(self, async_zeroconf_instance):

self.driver.persist()

def reset_accessories(self, entity_ids):
async def async_reset_accessories(self, entity_ids):
"""Reset the accessory to load the latest configuration."""
if not self.bridge:
self.driver.config_changed()
await self.async_reset_accessories_in_accessory_mode(entity_ids)
return
await self.async_reset_accessories_in_bridge_mode(entity_ids)

removed = []
async def async_reset_accessories_in_accessory_mode(self, entity_ids):
"""Reset accessories in accessory mode."""
acc = self.driver.accessory
if acc.entity_id not in entity_ids:
return
acc.async_stop()
if not (state := self.hass.states.get(acc.entity_id)):
_LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity
)
return
if new_acc := self._async_create_single_accessory([state]):
self.driver.accessory = new_acc
await self.async_config_changed()

async def async_reset_accessories_in_bridge_mode(self, entity_ids):
"""Reset accessories in bridge mode."""
new = []
for entity_id in entity_ids:
aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
if aid not in self.bridge.accessories:
continue

_LOGGER.info(
"HomeKit Bridge %s will reset accessory with linked entity_id %s",
self._name,
entity_id,
)

acc = self.remove_bridge_accessory(aid)
removed.append(acc)
if state := self.hass.states.get(acc.entity_id):
new.append(state)
else:
_LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity
)

if not removed:
if not new:
# No matched accessories, probably on another bridge
return

self.driver.config_changed()
await self.async_config_changed()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
for state in new:
self.add_bridge_accessory(state)
await self.async_config_changed()

for acc in removed:
self.bridge.add_accessory(acc)
self.driver.config_changed()
async def async_config_changed(self):
"""Call config changed which writes out the new config to disk."""
await self.hass.async_add_executor_job(self.driver.config_changed)

def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
Expand All @@ -541,7 +570,7 @@ def add_bridge_accessory(self, state):
)

aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id)
conf = self._config.pop(state.entity_id, {})
conf = self._config.get(state.entity_id, {}).copy()
# If an accessory cannot be created or added due to an exception
# of any kind (usually in pyhap) it should not prevent
# the rest of the accessories from being created
Expand All @@ -556,9 +585,9 @@ def add_bridge_accessory(self, state):

def remove_bridge_accessory(self, aid):
"""Try adding accessory to bridge if configured beforehand."""
acc = None
if aid in self.bridge.accessories:
acc = self.bridge.accessories.pop(aid)
acc = self.bridge.accessories.pop(aid, None)
if acc:
acc.async_stop()
return acc

async def async_configure_accessories(self):
Expand Down Expand Up @@ -665,33 +694,45 @@ def _async_purge_old_bridges(self, dev_reg, identifier, connection):
for device_id in devices_to_purge:
dev_reg.async_remove_device(device_id)

@callback
def _async_create_single_accessory(self, entity_states):
"""Create a single HomeKit accessory (accessory mode)."""
if not entity_states:
_LOGGER.error(
"HomeKit %s cannot startup: entity not available: %s",
self._name,
self._filter.config,
)
return None
state = entity_states[0]
conf = self._config.get(state.entity_id, {}).copy()
acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf)
if acc is None:
_LOGGER.error(
"HomeKit %s cannot startup: entity not supported: %s",
self._name,
self._filter.config,
)
return acc

@callback
def _async_create_bridge_accessory(self, entity_states):
"""Create a HomeKit bridge with accessories. (bridge mode)."""
self.bridge = HomeBridge(self.hass, self.driver, self._name)
for state in entity_states:
self.add_bridge_accessory(state)
return self.bridge

async def _async_create_accessories(self):
"""Create the accessories."""
entity_states = await self.async_configure_accessories()
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
if not entity_states:
_LOGGER.error(
"HomeKit %s cannot startup: entity not available: %s",
self._name,
self._filter.config,
)
return False
state = entity_states[0]
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf)
if acc is None:
_LOGGER.error(
"HomeKit %s cannot startup: entity not supported: %s",
self._name,
self._filter.config,
)
return False
acc = self._async_create_single_accessory(entity_states)
else:
self.bridge = HomeBridge(self.hass, self.driver, self._name)
for state in entity_states:
self.add_bridge_accessory(state)
acc = self.bridge
acc = self._async_create_bridge_accessory(entity_states)

if acc is None:
return False
# No need to load/persist as we do it in setup
self.driver.accessory = acc
return True
Expand Down