diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 01c98496de2..50823bb1d29 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -5,7 +5,7 @@ from asyncio import CancelledError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DATA_TASK, DOMAIN, PLATFORMS +from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS async def async_setup(hass, config: dict): @@ -23,12 +23,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, platform) ) + listener = entry.add_update_listener(async_update_options) + hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] = listener + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" task = hass.data[DOMAIN][entry.entry_id][DATA_TASK] + listener = hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] # Cancel the reconnect task task.cancel() @@ -46,6 +50,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: + listener() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 724f9393fbf..912deb7ffea 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -8,14 +8,18 @@ from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader import serial +import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from .const import ( # pylint:disable=unused-import CONF_DSMR_VERSION, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, + CONF_TIME_BETWEEN_UPDATE, + DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, ) @@ -115,6 +119,12 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DSMROptionFlowHandler(config_entry) + def _abort_if_host_port_configured( self, port: str, @@ -174,6 +184,33 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=name, data=data) +class DSMROptionFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TIME_BETWEEN_UPDATE, + default=self.config_entry.options.get( + CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + } + ), + ) + + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index edb138a60ff..da804857845 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -7,6 +7,7 @@ PLATFORMS = ["sensor"] CONF_DSMR_VERSION = "dsmr_version" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_PRECISION = "precision" +CONF_TIME_BETWEEN_UPDATE = "time_between_update" CONF_SERIAL_ID = "serial_id" CONF_SERIAL_ID_GAS = "serial_id_gas" @@ -15,7 +16,9 @@ DEFAULT_DSMR_VERSION = "2.2" DEFAULT_PORT = "/dev/ttyUSB0" DEFAULT_PRECISION = 3 DEFAULT_RECONNECT_INTERVAL = 30 +DEFAULT_TIME_BETWEEN_UPDATE = 30 +DATA_LISTENER = "listener" DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7931fcc26d1..cc1877fb5bb 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,6 +1,7 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio from asyncio import CancelledError +from datetime import timedelta from functools import partial import logging from typing import Dict @@ -22,6 +23,7 @@ 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 homeassistant.util import Throttle from .const import ( CONF_DSMR_VERSION, @@ -29,11 +31,13 @@ from .const import ( CONF_RECONNECT_INTERVAL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, + CONF_TIME_BETWEEN_UPDATE, DATA_TASK, DEFAULT_DSMR_VERSION, DEFAULT_PORT, DEFAULT_PRECISION, DEFAULT_RECONNECT_INTERVAL, + DEFAULT_TIME_BETWEEN_UPDATE, DEVICE_NAME_ENERGY, DEVICE_NAME_GAS, DOMAIN, @@ -72,6 +76,7 @@ async def async_setup_entry( ) -> None: """Set up the DSMR sensor.""" config = entry.data + options = entry.options dsmr_version = config[CONF_DSMR_VERSION] @@ -142,6 +147,11 @@ async def async_setup_entry( async_add_entities(devices) + min_time_between_updates = timedelta( + seconds=options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE) + ) + + @Throttle(min_time_between_updates) def update_entities_telegram(telegram): """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram @@ -242,7 +252,7 @@ class DSMREntity(Entity): def update_data(self, telegram): """Update data.""" self.telegram = telegram - if self.hass: + if self.hass and self._obis in self.telegram: self.async_write_ha_state() def get_dsmr_object_attr(self, attribute): diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json index bc498971960..57d38f78feb 100644 --- a/homeassistant/components/dsmr/strings.json +++ b/homeassistant/components/dsmr/strings.json @@ -5,5 +5,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minimum time between entity updates [s]" + }, + "title": "DSMR Options" + } + } } } diff --git a/homeassistant/components/dsmr/translations/en.json b/homeassistant/components/dsmr/translations/en.json index 1344d2f6988..2998f611134 100644 --- a/homeassistant/components/dsmr/translations/en.json +++ b/homeassistant/components/dsmr/translations/en.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Device is already configured" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minimum time between entity updates [s]" + }, + "title": "DSMR Options" + } + } } } \ No newline at end of file diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index c6fb57c6ecb..039002ca7a7 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -4,7 +4,7 @@ from itertools import chain, repeat import serial -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dsmr import DOMAIN from tests.async_mock import DEFAULT, AsyncMock, patch @@ -196,9 +196,49 @@ async def test_import_update(hass, dsmr_connection_send_validate_fixture): data=new_entry_data, ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["precision"] == 3 + + +async def test_options_flow(hass): + """Test options flow.""" + 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) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "time_between_update": 15, + }, + ) + + with patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.options == {"time_between_update": 15} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 4803aa57d7d..ceccc7d8c39 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -81,6 +81,9 @@ async def test_default_setup(hass, dsmr_connection_fixture): "serial_id": "1234", "serial_id_gas": "5678", } + entry_options = { + "time_between_update": 0, + } telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject( @@ -96,7 +99,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): } mock_entry = MockConfigEntry( - domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options ) mock_entry.add_to_hass(hass) @@ -232,6 +235,9 @@ async def test_v4_meter(hass, dsmr_connection_fixture): "serial_id": "1234", "serial_id_gas": "5678", } + entry_options = { + "time_between_update": 0, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -244,7 +250,7 @@ async def test_v4_meter(hass, dsmr_connection_fixture): } mock_entry = MockConfigEntry( - domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options ) mock_entry.add_to_hass(hass) @@ -289,6 +295,9 @@ async def test_v5_meter(hass, dsmr_connection_fixture): "serial_id": "1234", "serial_id_gas": "5678", } + entry_options = { + "time_between_update": 0, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -301,7 +310,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture): } mock_entry = MockConfigEntry( - domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options ) mock_entry.add_to_hass(hass) @@ -346,6 +355,9 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): "serial_id": "1234", "serial_id_gas": "5678", } + entry_options = { + "time_between_update": 0, + } telegram = { BELGIUM_HOURLY_GAS_METER_READING: MBusObject( @@ -358,7 +370,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): } mock_entry = MockConfigEntry( - domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options ) mock_entry.add_to_hass(hass) @@ -400,11 +412,14 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): "serial_id": "1234", "serial_id_gas": "5678", } + entry_options = { + "time_between_update": 0, + } telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])} mock_entry = MockConfigEntry( - domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options ) mock_entry.add_to_hass(hass)