From d57dbb43199e96ccc7ebd123ca3f7e57b4c757cc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 4 Jul 2020 00:28:34 +0200 Subject: [PATCH] Add Plugwise zeroconf discovery (#37289) Co-authored-by: Paulus Schoutsen Co-authored-by: Tom Scholten Co-authored-by: Tom --- .../components/plugwise/config_flow.py | 60 ++++++++++++++++--- .../components/plugwise/manifest.json | 1 + .../components/plugwise/strings.json | 3 +- homeassistant/generated/zeroconf.py | 3 + tests/components/plugwise/test_config_flow.py | 1 + 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 8f82a107576..e3a80f87cfd 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -7,32 +7,50 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_HOST): str, vol.Required(CONF_PASSWORD): str} -) +ZEROCONF_MAP = { + "smile": "P1 DSMR", + "smile_thermo": "Climate (Anna)", + "smile_open_therm": "Climate (Adam)", +} + + +def _base_schema(discovery_info): + """Generate base schema.""" + base_schema = {} + + if not discovery_info: + base_schema[vol.Required(CONF_HOST)] = str + + base_schema[vol.Required(CONF_PASSWORD)] = str + + return vol.Schema(base_schema) async def validate_input(hass: core.HomeAssistant, data): """ Validate the user input allows us to connect. - Data has the keys from DATA_SCHEMA with values provided by the user. + Data has the keys from _base_schema() with values provided by the user. """ websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( - host=data["host"], password=data["password"], timeout=30, websession=websession + host=data[CONF_HOST], + password=data[CONF_PASSWORD], + timeout=30, + websession=websession, ) try: await api.connect() except Smile.InvalidAuthentication: raise InvalidAuth - except Smile.ConnectionFailedError: + except Smile.PlugwiseError: raise CannotConnect return api @@ -44,12 +62,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize the Plugwise config flow.""" + self.discovery_info = {} + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Prepare configuration for a discovered Plugwise Smile.""" + self.discovery_info = discovery_info + _properties = self.discovery_info.get("properties") + + unique_id = self.discovery_info.get("hostname").split(".")[0] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + _product = _properties.get("product", None) + _version = _properties.get("version", "n/a") + _name = f"{ZEROCONF_MAP.get(_product,_product)} v{_version}" + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_HOST: discovery_info[CONF_HOST], + "name": _name, + } + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: + if self.discovery_info: + user_input[CONF_HOST] = self.discovery_info[CONF_HOST] + try: api = await validate_input(self.hass, user_input) @@ -64,12 +109,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(api.gateway_id) - self._abort_if_unique_id_configured() return self.async_create_entry(title=api.smile_name, data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 67456aca3bd..d485fa22607 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/plugwise", "requirements": ["Plugwise_Smile==1.1.0"], "codeowners": ["@CoMPaTech", "@bouwew"], + "zeroconf": ["_plugwise._tcp.local."], "config_flow": true } diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 00499a26ac2..70c1d127390 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -17,6 +17,7 @@ }, "abort": { "already_configured": "This Smile is already configured" - } + }, + "flow_title": "Smile: {name}" } } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index a4bd268199f..dc2bd289930 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -44,6 +44,9 @@ ZEROCONF = { "_nut._tcp.local.": [ "nut" ], + "_plugwise._tcp.local.": [ + "plugwise" + ], "_printer._tcp.local.": [ "brother" ], diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index b70b658bd65..feb695aae81 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -12,6 +12,7 @@ from tests.async_mock import patch def mock_smile(): """Create a Mock Smile for testing exceptions.""" with patch("homeassistant.components.plugwise.config_flow.Smile",) as smile_mock: + smile_mock.PlugwiseError = Smile.PlugwiseError smile_mock.InvalidAuthentication = Smile.InvalidAuthentication smile_mock.ConnectionFailedError = Smile.ConnectionFailedError smile_mock.return_value.connect.return_value = True