From c737378ee14c12f988118dc9d23f1fc0b1da8ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Fri, 23 Dec 2022 23:52:06 +0100 Subject: [PATCH] Add blebox discovery/zeroconf (#83837) Co-authored-by: J. Nick Koston --- homeassistant/components/blebox/__init__.py | 1 + .../components/blebox/config_flow.py | 70 ++++++++++- homeassistant/components/blebox/manifest.json | 3 +- .../components/blebox/translations/en.json | 7 +- homeassistant/generated/zeroconf.py | 5 + tests/components/blebox/test_config_flow.py | 113 +++++++++++++++++- 6 files changed, 191 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 1a7c8104652..a1e646c0b32 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -83,6 +83,7 @@ class BleBoxEntity(Entity, Generic[_FeatureT]): model=product.model, name=product.name, sw_version=product.firmware_version, + configuration_url=f"http://{product.address}", ) async def async_update(self) -> None: diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py index 5ae975f83d9..cf9a943b3df 100644 --- a/homeassistant/components/blebox/config_flow.py +++ b/homeassistant/components/blebox/config_flow.py @@ -1,13 +1,18 @@ """Config flow for BleBox devices integration.""" +from __future__ import annotations + import logging +from typing import Any from blebox_uniapi.box import Box -from blebox_uniapi.error import Error, UnsupportedBoxVersion +from blebox_uniapi.error import Error, UnsupportedBoxResponse, UnsupportedBoxVersion from blebox_uniapi.session import ApiHost import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -74,9 +79,67 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"address": f"{host}:{port}"}, ) + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + hass = self.hass + ipaddress = host_port(discovery_info.__dict__) + self.device_config["host"] = discovery_info.host + self.device_config["port"] = discovery_info.port + + websession = async_get_clientsession(hass) + + api_host = ApiHost( + *ipaddress, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER + ) + + try: + product = await Box.async_from_host(api_host) + except UnsupportedBoxVersion: + return self.async_abort(reason="unsupported_device_version") + except UnsupportedBoxResponse: + return self.async_abort(reason="unsupported_device_response") + + self.device_config["name"] = product.name + # Check if configured but IP changed since + await self.async_set_unique_id(product.unique_id) + self._abort_if_unique_id_configured() + self.context.update( + { + "title_placeholders": { + "name": self.device_config["name"], + "host": self.device_config["host"], + }, + "configuration_url": f"http://{discovery_info.host}", + } + ) + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle discovery confirmation.""" + if user_input is not None: + return self.async_create_entry( + title=self.device_config["name"], + data={ + "host": self.device_config["host"], + "port": self.device_config["port"], + }, + ) + + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={ + "name": self.device_config["name"], + "host": self.device_config["host"], + "port": self.device_config["port"], + }, + ) + async def async_step_user(self, user_input=None): """Handle initial user-triggered config step.""" - hass = self.hass schema = create_schema(user_input) @@ -97,7 +160,6 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): reason=ADDRESS_ALREADY_CONFIGURED, description_placeholders={"address": f"{host}:{port}"}, ) - websession = async_get_clientsession(hass) api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER) try: @@ -119,7 +181,7 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # Check if configured but IP changed since - await self.async_set_unique_id(product.unique_id) + await self.async_set_unique_id(product.unique_id, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry(title=product.name, data=user_input) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 78c7186eb31..39a0d57558f 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -6,5 +6,6 @@ "requirements": ["blebox_uniapi==2.1.3"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", - "loggers": ["blebox_uniapi"] + "loggers": ["blebox_uniapi"], + "zeroconf": ["_bbxsrv._tcp.local."] } diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json index d6f9f11498a..cce643e680d 100644 --- a/homeassistant/components/blebox/translations/en.json +++ b/homeassistant/components/blebox/translations/en.json @@ -13,12 +13,15 @@ "step": { "user": { "data": { - "host": "IP Address", + "host": "IP address / domain name / mDNS address", "port": "Port" }, "description": "Set up your BleBox to integrate with Home Assistant.", "title": "Set up your BleBox device" + }, + "confirm_discovery": { + "description": "Would you like to configure {name} {host}:{port}?." } } } -} \ No newline at end of file +} diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index bfd0c8cb342..e7c8bdaf1df 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -152,6 +152,11 @@ ZEROCONF = { }, }, ], + "_bbxsrv._tcp.local.": [ + { + "domain": "blebox", + }, + ], "_bond._tcp.local.": [ { "domain": "bond", diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 7db216e294e..030da33fcfe 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -6,10 +6,14 @@ import blebox_uniapi import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.blebox import config_flow +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component -from .conftest import mock_config, mock_only_feature, setup_product_mock +from ...common import MockConfigEntry +from .conftest import mock_config, mock_feature, mock_only_feature, setup_product_mock def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): @@ -190,3 +194,110 @@ async def test_async_remove_entry(hass, valid_feature_mock): assert hass.config_entries.async_entries() == [] assert config.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_flow_with_zeroconf(hass): + """Test setup from zeroconf discovery.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="172.100.123.4", + addresses=["172.100.123.4"], + port=80, + hostname="bbx-bbtest123456.local.", + type="_bbxsrv._tcp.local.", + name="bbx-bbtest123456._bbxsrv._tcp.local.", + properties={"_raw": {}}, + ), + ) + + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.blebox.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"host": "172.100.123.4", "port": 80} + + +async def test_flow_with_zeroconf_when_already_configured(hass): + """Test behaviour if device already configured.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, + data={CONF_IP_ADDRESS: "172.100.123.4"}, + unique_id="abcd0123ef5678", + ) + entry.add_to_hass(hass) + feature: AsyncMock = mock_feature( + "sensors", + blebox_uniapi.sensor.Temperature, + ) + with patch( + "homeassistant.components.blebox.config_flow.Box.async_from_host", + return_value=feature.product, + ): + result2 = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="172.100.123.4", + addresses=["172.100.123.4"], + port=80, + hostname="bbx-bbtest123456.local.", + type="_bbxsrv._tcp.local.", + name="bbx-bbtest123456._bbxsrv._tcp.local.", + properties={"_raw": {}}, + ), + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_flow_with_zeroconf_when_device_unsupported(hass): + """Test behaviour when device is not supported.""" + with patch( + "homeassistant.components.blebox.config_flow.Box.async_from_host", + side_effect=blebox_uniapi.error.UnsupportedBoxVersion, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="172.100.123.4", + addresses=["172.100.123.4"], + port=80, + hostname="bbx-bbtest123456.local.", + type="_bbxsrv._tcp.local.", + name="bbx-bbtest123456._bbxsrv._tcp.local.", + properties={"_raw": {}}, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "unsupported_device_version" + + +async def test_flow_with_zeroconf_when_device_response_unsupported(hass): + """Test behaviour when device returned unsupported response.""" + + with patch( + "homeassistant.components.blebox.config_flow.Box.async_from_host", + side_effect=blebox_uniapi.error.UnsupportedBoxResponse, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="172.100.123.4", + addresses=["172.100.123.4"], + port=80, + hostname="bbx-bbtest123456.local.", + type="_bbxsrv._tcp.local.", + name="bbx-bbtest123456._bbxsrv._tcp.local.", + properties={"_raw": {}}, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "unsupported_device_response"