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