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
This commit is contained in:
dupondje 2020-02-05 22:14:03 +01:00 committed by GitHub
parent cb2a9dfebf
commit 557f5763df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 153 additions and 14 deletions

View File

@ -1,6 +1,5 @@
"""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 datetime import timedelta
from functools import partial from functools import partial
import logging import logging
@ -32,9 +31,6 @@ ICON_POWER = "mdi:flash"
ICON_POWER_FAILURE = "mdi:flash-off" ICON_POWER_FAILURE = "mdi:flash-off"
ICON_SWELL_SAG = "mdi:pulse" ICON_SWELL_SAG = "mdi:pulse"
# Smart meter sends telegram every 10 seconds
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
RECONNECT_INTERVAL = 5 RECONNECT_INTERVAL = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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_PORT, default=DEFAULT_PORT): cv.string,
vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
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(["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=30): int,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(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 Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE],
["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY],
["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF],
["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL],
["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1],
["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2],
["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], ["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 L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE],
["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_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 L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE],
["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE],
["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_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], ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT],
["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT],
["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_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 L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1],
["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2],
["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], ["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 # Generate device entities
@ -91,6 +91,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Protocol version specific obis # Protocol version specific obis
if dsmr_version in ("4", "5"): if dsmr_version in ("4", "5"):
gas_obis = obis_ref.HOURLY_GAS_METER_READING gas_obis = obis_ref.HOURLY_GAS_METER_READING
elif dsmr_version in ("5B"):
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
else: else:
gas_obis = obis_ref.GAS_METER_READING gas_obis = obis_ref.GAS_METER_READING
@ -214,7 +216,7 @@ class DSMREntity(Entity):
value = self.get_dsmr_object_attr("value") value = self.get_dsmr_object_attr("value")
if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
return self.translate_tariff(value) return self.translate_tariff(value, self._config[CONF_DSMR_VERSION])
try: try:
value = round(float(value), self._config[CONF_PRECISION]) value = round(float(value), self._config[CONF_PRECISION])
@ -232,8 +234,15 @@ class DSMREntity(Entity):
return self.get_dsmr_object_attr("unit") return self.get_dsmr_object_attr("unit")
@staticmethod @staticmethod
def translate_tariff(value): def translate_tariff(value, dsmr_version):
"""Convert 2/1 to normal/low.""" """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 # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is
# used for normal rate. # used for normal rate.
if value == "0002": if value == "0002":

View File

@ -52,8 +52,9 @@ async def test_default_setup(hass, mock_connection_factory):
from dsmr_parser.obis_references import ( from dsmr_parser.obis_references import (
CURRENT_ELECTRICITY_USAGE, CURRENT_ELECTRICITY_USAGE,
ELECTRICITY_ACTIVE_TARIFF, ELECTRICITY_ACTIVE_TARIFF,
GAS_METER_READING,
) )
from dsmr_parser.objects import CosemObject from dsmr_parser.objects import CosemObject, MBusObject
config = {"platform": "dsmr"} config = {"platform": "dsmr"}
@ -62,6 +63,12 @@ async def test_default_setup(hass, mock_connection_factory):
[{"value": Decimal("0.0"), "unit": "kWh"}] [{"value": Decimal("0.0"), "unit": "kWh"}]
), ),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), 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): 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.state == "low"
assert power_tariff.attributes.get("unit_of_measurement") == "" 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(): async def test_derivative():
"""Test calculation of derivative value.""" """Test calculation of derivative value."""
@ -131,6 +143,124 @@ async def test_derivative():
assert entity.unit_of_measurement == "m3/h" 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): 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