Skip to content

Commit

Permalink
Display Homekit QR code when pairing (home-assistant#34449)
Browse files Browse the repository at this point in the history
* Display a QR code for homekit pairing

This will reduce the failure rate with HomeKit
pairing because there is less chance of entry
error.

* Add coverage

* Test that the qr code is created

* I cannot spell

* Update homeassistant/components/homekit/__init__.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

* Update homeassistant/components/homekit/__init__.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
bdraco and balloob authored Apr 21, 2020
1 parent ca08b70 commit d06fce6
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 14 deletions.
29 changes: 28 additions & 1 deletion homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import ipaddress
import logging

from aiohttp import web
import voluptuous as vol
from zeroconf import InterfaceChoice

from homeassistant.components import cover, vacuum
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
ATTR_DEVICE_CLASS,
Expand All @@ -28,6 +30,7 @@
UNIT_PERCENTAGE,
)
from homeassistant.core import callback
from homeassistant.exceptions import Unauthorized
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
Expand Down Expand Up @@ -56,6 +59,8 @@
DOMAIN,
EVENT_HOMEKIT_CHANGED,
HOMEKIT_FILE,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
SERVICE_HOMEKIT_RESET_ACCESSORY,
SERVICE_HOMEKIT_START,
TYPE_FAUCET,
Expand Down Expand Up @@ -129,6 +134,8 @@ async def async_setup(hass, config):
aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass)
await aid_storage.async_initialize()

hass.http.register_view(HomeKitPairingQRView)

conf = config[DOMAIN]
name = conf[CONF_NAME]
port = conf[CONF_PORT]
Expand Down Expand Up @@ -445,7 +452,9 @@ def start(self, *args):
self.driver.add_accessory(self.bridge)

if not self.driver.state.paired:
show_setup_message(self.hass, self.driver.state.pincode)
show_setup_message(
self.hass, self.driver.state.pincode, self.bridge.xhm_uri()
)

_LOGGER.debug("Driver start")
self.hass.add_job(self.driver.start)
Expand All @@ -459,3 +468,21 @@ def stop(self, *args):

_LOGGER.debug("Driver stop")
self.hass.add_job(self.driver.stop)


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."""
if request.query_string != request.app["hass"].data[HOMEKIT_PAIRING_QR_SECRET]:
raise Unauthorized()
return web.Response(
body=request.app["hass"].data[HOMEKIT_PAIRING_QR],
content_type="image/svg+xml",
)
2 changes: 1 addition & 1 deletion homeassistant/components/homekit/accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,4 @@ def pair(self, client_uuid, client_public):
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)
show_setup_message(self.hass, self.state.pincode, self.accessory.xhm_uri())
3 changes: 2 additions & 1 deletion homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
HOMEKIT_FILE = ".homekit.state"
HOMEKIT_NOTIFY_ID = 4663548
AID_STORAGE = "homekit-aid-allocations"

HOMEKIT_PAIRING_QR = "homekit-pairing-qr"
HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret"

# #### Attributes ####
ATTR_DISPLAY_NAME = "display_name"
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/homekit/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"domain": "homekit",
"name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": ["HAP-python==2.8.2", "fnvhash==0.1.0"],
"codeowners": ["@bdraco"],
"after_dependencies": ["logbook"]
"requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"],
"dependencies": ["http"],
"after_dependencies": ["logbook"],
"codeowners": ["@bdraco"]
}
22 changes: 19 additions & 3 deletions homeassistant/components/homekit/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Collection of useful functions for the HomeKit component."""
from collections import OrderedDict, namedtuple
import io
import logging
import secrets

import pyqrcode
import voluptuous as vol

from homeassistant.components import fan, media_player, sensor
Expand All @@ -27,6 +30,8 @@
FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE,
HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
Expand Down Expand Up @@ -205,13 +210,24 @@ def speed_to_states(self, speed):
return list(self.speed_ranges.keys())[0]


def show_setup_message(hass, pincode):
def show_setup_message(hass, pincode, uri):
"""Display persistent notification with setup information."""
pin = pincode.decode()
_LOGGER.info("Pincode: %s", pin)

buffer = io.BytesIO()
url = pyqrcode.create(uri)
url.svg(buffer, scale=5)
pairing_secret = secrets.token_hex(32)

hass.data[HOMEKIT_PAIRING_QR] = buffer.getvalue()
hass.data[HOMEKIT_PAIRING_QR_SECRET] = pairing_secret

message = (
f"To set up Home Assistant in the Home App, enter the "
f"following code:\n### {pin}"
f"To set up Home Assistant in the Home App, "
f"scan the QR code or enter the following code:\n"
f"### {pin}\n"
f"![image](/api/homekit/pairingqr?{pairing_secret})"
)
hass.components.persistent_notification.create(
message, "HomeKit Setup", HOMEKIT_NOTIFY_ID
Expand Down
4 changes: 4 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ PyMata==2.20
PyNaCl==1.3.0

# homeassistant.auth.mfa_modules.totp
# homeassistant.components.homekit
PyQRCode==1.2.1

# homeassistant.components.rmvtransport
Expand Down Expand Up @@ -304,6 +305,9 @@ azure-servicebus==0.50.1
# homeassistant.components.baidu
baidu-aip==1.6.6

# homeassistant.components.homekit
base36==0.1.1

# homeassistant.components.modem_callerid
basicmodem==0.7

Expand Down
4 changes: 4 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HAP-python==2.8.2
PyNaCl==1.3.0

# homeassistant.auth.mfa_modules.totp
# homeassistant.components.homekit
PyQRCode==1.2.1

# homeassistant.components.rmvtransport
Expand Down Expand Up @@ -130,6 +131,9 @@ av==6.1.2
# homeassistant.components.axis
axis==25

# homeassistant.components.homekit
base36==0.1.1

# homeassistant.components.zha
bellows-homeassistant==0.15.2

Expand Down
4 changes: 3 additions & 1 deletion tests/components/homekit/test_accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ def test_home_driver():

mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path)
driver.state = Mock(pincode=pin)
xhm_uri_mock = Mock(return_value="X-HM://0")
driver.accessory = Mock(xhm_uri=xhm_uri_mock)

# pair
with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch(
Expand All @@ -357,4 +359,4 @@ def test_home_driver():
driver.unpair("client_uuid")

mock_unpair.assert_called_with("client_uuid")
mock_show_msg.assert_called_with("hass", pin)
mock_show_msg.assert_called_with("hass", pin, "X-HM://0")
8 changes: 5 additions & 3 deletions tests/components/homekit/test_homekit.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher):
await hass.async_add_executor_job(homekit.start)

mock_add_acc.assert_called_with(state)
mock_setup_msg.assert_called_with(hass, pin)
mock_setup_msg.assert_called_with(hass, pin, ANY)
hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
Expand Down Expand Up @@ -328,7 +328,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p
) as hk_driver_start:
await hass.async_add_executor_job(homekit.start)

mock_setup_msg.assert_called_with(hass, pin)
mock_setup_msg.assert_called_with(hass, pin, ANY)
hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
Expand Down Expand Up @@ -405,6 +405,8 @@ async def test_homekit_too_many_accessories(hass, hk_driver):

with patch("pyhap.accessory_driver.AccessoryDriver.start"), patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn:
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn, patch(
f"{PATH_HOMEKIT}.show_setup_message"
):
await hass.async_add_executor_job(homekit.start)
assert mock_warn.called is True
6 changes: 5 additions & 1 deletion tests/components/homekit/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE,
HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
Expand Down Expand Up @@ -199,8 +201,10 @@ async def test_show_setup_msg(hass):

call_create_notification = async_mock_service(hass, DOMAIN, "create")

await hass.async_add_executor_job(show_setup_message, hass, pincode)
await hass.async_add_executor_job(show_setup_message, hass, pincode, "X-HM://0")
await hass.async_block_till_done()
assert hass.data[HOMEKIT_PAIRING_QR_SECRET]
assert hass.data[HOMEKIT_PAIRING_QR]

assert call_create_notification
assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID
Expand Down

0 comments on commit d06fce6

Please sign in to comment.