Display Homekit QR code when pairing (#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 <paulus@home-assistant.io>

* Update homeassistant/components/homekit/__init__.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/34519/head
J. Nick Koston 2020-04-21 17:38:43 -05:00 committed by GitHub
parent ca08b70984
commit d06fce6997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 75 additions and 14 deletions

View File

@ -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,
@ -28,6 +30,7 @@ from homeassistant.const import (
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
@ -56,6 +59,8 @@ from .const import (
DOMAIN,
EVENT_HOMEKIT_CHANGED,
HOMEKIT_FILE,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
SERVICE_HOMEKIT_RESET_ACCESSORY,
SERVICE_HOMEKIT_START,
TYPE_FAUCET,
@ -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]
@ -445,7 +452,9 @@ class HomeKit:
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)
@ -459,3 +468,21 @@ class HomeKit:
_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",
)

View File

@ -255,4 +255,4 @@ class HomeDriver(AccessoryDriver):
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())

View File

@ -6,7 +6,8 @@ DOMAIN = "homekit"
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"

View File

@ -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"]
}

View File

@ -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
@ -27,6 +30,8 @@ from .const import (
FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE,
HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
@ -205,13 +210,24 @@ class HomeKitSpeedMapping:
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

View File

@ -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
@ -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

View File

@ -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
@ -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

View File

@ -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(
@ -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")

View File

@ -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
@ -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
@ -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

View File

@ -10,6 +10,8 @@ from homeassistant.components.homekit.const import (
FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE,
HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
@ -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