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 ipaddress
import logging import logging
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from zeroconf import InterfaceChoice from zeroconf import InterfaceChoice
from homeassistant.components import cover, vacuum from homeassistant.components import cover, vacuum
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE 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.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
@ -28,6 +30,7 @@ from homeassistant.const import (
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import Unauthorized
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
@ -56,6 +59,8 @@ from .const import (
DOMAIN, DOMAIN,
EVENT_HOMEKIT_CHANGED, EVENT_HOMEKIT_CHANGED,
HOMEKIT_FILE, HOMEKIT_FILE,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_RESET_ACCESSORY,
SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_START,
TYPE_FAUCET, TYPE_FAUCET,
@ -129,6 +134,8 @@ async def async_setup(hass, config):
aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass) aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass)
await aid_storage.async_initialize() await aid_storage.async_initialize()
hass.http.register_view(HomeKitPairingQRView)
conf = config[DOMAIN] conf = config[DOMAIN]
name = conf[CONF_NAME] name = conf[CONF_NAME]
port = conf[CONF_PORT] port = conf[CONF_PORT]
@ -445,7 +452,9 @@ class HomeKit:
self.driver.add_accessory(self.bridge) self.driver.add_accessory(self.bridge)
if not self.driver.state.paired: 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") _LOGGER.debug("Driver start")
self.hass.add_job(self.driver.start) self.hass.add_job(self.driver.start)
@ -459,3 +468,21 @@ class HomeKit:
_LOGGER.debug("Driver stop") _LOGGER.debug("Driver stop")
self.hass.add_job(self.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): def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired.""" """Override super function to show setup message if unpaired."""
super().unpair(client_uuid) 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_FILE = ".homekit.state"
HOMEKIT_NOTIFY_ID = 4663548 HOMEKIT_NOTIFY_ID = 4663548
AID_STORAGE = "homekit-aid-allocations" AID_STORAGE = "homekit-aid-allocations"
HOMEKIT_PAIRING_QR = "homekit-pairing-qr"
HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret"
# #### Attributes #### # #### Attributes ####
ATTR_DISPLAY_NAME = "display_name" ATTR_DISPLAY_NAME = "display_name"

View File

@ -2,7 +2,8 @@
"domain": "homekit", "domain": "homekit",
"name": "HomeKit", "name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit", "documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": ["HAP-python==2.8.2", "fnvhash==0.1.0"], "requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"],
"codeowners": ["@bdraco"], "dependencies": ["http"],
"after_dependencies": ["logbook"] "after_dependencies": ["logbook"],
"codeowners": ["@bdraco"]
} }

View File

@ -1,7 +1,10 @@
"""Collection of useful functions for the HomeKit component.""" """Collection of useful functions for the HomeKit component."""
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
import io
import logging import logging
import secrets
import pyqrcode
import voluptuous as vol import voluptuous as vol
from homeassistant.components import fan, media_player, sensor from homeassistant.components import fan, media_player, sensor
@ -27,6 +30,8 @@ from .const import (
FEATURE_PLAY_STOP, FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE, FEATURE_TOGGLE_MUTE,
HOMEKIT_NOTIFY_ID, HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET, TYPE_FAUCET,
TYPE_OUTLET, TYPE_OUTLET,
TYPE_SHOWER, TYPE_SHOWER,
@ -205,13 +210,24 @@ class HomeKitSpeedMapping:
return list(self.speed_ranges.keys())[0] 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.""" """Display persistent notification with setup information."""
pin = pincode.decode() pin = pincode.decode()
_LOGGER.info("Pincode: %s", pin) _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 = ( message = (
f"To set up Home Assistant in the Home App, enter the " f"To set up Home Assistant in the Home App, "
f"following code:\n### {pin}" 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( hass.components.persistent_notification.create(
message, "HomeKit Setup", HOMEKIT_NOTIFY_ID message, "HomeKit Setup", HOMEKIT_NOTIFY_ID

View File

@ -63,6 +63,7 @@ PyMata==2.20
PyNaCl==1.3.0 PyNaCl==1.3.0
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
# homeassistant.components.homekit
PyQRCode==1.2.1 PyQRCode==1.2.1
# homeassistant.components.rmvtransport # homeassistant.components.rmvtransport
@ -304,6 +305,9 @@ azure-servicebus==0.50.1
# homeassistant.components.baidu # homeassistant.components.baidu
baidu-aip==1.6.6 baidu-aip==1.6.6
# homeassistant.components.homekit
base36==0.1.1
# homeassistant.components.modem_callerid # homeassistant.components.modem_callerid
basicmodem==0.7 basicmodem==0.7

View File

@ -11,6 +11,7 @@ HAP-python==2.8.2
PyNaCl==1.3.0 PyNaCl==1.3.0
# homeassistant.auth.mfa_modules.totp # homeassistant.auth.mfa_modules.totp
# homeassistant.components.homekit
PyQRCode==1.2.1 PyQRCode==1.2.1
# homeassistant.components.rmvtransport # homeassistant.components.rmvtransport
@ -130,6 +131,9 @@ av==6.1.2
# homeassistant.components.axis # homeassistant.components.axis
axis==25 axis==25
# homeassistant.components.homekit
base36==0.1.1
# homeassistant.components.zha # homeassistant.components.zha
bellows-homeassistant==0.15.2 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) mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path)
driver.state = Mock(pincode=pin) driver.state = Mock(pincode=pin)
xhm_uri_mock = Mock(return_value="X-HM://0")
driver.accessory = Mock(xhm_uri=xhm_uri_mock)
# pair # pair
with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch(
@ -357,4 +359,4 @@ def test_home_driver():
driver.unpair("client_uuid") driver.unpair("client_uuid")
mock_unpair.assert_called_with("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) await hass.async_add_executor_job(homekit.start)
mock_add_acc.assert_called_with(state) 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) hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING 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: ) as hk_driver_start:
await hass.async_add_executor_job(homekit.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) hk_driver_add_acc.assert_called_with(homekit.bridge)
assert hk_driver_start.called assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING 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( with patch("pyhap.accessory_driver.AccessoryDriver.start"), patch(
"pyhap.accessory_driver.AccessoryDriver.add_accessory" "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) await hass.async_add_executor_job(homekit.start)
assert mock_warn.called is True assert mock_warn.called is True

View File

@ -10,6 +10,8 @@ from homeassistant.components.homekit.const import (
FEATURE_ON_OFF, FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE, FEATURE_PLAY_PAUSE,
HOMEKIT_NOTIFY_ID, HOMEKIT_NOTIFY_ID,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET, TYPE_FAUCET,
TYPE_OUTLET, TYPE_OUTLET,
TYPE_SHOWER, TYPE_SHOWER,
@ -199,8 +201,10 @@ async def test_show_setup_msg(hass):
call_create_notification = async_mock_service(hass, DOMAIN, "create") 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() 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
assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID