Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor homekit_controller to be fully asynchronous #32111

Merged
merged 3 commits into from
Feb 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Support for Homekit device discovery."""
import logging

import homekit
from homekit.model.characteristics import CharacteristicsTypes
import aiohomekit
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
Expand Down Expand Up @@ -94,7 +94,8 @@ def setup(self):
def _setup_characteristic(self, char):
"""Configure an entity based on a HomeKit characteristics metadata."""
# Build up a list of (aid, iid) tuples to poll on update()
self.pollable_characteristics.append((self._aid, char["iid"]))
if "pr" in char["perms"]:
self.pollable_characteristics.append((self._aid, char["iid"]))

# Build a map of ctype -> iid
short_name = CharacteristicsTypes.get_short(char["type"])
Expand Down Expand Up @@ -223,7 +224,7 @@ async def async_setup(hass, config):
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
await map_storage.async_initialize()

hass.data[CONTROLLER] = homekit.Controller()
hass.data[CONTROLLER] = aiohomekit.Controller()
hass.data[KNOWN_DEVICES] = {}

return True
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Support for HomeKit Controller air quality sensors."""
from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.air_quality import AirQualityEntity
from homeassistant.core import callback
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit Alarm Control Panel."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.alarm_control_panel.const import (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit motion sensors."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_SMOKE,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/climate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit climate devices."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.climate import (
DEFAULT_MAX_HUMIDITY,
Expand Down
66 changes: 30 additions & 36 deletions homeassistant/components/homekit_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import os
import re

import homekit
from homekit.controller.ip_implementation import IpPairing
import aiohomekit
from aiohomekit import Controller
from aiohomekit.controller.ip import IpPairing
import voluptuous as vol

from homeassistant import config_entries
Expand Down Expand Up @@ -72,7 +73,7 @@ def ensure_pin_format(pin):
"""
match = PIN_FORMAT.search(pin)
if not match:
raise homekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
return "{}-{}-{}".format(*match.groups())


Expand All @@ -88,7 +89,7 @@ def __init__(self):
self.model = None
self.hkid = None
self.devices = {}
self.controller = homekit.Controller()
self.controller = Controller()
self.finish_pairing = None

async def async_step_user(self, user_input=None):
Expand All @@ -97,22 +98,22 @@ async def async_step_user(self, user_input=None):

if user_input is not None:
key = user_input["device"]
self.hkid = self.devices[key]["id"]
self.model = self.devices[key]["md"]
self.hkid = self.devices[key].device_id
self.model = self.devices[key].info["md"]
await self.async_set_unique_id(
normalize_hkid(self.hkid), raise_on_progress=False
)
return await self.async_step_pair()

all_hosts = await self.hass.async_add_executor_job(self.controller.discover, 5)
all_hosts = await self.controller.discover_ip()

self.devices = {}
for host in all_hosts:
status_flags = int(host["sf"])
status_flags = int(host.info["sf"])
paired = not status_flags & 0x01
if paired:
continue
self.devices[host["name"]] = host
self.devices[host.info["name"]] = host

if not self.devices:
return self.async_abort(reason="no_devices")
Expand All @@ -130,10 +131,11 @@ async def async_step_unignore(self, user_input):
unique_id = user_input["unique_id"]
await self.async_set_unique_id(unique_id)

records = await self.hass.async_add_executor_job(self.controller.discover, 5)
for record in records:
if normalize_hkid(record["id"]) != unique_id:
devices = await self.controller.discover_ip(5)
for device in devices:
if normalize_hkid(device.device_id) != unique_id:
continue
record = device.info
return await self.async_step_zeroconf(
{
"host": record["address"],
Expand Down Expand Up @@ -295,55 +297,49 @@ async def async_step_pair(self, pair_info=None):
code = pair_info["pairing_code"]
try:
code = ensure_pin_format(code)

await self.hass.async_add_executor_job(self.finish_pairing, code)

pairing = self.controller.pairings.get(self.hkid)
if pairing:
return await self._entry_from_accessory(pairing)

errors["pairing_code"] = "unable_to_pair"
except homekit.exceptions.MalformedPinError:
pairing = await self.finish_pairing(code)
return await self._entry_from_accessory(pairing)
except aiohomekit.exceptions.MalformedPinError:
# Library claimed pin was invalid before even making an API call
errors["pairing_code"] = "authentication_error"
except homekit.AuthenticationError:
except aiohomekit.AuthenticationError:
# PairSetup M4 - SRP proof failed
# PairSetup M6 - Ed25519 signature verification failed
# PairVerify M4 - Decryption failed
# PairVerify M4 - Device not recognised
# PairVerify M4 - Ed25519 signature verification failed
errors["pairing_code"] = "authentication_error"
except homekit.UnknownError:
except aiohomekit.UnknownError:
# An error occurred on the device whilst performing this
# operation.
errors["pairing_code"] = "unknown_error"
except homekit.MaxPeersError:
except aiohomekit.MaxPeersError:
# The device can't pair with any more accessories.
errors["pairing_code"] = "max_peers_error"
except homekit.AccessoryNotFoundError:
except aiohomekit.AccessoryNotFoundError:
# Can no longer find the device on the network
return self.async_abort(reason="accessory_not_found_error")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Pairing attempt failed with an unhandled exception")
errors["pairing_code"] = "pairing_failed"

start_pairing = self.controller.start_pairing
discovery = await self.controller.find_ip_by_device_id(self.hkid)

try:
self.finish_pairing = await self.hass.async_add_executor_job(
start_pairing, self.hkid, self.hkid
)
except homekit.BusyError:
self.finish_pairing = await discovery.start_pairing(self.hkid)

except aiohomekit.BusyError:
# Already performing a pair setup operation with a different
# controller
errors["pairing_code"] = "busy_error"
except homekit.MaxTriesError:
except aiohomekit.MaxTriesError:
# The accessory has received more than 100 unsuccessful auth
# attempts.
errors["pairing_code"] = "max_tries_error"
except homekit.UnavailableError:
except aiohomekit.UnavailableError:
# The accessory is already paired - cannot try to pair again.
return self.async_abort(reason="already_paired")
except homekit.AccessoryNotFoundError:
except aiohomekit.AccessoryNotFoundError:
# Can no longer find the device on the network
return self.async_abort(reason="accessory_not_found_error")
except Exception: # pylint: disable=broad-except
Expand Down Expand Up @@ -376,9 +372,7 @@ async def _entry_from_accessory(self, pairing):
# the same time.
accessories = pairing_data.pop("accessories", None)
if not accessories:
accessories = await self.hass.async_add_executor_job(
pairing.list_accessories_and_characteristics
)
accessories = await pairing.list_accessories_and_characteristics()

bridge_info = get_bridge_information(accessories)
name = get_accessory_name(bridge_info)
Expand Down
22 changes: 7 additions & 15 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import datetime
import logging

from homekit.controller.ip_implementation import IpPairing
from homekit.exceptions import (
from aiohomekit.controller.ip import IpPairing
from aiohomekit.exceptions import (
AccessoryDisconnectedError,
AccessoryNotFoundError,
EncryptionError,
)
from homekit.model.characteristics import CharacteristicsTypes
from homekit.model.services import ServicesTypes
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes

from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
Expand Down Expand Up @@ -186,10 +186,7 @@ async def async_unload(self):
async def async_refresh_entity_map(self, config_num):
"""Handle setup of a HomeKit accessory."""
try:
async with self.pairing_lock:
self.accessories = await self.hass.async_add_executor_job(
self.pairing.list_accessories_and_characteristics
)
self.accessories = await self.pairing.list_accessories_and_characteristics()
except AccessoryDisconnectedError:
# If we fail to refresh this data then we will naturally retry
# later when Bonjour spots c# is still not up to date.
Expand Down Expand Up @@ -305,10 +302,7 @@ def process_new_events(self, new_values_dict):
async def get_characteristics(self, *args, **kwargs):
"""Read latest state from homekit accessory."""
async with self.pairing_lock:
chars = await self.hass.async_add_executor_job(
self.pairing.get_characteristics, *args, **kwargs
)
return chars
return await self.pairing.get_characteristics(*args, **kwargs)

async def put_characteristics(self, characteristics):
"""Control a HomeKit device state from Home Assistant."""
Expand All @@ -317,9 +311,7 @@ async def put_characteristics(self, characteristics):
chars.append((row["aid"], row["iid"], row["value"]))

async with self.pairing_lock:
results = await self.hass.async_add_executor_job(
self.pairing.put_characteristics, chars
)
results = await self.pairing.put_characteristics(chars)

# Feed characteristics back into HA and update the current state
# results will only contain failures, so anythin in characteristics
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/cover.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit covers."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.cover import (
ATTR_POSITION,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/fan.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit fans."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.fan import (
DIRECTION_FORWARD,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/light.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit lights."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/lock.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for HomeKit Controller locks."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["homekit[IP]==0.15.0"],
"requirements": ["aiohomekit[IP]==0.2.10"],
"dependencies": [],
"zeroconf": ["_hap._tcp.local."],
"codeowners": ["@Jc2k"]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Support for Homekit sensors."""
from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.const import DEVICE_CLASS_BATTERY, TEMP_CELSIUS
from homeassistant.core import callback
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/switch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for Homekit switches."""
import logging

from homekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics import CharacteristicsTypes

from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
Expand Down
6 changes: 3 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ aioftp==0.12.0
# homeassistant.components.harmony
aioharmony==0.1.13

# homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.10

# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
Expand Down Expand Up @@ -688,9 +691,6 @@ home-assistant-frontend==20200220.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8

# homeassistant.components.homekit_controller
homekit[IP]==0.15.0

# homeassistant.components.homematicip_cloud
homematicip==0.10.17

Expand Down
6 changes: 3 additions & 3 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ aiobotocore==0.11.1
# homeassistant.components.esphome
aioesphomeapi==2.6.1

# homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.10

# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
Expand Down Expand Up @@ -259,9 +262,6 @@ home-assistant-frontend==20200220.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8

# homeassistant.components.homekit_controller
homekit[IP]==0.15.0

# homeassistant.components.homematicip_cloud
homematicip==0.10.17

Expand Down
Loading