Automatically create HomeKit accessory mode entries (#46473)

When we set up HomeKit, we asked users if they wanted
to create an entry in bridge or accessory mode.

This approach required the user to understand how HomeKit works and
choose which type to create.

When the user includes the media player or camera domains,
we exclude them from the bridge and create the additional entries
for each entity in accessory mode.
This commit is contained in:
J. Nick Koston 2021-02-23 18:22:23 -06:00 committed by GitHub
parent 9159f54900
commit 87cbbcb014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 472 additions and 428 deletions

View File

@ -5,7 +5,7 @@ import logging
import os
from aiohttp import web
from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION, STANDALONE_AID
from pyhap.const import STANDALONE_AID
import voluptuous as vol
from homeassistant.components import zeroconf
@ -70,6 +70,7 @@ from .const import (
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_ENTRY_INDEX,
CONF_EXCLUDE_ACCESSORY_MODE,
CONF_FILTER,
CONF_HOMEKIT_MODE,
CONF_LINKED_BATTERY_CHARGING_SENSOR,
@ -81,6 +82,7 @@ from .const import (
CONF_ZEROCONF_DEFAULT_INTERFACE,
CONFIG_OPTIONS,
DEFAULT_AUTO_START,
DEFAULT_EXCLUDE_ACCESSORY_MODE,
DEFAULT_HOMEKIT_MODE,
DEFAULT_PORT,
DEFAULT_SAFE_MODE,
@ -97,11 +99,13 @@ from .const import (
UNDO_UPDATE_LISTENER,
)
from .util import (
accessory_friendly_name,
dismiss_setup_message,
get_persist_fullpath_for_entry_id,
port_is_available,
remove_state_files_for_entry_id,
show_setup_message,
state_needs_accessory_mode,
validate_entity_config,
)
@ -243,6 +247,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# ip_address and advertise_ip are yaml only
ip_address = conf.get(CONF_IP_ADDRESS)
advertise_ip = conf.get(CONF_ADVERTISE_IP)
# exclude_accessory_mode is only used for config flow
# to indicate that the config entry was setup after
# we started creating config entries for entities that
# to run in accessory mode and that we should never include
# these entities on the bridge. For backwards compatibility
# with users who have not migrated yet we do not do exclude
# these entities by default as we cannot migrate automatically
# since it requires a re-pairing.
exclude_accessory_mode = conf.get(
CONF_EXCLUDE_ACCESSORY_MODE, DEFAULT_EXCLUDE_ACCESSORY_MODE
)
homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
@ -254,10 +269,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
port,
ip_address,
entity_filter,
exclude_accessory_mode,
entity_config,
homekit_mode,
advertise_ip,
entry.entry_id,
entry.title,
)
zeroconf_instance = await zeroconf.async_get_instance(hass)
@ -427,10 +444,12 @@ class HomeKit:
port,
ip_address,
entity_filter,
exclude_accessory_mode,
entity_config,
homekit_mode,
advertise_ip=None,
entry_id=None,
entry_title=None,
):
"""Initialize a HomeKit object."""
self.hass = hass
@ -439,8 +458,10 @@ class HomeKit:
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
self._exclude_accessory_mode = exclude_accessory_mode
self._advertise_ip = advertise_ip
self._entry_id = entry_id
self._entry_title = entry_title
self._homekit_mode = homekit_mode
self.status = STATUS_READY
@ -457,6 +478,7 @@ class HomeKit:
self.hass,
self._entry_id,
self._name,
self._entry_title,
loop=self.hass.loop,
address=ip_addr,
port=self._port,
@ -518,6 +540,18 @@ class HomeKit:
)
return
if state_needs_accessory_mode(state):
if self._exclude_accessory_mode:
return
_LOGGER.warning(
"The bridge %s has entity %s. For best performance, "
"and to prevent unexpected unavailability, create and "
"pair a separate HomeKit instance in accessory mode for "
"this entity.",
self._name,
state.entity_id,
)
aid = self.hass.data[DOMAIN][self._entry_id][
AID_STORAGE
].get_or_allocate_aid_for_entity_id(state.entity_id)
@ -528,24 +562,6 @@ class HomeKit:
try:
acc = get_accessory(self.hass, self.driver, state, aid, conf)
if acc is not None:
if acc.category == CATEGORY_CAMERA:
_LOGGER.warning(
"The bridge %s has camera %s. For best performance, "
"and to prevent unexpected unavailability, create and "
"pair a separate HomeKit instance in accessory mode for "
"each camera.",
self._name,
acc.entity_id,
)
elif acc.category == CATEGORY_TELEVISION:
_LOGGER.warning(
"The bridge %s has tv %s. For best performance, "
"and to prevent unexpected unavailability, create and "
"pair a separate HomeKit instance in accessory mode for "
"each tv media player.",
self._name,
acc.entity_id,
)
self.bridge.add_accessory(acc)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
@ -650,6 +666,7 @@ class HomeKit:
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)
@ -663,7 +680,7 @@ class HomeKit:
show_setup_message(
self.hass,
self._entry_id,
self._name,
accessory_friendly_name(self._entry_title, self.driver.accessory),
self.driver.state.pincode,
self.driver.accessory.xhm_uri(),
)

View File

@ -71,6 +71,7 @@ from .const import (
TYPE_VALVE,
)
from .util import (
accessory_friendly_name,
convert_to_float,
dismiss_setup_message,
format_sw_version,
@ -489,12 +490,13 @@ class HomeBridge(Bridge):
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""
def __init__(self, hass, entry_id, bridge_name, **kwargs):
def __init__(self, hass, entry_id, bridge_name, entry_title, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(**kwargs)
self.hass = hass
self._entry_id = entry_id
self._bridge_name = bridge_name
self._entry_title = entry_title
def pair(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
@ -506,10 +508,14 @@ class HomeDriver(AccessoryDriver):
def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().unpair(client_uuid)
if self.state.paired:
return
show_setup_message(
self.hass,
self._entry_id,
self._bridge_name,
accessory_friendly_name(self._entry_title, self.accessory),
self.state.pincode,
self.accessory.xhm_uri(),
)

View File

@ -1,10 +1,13 @@
"""Config flow for HomeKit integration."""
import random
import re
import string
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
@ -26,6 +29,7 @@ from homeassistant.helpers.entityfilter import (
from .const import (
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_EXCLUDE_ACCESSORY_MODE,
CONF_FILTER,
CONF_HOMEKIT_MODE,
CONF_VIDEO_CODEC,
@ -33,13 +37,13 @@ from .const import (
DEFAULT_CONFIG_FLOW_PORT,
DEFAULT_HOMEKIT_MODE,
HOMEKIT_MODE_ACCESSORY,
HOMEKIT_MODE_BRIDGE,
HOMEKIT_MODES,
SHORT_ACCESSORY_NAME,
SHORT_BRIDGE_NAME,
VIDEO_CODEC_COPY,
)
from .const import DOMAIN # pylint:disable=unused-import
from .util import async_find_next_available_port
from .util import async_find_next_available_port, state_needs_accessory_mode
CONF_CAMERA_COPY = "camera_copy"
CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode"
@ -49,11 +53,16 @@ MODE_EXCLUDE = "exclude"
INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE]
DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN]
NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN]
CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}."
SUPPORTED_DOMAINS = [
"alarm_control_panel",
"automation",
"binary_sensor",
"camera",
CAMERA_DOMAIN,
"climate",
"cover",
"demo",
@ -63,7 +72,7 @@ SUPPORTED_DOMAINS = [
"input_boolean",
"light",
"lock",
"media_player",
MEDIA_PLAYER_DOMAIN,
"person",
"remote",
"scene",
@ -77,22 +86,18 @@ SUPPORTED_DOMAINS = [
DEFAULT_DOMAINS = [
"alarm_control_panel",
"climate",
CAMERA_DOMAIN,
"cover",
"humidifier",
"fan",
"light",
"lock",
"media_player",
MEDIA_PLAYER_DOMAIN,
"switch",
"vacuum",
"water_heater",
]
DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"]
CAMERA_DOMAIN = "camera"
CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}."
_EMPTY_ENTITY_FILTER = {
CONF_INCLUDE_DOMAINS: [],
CONF_EXCLUDE_DOMAINS: [],
@ -110,32 +115,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize config flow."""
self.hk_data = {}
self.entry_title = None
async def async_step_accessory_mode(self, user_input=None):
"""Choose specific entity in accessory mode."""
if user_input is not None:
entity_id = user_input[CONF_ENTITY_ID]
entity_filter = _EMPTY_ENTITY_FILTER.copy()
entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
self.hk_data[CONF_FILTER] = entity_filter
if entity_id.startswith(CAMERA_ENTITY_PREFIX):
self.hk_data[CONF_ENTITY_CONFIG] = {
entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
}
return await self.async_step_pairing()
all_supported_entities = _async_get_matching_entities(
self.hass, domains=DOMAINS_PREFER_ACCESSORY_MODE
)
return self.async_show_form(
step_id="accessory_mode",
data_schema=vol.Schema(
{vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)}
),
)
async def async_step_bridge_mode(self, user_input=None):
async def async_step_user(self, user_input=None):
"""Choose specific domains in bridge mode."""
if user_input is not None:
entity_filter = _EMPTY_ENTITY_FILTER.copy()
@ -143,9 +124,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.hk_data[CONF_FILTER] = entity_filter
return await self.async_step_pairing()
self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE
default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS
return self.async_show_form(
step_id="bridge_mode",
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
@ -158,43 +140,72 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_pairing(self, user_input=None):
"""Pairing instructions."""
if user_input is not None:
return self.async_create_entry(title=self.entry_title, data=self.hk_data)
port = await async_find_next_available_port(
self.hass, DEFAULT_CONFIG_FLOW_PORT
)
await self._async_add_entries_for_accessory_mode_entities(port)
self.hk_data[CONF_PORT] = port
include_domains_filter = self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
for domain in NEVER_BRIDGED_DOMAINS:
if domain in include_domains_filter:
include_domains_filter.remove(domain)
return self.async_create_entry(
title=f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}",
data=self.hk_data,
)
self.hk_data[CONF_PORT] = await async_find_next_available_port(
self.hass, DEFAULT_CONFIG_FLOW_PORT
)
self.hk_data[CONF_NAME] = self._async_available_name(
self.hk_data[CONF_HOMEKIT_MODE]
)
self.entry_title = f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}"
self.hk_data[CONF_NAME] = self._async_available_name(SHORT_BRIDGE_NAME)
self.hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True
return self.async_show_form(
step_id="pairing",
description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]},
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
async def _async_add_entries_for_accessory_mode_entities(self, last_assigned_port):
"""Generate new flows for entities that need their own instances."""
accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode(
self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
)
exiting_entity_ids_accessory_mode = _async_entity_ids_with_accessory_mode(
self.hass
)
next_port_to_check = last_assigned_port + 1
for entity_id in accessory_mode_entity_ids:
if entity_id in exiting_entity_ids_accessory_mode:
continue
port = await async_find_next_available_port(self.hass, next_port_to_check)
next_port_to_check = port + 1
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "accessory"},
data={CONF_ENTITY_ID: entity_id, CONF_PORT: port},
)
)
if user_input is not None:
self.hk_data = {
CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE],
async def async_step_accessory(self, accessory_input):
"""Handle creation a single accessory in accessory mode."""
entity_id = accessory_input[CONF_ENTITY_ID]
port = accessory_input[CONF_PORT]
state = self.hass.states.get(entity_id)
name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id
entity_filter = _EMPTY_ENTITY_FILTER.copy()
entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
entry_data = {
CONF_PORT: port,
CONF_NAME: self._async_available_name(name),
CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY,
CONF_FILTER: entity_filter,
}
if entity_id.startswith(CAMERA_ENTITY_PREFIX):
entry_data[CONF_ENTITY_CONFIG] = {
entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
}
if user_input[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
return await self.async_step_accessory_mode()
return await self.async_step_bridge_mode()
homekit_mode = self.hk_data.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
HOMEKIT_MODES
)
}
),
errors=errors,
return self.async_create_entry(
title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data
)
async def async_step_import(self, user_input=None):
@ -215,21 +226,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
@callback
def _async_available_name(self, homekit_mode):
def _async_available_name(self, requested_name):
"""Return an available for the bridge."""
current_names = self._async_current_names()
valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name)
base_name = SHORT_BRIDGE_NAME
if homekit_mode == HOMEKIT_MODE_ACCESSORY:
base_name = SHORT_ACCESSORY_NAME
if valid_mdns_name not in current_names:
return valid_mdns_name
# We always pick a RANDOM name to avoid Zeroconf
# name collisions. If the name has been seen before
# pairing will probably fail.
acceptable_chars = string.ascii_uppercase + string.digits
acceptable_mdns_chars = string.ascii_uppercase + string.digits
suggested_name = None
while not suggested_name or suggested_name in self._async_current_names():
trailer = "".join(random.choices(acceptable_chars, k=4))
suggested_name = f"{base_name} {trailer}"
while not suggested_name or suggested_name in current_names:
trailer = "".join(random.choices(acceptable_mdns_chars, k=2))
suggested_name = f"{valid_mdns_name} {trailer}"
return suggested_name
@ -447,7 +456,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
def _async_get_matching_entities(hass, domains=None):
"""Fetch all entities or entities in the given domains."""
return {
state.entity_id: f"{state.entity_id} ({state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)})"
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
for state in sorted(
hass.states.async_all(domains and set(domains)),
key=lambda item: item.entity_id,
@ -457,7 +466,41 @@ def _async_get_matching_entities(hass, domains=None):
def _domains_set_from_entities(entity_ids):
"""Build a set of domains for the given entity ids."""
domains = set()
for entity_id in entity_ids:
domains.add(split_entity_id(entity_id)[0])
return domains
return {split_entity_id(entity_id)[0] for entity_id in entity_ids}
@callback
def _async_get_entity_ids_for_accessory_mode(hass, include_domains):
"""Build a list of entities that should be paired in accessory mode."""
accessory_mode_domains = {
domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE
}
if not accessory_mode_domains:
return []
return [
state.entity_id
for state in hass.states.async_all(accessory_mode_domains)
if state_needs_accessory_mode(state)
]
@callback
def _async_entity_ids_with_accessory_mode(hass):
"""Return a set of entity ids that have config entries in accessory mode."""
entity_ids = set()
current_entries = hass.config_entries.async_entries(DOMAIN)
for entry in current_entries:
# We have to handle the case where the data has not yet
# been migrated to options because the data was just
# imported and the entry was never started
target = entry.options if CONF_HOMEKIT_MODE in entry.options else entry.data
if target.get(CONF_HOMEKIT_MODE) != HOMEKIT_MODE_ACCESSORY:
continue
entity_ids.add(target[CONF_FILTER][CONF_INCLUDE_ENTITIES][0])
return entity_ids

View File

@ -42,6 +42,7 @@ CONF_ENTITY_CONFIG = "entity_config"
CONF_FEATURE = "feature"
CONF_FEATURE_LIST = "feature_list"
CONF_FILTER = "filter"
CONF_EXCLUDE_ACCESSORY_MODE = "exclude_accessory_mode"
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
CONF_LINKED_DOORBELL_SENSOR = "linked_doorbell_sensor"
@ -68,6 +69,7 @@ DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS
DEFAULT_AUDIO_MAP = "0:a:0"
DEFAULT_AUDIO_PACKET_SIZE = 188
DEFAULT_AUTO_START = True
DEFAULT_EXCLUDE_ACCESSORY_MODE = False
DEFAULT_LOW_BATTERY_THRESHOLD = 20
DEFAULT_MAX_FPS = 30
DEFAULT_MAX_HEIGHT = 1080

View File

@ -8,7 +8,7 @@
"init": {
"data": {
"mode": "[%key:common::config_flow::data::mode%]",
"include_domains": "[%key:component::homekit::config::step::bridge_mode::data::include_domains%]"
"include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
},
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
"title": "Select domains to be included."
@ -18,7 +18,7 @@
"mode": "[%key:common::config_flow::data::mode%]",
"entities": "Entities"
},
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.",
"title": "Select entities to be included"
},
"cameras": {
@ -40,29 +40,15 @@
"config": {
"step": {
"user": {
"data": {
"mode": "[%key:common::config_flow::data::mode%]"
},
"description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
"title": "Activate HomeKit"
},
"accessory_mode": {
"data": {
"entity_id": "Entity"
},
"description": "Choose the entity to be included. In accessory mode, only a single entity is included.",
"title": "Select entity to be included"
},
"bridge_mode": {
"data": {
"include_domains": "Domains to include"
},
"description": "Choose the domains to be included. All supported entities in the domain will be included.",
"description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.",
"title": "Select domains to be included"
},
"pairing": {
"title": "Pair HomeKit",
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
"description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d."
}
},
"abort": {

View File

@ -4,32 +4,16 @@
"port_name_in_use": "An accessory or bridge with the same name or port is already configured."
},
"step": {
"accessory_mode": {
"data": {
"entity_id": "Entity"
},
"description": "Choose the entity to be included. In accessory mode, only a single entity is included.",
"title": "Select entity to be included"
},
"bridge_mode": {
"data": {
"include_domains": "Domains to include"
},
"description": "Choose the domains to be included. All supported entities in the domain will be included.",
"title": "Select domains to be included"
},
"pairing": {
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.",
"description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.",
"title": "Pair HomeKit"
},
"user": {
"data": {
"auto_start": "Autostart (disable if using Z-Wave or other delayed start system)",
"include_domains": "Domains to include",
"mode": "Mode"
"include_domains": "Domains to include"
},
"description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
"title": "Activate HomeKit"
"description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.",
"title": "Select domains to be included"
}
}
},
@ -37,8 +21,7 @@
"step": {
"advanced": {
"data": {
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)",
"safe_mode": "Safe Mode (enable only if pairing fails)"
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
},
"description": "These settings only need to be adjusted if HomeKit is not functional.",
"title": "Advanced Configuration"
@ -55,7 +38,7 @@
"entities": "Entities",
"mode": "Mode"
},
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.",
"title": "Select entities to be included"
},
"init": {

View File

@ -11,8 +11,14 @@ import pyqrcode
import voluptuous as vol
from homeassistant.components import binary_sensor, media_player, sensor
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.media_player import (
DEVICE_CLASS_TV,
DOMAIN as MEDIA_PLAYER_DOMAIN,
)
from homeassistant.const import (
ATTR_CODE,
ATTR_DEVICE_CLASS,
ATTR_SUPPORTED_FEATURES,
CONF_NAME,
CONF_PORT,
@ -328,9 +334,7 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri):
f"### {pin}\n"
f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})"
)
hass.components.persistent_notification.create(
message, "HomeKit Bridge Setup", entry_id
)
hass.components.persistent_notification.create(message, "HomeKit Pairing", entry_id)
def dismiss_setup_message(hass, entry_id):
@ -473,3 +477,30 @@ def pid_is_alive(pid) -> bool:
except OSError:
pass
return False
def accessory_friendly_name(hass_name, accessory):
"""Return the combined name for the accessory.
The mDNS name and the Home Assistant config entry
name are usually different which means they need to
see both to identify the accessory.
"""
accessory_mdns_name = accessory.display_name
if hass_name.startswith(accessory_mdns_name):
return hass_name
return f"{hass_name} ({accessory_mdns_name})"
def state_needs_accessory_mode(state):
"""Return if the entity represented by the state must be paired in accessory mode."""
if state.domain == CAMERA_DOMAIN:
return True
if (
state.domain == MEDIA_PLAYER_DOMAIN
and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV
):
return True
return False

View File

@ -603,13 +603,19 @@ def test_home_driver():
with patch("pyhap.accessory_driver.AccessoryDriver.__init__") as mock_driver:
driver = HomeDriver(
"hass", "entry_id", "name", address=ip_address, port=port, persist_file=path
"hass",
"entry_id",
"name",
"title",
address=ip_address,
port=port,
persist_file=path,
)
mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path)
driver.state = Mock(pincode=pin)
driver.state = Mock(pincode=pin, paired=False)
xhm_uri_mock = Mock(return_value="X-HM://0")
driver.accessory = Mock(xhm_uri=xhm_uri_mock)
driver.accessory = Mock(display_name="any", xhm_uri=xhm_uri_mock)
# pair
with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch(
@ -627,4 +633,4 @@ def test_home_driver():
driver.unpair("client_uuid")
mock_unpair.assert_called_with("client_uuid")
mock_show_msg.assert_called_with("hass", "entry_id", "name", pin, "X-HM://0")
mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0")

View File

@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.homekit.const import DOMAIN
from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_NAME, CONF_PORT
@ -39,48 +39,41 @@ async def test_setup_in_bridge_mode(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"mode": "bridge"},
{"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "bridge_mode"
assert result2["step_id"] == "pairing"
with patch(
"homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{"include_domains": ["light"]},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == "pairing"
with patch(
), patch(
"homeassistant.components.homekit.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.homekit.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"][:11] == "HASS Bridge"
bridge_name = (result4["title"].split(":"))[0]
assert result4["data"] == {
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
bridge_name = (result3["title"].split(":"))[0]
assert bridge_name == SHORT_BRIDGE_NAME
assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": ["light"],
"include_entities": [],
},
"exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"port": 12345,
@ -89,64 +82,147 @@ async def test_setup_in_bridge_mode(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_setup_in_accessory_mode(hass):
"""Test we can setup a new instance in accessory."""
async def test_setup_in_bridge_mode_name_taken(hass):
"""Test we can setup a new instance in bridge mode when the name is taken."""
await setup.async_setup_component(hass, "persistent_notification", {})
hass.states.async_set("camera.mine", "off")
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_NAME: SHORT_BRIDGE_NAME, CONF_PORT: 8000},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"mode": "accessory"},
{"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "accessory_mode"
assert result2["step_id"] == "pairing"
with patch(
"homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{"entity_id": "camera.mine"},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == "pairing"
with patch(
), patch(
"homeassistant.components.homekit.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.homekit.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"][:14] == "HASS Accessory"
bridge_name = (result4["title"].split(":"))[0]
assert result4["data"] == {
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] != SHORT_BRIDGE_NAME
assert result3["title"].startswith(SHORT_BRIDGE_NAME)
bridge_name = (result3["title"].split(":"))[0]
assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": [],
"include_entities": ["camera.mine"],
"include_domains": ["light"],
"include_entities": [],
},
"mode": "accessory",
"exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"entity_config": {"camera.mine": {"video_codec": "copy"}},
"port": 12345,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 2
async def test_setup_creates_entries_for_accessory_mode_devices(hass):
"""Test we can setup a new instance and we create entries for accessory mode devices."""
hass.states.async_set("camera.one", "on")
hass.states.async_set("camera.existing", "on")
hass.states.async_set("media_player.two", "on", {"device_class": "tv"})
bridge_mode_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_NAME: "bridge", CONF_PORT: 8001},
options={
"mode": "bridge",
"filter": {
"include_entities": ["camera.existing"],
},
},
)
bridge_mode_entry.add_to_hass(hass)
accessory_mode_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_NAME: "accessory", CONF_PORT: 8000},
options={
"mode": "accessory",
"filter": {
"include_entities": ["camera.existing"],
},
},
)
accessory_mode_entry.add_to_hass(hass)
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"include_domains": ["camera", "media_player", "light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "pairing"
with patch(
"homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
), patch(
"homeassistant.components.homekit.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.homekit.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"][:11] == "HASS Bridge"
bridge_name = (result3["title"].split(":"))[0]
assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": ["media_player", "light"],
"include_entities": [],
},
"exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"port": 12345,
}
assert len(mock_setup.mock_calls) == 1
#
# Existing accessory mode entries should get setup but not duplicated
#
# 1 - existing accessory for camera.existing
# 2 - existing bridge for camera.one
# 3 - new bridge
# 4 - camera.one in accessory mode
# 5 - media_player.two in accessory mode
assert len(mock_setup_entry.mock_calls) == 5
async def test_import(hass):
@ -656,55 +732,48 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"mode": "bridge"},
{"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "bridge_mode"
with patch(
"homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{"include_domains": ["light"]},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == "pairing"
assert result2["step_id"] == "pairing"
# We need to actually setup the config entry or the data
# will not get migrated to options
with patch(
"homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
), patch(
"homeassistant.components.homekit.HomeKit.async_start",
return_value=True,
) as mock_async_start:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"][:11] == "HASS Bridge"
bridge_name = (result4["title"].split(":"))[0]
assert result4["data"] == {
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"][:11] == "HASS Bridge"
bridge_name = (result3["title"].split(":"))[0]
assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": ["light"],
"include_entities": [],
},
"exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"port": 12345,
}
assert len(mock_async_start.mock_calls) == 1
config_entry = result4["result"]
config_entry = result3["result"]
hass.states.async_set("camera.tv", "off")
hass.states.async_set("camera.sonos", "off")

View File

@ -82,6 +82,22 @@ def entity_reg_fixture(hass):
return mock_registry(hass)
def _mock_homekit(hass, entry, homekit_mode, entity_filter=None):
return HomeKit(
hass=hass,
name=BRIDGE_NAME,
port=DEFAULT_PORT,
ip_address=None,
entity_filter=entity_filter or generate_filter([], [], [], []),
exclude_accessory_mode=False,
entity_config={},
homekit_mode=homekit_mode,
advertise_ip=None,
entry_id=entry.entry_id,
entry_title=entry.title,
)
async def test_setup_min(hass, mock_zeroconf):
"""Test async_setup with min config options."""
entry = MockConfigEntry(
@ -103,10 +119,12 @@ async def test_setup_min(hass, mock_zeroconf):
DEFAULT_PORT,
None,
ANY,
ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
entry.title,
)
assert mock_homekit().setup.called is True
@ -139,10 +157,12 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf):
11111,
"172.0.0.0",
ANY,
ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
entry.title,
)
assert mock_homekit().setup.called is True
@ -184,11 +204,13 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
None,
True,
{},
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
entry_title=entry.title,
)
hass.states.async_set("light.demo", "on")
@ -205,6 +227,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf):
hass,
entry.entry_id,
BRIDGE_NAME,
entry.title,
loop=hass.loop,
address=IP_ADDRESS,
port=DEFAULT_PORT,
@ -230,11 +253,13 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
"172.0.0.0",
True,
{},
{},
HOMEKIT_MODE_BRIDGE,
None,
entry_id=entry.entry_id,
entry_title=entry.title,
)
mock_zeroconf = MagicMock()
@ -245,6 +270,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf):
hass,
entry.entry_id,
BRIDGE_NAME,
entry.title,
loop=hass.loop,
address="172.0.0.0",
port=DEFAULT_PORT,
@ -266,11 +292,13 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
"0.0.0.0",
True,
{},
{},
HOMEKIT_MODE_BRIDGE,
"192.168.1.100",
entry_id=entry.entry_id,
entry_title=entry.title,
)
zeroconf_instance = MagicMock()
@ -281,6 +309,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf):
hass,
entry.entry_id,
BRIDGE_NAME,
entry.title,
loop=hass.loop,
address="0.0.0.0",
port=DEFAULT_PORT,
@ -292,40 +321,40 @@ 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."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
lambda entity_id: True,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
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()
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")
await async_init_integration(hass)
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.side_effect = [None, mock_acc, None]
homekit.add_bridge_accessory(State("light.demo", "on"))
mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {})
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
homekit.add_bridge_accessory(State("demo.test", "on"))
mock_get_acc.assert_called_with(hass, "driver", ANY, 600325356, {})
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
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(mock_acc)
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
@pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA])
@ -333,37 +362,30 @@ 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."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
lambda entity_id: True,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
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()
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)
await async_init_integration(hass)
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.side_effect = [None, mock_camera_acc, None]
homekit.add_bridge_accessory(State("light.demo", "on"))
mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {})
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
homekit.add_bridge_accessory(State("camera.test", "on"))
mock_get_acc.assert_called_with(hass, "driver", ANY, 1508819236, {})
assert mock_bridge.add_accessory.called
assert "accessory mode" in caplog.text
@ -371,17 +393,8 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf):
"""Remove accessory from bridge."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
lambda entity_id: True,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.driver = "driver"
homekit.bridge = mock_bridge = Mock()
mock_bridge.accessories = {"light.demo": "acc"}
@ -396,17 +409,8 @@ async def test_homekit_entity_filter(hass, mock_zeroconf):
entry = await async_init_integration(hass)
entity_filter = generate_filter(["cover"], ["demo.test"], [], [])
homekit = HomeKit(
hass,
None,
None,
None,
entity_filter,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
homekit.bridge = Mock()
homekit.bridge.accessories = {}
@ -432,17 +436,8 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf):
entity_filter = generate_filter(
["cover"], ["demo.test"], [], [], ["*.included_*"], ["*.excluded_*"]
)
homekit = HomeKit(
hass,
None,
None,
None,
entity_filter,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
homekit.bridge = Mock()
homekit.bridge.accessories = {}
@ -471,17 +466,8 @@ async def test_homekit_start(hass, hk_driver, device_reg):
entry = await async_init_integration(hass)
pin = b"123-45-678"
homekit = HomeKit(
hass,
None,
None,
None,
{},
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.bridge = Mock()
homekit.bridge.accessories = []
homekit.driver = hk_driver
@ -513,7 +499,9 @@ 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, None, pin, ANY)
mock_setup_msg.assert_called_with(
hass, entry.entry_id, "Mock Title (any)", pin, ANY
)
hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
@ -563,17 +551,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
await async_init_entry(hass, entry)
homekit = HomeKit(
hass,
None,
None,
None,
entity_filter,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
homekit.bridge = Mock()
homekit.bridge.accessories = []
@ -593,7 +571,9 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc
await homekit.async_start()
await hass.async_block_till_done()
mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY)
mock_setup_msg.assert_called_with(
hass, entry.entry_id, "Mock Title (any)", pin, ANY
)
hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
@ -608,18 +588,8 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc
async def test_homekit_stop(hass):
"""Test HomeKit stop method."""
entry = await async_init_integration(hass)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit.driver = Mock()
homekit.driver.async_stop = AsyncMock()
homekit.bridge = Mock()
@ -649,17 +619,8 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf):
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "light.demo"
homekit = HomeKit(
hass,
None,
None,
None,
{},
{entity_id: {}},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.bridge = Mock()
homekit.bridge.accessories = {}
@ -697,17 +658,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
homekit = HomeKit(
hass,
None,
None,
None,
entity_filter,
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
def _mock_bridge(*_):
mock_bridge = HomeBridge(hass, hk_driver, "mock_bridge")
@ -738,17 +689,8 @@ async def test_homekit_finds_linked_batteries(
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"light.demo": {}},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.driver = hk_driver
# pylint: disable=protected-access
homekit._filter = Mock(return_value=True)
@ -792,9 +734,6 @@ async def test_homekit_finds_linked_batteries(
)
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(
@ -823,18 +762,8 @@ async def test_homekit_async_get_integration_fails(
):
"""Test that we continue if async_get_integration fails."""
entry = await async_init_integration(hass)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"light.demo": {}},
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)
@ -877,9 +806,6 @@ async def test_homekit_async_get_integration_fails(
)
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(
@ -927,10 +853,12 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf):
12345,
None,
ANY,
ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
entry.title,
)
assert mock_homekit().setup.called is True
@ -989,18 +917,8 @@ async def test_homekit_ignored_missing_devices(
):
"""Test HomeKit handles a device in the entity registry but missing from the device registry."""
entry = await async_init_integration(hass)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"light.demo": {}},
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)
@ -1041,9 +959,6 @@ async def test_homekit_ignored_missing_devices(
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(
@ -1071,17 +986,8 @@ async def test_homekit_finds_linked_motion_sensors(
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"camera.camera_demo": {}},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.driver = hk_driver
# pylint: disable=protected-access
homekit._filter = Mock(return_value=True)
@ -1115,9 +1021,6 @@ async def test_homekit_finds_linked_motion_sensors(
)
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(
@ -1146,17 +1049,8 @@ async def test_homekit_finds_linked_humidity_sensors(
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"humidifier.humidifier": {}},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
)
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")
@ -1192,9 +1086,6 @@ async def test_homekit_finds_linked_humidity_sensors(
)
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(
@ -1241,10 +1132,12 @@ async def test_reload(hass, mock_zeroconf):
12345,
None,
ANY,
False,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
entry.title,
)
assert mock_homekit().setup.called is True
yaml_path = os.path.join(
@ -1277,10 +1170,12 @@ async def test_reload(hass, mock_zeroconf):
45678,
None,
ANY,
False,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
entry.title,
)
assert mock_homekit2().setup.called is True
@ -1294,17 +1189,9 @@ async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg):
entry = await async_init_integration(hass)
pin = b"123-45-678"
homekit = HomeKit(
hass,
None,
None,
None,
{},
{},
HOMEKIT_MODE_ACCESSORY,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
homekit.bridge = Mock()
homekit.bridge.accessories = []
homekit.driver = hk_driver
@ -1323,6 +1210,8 @@ 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, None, pin, ANY)
mock_setup_msg.assert_called_with(
hass, entry.entry_id, "Mock Title (any)", pin, ANY
)
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING

View File

@ -1,4 +1,6 @@
"""Test HomeKit util module."""
from unittest.mock import Mock
import pytest
import voluptuous as vol
@ -22,6 +24,7 @@ from homeassistant.components.homekit.const import (
TYPE_VALVE,
)
from homeassistant.components.homekit.util import (
accessory_friendly_name,
async_find_next_available_port,
cleanup_name_for_homekit,
convert_to_float,
@ -284,3 +287,12 @@ async def test_format_sw_version():
assert format_sw_version("56.0-76060") == "56.0.76060"
assert format_sw_version(3.6) == "3.6"
assert format_sw_version("unknown") is None
async def test_accessory_friendly_name():
"""Test we provide a helpful friendly name."""
accessory = Mock()
accessory.display_name = "same"
assert accessory_friendly_name("same", accessory) == "same"
assert accessory_friendly_name("hass title", accessory) == "hass title (same)"