diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 9cfbab0395f..6021dc082a5 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -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", + ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3d014d13c5d..06a643c9f61 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -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()) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ac8d2571ac6..026cdc60e74 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -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" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 7b1cc0ec827..482cef57ca7 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -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"] } diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index c12f49e1b9c..0295440df49 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index fe010dd470b..8b10415c097 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5bf9a1d07a..4ae53970df8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index e4605392aba..ab6f58ebab8 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -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") diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index d5ae720dbc3..a7b46f57fd5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -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 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 48764d1ca05..41b134c10a5 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -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