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 <paulus@home-assistant.io>
Co-authored-by: Tom Scholten <tom@sue.nl>
Co-authored-by: Tom <CoMPaTech@users.noreply.github.com>
---
 .../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
-- 
GitLab