mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
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:
parent
cb2a9dfebf
commit
557f5763df
@ -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":
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user