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>
This commit is contained in:
Stephan Uhle 2023-03-01 21:19:20 +01:00 committed by GitHub
parent 07839cc971
commit adb0455bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 240 additions and 12 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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"]

View File

@ -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,

View File

@ -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."
}
}
}

View File

@ -107,6 +107,7 @@ FLOWS = {
"ecobee",
"econet",
"ecowitt",
"edl21",
"efergy",
"eight_sleep",
"elgato",

View File

@ -1272,7 +1272,7 @@
"edl21": {
"name": "EDL21",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_push"
},
"efergy": {

View File

@ -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

View File

@ -0,0 +1 @@
"""Tests for the EDL21 integration."""

View File

@ -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

View File

@ -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"