From adb0455bd297571e125fb69ad1d055161fe6f00f Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Wed, 1 Mar 2023 21:19:20 +0100 Subject: [PATCH] Add config flow to EDL21 (#87655) * Added config_flow for edl21. * Added already_configured check. * Added config_flow test * Added setup of the edl21 from configuration.yaml * Ran script.gen_requirements_all * Removed the generated translation file. * Added a deprecation warning when importing from configuration.yaml. * Readded the platform schema. * Added handling of optional name for legacy configuration. * Fixed handling of default value in legacy configuration. * Added duplication check entries created via legacy config. * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 3 +- homeassistant/components/edl21/__init__.py | 17 ++++ homeassistant/components/edl21/config_flow.py | 50 ++++++++++++ homeassistant/components/edl21/const.py | 12 +++ homeassistant/components/edl21/manifest.json | 2 + homeassistant/components/edl21/sensor.py | 44 +++++++--- homeassistant/components/edl21/strings.json | 21 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/edl21/__init__.py | 1 + tests/components/edl21/conftest.py | 15 ++++ tests/components/edl21/test_config_flow.py | 81 +++++++++++++++++++ 13 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/edl21/config_flow.py create mode 100644 homeassistant/components/edl21/const.py create mode 100644 homeassistant/components/edl21/strings.json create mode 100644 tests/components/edl21/__init__.py create mode 100644 tests/components/edl21/conftest.py create mode 100644 tests/components/edl21/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 3c9a1c378a8..5da330bb20a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -249,7 +249,8 @@ omit = homeassistant/components/ecowitt/sensor.py homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py - homeassistant/components/edl21/* + homeassistant/components/edl21/__init__.py + homeassistant/components/edl21/sensor.py homeassistant/components/egardia/* homeassistant/components/eight_sleep/__init__.py homeassistant/components/eight_sleep/binary_sensor.py diff --git a/homeassistant/components/edl21/__init__.py b/homeassistant/components/edl21/__init__.py index f1cd5984744..2ece8517dbd 100644 --- a/homeassistant/components/edl21/__init__.py +++ b/homeassistant/components/edl21/__init__.py @@ -1 +1,18 @@ """The edl21 component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up EDL21 integration from a config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/edl21/config_flow.py b/homeassistant/components/edl21/config_flow.py new file mode 100644 index 00000000000..b66a988958b --- /dev/null +++ b/homeassistant/components/edl21/config_flow.py @@ -0,0 +1,50 @@ +"""Config flow for EDL21 integration.""" +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_SERIAL_PORT, DEFAULT_TITLE, DOMAIN + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_SERIAL_PORT): str, + } +) + + +class EDL21ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """EDL21 config flow.""" + + VERSION = 1 + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + + self._async_abort_entries_match( + {CONF_SERIAL_PORT: import_config[CONF_SERIAL_PORT]} + ) + return self.async_create_entry( + title=import_config[CONF_NAME] or DEFAULT_TITLE, + data=import_config, + ) + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the user setup step.""" + if user_input is not None: + self._async_abort_entries_match( + {CONF_SERIAL_PORT: user_input[CONF_SERIAL_PORT]} + ) + + return self.async_create_entry( + title=DEFAULT_TITLE, + data=user_input, + ) + + data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input) + return self.async_show_form(step_id="user", data_schema=data_schema) diff --git a/homeassistant/components/edl21/const.py b/homeassistant/components/edl21/const.py new file mode 100644 index 00000000000..f57966a0003 --- /dev/null +++ b/homeassistant/components/edl21/const.py @@ -0,0 +1,12 @@ +"""Constants for the EDL21 component.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "edl21" + +CONF_SERIAL_PORT = "serial_port" + +SIGNAL_EDL21_TELEGRAM = "edl21_telegram" + +DEFAULT_TITLE = "Smart Meter" diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index dc7e861ce83..48bab7d84f1 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,7 +2,9 @@ "domain": "edl21", "name": "EDL21", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/edl21", + "integration_type": "hub", "iot_class": "local_push", "loggers": ["sml"], "requirements": ["pysml==0.0.8"] diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 497f6867dfa..e34c9c823f6 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -1,8 +1,9 @@ """Support for EDL21 Smart Meters.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta -import logging +from typing import Any from sml import SmlGetListResponse from sml.asyncio import SmlProtocol @@ -15,6 +16,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_NAME, DEGREE, @@ -31,15 +33,13 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow -_LOGGER = logging.getLogger(__name__) +from .const import CONF_SERIAL_PORT, DOMAIN, LOGGER, SIGNAL_EDL21_TELEGRAM -DOMAIN = "edl21" -CONF_SERIAL_PORT = "serial_port" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -SIGNAL_EDL21_TELEGRAM = "edl21_telegram" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -269,9 +269,33 @@ async def async_setup_platform( config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up EDL21 sensors via configuration.yaml and show deprecation warning.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the EDL21 sensor.""" - hass.data[DOMAIN] = EDL21(hass, config, async_add_entities) + hass.data[DOMAIN] = EDL21(hass, config_entry.data, async_add_entities) await hass.data[DOMAIN].connect() @@ -295,14 +319,14 @@ class EDL21: def __init__( self, hass: HomeAssistant, - config: ConfigType, + config: Mapping[str, Any], async_add_entities: AddEntitiesCallback, ) -> None: """Initialize an EDL21 object.""" self._registered_obis: set[tuple[str, str]] = set() self._hass = hass self._async_add_entities = async_add_entities - self._name = config[CONF_NAME] + self._name = config.get(CONF_NAME) self._proto = SmlProtocol(config[CONF_SERIAL_PORT]) self._proto.add_listener(self.event, ["SmlGetListResponse"]) @@ -347,7 +371,7 @@ class EDL21: ) self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: - _LOGGER.warning( + LOGGER.warning( "Unhandled sensor %s detected. Please report at %s", obis, "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+edl21%22", @@ -366,7 +390,7 @@ class EDL21: "sensor", DOMAIN, entity.old_unique_id ) if old_entity_id is not None: - _LOGGER.debug( + LOGGER.debug( "Migrating unique_id from [%s] to [%s]", entity.old_unique_id, entity.unique_id, diff --git a/homeassistant/components/edl21/strings.json b/homeassistant/components/edl21/strings.json new file mode 100644 index 00000000000..284e8229c59 --- /dev/null +++ b/homeassistant/components/edl21/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "title": "Add your EDL21 smart meter", + "data": { + "serial_port": "[%key:common::config_flow::data::usb_path%]" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "EDL21 YAML configuration is being removed", + "description": "Configuring EDL21 using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the EDL21 YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 50555476769..3621c1d48d1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -107,6 +107,7 @@ FLOWS = { "ecobee", "econet", "ecowitt", + "edl21", "efergy", "eight_sleep", "elgato", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6111681bd35..a79f06bbd36 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1272,7 +1272,7 @@ "edl21": { "name": "EDL21", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_push" }, "efergy": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9026fc51e1..a2dead79122 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1431,6 +1431,9 @@ pysmartapp==0.3.3 # homeassistant.components.smartthings pysmartthings==0.7.6 +# homeassistant.components.edl21 +pysml==0.0.8 + # homeassistant.components.snmp pysnmplib==5.0.20 diff --git a/tests/components/edl21/__init__.py b/tests/components/edl21/__init__.py new file mode 100644 index 00000000000..e9b70556892 --- /dev/null +++ b/tests/components/edl21/__init__.py @@ -0,0 +1 @@ +"""Tests for the EDL21 integration.""" diff --git a/tests/components/edl21/conftest.py b/tests/components/edl21/conftest.py new file mode 100644 index 00000000000..dc64659d2b8 --- /dev/null +++ b/tests/components/edl21/conftest.py @@ -0,0 +1,15 @@ +"""Define test fixtures for EDL21.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.edl21.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/edl21/test_config_flow.py b/tests/components/edl21/test_config_flow.py new file mode 100644 index 00000000000..4dbd69b2371 --- /dev/null +++ b/tests/components/edl21/test_config_flow.py @@ -0,0 +1,81 @@ +"""Test EDL21 config flow.""" + +import pytest + +from homeassistant.components.edl21.const import CONF_SERIAL_PORT, DEFAULT_TITLE, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +VALID_CONFIG = {CONF_SERIAL_PORT: "/dev/ttyUSB1"} +VALID_LEGACY_CONFIG = {CONF_NAME: "My Smart Meter", CONF_SERIAL_PORT: "/dev/ttyUSB1"} + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_show_form(hass: HomeAssistant) -> None: + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_TITLE + assert result["data"][CONF_SERIAL_PORT] == VALID_CONFIG[CONF_SERIAL_PORT] + + +async def test_integration_already_exists(hass: HomeAssistant) -> None: + """Test that a new entry must not have the same serial port as an existing entry.""" + + MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_create_entry_by_import(hass: HomeAssistant) -> None: + """Test that the import step works.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_LEGACY_CONFIG, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == VALID_LEGACY_CONFIG[CONF_NAME] + assert result["data"][CONF_NAME] == VALID_LEGACY_CONFIG[CONF_NAME] + assert result["data"][CONF_SERIAL_PORT] == VALID_LEGACY_CONFIG[CONF_SERIAL_PORT] + + # Test the import step with an empty string as name + # (the name is optional in the old schema and defaults to "") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_SERIAL_PORT: "/dev/ttyUSB2", CONF_NAME: ""}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_TITLE + assert result["data"][CONF_NAME] == "" + assert result["data"][CONF_SERIAL_PORT] == "/dev/ttyUSB2"