mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Support device triggers in HomeKit (#53869)
This commit is contained in:
parent
72410044cd
commit
bd0af57ef2
@ -8,7 +8,7 @@ from aiohttp import web
|
|||||||
from pyhap.const import STANDALONE_AID
|
from pyhap.const import STANDALONE_AID
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import network, zeroconf
|
from homeassistant.components import device_automation, network, zeroconf
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASS_BATTERY_CHARGING,
|
DEVICE_CLASS_BATTERY_CHARGING,
|
||||||
DEVICE_CLASS_MOTION,
|
DEVICE_CLASS_MOTION,
|
||||||
@ -28,6 +28,7 @@ from homeassistant.const import (
|
|||||||
ATTR_MANUFACTURER,
|
ATTR_MANUFACTURER,
|
||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
ATTR_SW_VERSION,
|
ATTR_SW_VERSION,
|
||||||
|
CONF_DEVICES,
|
||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
@ -99,6 +100,7 @@ from .const import (
|
|||||||
SERVICE_HOMEKIT_UNPAIR,
|
SERVICE_HOMEKIT_UNPAIR,
|
||||||
SHUTDOWN_TIMEOUT,
|
SHUTDOWN_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
from .type_triggers import DeviceTriggerAccessory
|
||||||
from .util import (
|
from .util import (
|
||||||
accessory_friendly_name,
|
accessory_friendly_name,
|
||||||
dismiss_setup_message,
|
dismiss_setup_message,
|
||||||
@ -158,6 +160,7 @@ BRIDGE_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA,
|
vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA,
|
||||||
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
|
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
|
||||||
vol.Optional(CONF_ZEROCONF_DEFAULT_INTERFACE): cv.boolean,
|
vol.Optional(CONF_ZEROCONF_DEFAULT_INTERFACE): cv.boolean,
|
||||||
|
vol.Optional(CONF_DEVICES): cv.ensure_list,
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
),
|
),
|
||||||
@ -237,8 +240,9 @@ def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf):
|
|||||||
data = conf.copy()
|
data = conf.copy()
|
||||||
options = {}
|
options = {}
|
||||||
for key in CONFIG_OPTIONS:
|
for key in CONFIG_OPTIONS:
|
||||||
options[key] = data[key]
|
if key in data:
|
||||||
del data[key]
|
options[key] = data[key]
|
||||||
|
del data[key]
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
||||||
return True
|
return True
|
||||||
@ -277,6 +281,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
|
entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
|
||||||
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
|
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
|
||||||
entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {}))
|
entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {}))
|
||||||
|
devices = options.get(CONF_DEVICES, [])
|
||||||
|
|
||||||
homekit = HomeKit(
|
homekit = HomeKit(
|
||||||
hass,
|
hass,
|
||||||
@ -290,6 +295,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
advertise_ip,
|
advertise_ip,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=devices,
|
||||||
)
|
)
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||||
@ -492,6 +498,7 @@ class HomeKit:
|
|||||||
advertise_ip=None,
|
advertise_ip=None,
|
||||||
entry_id=None,
|
entry_id=None,
|
||||||
entry_title=None,
|
entry_title=None,
|
||||||
|
devices=None,
|
||||||
):
|
):
|
||||||
"""Initialize a HomeKit object."""
|
"""Initialize a HomeKit object."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -505,6 +512,7 @@ class HomeKit:
|
|||||||
self._entry_id = entry_id
|
self._entry_id = entry_id
|
||||||
self._entry_title = entry_title
|
self._entry_title = entry_title
|
||||||
self._homekit_mode = homekit_mode
|
self._homekit_mode = homekit_mode
|
||||||
|
self._devices = devices or []
|
||||||
self.aid_storage = None
|
self.aid_storage = None
|
||||||
self.status = STATUS_READY
|
self.status = STATUS_READY
|
||||||
|
|
||||||
@ -594,13 +602,7 @@ class HomeKit:
|
|||||||
|
|
||||||
def add_bridge_accessory(self, state):
|
def add_bridge_accessory(self, state):
|
||||||
"""Try adding accessory to bridge if configured beforehand."""
|
"""Try adding accessory to bridge if configured beforehand."""
|
||||||
# The bridge itself counts as an accessory
|
if self._would_exceed_max_devices(state.entity_id):
|
||||||
if len(self.bridge.accessories) + 1 >= MAX_DEVICES:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Cannot add %s as this would exceed the %d device limit. Consider using the filter option",
|
|
||||||
state.entity_id,
|
|
||||||
MAX_DEVICES,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if state_needs_accessory_mode(state):
|
if state_needs_accessory_mode(state):
|
||||||
@ -631,6 +633,42 @@ class HomeKit:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _would_exceed_max_devices(self, name):
|
||||||
|
"""Check if adding another devices would reach the limit and log."""
|
||||||
|
# The bridge itself counts as an accessory
|
||||||
|
if len(self.bridge.accessories) + 1 >= MAX_DEVICES:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Cannot add %s as this would exceed the %d device limit. Consider using the filter option",
|
||||||
|
name,
|
||||||
|
MAX_DEVICES,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_bridge_triggers_accessory(self, device, device_triggers):
|
||||||
|
"""Add device automation triggers to the bridge."""
|
||||||
|
if self._would_exceed_max_devices(device.name):
|
||||||
|
return
|
||||||
|
|
||||||
|
aid = self.aid_storage.get_or_allocate_aid(device.id, device.id)
|
||||||
|
# If an accessory cannot be created or added due to an exception
|
||||||
|
# of any kind (usually in pyhap) it should not prevent
|
||||||
|
# the rest of the accessories from being created
|
||||||
|
config = {}
|
||||||
|
self._fill_config_from_device_registry_entry(device, config)
|
||||||
|
self.bridge.add_accessory(
|
||||||
|
DeviceTriggerAccessory(
|
||||||
|
self.hass,
|
||||||
|
self.driver,
|
||||||
|
device.name,
|
||||||
|
None,
|
||||||
|
aid,
|
||||||
|
config,
|
||||||
|
device_id=device.id,
|
||||||
|
device_triggers=device_triggers,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def remove_bridge_accessory(self, aid):
|
def remove_bridge_accessory(self, aid):
|
||||||
"""Try adding accessory to bridge if configured beforehand."""
|
"""Try adding accessory to bridge if configured beforehand."""
|
||||||
acc = self.bridge.accessories.pop(aid, None)
|
acc = self.bridge.accessories.pop(aid, None)
|
||||||
@ -778,12 +816,31 @@ class HomeKit:
|
|||||||
)
|
)
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
@callback
|
async def _async_create_bridge_accessory(self, entity_states):
|
||||||
def _async_create_bridge_accessory(self, entity_states):
|
|
||||||
"""Create a HomeKit bridge with accessories. (bridge mode)."""
|
"""Create a HomeKit bridge with accessories. (bridge mode)."""
|
||||||
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
self.add_bridge_accessory(state)
|
self.add_bridge_accessory(state)
|
||||||
|
dev_reg = device_registry.async_get(self.hass)
|
||||||
|
if self._devices:
|
||||||
|
valid_device_ids = []
|
||||||
|
for device_id in self._devices:
|
||||||
|
if not dev_reg.async_get(device_id):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"HomeKit %s cannot add device %s because it is missing from the device registry",
|
||||||
|
self._name,
|
||||||
|
device_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
valid_device_ids.append(device_id)
|
||||||
|
for device_id, device_triggers in (
|
||||||
|
await device_automation.async_get_device_automations(
|
||||||
|
self.hass, "trigger", valid_device_ids
|
||||||
|
)
|
||||||
|
).items():
|
||||||
|
self.add_bridge_triggers_accessory(
|
||||||
|
dev_reg.async_get(device_id), device_triggers
|
||||||
|
)
|
||||||
return self.bridge
|
return self.bridge
|
||||||
|
|
||||||
async def _async_create_accessories(self):
|
async def _async_create_accessories(self):
|
||||||
@ -792,7 +849,7 @@ class HomeKit:
|
|||||||
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
|
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
|
||||||
acc = self._async_create_single_accessory(entity_states)
|
acc = self._async_create_single_accessory(entity_states)
|
||||||
else:
|
else:
|
||||||
acc = self._async_create_bridge_accessory(entity_states)
|
acc = await self._async_create_bridge_accessory(entity_states)
|
||||||
|
|
||||||
if acc is None:
|
if acc is None:
|
||||||
return False
|
return False
|
||||||
@ -875,15 +932,8 @@ class HomeKit:
|
|||||||
"""Set attributes that will be used for homekit device info."""
|
"""Set attributes that will be used for homekit device info."""
|
||||||
ent_cfg = self._config.setdefault(entity_id, {})
|
ent_cfg = self._config.setdefault(entity_id, {})
|
||||||
if ent_reg_ent.device_id:
|
if ent_reg_ent.device_id:
|
||||||
dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id)
|
if dev_reg_ent := dev_reg.async_get(ent_reg_ent.device_id):
|
||||||
if dev_reg_ent is not None:
|
self._fill_config_from_device_registry_entry(dev_reg_ent, ent_cfg)
|
||||||
# Handle missing devices
|
|
||||||
if dev_reg_ent.manufacturer:
|
|
||||||
ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer
|
|
||||||
if dev_reg_ent.model:
|
|
||||||
ent_cfg[ATTR_MODEL] = dev_reg_ent.model
|
|
||||||
if dev_reg_ent.sw_version:
|
|
||||||
ent_cfg[ATTR_SW_VERSION] = dev_reg_ent.sw_version
|
|
||||||
if ATTR_MANUFACTURER not in ent_cfg:
|
if ATTR_MANUFACTURER not in ent_cfg:
|
||||||
try:
|
try:
|
||||||
integration = await async_get_integration(
|
integration = await async_get_integration(
|
||||||
@ -893,6 +943,19 @@ class HomeKit:
|
|||||||
except IntegrationNotFound:
|
except IntegrationNotFound:
|
||||||
ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
|
ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
|
||||||
|
|
||||||
|
def _fill_config_from_device_registry_entry(self, device_entry, config):
|
||||||
|
"""Populate a config dict from the registry."""
|
||||||
|
if device_entry.manufacturer:
|
||||||
|
config[ATTR_MANUFACTURER] = device_entry.manufacturer
|
||||||
|
if device_entry.model:
|
||||||
|
config[ATTR_MODEL] = device_entry.model
|
||||||
|
if device_entry.sw_version:
|
||||||
|
config[ATTR_SW_VERSION] = device_entry.sw_version
|
||||||
|
if device_entry.config_entries:
|
||||||
|
first_entry = list(device_entry.config_entries)[0]
|
||||||
|
if entry := self.hass.config_entries.async_get_entry(first_entry):
|
||||||
|
config[ATTR_INTEGRATION] = entry.domain
|
||||||
|
|
||||||
|
|
||||||
class HomeKitPairingQRView(HomeAssistantView):
|
class HomeKitPairingQRView(HomeAssistantView):
|
||||||
"""Display the homekit pairing code at a protected url."""
|
"""Display the homekit pairing code at a protected url."""
|
||||||
|
@ -224,6 +224,7 @@ class HomeAccessory(Accessory):
|
|||||||
config,
|
config,
|
||||||
*args,
|
*args,
|
||||||
category=CATEGORY_OTHER,
|
category=CATEGORY_OTHER,
|
||||||
|
device_id=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Initialize a Accessory object."""
|
"""Initialize a Accessory object."""
|
||||||
@ -231,18 +232,29 @@ class HomeAccessory(Accessory):
|
|||||||
driver=driver, display_name=name[:MAX_NAME_LENGTH], aid=aid, *args, **kwargs
|
driver=driver, display_name=name[:MAX_NAME_LENGTH], aid=aid, *args, **kwargs
|
||||||
)
|
)
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
domain = split_entity_id(entity_id)[0].replace("_", " ")
|
if device_id:
|
||||||
|
self.device_id = device_id
|
||||||
|
serial_number = device_id
|
||||||
|
domain = None
|
||||||
|
else:
|
||||||
|
self.device_id = None
|
||||||
|
serial_number = entity_id
|
||||||
|
domain = split_entity_id(entity_id)[0].replace("_", " ")
|
||||||
|
|
||||||
if self.config.get(ATTR_MANUFACTURER) is not None:
|
if self.config.get(ATTR_MANUFACTURER) is not None:
|
||||||
manufacturer = self.config[ATTR_MANUFACTURER]
|
manufacturer = self.config[ATTR_MANUFACTURER]
|
||||||
elif self.config.get(ATTR_INTEGRATION) is not None:
|
elif self.config.get(ATTR_INTEGRATION) is not None:
|
||||||
manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title()
|
manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title()
|
||||||
else:
|
elif domain:
|
||||||
manufacturer = f"{MANUFACTURER} {domain}".title()
|
manufacturer = f"{MANUFACTURER} {domain}".title()
|
||||||
|
else:
|
||||||
|
manufacturer = MANUFACTURER
|
||||||
if self.config.get(ATTR_MODEL) is not None:
|
if self.config.get(ATTR_MODEL) is not None:
|
||||||
model = self.config[ATTR_MODEL]
|
model = self.config[ATTR_MODEL]
|
||||||
else:
|
elif domain:
|
||||||
model = domain.title()
|
model = domain.title()
|
||||||
|
else:
|
||||||
|
model = MANUFACTURER
|
||||||
sw_version = None
|
sw_version = None
|
||||||
if self.config.get(ATTR_SW_VERSION) is not None:
|
if self.config.get(ATTR_SW_VERSION) is not None:
|
||||||
sw_version = format_sw_version(self.config[ATTR_SW_VERSION])
|
sw_version = format_sw_version(self.config[ATTR_SW_VERSION])
|
||||||
@ -252,7 +264,7 @@ class HomeAccessory(Accessory):
|
|||||||
self.set_info_service(
|
self.set_info_service(
|
||||||
manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH],
|
manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH],
|
||||||
model=model[:MAX_MODEL_LENGTH],
|
model=model[:MAX_MODEL_LENGTH],
|
||||||
serial_number=entity_id[:MAX_SERIAL_LENGTH],
|
serial_number=serial_number[:MAX_SERIAL_LENGTH],
|
||||||
firmware_revision=sw_version[:MAX_VERSION_LENGTH],
|
firmware_revision=sw_version[:MAX_VERSION_LENGTH],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -260,6 +272,10 @@ class HomeAccessory(Accessory):
|
|||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._subscriptions = []
|
self._subscriptions = []
|
||||||
|
|
||||||
|
if device_id:
|
||||||
|
return
|
||||||
|
|
||||||
self._char_battery = None
|
self._char_battery = None
|
||||||
self._char_charging = None
|
self._char_charging = None
|
||||||
self._char_low_battery = None
|
self._char_low_battery = None
|
||||||
|
@ -94,12 +94,12 @@ class AccessoryAidStorage:
|
|||||||
"""Generate a stable aid for an entity id."""
|
"""Generate a stable aid for an entity id."""
|
||||||
entity = self._entity_registry.async_get(entity_id)
|
entity = self._entity_registry.async_get(entity_id)
|
||||||
if not entity:
|
if not entity:
|
||||||
return self._get_or_allocate_aid(None, entity_id)
|
return self.get_or_allocate_aid(None, entity_id)
|
||||||
|
|
||||||
sys_unique_id = get_system_unique_id(entity)
|
sys_unique_id = get_system_unique_id(entity)
|
||||||
return self._get_or_allocate_aid(sys_unique_id, entity_id)
|
return self.get_or_allocate_aid(sys_unique_id, entity_id)
|
||||||
|
|
||||||
def _get_or_allocate_aid(self, unique_id: str, entity_id: str):
|
def get_or_allocate_aid(self, unique_id: str, entity_id: str):
|
||||||
"""Allocate (and return) a new aid for an accessory."""
|
"""Allocate (and return) a new aid for an accessory."""
|
||||||
if unique_id and unique_id in self.allocations:
|
if unique_id and unique_id in self.allocations:
|
||||||
return self.allocations[unique_id]
|
return self.allocations[unique_id]
|
||||||
|
@ -9,6 +9,7 @@ import string
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import device_automation
|
||||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||||
@ -16,6 +17,7 @@ from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
|
|||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
|
CONF_DEVICES,
|
||||||
CONF_DOMAINS,
|
CONF_DOMAINS,
|
||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
@ -23,6 +25,7 @@ from homeassistant.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||||
|
from homeassistant.helpers import device_registry
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entityfilter import (
|
from homeassistant.helpers.entityfilter import (
|
||||||
CONF_EXCLUDE_DOMAINS,
|
CONF_EXCLUDE_DOMAINS,
|
||||||
@ -318,20 +321,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
if key in self.hk_options:
|
if key in self.hk_options:
|
||||||
del self.hk_options[key]
|
del self.hk_options[key]
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.show_advanced_options
|
||||||
|
and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
|
||||||
|
):
|
||||||
|
self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
|
||||||
|
|
||||||
return self.async_create_entry(title="", data=self.hk_options)
|
return self.async_create_entry(title="", data=self.hk_options)
|
||||||
|
|
||||||
|
data_schema = {
|
||||||
|
vol.Optional(
|
||||||
|
CONF_AUTO_START,
|
||||||
|
default=self.hk_options.get(CONF_AUTO_START, DEFAULT_AUTO_START),
|
||||||
|
): bool
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE:
|
||||||
|
all_supported_devices = await _async_get_supported_devices(self.hass)
|
||||||
|
devices = self.hk_options.get(CONF_DEVICES, [])
|
||||||
|
data_schema[vol.Optional(CONF_DEVICES, default=devices)] = cv.multi_select(
|
||||||
|
all_supported_devices
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="advanced",
|
step_id="advanced",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(data_schema),
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_AUTO_START,
|
|
||||||
default=self.hk_options.get(
|
|
||||||
CONF_AUTO_START, DEFAULT_AUTO_START
|
|
||||||
),
|
|
||||||
): bool
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_cameras(self, user_input=None):
|
async def async_step_cameras(self, user_input=None):
|
||||||
@ -412,7 +426,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self.included_cameras = set()
|
self.included_cameras = set()
|
||||||
|
|
||||||
self.hk_options[CONF_FILTER] = entity_filter
|
self.hk_options[CONF_FILTER] = entity_filter
|
||||||
|
|
||||||
if self.included_cameras:
|
if self.included_cameras:
|
||||||
return await self.async_step_cameras()
|
return await self.async_step_cameras()
|
||||||
|
|
||||||
@ -481,6 +494,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_supported_devices(hass):
|
||||||
|
"""Return all supported devices."""
|
||||||
|
results = await device_automation.async_get_device_automations(hass, "trigger")
|
||||||
|
dev_reg = device_registry.async_get(hass)
|
||||||
|
unsorted = {device_id: dev_reg.async_get(device_id).name for device_id in results}
|
||||||
|
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
|
||||||
|
|
||||||
|
|
||||||
def _async_get_matching_entities(hass, domains=None):
|
def _async_get_matching_entities(hass, domains=None):
|
||||||
"""Fetch all entities or entities in the given domains."""
|
"""Fetch all entities or entities in the given domains."""
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Constants used be the HomeKit component."""
|
"""Constants used be the HomeKit component."""
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_DEVICES
|
||||||
|
|
||||||
# #### Misc ####
|
# #### Misc ####
|
||||||
DEBOUNCE_TIMEOUT = 0.5
|
DEBOUNCE_TIMEOUT = 0.5
|
||||||
DEVICE_PRECISION_LEEWAY = 6
|
DEVICE_PRECISION_LEEWAY = 6
|
||||||
@ -136,6 +138,7 @@ SERV_MOTION_SENSOR = "MotionSensor"
|
|||||||
SERV_OCCUPANCY_SENSOR = "OccupancySensor"
|
SERV_OCCUPANCY_SENSOR = "OccupancySensor"
|
||||||
SERV_OUTLET = "Outlet"
|
SERV_OUTLET = "Outlet"
|
||||||
SERV_SECURITY_SYSTEM = "SecuritySystem"
|
SERV_SECURITY_SYSTEM = "SecuritySystem"
|
||||||
|
SERV_SERVICE_LABEL = "ServiceLabel"
|
||||||
SERV_SMOKE_SENSOR = "SmokeSensor"
|
SERV_SMOKE_SENSOR = "SmokeSensor"
|
||||||
SERV_SPEAKER = "Speaker"
|
SERV_SPEAKER = "Speaker"
|
||||||
SERV_STATELESS_PROGRAMMABLE_SWITCH = "StatelessProgrammableSwitch"
|
SERV_STATELESS_PROGRAMMABLE_SWITCH = "StatelessProgrammableSwitch"
|
||||||
@ -205,6 +208,8 @@ CHAR_ROTATION_DIRECTION = "RotationDirection"
|
|||||||
CHAR_ROTATION_SPEED = "RotationSpeed"
|
CHAR_ROTATION_SPEED = "RotationSpeed"
|
||||||
CHAR_SATURATION = "Saturation"
|
CHAR_SATURATION = "Saturation"
|
||||||
CHAR_SERIAL_NUMBER = "SerialNumber"
|
CHAR_SERIAL_NUMBER = "SerialNumber"
|
||||||
|
CHAR_SERVICE_LABEL_INDEX = "ServiceLabelIndex"
|
||||||
|
CHAR_SERVICE_LABEL_NAMESPACE = "ServiceLabelNamespace"
|
||||||
CHAR_SLEEP_DISCOVER_MODE = "SleepDiscoveryMode"
|
CHAR_SLEEP_DISCOVER_MODE = "SleepDiscoveryMode"
|
||||||
CHAR_SMOKE_DETECTED = "SmokeDetected"
|
CHAR_SMOKE_DETECTED = "SmokeDetected"
|
||||||
CHAR_STATUS_LOW_BATTERY = "StatusLowBattery"
|
CHAR_STATUS_LOW_BATTERY = "StatusLowBattery"
|
||||||
@ -292,6 +297,7 @@ CONFIG_OPTIONS = [
|
|||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
CONF_HOMEKIT_MODE,
|
CONF_HOMEKIT_MODE,
|
||||||
|
CONF_DEVICES,
|
||||||
]
|
]
|
||||||
|
|
||||||
# ### Maximum Lengths ###
|
# ### Maximum Lengths ###
|
||||||
|
@ -30,9 +30,10 @@
|
|||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"devices": "Devices (Triggers)",
|
||||||
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
|
"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.",
|
"description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.",
|
||||||
"title": "Advanced Configuration"
|
"title": "Advanced Configuration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,10 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"data": {
|
"data": {
|
||||||
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
|
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)",
|
||||||
|
"devices": "Devices (Triggers)"
|
||||||
},
|
},
|
||||||
"description": "These settings only need to be adjusted if HomeKit is not functional.",
|
"description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.",
|
||||||
"title": "Advanced Configuration"
|
"title": "Advanced Configuration"
|
||||||
},
|
},
|
||||||
"cameras": {
|
"cameras": {
|
||||||
|
89
homeassistant/components/homekit/type_triggers.py
Normal file
89
homeassistant/components/homekit/type_triggers.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""Class to hold all sensor accessories."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_SENSOR
|
||||||
|
|
||||||
|
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||||
|
|
||||||
|
from .accessories import TYPES, HomeAccessory
|
||||||
|
from .const import (
|
||||||
|
CHAR_NAME,
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
CHAR_SERVICE_LABEL_INDEX,
|
||||||
|
CHAR_SERVICE_LABEL_NAMESPACE,
|
||||||
|
SERV_SERVICE_LABEL,
|
||||||
|
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@TYPES.register("DeviceTriggerAccessory")
|
||||||
|
class DeviceTriggerAccessory(HomeAccessory):
|
||||||
|
"""Generate a Programmable switch."""
|
||||||
|
|
||||||
|
def __init__(self, *args, device_triggers=None, device_id=None):
|
||||||
|
"""Initialize a Programmable switch accessory object."""
|
||||||
|
super().__init__(*args, category=CATEGORY_SENSOR, device_id=device_id)
|
||||||
|
self._device_triggers = device_triggers
|
||||||
|
self._remove_triggers = None
|
||||||
|
self.triggers = []
|
||||||
|
for idx, trigger in enumerate(device_triggers):
|
||||||
|
type_ = trigger.get("type")
|
||||||
|
subtype = trigger.get("subtype")
|
||||||
|
trigger_name = (
|
||||||
|
f"{type_.title()} {subtype.title()}" if subtype else type_.title()
|
||||||
|
)
|
||||||
|
serv_stateless_switch = self.add_preload_service(
|
||||||
|
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||||
|
[CHAR_NAME, CHAR_SERVICE_LABEL_INDEX],
|
||||||
|
)
|
||||||
|
self.triggers.append(
|
||||||
|
serv_stateless_switch.configure_char(
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
value=0,
|
||||||
|
valid_values={"Trigger": 0},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
serv_stateless_switch.configure_char(CHAR_NAME, value=trigger_name)
|
||||||
|
serv_stateless_switch.configure_char(
|
||||||
|
CHAR_SERVICE_LABEL_INDEX, value=idx + 1
|
||||||
|
)
|
||||||
|
serv_service_label = self.add_preload_service(SERV_SERVICE_LABEL)
|
||||||
|
serv_service_label.configure_char(CHAR_SERVICE_LABEL_NAMESPACE, value=1)
|
||||||
|
serv_stateless_switch.add_linked_service(serv_service_label)
|
||||||
|
|
||||||
|
async def async_trigger(self, run_variables, context=None, skip_condition=False):
|
||||||
|
"""Trigger button press.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
reason = ""
|
||||||
|
if "trigger" in run_variables and "description" in run_variables["trigger"]:
|
||||||
|
reason = f' by {run_variables["trigger"]["description"]}'
|
||||||
|
_LOGGER.debug("Button triggered%s - %s", reason, run_variables)
|
||||||
|
idx = int(run_variables["trigger"]["idx"])
|
||||||
|
self.triggers[idx].set_value(0)
|
||||||
|
|
||||||
|
# Attach the trigger using the helper in async run
|
||||||
|
# and detach it in async stop
|
||||||
|
async def run(self):
|
||||||
|
"""Handle accessory driver started event."""
|
||||||
|
self._remove_triggers = await async_initialize_triggers(
|
||||||
|
self.hass,
|
||||||
|
self._device_triggers,
|
||||||
|
self.async_trigger,
|
||||||
|
"homekit",
|
||||||
|
self.display_name,
|
||||||
|
_LOGGER.log,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Handle accessory driver stop event."""
|
||||||
|
if self._remove_triggers:
|
||||||
|
self._remove_triggers()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return available."""
|
||||||
|
return True
|
@ -1,12 +1,15 @@
|
|||||||
"""HomeKit session fixtures."""
|
"""HomeKit session fixtures."""
|
||||||
|
from contextlib import suppress
|
||||||
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pyhap.accessory_driver import AccessoryDriver
|
from pyhap.accessory_driver import AccessoryDriver
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.device_tracker.legacy import YAML_DEVICES
|
||||||
from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
|
from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
|
||||||
|
|
||||||
from tests.common import async_capture_events
|
from tests.common import async_capture_events, mock_device_registry, mock_registry
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -24,7 +27,46 @@ def hk_driver(loop):
|
|||||||
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
|
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_hap(loop, mock_zeroconf):
|
||||||
|
"""Return a custom AccessoryDriver instance for HomeKit accessory init."""
|
||||||
|
with patch("pyhap.accessory_driver.AsyncZeroconf"), patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryEncoder"
|
||||||
|
), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch(
|
||||||
|
"pyhap.accessory_driver.HAPServer.async_start"
|
||||||
|
), patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.publish"
|
||||||
|
), patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.async_start"
|
||||||
|
), patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.async_stop"
|
||||||
|
), patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.persist"
|
||||||
|
):
|
||||||
|
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def events(hass):
|
def events(hass):
|
||||||
"""Yield caught homekit_changed events."""
|
"""Yield caught homekit_changed events."""
|
||||||
return async_capture_events(hass, EVENT_HOMEKIT_CHANGED)
|
return async_capture_events(hass, EVENT_HOMEKIT_CHANGED)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="device_reg")
|
||||||
|
def device_reg_fixture(hass):
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_device_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="entity_reg")
|
||||||
|
def entity_reg_fixture(hass):
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def demo_cleanup(hass):
|
||||||
|
"""Clean up device tracker demo file."""
|
||||||
|
yield
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove(hass.config.path(YAML_DEVICES))
|
||||||
|
@ -7,6 +7,7 @@ from homeassistant import config_entries, data_entry_flow, setup
|
|||||||
from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME
|
from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME
|
||||||
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_NAME, CONF_PORT
|
from homeassistant.const import CONF_NAME, CONF_PORT
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -314,6 +315,7 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass):
|
|||||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert config_entry.options == {
|
assert config_entry.options == {
|
||||||
"auto_start": auto_start,
|
"auto_start": auto_start,
|
||||||
|
"devices": [],
|
||||||
"mode": "bridge",
|
"mode": "bridge",
|
||||||
"filter": {
|
"filter": {
|
||||||
"exclude_domains": [],
|
"exclude_domains": [],
|
||||||
@ -365,6 +367,138 @@ async def test_options_flow_exclude_mode_basic(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow_devices(
|
||||||
|
mock_hap, hass, demo_cleanup, device_reg, entity_reg
|
||||||
|
):
|
||||||
|
"""Test devices can be bridged."""
|
||||||
|
config_entry = _mock_config_entry_with_options_populated()
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
demo_config_entry = MockConfigEntry(domain="domain")
|
||||||
|
demo_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "persistent_notification", {})
|
||||||
|
assert await async_setup_component(hass, "demo", {"demo": {}})
|
||||||
|
assert await async_setup_component(hass, "homekit", {"homekit": {}})
|
||||||
|
|
||||||
|
hass.states.async_set("climate.old", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(
|
||||||
|
config_entry.entry_id, context={"show_advanced_options": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "include_exclude"
|
||||||
|
|
||||||
|
entry = entity_reg.async_get("light.ceiling_lights")
|
||||||
|
assert entry is not None
|
||||||
|
device_id = entry.device_id
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
"entities": ["climate.old"],
|
||||||
|
"include_exclude_mode": "exclude",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.homekit.async_setup_entry", return_value=True):
|
||||||
|
result3 = await hass.config_entries.options.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
user_input={"auto_start": True, "devices": [device_id]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert config_entry.options == {
|
||||||
|
"auto_start": True,
|
||||||
|
"devices": [device_id],
|
||||||
|
"mode": "bridge",
|
||||||
|
"filter": {
|
||||||
|
"exclude_domains": [],
|
||||||
|
"exclude_entities": ["climate.old"],
|
||||||
|
"include_domains": ["fan", "vacuum", "climate"],
|
||||||
|
"include_entities": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow_devices_preserved_when_advanced_off(mock_hap, hass):
|
||||||
|
"""Test devices are preserved if they were added in advanced mode but it was turned off."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_NAME: "mock_name", CONF_PORT: 12345},
|
||||||
|
options={
|
||||||
|
"devices": ["1fabcabcabcabcabcabcabcabcabc"],
|
||||||
|
"filter": {
|
||||||
|
"include_domains": [
|
||||||
|
"fan",
|
||||||
|
"humidifier",
|
||||||
|
"vacuum",
|
||||||
|
"media_player",
|
||||||
|
"climate",
|
||||||
|
"alarm_control_panel",
|
||||||
|
],
|
||||||
|
"exclude_entities": ["climate.front_gate"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
demo_config_entry = MockConfigEntry(domain="domain")
|
||||||
|
demo_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "persistent_notification", {})
|
||||||
|
assert await async_setup_component(hass, "homekit", {"homekit": {}})
|
||||||
|
|
||||||
|
hass.states.async_set("climate.old", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(
|
||||||
|
config_entry.entry_id, context={"show_advanced_options": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "include_exclude"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
"entities": ["climate.old"],
|
||||||
|
"include_exclude_mode": "exclude",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert config_entry.options == {
|
||||||
|
"auto_start": True,
|
||||||
|
"devices": ["1fabcabcabcabcabcabcabcabcabc"],
|
||||||
|
"mode": "bridge",
|
||||||
|
"filter": {
|
||||||
|
"exclude_domains": [],
|
||||||
|
"exclude_entities": ["climate.old"],
|
||||||
|
"include_domains": ["fan", "vacuum", "climate"],
|
||||||
|
"include_entities": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_include_mode_basic(hass):
|
async def test_options_flow_include_mode_basic(hass):
|
||||||
"""Test config flow options in include mode."""
|
"""Test config flow options in include mode."""
|
||||||
|
|
||||||
@ -646,6 +780,7 @@ async def test_options_flow_blocked_when_from_yaml(hass):
|
|||||||
data={CONF_NAME: "mock_name", CONF_PORT: 12345},
|
data={CONF_NAME: "mock_name", CONF_PORT: 12345},
|
||||||
options={
|
options={
|
||||||
"auto_start": True,
|
"auto_start": True,
|
||||||
|
"devices": [],
|
||||||
"filter": {
|
"filter": {
|
||||||
"include_domains": [
|
"include_domains": [
|
||||||
"fan",
|
"fan",
|
||||||
|
@ -37,6 +37,7 @@ from homeassistant.components.homekit.const import (
|
|||||||
SERVICE_HOMEKIT_START,
|
SERVICE_HOMEKIT_START,
|
||||||
SERVICE_HOMEKIT_UNPAIR,
|
SERVICE_HOMEKIT_UNPAIR,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory
|
||||||
from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id
|
from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -70,7 +71,7 @@ from homeassistant.util import json as json_util
|
|||||||
|
|
||||||
from .util import PATH_HOMEKIT, async_init_entry, async_init_integration
|
from .util import PATH_HOMEKIT, async_init_entry, async_init_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
IP_ADDRESS = "127.0.0.1"
|
IP_ADDRESS = "127.0.0.1"
|
||||||
|
|
||||||
@ -101,19 +102,7 @@ def always_patch_driver(hk_driver):
|
|||||||
"""Load the hk_driver fixture."""
|
"""Load the hk_driver fixture."""
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="device_reg")
|
def _mock_homekit(hass, entry, homekit_mode, entity_filter=None, devices=None):
|
||||||
def device_reg_fixture(hass):
|
|
||||||
"""Return an empty, loaded, registry."""
|
|
||||||
return mock_device_registry(hass)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="entity_reg")
|
|
||||||
def entity_reg_fixture(hass):
|
|
||||||
"""Return an empty, loaded, registry."""
|
|
||||||
return mock_registry(hass)
|
|
||||||
|
|
||||||
|
|
||||||
def _mock_homekit(hass, entry, homekit_mode, entity_filter=None):
|
|
||||||
return HomeKit(
|
return HomeKit(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
name=BRIDGE_NAME,
|
name=BRIDGE_NAME,
|
||||||
@ -126,6 +115,7 @@ def _mock_homekit(hass, entry, homekit_mode, entity_filter=None):
|
|||||||
advertise_ip=None,
|
advertise_ip=None,
|
||||||
entry_id=entry.entry_id,
|
entry_id=entry.entry_id,
|
||||||
entry_title=entry.title,
|
entry_title=entry.title,
|
||||||
|
devices=devices,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -178,6 +168,7 @@ async def test_setup_min(hass, mock_zeroconf):
|
|||||||
None,
|
None,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test auto start enabled
|
# Test auto start enabled
|
||||||
@ -214,6 +205,7 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf):
|
|||||||
None,
|
None,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test auto_start disabled
|
# Test auto_start disabled
|
||||||
@ -602,6 +594,41 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc
|
|||||||
assert not hk_driver_start.called
|
assert not hk_driver_start.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_homekit_start_with_a_device(
|
||||||
|
hass, hk_driver, mock_zeroconf, demo_cleanup, device_reg, entity_reg
|
||||||
|
):
|
||||||
|
"""Test HomeKit start method with a device."""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
|
||||||
|
)
|
||||||
|
assert await async_setup_component(hass, "demo", {"demo": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
reg_entry = entity_reg.async_get("light.ceiling_lights")
|
||||||
|
assert reg_entry is not None
|
||||||
|
device_id = reg_entry.device_id
|
||||||
|
await async_init_entry(hass, entry)
|
||||||
|
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, None, devices=[device_id])
|
||||||
|
homekit.driver = hk_driver
|
||||||
|
|
||||||
|
with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch(
|
||||||
|
f"{PATH_HOMEKIT}.show_setup_message"
|
||||||
|
) as mock_setup_msg:
|
||||||
|
await homekit.async_start()
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_setup_msg.assert_called_with(
|
||||||
|
hass, entry.entry_id, "Mock Title (Home Assistant Bridge)", ANY, ANY
|
||||||
|
)
|
||||||
|
assert homekit.status == STATUS_RUNNING
|
||||||
|
|
||||||
|
assert isinstance(
|
||||||
|
list(homekit.driver.accessory.accessories.values())[0], DeviceTriggerAccessory
|
||||||
|
)
|
||||||
|
await homekit.async_stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_homekit_stop(hass):
|
async def test_homekit_stop(hass):
|
||||||
"""Test HomeKit stop method."""
|
"""Test HomeKit stop method."""
|
||||||
entry = await async_init_integration(hass)
|
entry = await async_init_integration(hass)
|
||||||
@ -1141,6 +1168,7 @@ async def test_homekit_finds_linked_batteries(
|
|||||||
"manufacturer": "Tesla",
|
"manufacturer": "Tesla",
|
||||||
"model": "Powerwall 2",
|
"model": "Powerwall 2",
|
||||||
"sw_version": "0.16.0",
|
"sw_version": "0.16.0",
|
||||||
|
"platform": "test",
|
||||||
"linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging",
|
"linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging",
|
||||||
"linked_battery_sensor": "sensor.powerwall_battery",
|
"linked_battery_sensor": "sensor.powerwall_battery",
|
||||||
},
|
},
|
||||||
@ -1250,6 +1278,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf):
|
|||||||
None,
|
None,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test auto start enabled
|
# Test auto start enabled
|
||||||
@ -1416,6 +1445,7 @@ async def test_homekit_finds_linked_motion_sensors(
|
|||||||
{
|
{
|
||||||
"manufacturer": "Ubq",
|
"manufacturer": "Ubq",
|
||||||
"model": "Camera Server",
|
"model": "Camera Server",
|
||||||
|
"platform": "test",
|
||||||
"sw_version": "0.16.0",
|
"sw_version": "0.16.0",
|
||||||
"linked_motion_sensor": "binary_sensor.camera_motion_sensor",
|
"linked_motion_sensor": "binary_sensor.camera_motion_sensor",
|
||||||
},
|
},
|
||||||
@ -1480,6 +1510,7 @@ async def test_homekit_finds_linked_humidity_sensors(
|
|||||||
{
|
{
|
||||||
"manufacturer": "Home Assistant",
|
"manufacturer": "Home Assistant",
|
||||||
"model": "Smart Brainy Clever Humidifier",
|
"model": "Smart Brainy Clever Humidifier",
|
||||||
|
"platform": "test",
|
||||||
"sw_version": "0.16.1",
|
"sw_version": "0.16.1",
|
||||||
"linked_humidity_sensor": "sensor.humidifier_humidity_sensor",
|
"linked_humidity_sensor": "sensor.humidifier_humidity_sensor",
|
||||||
},
|
},
|
||||||
@ -1518,6 +1549,7 @@ async def test_reload(hass, mock_zeroconf):
|
|||||||
None,
|
None,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=[],
|
||||||
)
|
)
|
||||||
yaml_path = os.path.join(
|
yaml_path = os.path.join(
|
||||||
_get_fixtures_base_path(),
|
_get_fixtures_base_path(),
|
||||||
@ -1556,6 +1588,7 @@ async def test_reload(hass, mock_zeroconf):
|
|||||||
None,
|
None,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
entry.title,
|
entry.title,
|
||||||
|
devices=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
57
tests/components/homekit/test_type_triggers.py
Normal file
57
tests/components/homekit/test_type_triggers.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Test different accessory types: Triggers (Programmable Switches)."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory
|
||||||
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_get_device_automations
|
||||||
|
|
||||||
|
|
||||||
|
async def test_programmable_switch_button_fires_on_trigger(
|
||||||
|
hass, hk_driver, events, demo_cleanup, device_reg, entity_reg
|
||||||
|
):
|
||||||
|
"""Test that DeviceTriggerAccessory fires the programmable switch event on trigger."""
|
||||||
|
hk_driver.publish = MagicMock()
|
||||||
|
|
||||||
|
demo_config_entry = MockConfigEntry(domain="domain")
|
||||||
|
demo_config_entry.add_to_hass(hass)
|
||||||
|
assert await async_setup_component(hass, "demo", {"demo": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hass.states.async_set("light.ceiling_lights", STATE_OFF)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry = entity_reg.async_get("light.ceiling_lights")
|
||||||
|
assert entry is not None
|
||||||
|
device_id = entry.device_id
|
||||||
|
|
||||||
|
device_triggers = await async_get_device_automations(hass, "trigger", device_id)
|
||||||
|
acc = DeviceTriggerAccessory(
|
||||||
|
hass,
|
||||||
|
hk_driver,
|
||||||
|
"DeviceTriggerAccessory",
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
device_id=device_id,
|
||||||
|
device_triggers=device_triggers,
|
||||||
|
)
|
||||||
|
await acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.entity_id is None
|
||||||
|
assert acc.device_id is device_id
|
||||||
|
assert acc.available is True
|
||||||
|
|
||||||
|
hk_driver.publish.reset_mock()
|
||||||
|
hass.states.async_set("light.ceiling_lights", STATE_ON)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hk_driver.publish.assert_called_once()
|
||||||
|
|
||||||
|
hk_driver.publish.reset_mock()
|
||||||
|
hass.states.async_set("light.ceiling_lights", STATE_OFF)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hk_driver.publish.assert_called_once()
|
||||||
|
|
||||||
|
await acc.stop()
|
Loading…
x
Reference in New Issue
Block a user