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 <chris@talkingtontech.com>

* 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 <chris@talkingtontech.com>
This commit is contained in:
Rob Bierbooms 2020-09-03 23:19:45 +02:00 committed by GitHub
parent 77f5fb765b
commit d0120d5e0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 498 additions and 68 deletions

View File

@ -102,6 +102,7 @@ homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/directv/* @ctalkington homeassistant/components/directv/* @ctalkington
homeassistant/components/discogs/* @thibmaek homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/doorbird/* @oblogic7 @bdraco
homeassistant/components/dsmr/* @Robbie1221
homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dunehd/* @bieniu homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95

View File

@ -1 +1,54 @@
"""The dsmr component.""" """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

View File

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

View File

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

View File

@ -3,5 +3,6 @@
"name": "DSMR Slimme Meter", "name": "DSMR Slimme Meter",
"documentation": "https://www.home-assistant.io/integrations/dsmr", "documentation": "https://www.home-assistant.io/integrations/dsmr",
"requirements": ["dsmr_parser==0.18"], "requirements": ["dsmr_parser==0.18"],
"codeowners": [] "codeowners": ["@Robbie1221"],
"config_flow": false
} }

View File

@ -1,5 +1,6 @@
"""Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" """Support for Dutch Smart Meter (also known as Smartmeter or P1 port)."""
import asyncio import asyncio
from asyncio import CancelledError
from functools import partial from functools import partial
import logging import logging
@ -9,6 +10,7 @@ import serial
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
@ -18,26 +20,26 @@ from homeassistant.const import (
from homeassistant.core import CoreState, callback from homeassistant.core import CoreState, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity 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__) _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( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, 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( vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
cv.string, vol.In(["5B", "5", "4", "2.2"]) 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), vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
} }
) )
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 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.""" """Set up the DSMR sensor."""
# Suppress logging # Suppress logging
logging.getLogger("dsmr_parser").setLevel(logging.ERROR) logging.getLogger("dsmr_parser").setLevel(logging.ERROR)
config = entry.data
dsmr_version = config[CONF_DSMR_VERSION] dsmr_version = config[CONF_DSMR_VERSION]
# Define list of name,obis mappings to generate entities # 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 # Start DSMR asyncio.Protocol reader
try: try:
transport, protocol = await hass.loop.create_task(reader_factory()) 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: if transport:
# Register listener to close transport on HA shutdown # Register listener to close transport on HA shutdown
stop_listener = hass.bus.async_listen_once( stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, transport.close EVENT_HOMEASSISTANT_STOP, transport.close
) )
# Wait for reader to close # Wait for reader to close
await protocol.wait_closed() await protocol.wait_closed()
if hass.state != CoreState.stopping:
# Unexpected disconnect # Unexpected disconnect
if transport: if transport:
# remove listener # remove listener
stop_listener() stop_listener()
transport = None
protocol = None
# Reflect disconnect state in devices state by setting an # Reflect disconnect state in devices state by setting an
# empty telegram resulting in `unknown` states # empty telegram resulting in `unknown` states
update_entities_telegram({}) update_entities_telegram({})
@ -173,8 +181,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# throttle reconnect attempts # throttle reconnect attempts
await asyncio.sleep(config[CONF_RECONNECT_INTERVAL]) 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 # 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): class DSMREntity(Entity):

View File

@ -0,0 +1,9 @@
{
"config": {
"step": {},
"error": {},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

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

View File

@ -12,13 +12,15 @@ from itertools import chain, repeat
import pytest 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.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.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS
from homeassistant.setup import async_setup_component
import tests.async_mock import tests.async_mock
from tests.async_mock import DEFAULT, Mock from tests.async_mock import DEFAULT, MagicMock, Mock
from tests.common import assert_setup_component from tests.common import MockConfigEntry, patch
@pytest.fixture @pytest.fixture
@ -47,6 +49,39 @@ def mock_connection_factory(monkeypatch):
return connection_factory, transport, protocol 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): async def test_default_setup(hass, mock_connection_factory):
"""Test the default setup.""" """Test the default setup."""
(connection_factory, transport, protocol) = mock_connection_factory (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 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 = { telegram = {
CURRENT_ELECTRICITY_USAGE: CosemObject( CURRENT_ELECTRICITY_USAGE: CosemObject(
@ -73,9 +113,14 @@ async def test_default_setup(hass, mock_connection_factory):
), ),
} }
with assert_setup_component(1): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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] 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.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS 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(): async def test_derivative():
"""Test calculation of derivative value.""" """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 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 = { telegram = {
HOURLY_GAS_METER_READING: MBusObject( 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": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
} }
with assert_setup_component(1): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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] 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.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS 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): async def test_v5_meter(hass, mock_connection_factory):
"""Test if v5 meter is correctly parsed.""" """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 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 = { telegram = {
HOURLY_GAS_METER_READING: MBusObject( 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": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
} }
with assert_setup_component(1): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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] 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.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS 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): async def test_belgian_meter(hass, mock_connection_factory):
"""Test if Belgian meter is correctly parsed.""" """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 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 = { telegram = {
BELGIUM_HOURLY_GAS_METER_READING: MBusObject( 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": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
} }
with assert_setup_component(1): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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] 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.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS 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): async def test_belgian_meter_low(hass, mock_connection_factory):
"""Test if Belgian meter is correctly parsed.""" """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.obis_references import ELECTRICITY_ACTIVE_TARIFF
from dsmr_parser.objects import CosemObject 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": ""}])} telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])}
with assert_setup_component(1): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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] 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.state == "low"
assert power_tariff.attributes.get("unit_of_measurement") == "" 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): async def test_tcp(hass, mock_connection_factory):
"""If proper config provided TCP connection should be made.""" """If proper config provided TCP connection should be made."""
(connection_factory, transport, protocol) = mock_connection_factory (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): mock_entry = MockConfigEntry(
await async_setup_component(hass, "sensor", {"sensor": config}) domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
await hass.async_block_till_done() )
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][0] == "localhost"
assert connection_factory.call_args_list[0][0][1] == "1234" 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): async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory):
"""Connection should be retried on error during setup.""" """Connection should be retried on error during setup."""
(connection_factory, transport, protocol) = mock_connection_factory (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 # override the mock to have it fail the first time and succeed after
first_fail_connection_factory = tests.async_mock.AsyncMock( 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", "homeassistant.components.dsmr.sensor.create_dsmr_reader",
first_fail_connection_factory, 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 # wait for sleep to resolve
await hass.async_block_till_done() await hass.async_block_till_done()
assert first_fail_connection_factory.call_count >= 2, "connecting not retried" 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): async def test_reconnect(hass, monkeypatch, mock_connection_factory):
"""If transport disconnects, the connection should be retried.""" """If transport disconnects, the connection should be retried."""
(connection_factory, transport, protocol) = mock_connection_factory (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 # mock waiting coroutine while connection lasts
closed = asyncio.Event() closed = asyncio.Event()
@ -365,7 +508,13 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory):
protocol.wait_closed = wait_closed 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() await hass.async_block_till_done()
assert connection_factory.call_count == 1 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" assert connection_factory.call_count >= 2, "connecting not retried"
# setting it so teardown can be successful # setting it so teardown can be successful
closed.set() closed.set()
await hass.config_entries.async_unload(mock_entry.entry_id)
assert mock_entry.state == "not_loaded"