From 557f5763dffead7b2b3353553184bda3686b2a2d Mon Sep 17 00:00:00 2001 From: dupondje Date: Wed, 5 Feb 2020 22:14:03 +0100 Subject: [PATCH] Add belgian meter and rename some dsmr sensors (#30121) * Add support for belgian meter and rename some sensors * DSMR Fixes * Add test * More tests * Adjust test to latest dev * Remove unused code * Depend on dsmr_parser 0.18 --- homeassistant/components/dsmr/sensor.py | 35 ++++--- tests/components/dsmr/test_sensor.py | 132 +++++++++++++++++++++++- 2 files changed, 153 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 253e8409f1b..86a3b0b6fbc 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,6 +1,5 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio -from datetime import timedelta from functools import partial import logging @@ -32,9 +31,6 @@ ICON_POWER = "mdi:flash" ICON_POWER_FAILURE = "mdi:flash-off" ICON_SWELL_SAG = "mdi:pulse" -# Smart meter sends telegram every 10 seconds -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - RECONNECT_INTERVAL = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -42,7 +38,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(["5", "4", "2.2"]) + cv.string, vol.In(["5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -62,17 +58,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], - ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], - ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], - ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + ["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], + ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], + ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT], ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT], ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT], @@ -83,6 +80,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1], ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], + ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1], + ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2], + ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] # Generate device entities @@ -91,6 +91,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Protocol version specific obis if dsmr_version in ("4", "5"): gas_obis = obis_ref.HOURLY_GAS_METER_READING + elif dsmr_version in ("5B"): + gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING else: gas_obis = obis_ref.GAS_METER_READING @@ -214,7 +216,7 @@ class DSMREntity(Entity): value = self.get_dsmr_object_attr("value") if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: - return self.translate_tariff(value) + return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) try: value = round(float(value), self._config[CONF_PRECISION]) @@ -232,8 +234,15 @@ class DSMREntity(Entity): return self.get_dsmr_object_attr("unit") @staticmethod - def translate_tariff(value): - """Convert 2/1 to normal/low.""" + def translate_tariff(value, dsmr_version): + """Convert 2/1 to normal/low depening on DSMR version.""" + # DSMR V5B: Note: In Belgium values are swapped: + # Rate code 2 is used for low rate and rate code 1 is used for normal rate. + if dsmr_version in ("5B"): + if value == "0001": + value = "0002" + elif value == "0002": + value = "0001" # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is # used for normal rate. if value == "0002": diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 81249c04046..c881f4b9168 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -52,8 +52,9 @@ async def test_default_setup(hass, mock_connection_factory): from dsmr_parser.obis_references import ( CURRENT_ELECTRICITY_USAGE, ELECTRICITY_ACTIVE_TARIFF, + GAS_METER_READING, ) - from dsmr_parser.objects import CosemObject + from dsmr_parser.objects import CosemObject, MBusObject config = {"platform": "dsmr"} @@ -62,6 +63,12 @@ async def test_default_setup(hass, mock_connection_factory): [{"value": Decimal("0.0"), "unit": "kWh"}] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), } with assert_setup_component(1): @@ -90,6 +97,11 @@ async def test_default_setup(hass, mock_connection_factory): assert power_tariff.state == "low" assert power_tariff.attributes.get("unit_of_measurement") == "" + # 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") == "m3" + async def test_derivative(): """Test calculation of derivative value.""" @@ -131,6 +143,124 @@ async def test_derivative(): assert entity.unit_of_measurement == "m3/h" +async def test_v4_meter(hass, mock_connection_factory): + """Test if v4 meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ( + HOURLY_GAS_METER_READING, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + config = {"platform": "dsmr", "dsmr_version": "4"} + + telegram = { + HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + 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) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "low" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + # 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") == "m3" + + +async def test_belgian_meter(hass, mock_connection_factory): + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ( + BELGIUM_HOURLY_GAS_METER_READING, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + config = {"platform": "dsmr", "dsmr_version": "5B"} + + telegram = { + BELGIUM_HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + 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) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "normal" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + # 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") == "m3" + + +async def test_belgian_meter_low(hass, mock_connection_factory): + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF + from dsmr_parser.objects import CosemObject + + config = {"platform": "dsmr", "dsmr_version": "5B"} + + telegram = { + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + 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) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "low" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory