From 82160fa350359ccf6b7fff08ff4b4771d819d76a Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 8 Oct 2021 11:34:22 +0200
Subject: [PATCH] Add config flow to Stookalert (#57119)

---
 .coveragerc                                   |   3 +-
 .strict-typing                                |   1 +
 CODEOWNERS                                    |   2 +-
 .../components/stookalert/__init__.py         |  29 +++-
 .../components/stookalert/binary_sensor.py    | 133 ++++++++++--------
 .../components/stookalert/config_flow.py      |  37 +++++
 homeassistant/components/stookalert/const.py  |  26 ++++
 .../components/stookalert/manifest.json       |   3 +-
 .../components/stookalert/strings.json        |  14 ++
 .../stookalert/translations/en.json           |  14 ++
 homeassistant/generated/config_flows.py       |   1 +
 mypy.ini                                      |  11 ++
 requirements_test_all.txt                     |   3 +
 tests/components/stookalert/__init__.py       |   1 +
 .../components/stookalert/test_config_flow.py |  78 ++++++++++
 15 files changed, 295 insertions(+), 61 deletions(-)
 create mode 100644 homeassistant/components/stookalert/config_flow.py
 create mode 100644 homeassistant/components/stookalert/const.py
 create mode 100644 homeassistant/components/stookalert/strings.json
 create mode 100644 homeassistant/components/stookalert/translations/en.json
 create mode 100644 tests/components/stookalert/__init__.py
 create mode 100644 tests/components/stookalert/test_config_flow.py

diff --git a/.coveragerc b/.coveragerc
index 8ee3a3ceab6..70f2b8cee34 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1000,7 +1000,8 @@ omit =
     homeassistant/components/starlingbank/sensor.py
     homeassistant/components/steam_online/sensor.py
     homeassistant/components/stiebel_eltron/*
-    homeassistant/components/stookalert/*
+    homeassistant/components/stookalert/__init__.py
+    homeassistant/components/stookalert/binary_sensor.py
     homeassistant/components/stream/*
     homeassistant/components/streamlabswater/*
     homeassistant/components/suez_water/*
diff --git a/.strict-typing b/.strict-typing
index b5ae496fcf0..7710f637090 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -101,6 +101,7 @@ homeassistant.components.simplisafe.*
 homeassistant.components.slack.*
 homeassistant.components.sonos.media_player
 homeassistant.components.ssdp.*
+homeassistant.components.stookalert.*
 homeassistant.components.stream.*
 homeassistant.components.sun.*
 homeassistant.components.surepetcare.*
diff --git a/CODEOWNERS b/CODEOWNERS
index 73c6e63eb02..a4756f961be 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -498,7 +498,7 @@ homeassistant/components/srp_energy/* @briglx
 homeassistant/components/starline/* @anonym-tsk
 homeassistant/components/statistics/* @fabaff
 homeassistant/components/stiebel_eltron/* @fucm
-homeassistant/components/stookalert/* @fwestenberg
+homeassistant/components/stookalert/* @fwestenberg @frenck
 homeassistant/components/stream/* @hunterjm @uvjustin @allenporter
 homeassistant/components/stt/* @pvizeli
 homeassistant/components/subaru/* @G-Two
diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py
index c9f86228515..8dfc208d945 100644
--- a/homeassistant/components/stookalert/__init__.py
+++ b/homeassistant/components/stookalert/__init__.py
@@ -1 +1,28 @@
-"""The Stookalert component."""
+"""The Stookalert integration."""
+from __future__ import annotations
+
+import stookalert
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+from .const import CONF_PROVINCE, DOMAIN
+
+PLATFORMS = (BINARY_SENSOR_DOMAIN,)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Stookalert from a config entry."""
+    hass.data.setdefault(DOMAIN, {})
+    hass.data[DOMAIN][entry.entry_id] = stookalert.stookalert(entry.data[CONF_PROVINCE])
+    hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload Stookalert config entry."""
+    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+    if unload_ok:
+        del hass.data[DOMAIN][entry.entry_id]
+    return unload_ok
diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py
index 033af78560c..91634bfffa0 100644
--- a/homeassistant/components/stookalert/binary_sensor.py
+++ b/homeassistant/components/stookalert/binary_sensor.py
@@ -1,4 +1,6 @@
-"""This component provides support for Stookalert Binary Sensor."""
+"""This integration provides support for Stookalert Binary Sensor."""
+from __future__ import annotations
+
 from datetime import timedelta
 
 import stookalert
@@ -9,28 +11,32 @@ from homeassistant.components.binary_sensor import (
     PLATFORM_SCHEMA,
     BinarySensorEntity,
 )
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import (
+    ATTR_ATTRIBUTION,
+    ATTR_IDENTIFIERS,
+    ATTR_MANUFACTURER,
+    ATTR_MODEL,
+    ATTR_NAME,
+    CONF_NAME,
+)
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from .const import (
+    ATTR_ENTRY_TYPE,
+    CONF_PROVINCE,
+    DOMAIN,
+    ENTRY_TYPE_SERVICE,
+    LOGGER,
+    PROVINCES,
+)
 
-SCAN_INTERVAL = timedelta(minutes=60)
-CONF_PROVINCE = "province"
-DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY
 DEFAULT_NAME = "Stookalert"
 ATTRIBUTION = "Data provided by rivm.nl"
-PROVINCES = [
-    "Drenthe",
-    "Flevoland",
-    "Friesland",
-    "Gelderland",
-    "Groningen",
-    "Limburg",
-    "Noord-Brabant",
-    "Noord-Holland",
-    "Overijssel",
-    "Utrecht",
-    "Zeeland",
-    "Zuid-Holland",
-]
+SCAN_INTERVAL = timedelta(minutes=60)
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
@@ -40,47 +46,60 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
 )
 
 
-def setup_platform(hass, config, add_entities, discovery_info=None):
-    """Set up the Stookalert binary sensor platform."""
-    province = config[CONF_PROVINCE]
-    name = config[CONF_NAME]
-    api_handler = stookalert.stookalert(province)
-    add_entities([StookalertBinarySensor(name, api_handler)], update_before_add=True)
+async def async_setup_platform(
+    hass: HomeAssistant,
+    config: ConfigType,
+    async_add_entities: AddEntitiesCallback,
+    discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+    """Import the Stookalert platform into a config entry."""
+    LOGGER.warning(
+        "Configuration of the Stookalert platform in YAML is deprecated and will be "
+        "removed in Home Assistant 2022.1; Your existing configuration "
+        "has been imported into the UI automatically and can be safely removed "
+        "from your configuration.yaml file"
+    )
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": SOURCE_IMPORT},
+            data={
+                CONF_PROVINCE: config[CONF_PROVINCE],
+            },
+        )
+    )
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up Stookalert binary sensor from a config entry."""
+    client = hass.data[DOMAIN][entry.entry_id]
+    async_add_entities([StookalertBinarySensor(client, entry)], update_before_add=True)
 
 
 class StookalertBinarySensor(BinarySensorEntity):
-    """An implementation of RIVM Stookalert."""
-
-    def __init__(self, name, api_handler):
-        """Initialize a Stookalert device."""
-        self._name = name
-        self._api_handler = api_handler
+    """Defines a Stookalert binary sensor."""
 
-    @property
-    def extra_state_attributes(self):
-        """Return the attribute(s) of the sensor."""
-        state_attr = {ATTR_ATTRIBUTION: ATTRIBUTION}
+    _attr_device_class = DEVICE_CLASS_SAFETY
 
-        if self._api_handler.last_updated is not None:
-            state_attr["last_updated"] = self._api_handler.last_updated.isoformat()
-
-        return state_attr
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def is_on(self):
-        """Return True if the Alert is active."""
-        return self._api_handler.state == 1
-
-    @property
-    def device_class(self):
-        """Return the device class of this binary sensor."""
-        return DEFAULT_DEVICE_CLASS
-
-    def update(self):
+    def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None:
+        """Initialize a Stookalert device."""
+        self._client = client
+        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+        self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}"
+        self._attr_unique_id = entry.unique_id
+        self._attr_device_info = {
+            ATTR_IDENTIFIERS: {(DOMAIN, f"{entry.entry_id}")},
+            ATTR_NAME: entry.data[CONF_PROVINCE],
+            ATTR_MANUFACTURER: "RIVM",
+            ATTR_MODEL: "Stookalert",
+            ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE,
+        }
+
+    def update(self) -> None:
         """Update the data from the Stookalert handler."""
-        self._api_handler.get_alerts()
+        self._client.get_alerts()
+        self._attr_is_on = self._client.state == 1
diff --git a/homeassistant/components/stookalert/config_flow.py b/homeassistant/components/stookalert/config_flow.py
new file mode 100644
index 00000000000..4f625ec2d1a
--- /dev/null
+++ b/homeassistant/components/stookalert/config_flow.py
@@ -0,0 +1,37 @@
+"""Config flow to configure the Stookalert integration."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.data_entry_flow import FlowResult
+
+from .const import CONF_PROVINCE, DOMAIN, PROVINCES
+
+
+class StookalertFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Config flow for Stookalert."""
+
+    VERSION = 1
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle a flow initialized by the user."""
+        if user_input is not None:
+            await self.async_set_unique_id(user_input[CONF_PROVINCE])
+            self._abort_if_unique_id_configured()
+            return self.async_create_entry(
+                title=user_input[CONF_PROVINCE], data=user_input
+            )
+
+        return self.async_show_form(
+            step_id="user",
+            data_schema=vol.Schema({vol.Required(CONF_PROVINCE): vol.In(PROVINCES)}),
+        )
+
+    async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
+        """Handle a flow initialized by importing a config."""
+        return await self.async_step_user(config)
diff --git a/homeassistant/components/stookalert/const.py b/homeassistant/components/stookalert/const.py
new file mode 100644
index 00000000000..bbd5922b82a
--- /dev/null
+++ b/homeassistant/components/stookalert/const.py
@@ -0,0 +1,26 @@
+"""Constants for the Stookalert integration."""
+import logging
+from typing import Final
+
+DOMAIN: Final = "stookalert"
+LOGGER = logging.getLogger(__package__)
+
+CONF_PROVINCE: Final = "province"
+
+PROVINCES: Final = (
+    "Drenthe",
+    "Flevoland",
+    "Friesland",
+    "Gelderland",
+    "Groningen",
+    "Limburg",
+    "Noord-Brabant",
+    "Noord-Holland",
+    "Overijssel",
+    "Utrecht",
+    "Zeeland",
+    "Zuid-Holland",
+)
+
+ATTR_ENTRY_TYPE: Final = "entry_type"
+ENTRY_TYPE_SERVICE: Final = "service"
diff --git a/homeassistant/components/stookalert/manifest.json b/homeassistant/components/stookalert/manifest.json
index 094f4c45670..401ed5a27e5 100644
--- a/homeassistant/components/stookalert/manifest.json
+++ b/homeassistant/components/stookalert/manifest.json
@@ -1,8 +1,9 @@
 {
   "domain": "stookalert",
   "name": "RIVM Stookalert",
+  "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/stookalert",
-  "codeowners": ["@fwestenberg"],
+  "codeowners": ["@fwestenberg", "@frenck"],
   "requirements": ["stookalert==0.1.4"],
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/stookalert/strings.json b/homeassistant/components/stookalert/strings.json
new file mode 100644
index 00000000000..a05ae4e61e7
--- /dev/null
+++ b/homeassistant/components/stookalert/strings.json
@@ -0,0 +1,14 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "data": {
+          "province": "Province"
+        }
+      }
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
+    }
+  }
+}
diff --git a/homeassistant/components/stookalert/translations/en.json b/homeassistant/components/stookalert/translations/en.json
new file mode 100644
index 00000000000..3c3480b85ae
--- /dev/null
+++ b/homeassistant/components/stookalert/translations/en.json
@@ -0,0 +1,14 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Service is already configured"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "province": "Province"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 2a04ec39478..30617373dbf 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -271,6 +271,7 @@ FLOWS = [
     "squeezebox",
     "srp_energy",
     "starline",
+    "stookalert",
     "subaru",
     "surepetcare",
     "switchbot",
diff --git a/mypy.ini b/mypy.ini
index 0d4ca87ac64..440f410d0ab 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1122,6 +1122,17 @@ no_implicit_optional = true
 warn_return_any = true
 warn_unreachable = true
 
+[mypy-homeassistant.components.stookalert.*]
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+no_implicit_optional = true
+warn_return_any = true
+warn_unreachable = true
+
 [mypy-homeassistant.components.stream.*]
 check_untyped_defs = true
 disallow_incomplete_defs = true
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b9e8363683c..cadc30b0a18 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1284,6 +1284,9 @@ starline==0.1.5
 # homeassistant.components.statsd
 statsd==3.2.1
 
+# homeassistant.components.stookalert
+stookalert==0.1.4
+
 # homeassistant.components.huawei_lte
 # homeassistant.components.solaredge
 # homeassistant.components.thermoworks_smoke
diff --git a/tests/components/stookalert/__init__.py b/tests/components/stookalert/__init__.py
new file mode 100644
index 00000000000..3785c76639a
--- /dev/null
+++ b/tests/components/stookalert/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Stookalert integration."""
diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py
new file mode 100644
index 00000000000..ceee26fa8e2
--- /dev/null
+++ b/tests/components/stookalert/test_config_flow.py
@@ -0,0 +1,78 @@
+"""Tests for the Stookalert config flow."""
+from unittest.mock import patch
+
+from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import (
+    RESULT_TYPE_ABORT,
+    RESULT_TYPE_CREATE_ENTRY,
+    RESULT_TYPE_FORM,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_full_user_flow(hass: HomeAssistant) -> None:
+    """Test the full user configuration flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_USER}
+    )
+
+    assert result.get("type") == RESULT_TYPE_FORM
+    assert result.get("step_id") == SOURCE_USER
+    assert "flow_id" in result
+
+    with patch(
+        "homeassistant.components.stookalert.async_setup_entry", return_value=True
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={
+                CONF_PROVINCE: "Overijssel",
+            },
+        )
+
+    assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+    assert result2.get("title") == "Overijssel"
+    assert result2.get("data") == {
+        CONF_PROVINCE: "Overijssel",
+    }
+
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_already_configured(hass: HomeAssistant) -> None:
+    """Test we abort if the Stookalert province is already configured."""
+    MockConfigEntry(
+        domain=DOMAIN, data={CONF_PROVINCE: "Overijssel"}, unique_id="Overijssel"
+    ).add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_USER}
+    )
+
+    assert "flow_id" in result
+
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={
+            CONF_PROVINCE: "Overijssel",
+        },
+    )
+
+    assert result2.get("type") == RESULT_TYPE_ABORT
+    assert result2.get("reason") == "already_configured"
+
+
+async def test_import_flow(hass: HomeAssistant) -> None:
+    """Test the import configuration flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_IMPORT}, data={"province": "Overijssel"}
+    )
+
+    assert result.get("type") == RESULT_TYPE_CREATE_ENTRY
+    assert result.get("title") == "Overijssel"
+    assert result.get("data") == {
+        CONF_PROVINCE: "Overijssel",
+    }
-- 
GitLab