mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
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:
parent
77f5fb765b
commit
d0120d5e0a
@ -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
|
||||||
|
@ -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
|
||||||
|
61
homeassistant/components/dsmr/config_flow.py
Normal file
61
homeassistant/components/dsmr/config_flow.py
Normal 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)
|
21
homeassistant/components/dsmr/const.py
Normal file
21
homeassistant/components/dsmr/const.py
Normal 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"
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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):
|
||||||
|
9
homeassistant/components/dsmr/strings.json
Normal file
9
homeassistant/components/dsmr/strings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {},
|
||||||
|
"error": {},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
tests/components/dsmr/test_config_flow.py
Normal file
102
tests/components/dsmr/test_config_flow.py
Normal 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
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user