Skip to content

Commit d06fce6

Browse files
bdracoballoob
andauthored
Display Homekit QR code when pairing (home-assistant#34449)
* 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]>
1 parent ca08b70 commit d06fce6

File tree

10 files changed

+75
-14
lines changed

10 files changed

+75
-14
lines changed

homeassistant/components/homekit/__init__.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import ipaddress
33
import logging
44

5+
from aiohttp import web
56
import voluptuous as vol
67
from zeroconf import InterfaceChoice
78

89
from homeassistant.components import cover, vacuum
910
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
11+
from homeassistant.components.http import HomeAssistantView
1012
from homeassistant.components.media_player import DEVICE_CLASS_TV
1113
from homeassistant.const import (
1214
ATTR_DEVICE_CLASS,
@@ -28,6 +30,7 @@
2830
UNIT_PERCENTAGE,
2931
)
3032
from homeassistant.core import callback
33+
from homeassistant.exceptions import Unauthorized
3134
import homeassistant.helpers.config_validation as cv
3235
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
3336
from homeassistant.util import get_local_ip
@@ -56,6 +59,8 @@
5659
DOMAIN,
5760
EVENT_HOMEKIT_CHANGED,
5861
HOMEKIT_FILE,
62+
HOMEKIT_PAIRING_QR,
63+
HOMEKIT_PAIRING_QR_SECRET,
5964
SERVICE_HOMEKIT_RESET_ACCESSORY,
6065
SERVICE_HOMEKIT_START,
6166
TYPE_FAUCET,
@@ -129,6 +134,8 @@ async def async_setup(hass, config):
129134
aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass)
130135
await aid_storage.async_initialize()
131136

137+
hass.http.register_view(HomeKitPairingQRView)
138+
132139
conf = config[DOMAIN]
133140
name = conf[CONF_NAME]
134141
port = conf[CONF_PORT]
@@ -445,7 +452,9 @@ def start(self, *args):
445452
self.driver.add_accessory(self.bridge)
446453

447454
if not self.driver.state.paired:
448-
show_setup_message(self.hass, self.driver.state.pincode)
455+
show_setup_message(
456+
self.hass, self.driver.state.pincode, self.bridge.xhm_uri()
457+
)
449458

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

460469
_LOGGER.debug("Driver stop")
461470
self.hass.add_job(self.driver.stop)
471+
472+
473+
class HomeKitPairingQRView(HomeAssistantView):
474+
"""Display the homekit pairing code at a protected url."""
475+
476+
url = "/api/homekit/pairingqr"
477+
name = "api:homekit:pairingqr"
478+
requires_auth = False
479+
480+
# pylint: disable=no-self-use
481+
async def get(self, request):
482+
"""Retrieve the pairing QRCode image."""
483+
if request.query_string != request.app["hass"].data[HOMEKIT_PAIRING_QR_SECRET]:
484+
raise Unauthorized()
485+
return web.Response(
486+
body=request.app["hass"].data[HOMEKIT_PAIRING_QR],
487+
content_type="image/svg+xml",
488+
)

homeassistant/components/homekit/accessories.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,4 @@ def pair(self, client_uuid, client_public):
255255
def unpair(self, client_uuid):
256256
"""Override super function to show setup message if unpaired."""
257257
super().unpair(client_uuid)
258-
show_setup_message(self.hass, self.state.pincode)
258+
show_setup_message(self.hass, self.state.pincode, self.accessory.xhm_uri())

homeassistant/components/homekit/const.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
HOMEKIT_FILE = ".homekit.state"
77
HOMEKIT_NOTIFY_ID = 4663548
88
AID_STORAGE = "homekit-aid-allocations"
9-
9+
HOMEKIT_PAIRING_QR = "homekit-pairing-qr"
10+
HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret"
1011

1112
# #### Attributes ####
1213
ATTR_DISPLAY_NAME = "display_name"

homeassistant/components/homekit/manifest.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"domain": "homekit",
33
"name": "HomeKit",
44
"documentation": "https://www.home-assistant.io/integrations/homekit",
5-
"requirements": ["HAP-python==2.8.2", "fnvhash==0.1.0"],
6-
"codeowners": ["@bdraco"],
7-
"after_dependencies": ["logbook"]
5+
"requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"],
6+
"dependencies": ["http"],
7+
"after_dependencies": ["logbook"],
8+
"codeowners": ["@bdraco"]
89
}

homeassistant/components/homekit/util.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Collection of useful functions for the HomeKit component."""
22
from collections import OrderedDict, namedtuple
3+
import io
34
import logging
5+
import secrets
46

7+
import pyqrcode
58
import voluptuous as vol
69

710
from homeassistant.components import fan, media_player, sensor
@@ -27,6 +30,8 @@
2730
FEATURE_PLAY_STOP,
2831
FEATURE_TOGGLE_MUTE,
2932
HOMEKIT_NOTIFY_ID,
33+
HOMEKIT_PAIRING_QR,
34+
HOMEKIT_PAIRING_QR_SECRET,
3035
TYPE_FAUCET,
3136
TYPE_OUTLET,
3237
TYPE_SHOWER,
@@ -205,13 +210,24 @@ def speed_to_states(self, speed):
205210
return list(self.speed_ranges.keys())[0]
206211

207212

208-
def show_setup_message(hass, pincode):
213+
def show_setup_message(hass, pincode, uri):
209214
"""Display persistent notification with setup information."""
210215
pin = pincode.decode()
211216
_LOGGER.info("Pincode: %s", pin)
217+
218+
buffer = io.BytesIO()
219+
url = pyqrcode.create(uri)
220+
url.svg(buffer, scale=5)
221+
pairing_secret = secrets.token_hex(32)
222+
223+
hass.data[HOMEKIT_PAIRING_QR] = buffer.getvalue()
224+
hass.data[HOMEKIT_PAIRING_QR_SECRET] = pairing_secret
225+
212226
message = (
213-
f"To set up Home Assistant in the Home App, enter the "
214-
f"following code:\n### {pin}"
227+
f"To set up Home Assistant in the Home App, "
228+
f"scan the QR code or enter the following code:\n"
229+
f"### {pin}\n"
230+
f"![image](/api/homekit/pairingqr?{pairing_secret})"
215231
)
216232
hass.components.persistent_notification.create(
217233
message, "HomeKit Setup", HOMEKIT_NOTIFY_ID

requirements_all.txt

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ PyMata==2.20
6363
PyNaCl==1.3.0
6464

6565
# homeassistant.auth.mfa_modules.totp
66+
# homeassistant.components.homekit
6667
PyQRCode==1.2.1
6768

6869
# homeassistant.components.rmvtransport
@@ -304,6 +305,9 @@ azure-servicebus==0.50.1
304305
# homeassistant.components.baidu
305306
baidu-aip==1.6.6
306307

308+
# homeassistant.components.homekit
309+
base36==0.1.1
310+
307311
# homeassistant.components.modem_callerid
308312
basicmodem==0.7
309313

requirements_test_all.txt

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ HAP-python==2.8.2
1111
PyNaCl==1.3.0
1212

1313
# homeassistant.auth.mfa_modules.totp
14+
# homeassistant.components.homekit
1415
PyQRCode==1.2.1
1516

1617
# homeassistant.components.rmvtransport
@@ -130,6 +131,9 @@ av==6.1.2
130131
# homeassistant.components.axis
131132
axis==25
132133

134+
# homeassistant.components.homekit
135+
base36==0.1.1
136+
133137
# homeassistant.components.zha
134138
bellows-homeassistant==0.15.2
135139

tests/components/homekit/test_accessories.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ def test_home_driver():
340340

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

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

359361
mock_unpair.assert_called_with("client_uuid")
360-
mock_show_msg.assert_called_with("hass", pin)
362+
mock_show_msg.assert_called_with("hass", pin, "X-HM://0")

tests/components/homekit/test_homekit.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher):
293293
await hass.async_add_executor_job(homekit.start)
294294

295295
mock_add_acc.assert_called_with(state)
296-
mock_setup_msg.assert_called_with(hass, pin)
296+
mock_setup_msg.assert_called_with(hass, pin, ANY)
297297
hk_driver_add_acc.assert_called_with(homekit.bridge)
298298
assert hk_driver_start.called
299299
assert homekit.status == STATUS_RUNNING
@@ -328,7 +328,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p
328328
) as hk_driver_start:
329329
await hass.async_add_executor_job(homekit.start)
330330

331-
mock_setup_msg.assert_called_with(hass, pin)
331+
mock_setup_msg.assert_called_with(hass, pin, ANY)
332332
hk_driver_add_acc.assert_called_with(homekit.bridge)
333333
assert hk_driver_start.called
334334
assert homekit.status == STATUS_RUNNING
@@ -405,6 +405,8 @@ async def test_homekit_too_many_accessories(hass, hk_driver):
405405

406406
with patch("pyhap.accessory_driver.AccessoryDriver.start"), patch(
407407
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
408-
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn:
408+
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn, patch(
409+
f"{PATH_HOMEKIT}.show_setup_message"
410+
):
409411
await hass.async_add_executor_job(homekit.start)
410412
assert mock_warn.called is True

tests/components/homekit/test_util.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
FEATURE_ON_OFF,
1111
FEATURE_PLAY_PAUSE,
1212
HOMEKIT_NOTIFY_ID,
13+
HOMEKIT_PAIRING_QR,
14+
HOMEKIT_PAIRING_QR_SECRET,
1315
TYPE_FAUCET,
1416
TYPE_OUTLET,
1517
TYPE_SHOWER,
@@ -199,8 +201,10 @@ async def test_show_setup_msg(hass):
199201

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

202-
await hass.async_add_executor_job(show_setup_message, hass, pincode)
204+
await hass.async_add_executor_job(show_setup_message, hass, pincode, "X-HM://0")
203205
await hass.async_block_till_done()
206+
assert hass.data[HOMEKIT_PAIRING_QR_SECRET]
207+
assert hass.data[HOMEKIT_PAIRING_QR]
204208

205209
assert call_create_notification
206210
assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID

0 commit comments

Comments
 (0)