From 9531b08f2a571716f375d26ad4579a46099003ab Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sun, 27 Dec 2020 09:39:36 +0100 Subject: [PATCH] Add explicit support for Luxembourg Smarty meter in dsmr integration (#43975) * Add support for Luxembourg Smarty meter * Add config flow test * Add sensor tests --- homeassistant/components/dsmr/config_flow.py | 10 ++- homeassistant/components/dsmr/sensor.py | 23 ++++++- tests/components/dsmr/conftest.py | 28 ++++++-- tests/components/dsmr/test_config_flow.py | 23 +++++++ tests/components/dsmr/test_sensor.py | 69 ++++++++++++++++++++ 5 files changed, 140 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 912deb7ffea..f0899598351 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -35,11 +35,15 @@ class DSMRConnection: self._port = port self._dsmr_version = dsmr_version self._telegram = {} + if dsmr_version == "5L": + self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER + else: + self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER def equipment_identifier(self): """Equipment identifier.""" - if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram: - dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER] + if self._equipment_identifier in self._telegram: + dsmr_object = self._telegram[self._equipment_identifier] return getattr(dsmr_object, "value", None) def equipment_identifier_gas(self): @@ -52,7 +56,7 @@ class DSMRConnection: """Test if we can validate connection with the device.""" def update_telegram(telegram): - if obis_ref.EQUIPMENT_IDENTIFIER in telegram: + if self._equipment_identifier in telegram: self._telegram = telegram transport.close() diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index cc1877fb5bb..0c53fbd6079 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(["5B", "5", "4", "2.2"]) + cv.string, vol.In(["5L", "5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -85,7 +85,6 @@ async def async_setup_entry( ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1], ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2], ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], @@ -112,6 +111,24 @@ async def async_setup_entry( ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] + if dsmr_version == "5L": + obis_mapping.extend( + [ + [ + "Energy Consumption (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ], + [ + "Energy Production (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + ], + ] + ) + else: + obis_mapping.extend( + [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL]] + ) + # Generate device entities devices = [ DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config) @@ -120,7 +137,7 @@ async def async_setup_entry( # Protocol version specific obis if CONF_SERIAL_ID_GAS in config: - if dsmr_version in ("4", "5"): + if dsmr_version in ("4", "5", "5L"): gas_obis = obis_ref.HOURLY_GAS_METER_READING elif dsmr_version in ("5B",): gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index d2cec93df95..d57828fdfa4 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -2,7 +2,11 @@ import asyncio from dsmr_parser.clients.protocol import DSMRProtocol -from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS +from dsmr_parser.obis_references import ( + EQUIPMENT_IDENTIFIER, + EQUIPMENT_IDENTIFIER_GAS, + LUXEMBOURG_EQUIPMENT_IDENTIFIER, +) from dsmr_parser.objects import CosemObject import pytest @@ -38,17 +42,27 @@ async def dsmr_connection_send_validate_fixture(hass): transport = MagicMock(spec=asyncio.Transport) protocol = MagicMock(spec=DSMRProtocol) - async def connection_factory(*args, **kwargs): - """Return mocked out Asyncio classes.""" - return (transport, protocol) - - connection_factory = MagicMock(wraps=connection_factory) - protocol.telegram = { EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), } + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + if args[1] == "5L": + protocol.telegram = { + LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject( + [{"value": "12345678", "unit": ""}] + ), + EQUIPMENT_IDENTIFIER_GAS: CosemObject( + [{"value": "123456789", "unit": ""}] + ), + } + + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + async def wait_closed(): if isinstance(connection_factory.call_args_list[0][0][2], str): # TCP diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 039002ca7a7..9ae49419bf4 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -242,3 +242,26 @@ async def test_options_flow(hass): await hass.async_block_till_done() assert entry.options == {"time_between_update": 15} + + +async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "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, **SERIAL_DATA} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ceccc7d8c39..76a9a5bb070 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -337,6 +337,75 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS +async def test_luxembourg_meter(hass, dsmr_connection_fixture): + """Test if v5 meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + HOURLY_GAS_METER_READING, + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + ] + ), + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + ), + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + 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] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + power_tariff = hass.states.get("sensor.energy_consumption_total") + assert power_tariff.state == "123.456" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + power_tariff = hass.states.get("sensor.energy_production_total") + assert power_tariff.state == "654.321" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_consumption") + assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + + async def test_belgian_meter(hass, dsmr_connection_fixture): """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture