Add a homekit.unpair service to forcefully remove pairings (#53303)

- Sometimes homekit will go unresponsive because a pairing for a specific
  device is missing. To avoid deleting the config entry and recreating
  it, which can be a painful process if there are many bridged entities,
  the homekit.unpair service allows forceful removal of the pairings so
  the accessory can be paired again.
This commit is contained in:
J. Nick Koston 2021-07-22 00:44:36 -10:00 committed by GitHub
parent 80c535f02e
commit 009f34bfed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 193 additions and 3 deletions

View file

@ -23,6 +23,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_IP_ADDRESS,
CONF_NAME,
@ -34,11 +35,12 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.exceptions import Unauthorized
from homeassistant.exceptions import HomeAssistantError, 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
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_extract_referenced_entity_ids
from homeassistant.loader import IntegrationNotFound, async_get_integration
from . import ( # noqa: F401
@ -93,6 +95,7 @@ from .const import (
MANUFACTURER,
SERVICE_HOMEKIT_RESET_ACCESSORY,
SERVICE_HOMEKIT_START,
SERVICE_HOMEKIT_UNPAIR,
SHUTDOWN_TIMEOUT,
)
from .util import (
@ -170,6 +173,12 @@ RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema(
)
UNPAIR_SERVICE_SCHEMA = vol.All(
vol.Schema(cv.ENTITY_SERVICE_FIELDS),
cv.has_at_least_one_key(ATTR_DEVICE_ID),
)
def _async_get_entries_by_name(current_entries):
"""Return a dict of the entries by name."""
@ -356,7 +365,7 @@ def _async_register_events_and_services(hass: HomeAssistant):
hass.http.register_view(HomeKitPairingQRView)
async def async_handle_homekit_reset_accessory(service):
"""Handle start HomeKit service call."""
"""Handle reset accessory HomeKit service call."""
for entry_id in hass.data[DOMAIN]:
if HOMEKIT not in hass.data[DOMAIN][entry_id]:
continue
@ -378,6 +387,44 @@ def _async_register_events_and_services(hass: HomeAssistant):
schema=RESET_ACCESSORY_SERVICE_SCHEMA,
)
async def async_handle_homekit_unpair(service):
"""Handle unpair HomeKit service call."""
referenced = await async_extract_referenced_entity_ids(hass, service)
dev_reg = device_registry.async_get(hass)
for device_id in referenced.referenced_devices:
dev_reg_ent = dev_reg.async_get(device_id)
if not dev_reg_ent:
raise HomeAssistantError(f"No device found for device id: {device_id}")
macs = [
cval
for ctype, cval in dev_reg_ent.connections
if ctype == device_registry.CONNECTION_NETWORK_MAC
]
domain_data = hass.data[DOMAIN]
matching_instances = [
domain_data[entry_id][HOMEKIT]
for entry_id in domain_data
if HOMEKIT in domain_data[entry_id]
and domain_data[entry_id][HOMEKIT].driver
and device_registry.format_mac(
domain_data[entry_id][HOMEKIT].driver.state.mac
)
in macs
]
if not matching_instances:
raise HomeAssistantError(
f"No homekit accessory found for device id: {device_id}"
)
for homekit in matching_instances:
homekit.async_unpair()
hass.services.async_register(
DOMAIN,
SERVICE_HOMEKIT_UNPAIR,
async_handle_homekit_unpair,
schema=UNPAIR_SERVICE_SCHEMA,
)
async def async_handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
tasks = []
@ -639,7 +686,11 @@ class HomeKit:
if self.driver.state.paired:
return
self._async_show_setup_message()
@callback
def _async_show_setup_message(self):
"""Show the pairing setup message."""
show_setup_message(
self.hass,
self._entry_id,
@ -648,6 +699,16 @@ class HomeKit:
self.driver.accessory.xhm_uri(),
)
@callback
def async_unpair(self):
"""Remove all pairings for an accessory so it can be repaired."""
state = self.driver.state
for client_uuid in list(state.paired_clients):
state.remove_paired_client(client_uuid)
self.driver.async_persist()
self.driver.async_update_advertisement()
self._async_show_setup_message()
@callback
def _async_register_bridge(self):
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""