2019-02-14 15:01:46 +00:00
|
|
|
"""Support for Apple HomeKit."""
|
2020-05-01 04:05:06 +00:00
|
|
|
import asyncio
|
2018-04-30 12:58:17 +00:00
|
|
|
import ipaddress
|
2018-02-19 22:46:22 +00:00
|
|
|
import logging
|
|
|
|
|
2020-04-21 22:38:43 +00:00
|
|
|
from aiohttp import web
|
2018-02-19 22:46:22 +00:00
|
|
|
import voluptuous as vol
|
2020-04-12 14:59:50 +00:00
|
|
|
from zeroconf import InterfaceChoice
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING
|
2020-04-21 22:38:43 +00:00
|
|
|
from homeassistant.components.http import HomeAssistantView
|
2020-05-01 04:05:06 +00:00
|
|
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
2018-03-15 01:48:21 +00:00
|
|
|
from homeassistant.const import (
|
2020-04-22 00:43:49 +00:00
|
|
|
ATTR_BATTERY_CHARGING,
|
|
|
|
ATTR_BATTERY_LEVEL,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
2020-04-21 00:48:09 +00:00
|
|
|
ATTR_SERVICE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_IP_ADDRESS,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_PORT,
|
2020-04-22 00:43:49 +00:00
|
|
|
DEVICE_CLASS_BATTERY,
|
2020-05-01 04:05:06 +00:00
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
)
|
2020-05-01 04:05:06 +00:00
|
|
|
from homeassistant.core import CoreState, HomeAssistant, callback
|
|
|
|
from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized
|
|
|
|
from homeassistant.helpers import device_registry, entity_registry
|
2018-03-15 01:48:21 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2020-05-01 04:05:06 +00:00
|
|
|
from homeassistant.helpers.entityfilter import (
|
|
|
|
BASE_FILTER_SCHEMA,
|
|
|
|
CONF_EXCLUDE_DOMAINS,
|
|
|
|
CONF_EXCLUDE_ENTITIES,
|
|
|
|
CONF_INCLUDE_DOMAINS,
|
|
|
|
CONF_INCLUDE_ENTITIES,
|
|
|
|
convert_filter,
|
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
from homeassistant.util import get_local_ip
|
2019-02-14 15:01:46 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
from .accessories import get_accessory
|
2020-04-19 18:51:09 +00:00
|
|
|
from .aidmanager import AccessoryAidStorage
|
2018-03-15 01:48:21 +00:00
|
|
|
from .const import (
|
2020-04-19 18:51:09 +00:00
|
|
|
AID_STORAGE,
|
2020-04-21 00:48:09 +00:00
|
|
|
ATTR_DISPLAY_NAME,
|
|
|
|
ATTR_VALUE,
|
2019-07-31 19:25:30 +00:00
|
|
|
BRIDGE_NAME,
|
2019-10-23 05:06:21 +00:00
|
|
|
CONF_ADVERTISE_IP,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_AUTO_START,
|
|
|
|
CONF_ENTITY_CONFIG,
|
2020-05-01 04:05:06 +00:00
|
|
|
CONF_ENTRY_INDEX,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_FILTER,
|
2020-04-22 00:43:49 +00:00
|
|
|
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
|
|
|
CONF_LINKED_BATTERY_SENSOR,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SAFE_MODE,
|
2020-04-12 14:59:50 +00:00
|
|
|
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
2020-05-01 04:05:06 +00:00
|
|
|
CONFIG_OPTIONS,
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_AUTO_START,
|
|
|
|
DEFAULT_PORT,
|
|
|
|
DEFAULT_SAFE_MODE,
|
2020-04-12 14:59:50 +00:00
|
|
|
DEFAULT_ZEROCONF_DEFAULT_INTERFACE,
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN,
|
2020-04-21 00:48:09 +00:00
|
|
|
EVENT_HOMEKIT_CHANGED,
|
2020-05-01 04:05:06 +00:00
|
|
|
HOMEKIT,
|
2020-04-21 22:38:43 +00:00
|
|
|
HOMEKIT_PAIRING_QR,
|
|
|
|
HOMEKIT_PAIRING_QR_SECRET,
|
2020-05-01 04:05:06 +00:00
|
|
|
MANUFACTURER,
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_HOMEKIT_RESET_ACCESSORY,
|
2019-12-08 17:16:49 +00:00
|
|
|
SERVICE_HOMEKIT_START,
|
2020-05-01 04:05:06 +00:00
|
|
|
SHUTDOWN_TIMEOUT,
|
|
|
|
UNDO_UPDATE_LISTENER,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-05-25 09:37:20 +00:00
|
|
|
from .util import (
|
2020-05-01 04:05:06 +00:00
|
|
|
dismiss_setup_message,
|
|
|
|
get_persist_fullpath_for_entry_id,
|
|
|
|
migrate_filesystem_state_data_for_primary_imported_entry_id,
|
|
|
|
port_is_available,
|
|
|
|
remove_state_files_for_entry_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
show_setup_message,
|
|
|
|
validate_entity_config,
|
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2020-04-16 02:40:38 +00:00
|
|
|
MAX_DEVICES = 150
|
2018-05-04 14:46:00 +00:00
|
|
|
|
|
|
|
# #### Driver Status ####
|
|
|
|
STATUS_READY = 0
|
|
|
|
STATUS_RUNNING = 1
|
|
|
|
STATUS_STOPPED = 2
|
|
|
|
STATUS_WAIT = 3
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
def _has_all_unique_names_and_ports(bridges):
|
|
|
|
"""Validate that each homekit bridge configured has a unique name."""
|
|
|
|
names = [bridge[CONF_NAME] for bridge in bridges]
|
|
|
|
ports = [bridge[CONF_PORT] for bridge in bridges]
|
|
|
|
vol.Schema(vol.Unique())(names)
|
|
|
|
vol.Schema(vol.Unique())(ports)
|
|
|
|
return bridges
|
|
|
|
|
|
|
|
|
|
|
|
BRIDGE_SCHEMA = vol.Schema(
|
2019-07-31 19:25:30 +00:00
|
|
|
{
|
2020-05-01 04:05:06 +00:00
|
|
|
vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All(
|
|
|
|
cv.string, vol.Length(min=3, max=25)
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
|
|
vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
|
|
|
|
vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string),
|
|
|
|
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
|
|
|
|
vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean,
|
|
|
|
vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA,
|
|
|
|
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
|
|
|
|
vol.Optional(
|
|
|
|
CONF_ZEROCONF_DEFAULT_INTERFACE, default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE,
|
|
|
|
): cv.boolean,
|
2019-07-31 19:25:30 +00:00
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{DOMAIN: vol.All(cv.ensure_list, [BRIDGE_SCHEMA], _has_all_unique_names_and_ports)},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids}
|
|
|
|
)
|
2019-07-16 01:43:37 +00:00
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: dict):
|
|
|
|
"""Set up the HomeKit from yaml."""
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
|
|
|
|
_async_register_events_and_services(hass)
|
|
|
|
|
|
|
|
if DOMAIN not in config:
|
|
|
|
return True
|
|
|
|
|
|
|
|
current_entries = hass.config_entries.async_entries(DOMAIN)
|
|
|
|
|
|
|
|
entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries}
|
|
|
|
|
|
|
|
for index, conf in enumerate(config[DOMAIN]):
|
|
|
|
bridge_name = conf[CONF_NAME]
|
|
|
|
|
|
|
|
if (
|
|
|
|
bridge_name in entries_by_name
|
|
|
|
and entries_by_name[bridge_name].source == SOURCE_IMPORT
|
|
|
|
):
|
|
|
|
entry = entries_by_name[bridge_name]
|
|
|
|
# If they alter the yaml config we import the changes
|
|
|
|
# since there currently is no practical way to support
|
|
|
|
# all the options in the UI at this time.
|
|
|
|
data = conf.copy()
|
|
|
|
options = {}
|
|
|
|
for key in CONFIG_OPTIONS:
|
|
|
|
options[key] = data[key]
|
|
|
|
del data[key]
|
|
|
|
|
|
|
|
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
|
|
|
continue
|
|
|
|
|
|
|
|
conf[CONF_ENTRY_INDEX] = index
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
2020-04-19 18:51:09 +00:00
|
|
|
|
2020-04-21 22:38:43 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Set up HomeKit from a config entry."""
|
|
|
|
_async_import_options_from_data_if_missing(hass, entry)
|
|
|
|
|
|
|
|
conf = entry.data
|
|
|
|
options = entry.options
|
|
|
|
|
2018-07-22 07:51:42 +00:00
|
|
|
name = conf[CONF_NAME]
|
2018-03-15 01:48:21 +00:00
|
|
|
port = conf[CONF_PORT]
|
2020-05-01 04:05:06 +00:00
|
|
|
_LOGGER.debug("Begin setup HomeKit for %s", name)
|
|
|
|
|
|
|
|
# If the previous instance hasn't cleaned up yet
|
|
|
|
# we need to wait a bit
|
|
|
|
if not await hass.async_add_executor_job(port_is_available, port):
|
|
|
|
raise ConfigEntryNotReady
|
|
|
|
|
|
|
|
if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0:
|
|
|
|
_LOGGER.debug("Migrating legacy HomeKit data for %s", name)
|
|
|
|
hass.async_add_executor_job(
|
|
|
|
migrate_filesystem_state_data_for_primary_imported_entry_id,
|
|
|
|
hass,
|
|
|
|
entry.entry_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
aid_storage = AccessoryAidStorage(hass, entry.entry_id)
|
|
|
|
|
|
|
|
await aid_storage.async_initialize()
|
|
|
|
# These are yaml only
|
2018-04-30 12:58:17 +00:00
|
|
|
ip_address = conf.get(CONF_IP_ADDRESS)
|
2019-10-23 05:06:21 +00:00
|
|
|
advertise_ip = conf.get(CONF_ADVERTISE_IP)
|
2020-05-01 04:05:06 +00:00
|
|
|
entity_config = conf.get(CONF_ENTITY_CONFIG, {})
|
|
|
|
|
|
|
|
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
|
|
|
|
safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE)
|
|
|
|
entity_filter = convert_filter(
|
|
|
|
options.get(
|
|
|
|
CONF_FILTER,
|
|
|
|
{
|
|
|
|
CONF_INCLUDE_DOMAINS: [],
|
|
|
|
CONF_EXCLUDE_DOMAINS: [],
|
|
|
|
CONF_INCLUDE_ENTITIES: [],
|
|
|
|
CONF_EXCLUDE_ENTITIES: [],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
2020-04-12 14:59:50 +00:00
|
|
|
interface_choice = (
|
2020-05-01 04:05:06 +00:00
|
|
|
InterfaceChoice.Default
|
|
|
|
if options.get(CONF_ZEROCONF_DEFAULT_INTERFACE)
|
|
|
|
else None
|
2020-04-12 14:59:50 +00:00
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
homekit = HomeKit(
|
2019-10-23 05:06:21 +00:00
|
|
|
hass,
|
|
|
|
name,
|
|
|
|
port,
|
|
|
|
ip_address,
|
|
|
|
entity_filter,
|
|
|
|
entity_config,
|
|
|
|
safe_mode,
|
|
|
|
advertise_ip,
|
2020-04-12 14:59:50 +00:00
|
|
|
interface_choice,
|
2020-05-01 04:05:06 +00:00
|
|
|
entry.entry_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-10-19 22:14:05 +00:00
|
|
|
await hass.async_add_executor_job(homekit.setup)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
undo_listener = entry.add_update_listener(_async_update_listener)
|
|
|
|
|
|
|
|
hass.data[DOMAIN][entry.entry_id] = {
|
|
|
|
AID_STORAGE: aid_storage,
|
|
|
|
HOMEKIT: homekit,
|
|
|
|
UNDO_UPDATE_LISTENER: undo_listener,
|
|
|
|
}
|
|
|
|
|
|
|
|
if hass.state == CoreState.running:
|
|
|
|
await homekit.async_start()
|
|
|
|
elif auto_start:
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Handle options update."""
|
|
|
|
if entry.source == SOURCE_IMPORT:
|
|
|
|
return
|
|
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Unload a config entry."""
|
|
|
|
|
|
|
|
dismiss_setup_message(hass, entry.entry_id)
|
|
|
|
|
|
|
|
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
|
|
|
|
|
|
|
homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
|
|
|
|
|
|
|
|
if homekit.status == STATUS_RUNNING:
|
|
|
|
await homekit.async_stop()
|
|
|
|
|
|
|
|
for _ in range(0, SHUTDOWN_TIMEOUT):
|
|
|
|
if not await hass.async_add_executor_job(
|
|
|
|
port_is_available, entry.data[CONF_PORT]
|
|
|
|
):
|
|
|
|
_LOGGER.info("Waiting for the HomeKit server to shutdown.")
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Remove a config entry."""
|
|
|
|
return await hass.async_add_executor_job(
|
|
|
|
remove_state_files_for_entry_id, hass, entry.entry_id
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
options = dict(entry.options)
|
|
|
|
data = dict(entry.data)
|
|
|
|
modified = False
|
|
|
|
for importable_option in CONFIG_OPTIONS:
|
|
|
|
if importable_option not in entry.options and importable_option in entry.data:
|
|
|
|
options[importable_option] = entry.data[importable_option]
|
|
|
|
del data[importable_option]
|
|
|
|
modified = True
|
|
|
|
|
|
|
|
if modified:
|
|
|
|
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_register_events_and_services(hass: HomeAssistant):
|
|
|
|
"""Register events and services for HomeKit."""
|
|
|
|
|
|
|
|
hass.http.register_view(HomeKitPairingQRView)
|
|
|
|
|
2019-07-16 01:43:37 +00:00
|
|
|
def handle_homekit_reset_accessory(service):
|
|
|
|
"""Handle start HomeKit service call."""
|
2020-05-01 04:05:06 +00:00
|
|
|
for entry_id in hass.data[DOMAIN]:
|
|
|
|
if HOMEKIT not in hass.data[DOMAIN][entry_id]:
|
|
|
|
continue
|
|
|
|
homekit = hass.data[DOMAIN][entry_id][HOMEKIT]
|
|
|
|
if homekit.status != STATUS_RUNNING:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"HomeKit is not running. Either it is waiting to be "
|
|
|
|
"started or has been stopped."
|
|
|
|
)
|
|
|
|
continue
|
2019-07-16 01:43:37 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
entity_ids = service.data.get("entity_id")
|
|
|
|
homekit.reset_accessories(entity_ids)
|
2019-07-16 01:43:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN,
|
|
|
|
SERVICE_HOMEKIT_RESET_ACCESSORY,
|
|
|
|
handle_homekit_reset_accessory,
|
|
|
|
schema=RESET_ACCESSORY_SERVICE_SCHEMA,
|
|
|
|
)
|
2019-07-16 01:43:37 +00:00
|
|
|
|
2020-04-21 00:48:09 +00:00
|
|
|
@callback
|
|
|
|
def async_describe_logbook_event(event):
|
|
|
|
"""Describe a logbook event."""
|
|
|
|
data = event.data
|
|
|
|
entity_id = data.get(ATTR_ENTITY_ID)
|
|
|
|
value = data.get(ATTR_VALUE)
|
|
|
|
|
|
|
|
value_msg = f" to {value}" if value else ""
|
|
|
|
message = f"send command {data[ATTR_SERVICE]}{value_msg} for {data[ATTR_DISPLAY_NAME]}"
|
|
|
|
|
|
|
|
return {
|
|
|
|
"name": "HomeKit",
|
|
|
|
"message": message,
|
|
|
|
"entity_id": entity_id,
|
|
|
|
}
|
|
|
|
|
|
|
|
hass.components.logbook.async_describe_event(
|
|
|
|
DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event
|
|
|
|
)
|
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
async def async_handle_homekit_service_start(service):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Handle start HomeKit service call."""
|
2020-05-01 04:05:06 +00:00
|
|
|
for entry_id in hass.data[DOMAIN]:
|
|
|
|
if HOMEKIT not in hass.data[DOMAIN][entry_id]:
|
|
|
|
continue
|
|
|
|
homekit = hass.data[DOMAIN][entry_id][HOMEKIT]
|
|
|
|
if homekit.status != STATUS_READY:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"HomeKit is not ready. Either it is already running or has "
|
|
|
|
"been stopped."
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
await homekit.async_start()
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.async_register(
|
2020-04-22 00:43:49 +00:00
|
|
|
DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
class HomeKit:
|
2018-02-25 09:58:13 +00:00
|
|
|
"""Class to handle all actions between HomeKit and Home Assistant."""
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
2019-10-23 05:06:21 +00:00
|
|
|
self,
|
|
|
|
hass,
|
|
|
|
name,
|
|
|
|
port,
|
|
|
|
ip_address,
|
|
|
|
entity_filter,
|
|
|
|
entity_config,
|
|
|
|
safe_mode,
|
|
|
|
advertise_ip=None,
|
2020-04-12 14:59:50 +00:00
|
|
|
interface_choice=None,
|
2020-05-01 04:05:06 +00:00
|
|
|
entry_id=None,
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2018-02-25 09:58:13 +00:00
|
|
|
"""Initialize a HomeKit object."""
|
2018-04-11 20:24:14 +00:00
|
|
|
self.hass = hass
|
2018-07-22 07:51:42 +00:00
|
|
|
self._name = name
|
2018-02-19 22:46:22 +00:00
|
|
|
self._port = port
|
2018-04-30 12:58:17 +00:00
|
|
|
self._ip_address = ip_address
|
2018-03-15 01:48:21 +00:00
|
|
|
self._filter = entity_filter
|
|
|
|
self._config = entity_config
|
2018-11-16 10:08:39 +00:00
|
|
|
self._safe_mode = safe_mode
|
2019-10-23 05:06:21 +00:00
|
|
|
self._advertise_ip = advertise_ip
|
2020-04-12 14:59:50 +00:00
|
|
|
self._interface_choice = interface_choice
|
2020-05-01 04:05:06 +00:00
|
|
|
self._entry_id = entry_id
|
2018-05-04 14:46:00 +00:00
|
|
|
self.status = STATUS_READY
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
self.bridge = None
|
|
|
|
self.driver = None
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def setup(self):
|
2018-08-19 20:29:08 +00:00
|
|
|
"""Set up bridge and accessory driver."""
|
2019-12-22 09:23:44 +00:00
|
|
|
# pylint: disable=import-outside-toplevel
|
|
|
|
from .accessories import HomeBridge, HomeDriver
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
2018-04-30 12:58:17 +00:00
|
|
|
ip_addr = self._ip_address or get_local_ip()
|
2020-05-01 04:05:06 +00:00
|
|
|
persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id)
|
2019-07-31 19:25:30 +00:00
|
|
|
self.driver = HomeDriver(
|
2019-10-23 05:06:21 +00:00
|
|
|
self.hass,
|
2020-05-01 04:05:06 +00:00
|
|
|
self._entry_id,
|
|
|
|
self._name,
|
2019-10-23 05:06:21 +00:00
|
|
|
address=ip_addr,
|
|
|
|
port=self._port,
|
2020-05-01 04:05:06 +00:00
|
|
|
persist_file=persist_file,
|
2019-10-23 05:06:21 +00:00
|
|
|
advertised_address=self._advertise_ip,
|
2020-04-12 14:59:50 +00:00
|
|
|
interface_choice=self._interface_choice,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-07-22 07:51:42 +00:00
|
|
|
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
2018-11-16 10:08:39 +00:00
|
|
|
if self._safe_mode:
|
2020-05-01 04:05:06 +00:00
|
|
|
_LOGGER.debug("Safe_mode selected for %s", self._name)
|
2018-11-16 10:08:39 +00:00
|
|
|
self.driver.safe_mode = True
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2019-07-16 01:43:37 +00:00
|
|
|
def reset_accessories(self, entity_ids):
|
|
|
|
"""Reset the accessory to load the latest configuration."""
|
2020-05-01 04:05:06 +00:00
|
|
|
aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE]
|
2019-07-16 01:43:37 +00:00
|
|
|
removed = []
|
|
|
|
for entity_id in entity_ids:
|
2020-04-19 18:51:09 +00:00
|
|
|
aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
|
2019-07-16 01:43:37 +00:00
|
|
|
if aid not in self.bridge.accessories:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
2020-01-02 19:17:10 +00:00
|
|
|
"Could not reset accessory. entity_id not found %s", entity_id
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-07-16 01:43:37 +00:00
|
|
|
continue
|
|
|
|
acc = self.remove_bridge_accessory(aid)
|
|
|
|
removed.append(acc)
|
|
|
|
self.driver.config_changed()
|
|
|
|
|
|
|
|
for acc in removed:
|
|
|
|
self.bridge.add_accessory(acc)
|
|
|
|
self.driver.config_changed()
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def add_bridge_accessory(self, state):
|
|
|
|
"""Try adding accessory to bridge if configured beforehand."""
|
2020-04-22 00:43:49 +00:00
|
|
|
if not self._filter(state.entity_id):
|
2018-03-15 01:48:21 +00:00
|
|
|
return
|
2020-04-19 18:51:09 +00:00
|
|
|
|
|
|
|
# The bridge itself counts as an accessory
|
|
|
|
if len(self.bridge.accessories) + 1 >= MAX_DEVICES:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Cannot add %s as this would exceeded the %d device limit. Consider using the filter option.",
|
|
|
|
state.entity_id,
|
|
|
|
MAX_DEVICES,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
aid = self.hass.data[DOMAIN][self._entry_id][
|
|
|
|
AID_STORAGE
|
|
|
|
].get_or_allocate_aid_for_entity_id(state.entity_id)
|
2018-03-15 01:48:21 +00:00
|
|
|
conf = self._config.pop(state.entity_id, {})
|
2020-04-16 02:40:38 +00:00
|
|
|
# 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
|
|
|
|
try:
|
|
|
|
acc = get_accessory(self.hass, self.driver, state, aid, conf)
|
|
|
|
if acc is not None:
|
|
|
|
self.bridge.add_accessory(acc)
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
_LOGGER.exception(
|
|
|
|
"Failed to create a HomeKit accessory for %s", state.entity_id
|
|
|
|
)
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2019-07-16 01:43:37 +00:00
|
|
|
def remove_bridge_accessory(self, aid):
|
|
|
|
"""Try adding accessory to bridge if configured beforehand."""
|
|
|
|
acc = None
|
|
|
|
if aid in self.bridge.accessories:
|
|
|
|
acc = self.bridge.accessories.pop(aid)
|
|
|
|
return acc
|
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
async def async_start(self, *args):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Start the accessory driver."""
|
2020-05-01 04:05:06 +00:00
|
|
|
|
2018-05-04 14:46:00 +00:00
|
|
|
if self.status != STATUS_READY:
|
2018-03-15 01:48:21 +00:00
|
|
|
return
|
2018-05-04 14:46:00 +00:00
|
|
|
self.status = STATUS_WAIT
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
ent_reg = await entity_registry.async_get_registry(self.hass)
|
|
|
|
|
|
|
|
device_lookup = ent_reg.async_get_device_class_lookup(
|
|
|
|
{
|
|
|
|
("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING),
|
|
|
|
("sensor", DEVICE_CLASS_BATTERY),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
bridged_states = []
|
|
|
|
for state in self.hass.states.async_all():
|
|
|
|
if not self._filter(state.entity_id):
|
|
|
|
continue
|
|
|
|
|
|
|
|
self._async_configure_linked_battery_sensors(ent_reg, device_lookup, state)
|
|
|
|
bridged_states.append(state)
|
|
|
|
|
|
|
|
await self.hass.async_add_executor_job(self._start, bridged_states)
|
2020-05-01 04:05:06 +00:00
|
|
|
await self._async_register_bridge()
|
|
|
|
|
|
|
|
async def _async_register_bridge(self):
|
|
|
|
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
|
|
|
|
registry = await device_registry.async_get_registry(self.hass)
|
|
|
|
registry.async_get_or_create(
|
|
|
|
config_entry_id=self._entry_id,
|
|
|
|
connections={
|
|
|
|
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
|
|
|
|
},
|
|
|
|
manufacturer=MANUFACTURER,
|
|
|
|
name=self._name,
|
|
|
|
model="Home Assistant HomeKit Bridge",
|
|
|
|
)
|
2020-04-22 00:43:49 +00:00
|
|
|
|
|
|
|
def _start(self, bridged_states):
|
2019-12-09 15:10:02 +00:00
|
|
|
from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel
|
2019-07-31 19:25:30 +00:00
|
|
|
type_covers,
|
|
|
|
type_fans,
|
|
|
|
type_lights,
|
|
|
|
type_locks,
|
|
|
|
type_media_players,
|
|
|
|
type_security_systems,
|
|
|
|
type_sensors,
|
|
|
|
type_switches,
|
|
|
|
type_thermostats,
|
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
for state in bridged_states:
|
2018-03-15 01:48:21 +00:00
|
|
|
self.add_bridge_accessory(state)
|
2020-04-19 18:51:09 +00:00
|
|
|
|
2018-05-29 20:43:26 +00:00
|
|
|
self.driver.add_accessory(self.bridge)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-05-18 14:32:57 +00:00
|
|
|
if not self.driver.state.paired:
|
2020-04-21 22:38:43 +00:00
|
|
|
show_setup_message(
|
2020-05-01 04:05:06 +00:00
|
|
|
self.hass,
|
|
|
|
self._entry_id,
|
|
|
|
self._name,
|
|
|
|
self.driver.state.pincode,
|
|
|
|
self.bridge.xhm_uri(),
|
2020-04-21 22:38:43 +00:00
|
|
|
)
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
_LOGGER.debug("Driver start for %s", self._name)
|
2020-04-30 20:29:50 +00:00
|
|
|
self.hass.add_job(self.driver.start)
|
2018-05-04 14:46:00 +00:00
|
|
|
self.status = STATUS_RUNNING
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2020-04-22 00:43:49 +00:00
|
|
|
async def async_stop(self, *args):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Stop the accessory driver."""
|
2018-05-04 14:46:00 +00:00
|
|
|
if self.status != STATUS_RUNNING:
|
2018-03-15 01:48:21 +00:00
|
|
|
return
|
2018-05-04 14:46:00 +00:00
|
|
|
self.status = STATUS_STOPPED
|
2020-05-01 04:05:06 +00:00
|
|
|
_LOGGER.debug("Driver stop for %s", self._name)
|
2020-04-30 22:49:16 +00:00
|
|
|
self.hass.add_job(self.driver.stop)
|
2020-04-22 00:43:49 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state):
|
|
|
|
entry = ent_reg.async_get(state.entity_id)
|
|
|
|
|
|
|
|
if (
|
|
|
|
entry is None
|
|
|
|
or entry.device_id is None
|
|
|
|
or entry.device_id not in device_lookup
|
|
|
|
or entry.device_class
|
|
|
|
in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY)
|
|
|
|
):
|
|
|
|
return
|
|
|
|
|
|
|
|
if ATTR_BATTERY_CHARGING not in state.attributes:
|
|
|
|
battery_charging_binary_sensor_entity_id = device_lookup[
|
|
|
|
entry.device_id
|
|
|
|
].get(("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING))
|
|
|
|
if battery_charging_binary_sensor_entity_id:
|
|
|
|
self._config.setdefault(state.entity_id, {}).setdefault(
|
|
|
|
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
|
|
|
battery_charging_binary_sensor_entity_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
if ATTR_BATTERY_LEVEL not in state.attributes:
|
|
|
|
battery_sensor_entity_id = device_lookup[entry.device_id].get(
|
|
|
|
("sensor", DEVICE_CLASS_BATTERY)
|
|
|
|
)
|
|
|
|
if battery_sensor_entity_id:
|
|
|
|
self._config.setdefault(state.entity_id, {}).setdefault(
|
|
|
|
CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id
|
|
|
|
)
|
2020-04-21 22:38:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HomeKitPairingQRView(HomeAssistantView):
|
|
|
|
"""Display the homekit pairing code at a protected url."""
|
|
|
|
|
|
|
|
url = "/api/homekit/pairingqr"
|
|
|
|
name = "api:homekit:pairingqr"
|
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
# pylint: disable=no-self-use
|
|
|
|
async def get(self, request):
|
|
|
|
"""Retrieve the pairing QRCode image."""
|
2020-05-01 04:05:06 +00:00
|
|
|
if not request.query_string:
|
|
|
|
raise Unauthorized()
|
|
|
|
entry_id, secret = request.query_string.split("-")
|
|
|
|
|
|
|
|
if (
|
|
|
|
entry_id not in request.app["hass"].data[DOMAIN]
|
|
|
|
or secret
|
|
|
|
!= request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET]
|
|
|
|
):
|
2020-04-21 22:38:43 +00:00
|
|
|
raise Unauthorized()
|
|
|
|
return web.Response(
|
2020-05-01 04:05:06 +00:00
|
|
|
body=request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR],
|
2020-04-21 22:38:43 +00:00
|
|
|
content_type="image/svg+xml",
|
|
|
|
)
|