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:
parent
07839cc971
commit
adb0455bd2
13 changed files with 240 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
50
homeassistant/components/edl21/config_flow.py
Normal file
50
homeassistant/components/edl21/config_flow.py
Normal 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)
|
12
homeassistant/components/edl21/const.py
Normal file
12
homeassistant/components/edl21/const.py
Normal 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"
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
21
homeassistant/components/edl21/strings.json
Normal file
21
homeassistant/components/edl21/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,6 +107,7 @@ FLOWS = {
|
|||
"ecobee",
|
||||
"econet",
|
||||
"ecowitt",
|
||||
"edl21",
|
||||
"efergy",
|
||||
"eight_sleep",
|
||||
"elgato",
|
||||
|
|
|
@ -1272,7 +1272,7 @@
|
|||
"edl21": {
|
||||
"name": "EDL21",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"efergy": {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
1
tests/components/edl21/__init__.py
Normal file
1
tests/components/edl21/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the EDL21 integration."""
|
15
tests/components/edl21/conftest.py
Normal file
15
tests/components/edl21/conftest.py
Normal 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
|
81
tests/components/edl21/test_config_flow.py
Normal file
81
tests/components/edl21/test_config_flow.py
Normal 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"
|
Loading…
Add table
Add a link
Reference in a new issue