Skip to content

Commit

Permalink
Config flow for homekit (home-assistant#34560)
Browse files Browse the repository at this point in the history
* Config flow for homekit

Allows multiple homekit bridges to run

HAP-python state is now stored at .storage/homekit.{entry_id}.state
aids is now stored at .storage/homekit.{entry_id}.aids

Overcomes 150 device limit by supporting
multiple bridges.

Name and port are now automatically allocated
to avoid conflicts which was one of the main
reasons pairing failed.

YAML configuration remains available in order to offer entity
specific configuration.

Entries created by config flow can add and remove
included domains and entities without having to restart

* Fix services as there are multiple now

* migrate in executor

* drop title from strings

* Update homeassistant/components/homekit/strings.json

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Make auto_start advanced mode only, add coverage

* put back title

* more references

* delete port since manual config is no longer needed

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
  • Loading branch information
bdraco and balloob authored May 1, 2020
1 parent 5699cb8 commit 793592b
Show file tree
Hide file tree
Showing 29 changed files with 1,754 additions and 322 deletions.
488 changes: 282 additions & 206 deletions homeassistant/components/homekit/__init__.py

Large diffs are not rendered by default.

153 changes: 149 additions & 4 deletions homeassistant/components/homekit/accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_OTHER

from homeassistant.components import cover, vacuum
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_SERVICE,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_TYPE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
STATE_ON,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
UNIT_PERCENTAGE,
__version__,
)
from homeassistant.core import callback as ha_callback, split_entity_id
Expand All @@ -22,6 +36,7 @@
track_point_in_utc_time,
)
from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry

from .const import (
ATTR_DISPLAY_NAME,
Expand All @@ -31,21 +46,45 @@
CHAR_BATTERY_LEVEL,
CHAR_CHARGING_STATE,
CHAR_STATUS_LOW_BATTERY,
CONF_FEATURE_LIST,
CONF_LINKED_BATTERY_CHARGING_SENSOR,
CONF_LINKED_BATTERY_SENSOR,
CONF_LOW_BATTERY_THRESHOLD,
DEBOUNCE_TIMEOUT,
DEFAULT_LOW_BATTERY_THRESHOLD,
DEVICE_CLASS_CO,
DEVICE_CLASS_CO2,
DEVICE_CLASS_PM25,
EVENT_HOMEKIT_CHANGED,
HK_CHARGING,
HK_NOT_CHARGABLE,
HK_NOT_CHARGING,
MANUFACTURER,
SERV_BATTERY_SERVICE,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
TYPE_SPRINKLER,
TYPE_SWITCH,
TYPE_VALVE,
)
from .util import (
convert_to_float,
dismiss_setup_message,
show_setup_message,
validate_media_player_features,
)
from .util import convert_to_float, dismiss_setup_message, show_setup_message

_LOGGER = logging.getLogger(__name__)
SWITCH_TYPES = {
TYPE_FAUCET: "Valve",
TYPE_OUTLET: "Outlet",
TYPE_SHOWER: "Valve",
TYPE_SPRINKLER: "Valve",
TYPE_SWITCH: "Switch",
TYPE_VALVE: "Valve",
}
TYPES = Registry()


def debounce(func):
Expand Down Expand Up @@ -79,6 +118,104 @@ def wrapper(self, *args):
return wrapper


def get_accessory(hass, driver, state, aid, config):
"""Take state and return an accessory object if supported."""
if not aid:
_LOGGER.warning(
'The entity "%s" is not supported, since it '
"generates an invalid aid, please change it.",
state.entity_id,
)
return None

a_type = None
name = config.get(CONF_NAME, state.name)

if state.domain == "alarm_control_panel":
a_type = "SecuritySystem"

elif state.domain in ("binary_sensor", "device_tracker", "person"):
a_type = "BinarySensor"

elif state.domain == "climate":
a_type = "Thermostat"

elif state.domain == "cover":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)

if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & (
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
):
a_type = "GarageDoorOpener"
elif features & cover.SUPPORT_SET_POSITION:
a_type = "WindowCovering"
elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
a_type = "WindowCoveringBasic"

elif state.domain == "fan":
a_type = "Fan"

elif state.domain == "light":
a_type = "Light"

elif state.domain == "lock":
a_type = "Lock"

elif state.domain == "media_player":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
feature_list = config.get(CONF_FEATURE_LIST)

if device_class == DEVICE_CLASS_TV:
a_type = "TelevisionMediaPlayer"
else:
if feature_list and validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"

elif state.domain == "sensor":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

if device_class == DEVICE_CLASS_TEMPERATURE or unit in (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
):
a_type = "TemperatureSensor"
elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE:
a_type = "HumiditySensor"
elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id:
a_type = "AirQualitySensor"
elif device_class == DEVICE_CLASS_CO:
a_type = "CarbonMonoxideSensor"
elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id:
a_type = "CarbonDioxideSensor"
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"):
a_type = "LightSensor"

elif state.domain == "switch":
switch_type = config.get(CONF_TYPE, TYPE_SWITCH)
a_type = SWITCH_TYPES[switch_type]

elif state.domain == "vacuum":
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME):
a_type = "DockVacuum"
else:
a_type = "Switch"

elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"):
a_type = "Switch"

elif state.domain == "water_heater":
a_type = "WaterHeater"

if a_type is None:
return None

_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)


class HomeAccessory(Accessory):
"""Adapter class for Accessory."""

Expand Down Expand Up @@ -327,19 +464,27 @@ def setup_message(self):
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""

def __init__(self, hass, **kwargs):
def __init__(self, hass, entry_id, bridge_name, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(**kwargs)
self.hass = hass
self._entry_id = entry_id
self._bridge_name = bridge_name

def pair(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
success = super().pair(client_uuid, client_public)
if success:
dismiss_setup_message(self.hass)
dismiss_setup_message(self.hass, self._entry_id)
return success

def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().unpair(client_uuid)
show_setup_message(self.hass, self.state.pincode, self.accessory.xhm_uri())
show_setup_message(
self.hass,
self._entry_id,
self._bridge_name,
self.state.pincode,
self.accessory.xhm_uri(),
)
12 changes: 7 additions & 5 deletions homeassistant/components/homekit/aidmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@

from fnvhash import fnv1a_32

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.storage import Store

from .const import DOMAIN
from .util import get_aid_storage_filename_for_entry_id

AID_MANAGER_STORAGE_KEY = f"{DOMAIN}.aids"
AID_MANAGER_STORAGE_VERSION = 1
AID_MANAGER_SAVE_DELAY = 2

Expand Down Expand Up @@ -74,20 +74,22 @@ class AccessoryAidStorage:
persist over reboots.
"""

def __init__(self, hass: HomeAssistant):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
"""Create a new entity map store."""
self.hass = hass
self.store = Store(hass, AID_MANAGER_STORAGE_VERSION, AID_MANAGER_STORAGE_KEY)
self.allocations = {}
self.allocated_aids = set()

self._entry = entry
self.store = None
self._entity_registry = None

async def async_initialize(self):
"""Load the latest AID data."""
self._entity_registry = (
await self.hass.helpers.entity_registry.async_get_registry()
)
aidstore = get_aid_storage_filename_for_entry_id(self._entry)
self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore)

raw_storage = await self.store.async_load()
if not raw_storage:
Expand Down
Loading

0 comments on commit 793592b

Please sign in to comment.