From d0120d5e0ad02f4be55af1bbcbe6b5aece50c964 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 3 Sep 2020 23:19:45 +0200 Subject: [PATCH] Update DSMR integration to import yaml to ConfigEntry (#39473) * Rewrite to import from platform setup * Add config flow for import * Implement reload * Update sensor tests * Add config flow tests * Remove some code * Fix pylint issue * Remove update options code * Add platform import test * Remove infinite while loop * Move async_setup_platform * Check for unload_ok * Remove commented out test code * Implement function to check on host/port already existing Co-authored-by: Chris Talkington * Implement new method in import * Update tests * Fix test setup platform * Add string * Patch setup_platform * Add block till done to patch block Co-authored-by: Chris Talkington --- CODEOWNERS | 1 + homeassistant/components/dsmr/__init__.py | 53 +++++ homeassistant/components/dsmr/config_flow.py | 61 ++++++ homeassistant/components/dsmr/const.py | 21 ++ homeassistant/components/dsmr/manifest.json | 3 +- homeassistant/components/dsmr/sensor.py | 101 +++++---- homeassistant/components/dsmr/strings.json | 9 + tests/components/dsmr/test_config_flow.py | 102 +++++++++ tests/components/dsmr/test_sensor.py | 215 ++++++++++++++++--- 9 files changed, 498 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/dsmr/config_flow.py create mode 100644 homeassistant/components/dsmr/const.py create mode 100644 homeassistant/components/dsmr/strings.json create mode 100644 tests/components/dsmr/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 0f0757cb983..ddd36cc7da8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,6 +102,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 @bdraco +homeassistant/components/dsmr/* @Robbie1221 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dunehd/* @bieniu homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 107e221a465..7eaa57fe8d3 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1 +1,54 @@ """The dsmr component.""" +import asyncio +from asyncio import CancelledError +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_TASK, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config: dict): + """Set up the DSMR platform.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up DSMR from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + task = hass.data[DOMAIN][entry.entry_id][DATA_TASK] + + # Cancel the reconnect task + task.cancel() + try: + await task + except CancelledError: + pass + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py new file mode 100644 index 00000000000..ecf93177334 --- /dev/null +++ b/homeassistant/components/dsmr/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for DSMR integration.""" +import logging +from typing import Any, Dict, Optional + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for DSMR.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def _abort_if_host_port_configured( + self, + port: str, + host: str = None, + updates: Optional[Dict[Any, Any]] = None, + reload_on_update: bool = True, + ): + """Test if host and port are already configured.""" + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port: + if updates is not None: + changed = self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + if ( + changed + and reload_on_update + and entry.state + in ( + config_entries.ENTRY_STATE_LOADED, + config_entries.ENTRY_STATE_SETUP_RETRY, + ) + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + async def async_step_import(self, import_config=None): + """Handle the initial step.""" + host = import_config.get(CONF_HOST) + port = import_config[CONF_PORT] + + status = self._abort_if_host_port_configured(port, host, import_config) + if status is not None: + return status + + if host is not None: + name = f"{host}:{port}" + else: + name = port + + return self.async_create_entry(title=name, data=import_config) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py new file mode 100644 index 00000000000..110e6b46a99 --- /dev/null +++ b/homeassistant/components/dsmr/const.py @@ -0,0 +1,21 @@ +"""Constants for the DSMR integration.""" + +DOMAIN = "dsmr" + +PLATFORMS = ["sensor"] + +CONF_DSMR_VERSION = "dsmr_version" +CONF_RECONNECT_INTERVAL = "reconnect_interval" +CONF_PRECISION = "precision" + +DEFAULT_DSMR_VERSION = "2.2" +DEFAULT_PORT = "/dev/ttyUSB0" +DEFAULT_PRECISION = 3 +DEFAULT_RECONNECT_INTERVAL = 30 + +DATA_TASK = "task" + +ICON_GAS = "mdi:fire" +ICON_POWER = "mdi:flash" +ICON_POWER_FAILURE = "mdi:flash-off" +ICON_SWELL_SAG = "mdi:pulse" diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 42e6b81dc1f..964c68ab182 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -3,5 +3,6 @@ "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": ["dsmr_parser==0.18"], - "codeowners": [] + "codeowners": ["@Robbie1221"], + "config_flow": false } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 4d780d48cd1..4298afe3cf6 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,5 +1,6 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio +from asyncio import CancelledError from functools import partial import logging @@ -9,6 +10,7 @@ import serial import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -18,26 +20,26 @@ from homeassistant.const import ( from homeassistant.core import CoreState, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + CONF_DSMR_VERSION, + CONF_PRECISION, + CONF_RECONNECT_INTERVAL, + DATA_TASK, + DEFAULT_DSMR_VERSION, + DEFAULT_PORT, + DEFAULT_PRECISION, + DEFAULT_RECONNECT_INTERVAL, + DOMAIN, + ICON_GAS, + ICON_POWER, + ICON_POWER_FAILURE, + ICON_SWELL_SAG, +) _LOGGER = logging.getLogger(__name__) -CONF_DSMR_VERSION = "dsmr_version" -CONF_RECONNECT_INTERVAL = "reconnect_interval" -CONF_PRECISION = "precision" - -DEFAULT_DSMR_VERSION = "2.2" -DEFAULT_PORT = "/dev/ttyUSB0" -DEFAULT_PRECISION = 3 - -DOMAIN = "dsmr" - -ICON_GAS = "mdi:fire" -ICON_POWER = "mdi:flash" -ICON_POWER_FAILURE = "mdi:flash-off" -ICON_SWELL_SAG = "mdi:pulse" - -RECONNECT_INTERVAL = 5 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, @@ -45,17 +47,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( cv.string, vol.In(["5B", "5", "4", "2.2"]) ), - vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, + vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import the platform into a config entry.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the DSMR sensor.""" # Suppress logging logging.getLogger("dsmr_parser").setLevel(logging.ERROR) + config = entry.data + dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities @@ -141,31 +156,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Start DSMR asyncio.Protocol reader try: transport, protocol = await hass.loop.create_task(reader_factory()) - except ( - serial.serialutil.SerialException, - ConnectionRefusedError, - TimeoutError, - ): - # Log any error while establishing connection and drop to retry - # connection wait - _LOGGER.exception("Error connecting to DSMR") - transport = None - if transport: - # Register listener to close transport on HA shutdown - stop_listener = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, transport.close - ) + if transport: + # Register listener to close transport on HA shutdown + stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, transport.close + ) - # Wait for reader to close - await protocol.wait_closed() + # Wait for reader to close + await protocol.wait_closed() - if hass.state != CoreState.stopping: # Unexpected disconnect if transport: # remove listener stop_listener() + transport = None + protocol = None + # Reflect disconnect state in devices state by setting an # empty telegram resulting in `unknown` states update_entities_telegram({}) @@ -173,8 +181,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # throttle reconnect attempts await asyncio.sleep(config[CONF_RECONNECT_INTERVAL]) + except (serial.serialutil.SerialException, OSError): + # Log any error while establishing connection and drop to retry + # connection wait + _LOGGER.exception("Error connecting to DSMR") + transport = None + protocol = None + except CancelledError: + if stop_listener: + stop_listener() + + if transport: + transport.close() + + if protocol: + await protocol.wait_closed() + + return + # Can't be hass.async_add_job because job runs forever - hass.loop.create_task(connect_and_reconnect()) + task = hass.loop.create_task(connect_and_reconnect()) + + # Save the task to be able to cancel it when unloading + hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task class DSMREntity(Entity): diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json new file mode 100644 index 00000000000..bc498971960 --- /dev/null +++ b/homeassistant/components/dsmr/strings.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": {}, + "error": {}, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py new file mode 100644 index 00000000000..1d25d2cd915 --- /dev/null +++ b/tests/components/dsmr/test_config_flow.py @@ -0,0 +1,102 @@ +"""Test the DSMR config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.dsmr import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_import_usb(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == entry_data + + +async def test_import_network(hass): + """Test we can import from network.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "localhost:1234" + assert result["data"] == entry_data + + +async def test_import_update(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + entry = MockConfigEntry( + domain=DOMAIN, + data=entry_data, + unique_id="/dev/ttyUSB0", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + new_entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 3, + "reconnect_interval": 30, + } + + with patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=new_entry_data, + ) + + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + assert entry.data["precision"] == 3 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 95608aedba7..73c11579070 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -12,13 +12,15 @@ from itertools import chain, repeat import pytest -from homeassistant.bootstrap import async_setup_component +from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS +from homeassistant.setup import async_setup_component import tests.async_mock -from tests.async_mock import DEFAULT, Mock -from tests.common import assert_setup_component +from tests.async_mock import DEFAULT, MagicMock, Mock +from tests.common import MockConfigEntry, patch @pytest.fixture @@ -47,6 +49,39 @@ def mock_connection_factory(monkeypatch): return connection_factory, transport, protocol +async def test_setup_platform(hass, mock_connection_factory): + """Test setup of platform.""" + async_add_entities = MagicMock() + + entry_data = { + "platform": DOMAIN, + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup", return_value=True), patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ): + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: entry_data} + ) + await hass.async_block_till_done() + + assert not async_add_entities.called + + # Check config entry + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == entry_data + + async def test_default_setup(hass, mock_connection_factory): """Test the default setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -58,7 +93,12 @@ async def test_default_setup(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject( @@ -73,9 +113,14 @@ async def test_default_setup(hass, mock_connection_factory): ), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -107,6 +152,10 @@ async def test_default_setup(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_derivative(): """Test calculation of derivative value.""" @@ -158,7 +207,12 @@ async def test_v4_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "4"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "4", + "precision": 4, + "reconnect_interval": 30, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -170,9 +224,14 @@ async def test_v4_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -192,6 +251,10 @@ async def test_v4_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_v5_meter(hass, mock_connection_factory): """Test if v5 meter is correctly parsed.""" @@ -203,7 +266,12 @@ async def test_v5_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "5"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "precision": 4, + "reconnect_interval": 30, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -215,9 +283,14 @@ async def test_v5_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -237,6 +310,10 @@ async def test_v5_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_belgian_meter(hass, mock_connection_factory): """Test if Belgian meter is correctly parsed.""" @@ -248,7 +325,12 @@ async def test_belgian_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "5B"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + } telegram = { BELGIUM_HOURLY_GAS_METER_READING: MBusObject( @@ -260,9 +342,14 @@ async def test_belgian_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -282,6 +369,10 @@ async def test_belgian_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_belgian_meter_low(hass, mock_connection_factory): """Test if Belgian meter is correctly parsed.""" @@ -290,13 +381,23 @@ async def test_belgian_meter_low(hass, mock_connection_factory): from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF from dsmr_parser.objects import CosemObject - config = {"platform": "dsmr", "dsmr_version": "5B"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + } telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])} - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -311,26 +412,50 @@ async def test_belgian_meter_low(hass, mock_connection_factory): assert power_tariff.state == "low" assert power_tariff.attributes.get("unit_of_measurement") == "" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "host": "localhost", "port": 1234} + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() assert connection_factory.call_args_list[0][0][0] == "localhost" assert connection_factory.call_args_list[0][0][1] == "1234" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "reconnect_interval": 0} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 0, + } # override the mock to have it fail the first time and succeed after first_fail_connection_factory = tests.async_mock.AsyncMock( @@ -342,17 +467,35 @@ async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factor "homeassistant.components.dsmr.sensor.create_dsmr_reader", first_fail_connection_factory, ) - await async_setup_component(hass, "sensor", {"sensor": config}) + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() # wait for sleep to resolve await hass.async_block_till_done() assert first_fail_connection_factory.call_count >= 2, "connecting not retried" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_reconnect(hass, monkeypatch, mock_connection_factory): """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "reconnect_interval": 0} + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 0, + } # mock waiting coroutine while connection lasts closed = asyncio.Event() @@ -365,7 +508,13 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory): protocol.wait_closed = wait_closed - await async_setup_component(hass, "sensor", {"sensor": config}) + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert connection_factory.call_count == 1 @@ -382,3 +531,7 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory): assert connection_factory.call_count >= 2, "connecting not retried" # setting it so teardown can be successful closed.set() + + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded"