diff --git a/.coveragerc b/.coveragerc index 369944d60b0..44f8a54fe7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -238,7 +238,9 @@ omit = homeassistant/components/doorbird/util.py homeassistant/components/dovado/* homeassistant/components/downloader/* - homeassistant/components/dsmr_reader/* + homeassistant/components/dsmr_reader/__init__.py + homeassistant/components/dsmr_reader/definitions.py + homeassistant/components/dsmr_reader/sensor.py homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 50d62976dfb..3d610993dfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -263,7 +263,8 @@ build.json @home-assistant/supervisor /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /homeassistant/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck -/homeassistant/components/dsmr_reader/ @depl0y +/homeassistant/components/dsmr_reader/ @depl0y @glodenox +/tests/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dunehd/ @bieniu /tests/components/dunehd/ @bieniu /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py index 946be91d1a5..89b0da699e5 100644 --- a/homeassistant/components/dsmr_reader/__init__.py +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -1 +1,19 @@ """The DSMR Reader 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, entry: ConfigEntry) -> bool: + """Set up the DSMR Reader integration.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the DSMR Reader integration.""" + # no data stored in hass.data + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/dsmr_reader/config_flow.py b/homeassistant/components/dsmr_reader/config_flow.py new file mode 100644 index 00000000000..2f08894d125 --- /dev/null +++ b/homeassistant/components/dsmr_reader/config_flow.py @@ -0,0 +1,40 @@ +"""Config flow to configure DSMR Reader.""" +from __future__ import annotations + +from collections.abc import Awaitable +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(_: HomeAssistant) -> bool: + """MQTT is set as dependency, so that should be sufficient.""" + return True + + +class DsmrReaderFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): + """Handle DSMR Reader config flow. The MQTT step is inherited from the parent class.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up the config flow.""" + super().__init__(DOMAIN, "DSMR Reader", _async_has_devices) + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm setup.""" + if user_input is None: + return self.async_show_form( + step_id="confirm", + ) + + return await super().async_step_confirm(user_input) diff --git a/homeassistant/components/dsmr_reader/const.py b/homeassistant/components/dsmr_reader/const.py new file mode 100644 index 00000000000..1f1679028d5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/const.py @@ -0,0 +1,3 @@ +"""Constant values for DSMR Reader.""" + +DOMAIN = "dsmr_reader" diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index 0b631e790ed..df68e183fdf 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,8 +1,10 @@ { "domain": "dsmr_reader", "name": "DSMR Reader", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "dependencies": ["mqtt"], - "codeowners": ["@depl0y"], + "mqtt": ["dsmr/#"], + "codeowners": ["@depl0y", "@glodenox"], "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 603b5682f42..81458a94739 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -3,15 +3,16 @@ from __future__ import annotations from homeassistant.components import mqtt from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, callback 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 import slugify +from .const import DOMAIN from .definitions import SENSORS, DSMRReaderSensorEntityDescription -DOMAIN = "dsmr_reader" - async def async_setup_platform( hass: HomeAssistant, @@ -19,7 +20,31 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up DSMR Reader sensors.""" + """Set up DSMR Reader sensors via configuration.yaml and show deprecation warning.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.12.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: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up DSMR Reader sensors from config entry.""" async_add_entities(DSMRSensor(description) for description in SENSORS) diff --git a/homeassistant/components/dsmr_reader/strings.json b/homeassistant/components/dsmr_reader/strings.json new file mode 100644 index 00000000000..17e28cca884 --- /dev/null +++ b/homeassistant/components/dsmr_reader/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The DSMR Reader configuration is being removed", + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/dsmr_reader/translations/en.json b/homeassistant/components/dsmr_reader/translations/en.json new file mode 100644 index 00000000000..a2acb20b9b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The DSMR Reader configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21154b35a7a..23da091c09c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -83,6 +83,7 @@ FLOWS = { "dnsip", "doorbird", "dsmr", + "dsmr_reader", "dunehd", "dynalite", "eafm", diff --git a/homeassistant/generated/mqtt.py b/homeassistant/generated/mqtt.py index 6b22ca26221..7c4203eaec2 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -4,6 +4,9 @@ To update, run python3 -m script.hassfest """ MQTT = { + "dsmr_reader": [ + "dsmr/#", + ], "tasmota": [ "tasmota/discovery/#", ], diff --git a/tests/components/dsmr_reader/__init__.py b/tests/components/dsmr_reader/__init__.py new file mode 100644 index 00000000000..27b818f0bfe --- /dev/null +++ b/tests/components/dsmr_reader/__init__.py @@ -0,0 +1 @@ +"""Tests for the dsmr_reader component.""" diff --git a/tests/components/dsmr_reader/test_config_flow.py b/tests/components/dsmr_reader/test_config_flow.py new file mode 100644 index 00000000000..1de79b0d02d --- /dev/null +++ b/tests/components/dsmr_reader/test_config_flow.py @@ -0,0 +1,47 @@ +"""Tests for the config flow.""" +from homeassistant.components.dsmr_reader.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_import_step(hass: HomeAssistant): + """Test the import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "DSMR Reader" + + second_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + assert second_result["type"] == FlowResultType.ABORT + assert second_result["reason"] == "single_instance_allowed" + + +async def test_user_step(hass: HomeAssistant): + """Test the user step call.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + config_result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == "DSMR Reader" + + duplicate_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert duplicate_result["type"] == FlowResultType.ABORT + assert duplicate_result["reason"] == "single_instance_allowed"