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
parent
ca08b70984
commit
d06fce6997
|
@ -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",
|
||||||
|
)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue