mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
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:
parent
9159f54900
commit
87cbbcb014
@ -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(),
|
||||
)
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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""
|
||||
)
|
||||
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
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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)"
|
||||
|
Loading…
x
Reference in New Issue
Block a user