diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 49be969db..e5be7caaa 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -35,6 +35,7 @@ v5.10.0 - January 2023 - ``Added`` [`#1770 `_] Now tracking meter position timestamps in day statistics. Added them to Archive (day view) and Export. - ``Added`` [`#1770 `_] Automatic migrations retroactively reassessing meter positions and timestamps. Additionally checks/fixes gas consumption mismatch. +- ``Added`` [`#1794 `_] Added new datalogger configuration option for selecting extra device channel for specific vendor(s) - ``Added`` Old Dashboard notifications can now be viewed and permanently deleted in the Frontend admin section. - ``Changed`` [`#1725 `_] The value of ``DSMRREADER_REMOTE_DATALOGGER_INPUT_METHOD`` is now restricted to: ``DEBUG``, ``WARNING`` or ``ERROR`` diff --git a/dsmr_datalogger/admin.py b/dsmr_datalogger/admin.py index 572b9d76d..053eb59b7 100644 --- a/dsmr_datalogger/admin.py +++ b/dsmr_datalogger/admin.py @@ -44,7 +44,11 @@ class DataloggerSettingsAdmin(SingletonModelAdmin): ( _("Advanced"), { - "fields": ["process_sleep", "override_telegram_timestamp"], + "fields": [ + "dsmr_extra_device_channel", + "process_sleep", + "override_telegram_timestamp", + ], }, ), ( diff --git a/dsmr_datalogger/migrations/0032_dsmr_extra_device_channel.py b/dsmr_datalogger/migrations/0032_dsmr_extra_device_channel.py new file mode 100644 index 000000000..38f8c688f --- /dev/null +++ b/dsmr_datalogger/migrations/0032_dsmr_extra_device_channel.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.16 on 2023-01-23 21:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dsmr_datalogger", "0031_meter_statistics_meta"), + ] + + operations = [ + migrations.AddField( + model_name="dataloggersettings", + name="dsmr_extra_device_channel", + field=models.IntegerField( + blank=True, + choices=[ + (None, "Auto (default)"), + (1, "Belgium - Fluvius (channel 1)"), + (2, "Belgium - Fluvius (channel 2)"), + (3, "Belgium - Fluvius (channel 3)"), + (4, "Belgium - Fluvius (channel 4)"), + ], + default=None, + help_text="Only use when your extra device is read incorrectly (e.g. gas). Also, only works with specific vendor(s).", + null=True, + verbose_name="Extra device channel", + ), + ), + migrations.AlterField( + model_name="dataloggersettings", + name="dsmr_version", + field=models.IntegerField( + choices=[ + (4, "Netherlands - DSMR version 4/5 (default)"), + (3, "Netherlands - DSMR version 2/3"), + (101, "Belgium - Fluvius (gas meter fix)"), + (102, "Luxembourg - Smarty (single tariff fix)"), + ], + default=4, + help_text="The DSMR version your meter supports or the vendor related to it. Version should be printed on meter.", + verbose_name="DSMR version/vendor", + ), + ), + ] diff --git a/dsmr_datalogger/models/settings.py b/dsmr_datalogger/models/settings.py index f9d772cc3..b0895949d 100644 --- a/dsmr_datalogger/models/settings.py +++ b/dsmr_datalogger/models/settings.py @@ -28,6 +28,19 @@ class DataloggerSettings(ModelUpdateMixin, SingletonModel): (DSMR_LUXEMBOURG_SMARTY, _("Luxembourg - Smarty (single tariff fix)")), ) + DSMR_EXTRA_DEVICE_CHANNEL_AUTO = None + DSMR_EXTRA_DEVICE_CHANNEL_1 = 1 + DSMR_EXTRA_DEVICE_CHANNEL_2 = 2 + DSMR_EXTRA_DEVICE_CHANNEL_3 = 3 + DSMR_EXTRA_DEVICE_CHANNEL_4 = 4 + DSMR_EXTRA_DEVICE_CHANNEL_CHOICES = ( + (DSMR_EXTRA_DEVICE_CHANNEL_AUTO, _("Auto (default)")), + (DSMR_EXTRA_DEVICE_CHANNEL_1, _("Belgium - Fluvius (channel 1)")), + (DSMR_EXTRA_DEVICE_CHANNEL_2, _("Belgium - Fluvius (channel 2)")), + (DSMR_EXTRA_DEVICE_CHANNEL_3, _("Belgium - Fluvius (channel 3)")), + (DSMR_EXTRA_DEVICE_CHANNEL_4, _("Belgium - Fluvius (channel 4)")), + ) + input_method = models.CharField( max_length=16, default=INPUT_METHOD_SERIAL, @@ -38,9 +51,19 @@ class DataloggerSettings(ModelUpdateMixin, SingletonModel): dsmr_version = models.IntegerField( default=DSMR_VERSION_4_PLUS, choices=DSMR_VERSION_CHOICES, - verbose_name=_("DSMR version"), + verbose_name=_("DSMR version/vendor"), + help_text=_( + "The DSMR version your meter supports or the vendor related to it. Version should be printed on meter." + ), + ) + dsmr_extra_device_channel = models.IntegerField( + default=None, + blank=True, + null=True, + choices=DSMR_EXTRA_DEVICE_CHANNEL_CHOICES, + verbose_name=_("Extra device channel"), help_text=_( - "The DSMR version your meter supports. Version should be printed on meter." + "Only use when your extra device is read incorrectly (e.g. gas). Also, only works with specific vendor(s)." ), ) serial_port = models.CharField( diff --git a/dsmr_datalogger/services/datalogger.py b/dsmr_datalogger/services/datalogger.py index da2c44aae..d1b071d49 100644 --- a/dsmr_datalogger/services/datalogger.py +++ b/dsmr_datalogger/services/datalogger.py @@ -108,7 +108,7 @@ def _map_telegram_to_model(parsed_telegram: Dict, data: str): datalogger_settings = DataloggerSettings.get_solo() model_fields = {k: None for k in READING_FIELDS + STATISTICS_FIELDS} - mapping = _get_dsmrreader_mapping(datalogger_settings.dsmr_version) + mapping = _get_dsmrreader_mapping(datalogger_settings) for obis_ref, obis_data in parsed_telegram.items(): try: @@ -188,7 +188,7 @@ def _map_telegram_to_model(parsed_telegram: Dict, data: str): return new_instance -def _get_dsmrreader_mapping(version: int) -> Dict: +def _get_dsmrreader_mapping(datalogger_settings: DataloggerSettings) -> Dict: """Returns the mapping for OBIS to DSMR-reader (model fields).""" SPLIT_GAS_FIELD = { "value": "extra_device_delivered", @@ -232,14 +232,25 @@ def _get_dsmrreader_mapping(version: int) -> Dict: obis_references.VOLTAGE_SWELL_L3_COUNT: "voltage_swell_count_l3", } - if version == DataloggerSettings.DSMR_BELGIUM_FLUVIUS: + if datalogger_settings.dsmr_version == DataloggerSettings.DSMR_BELGIUM_FLUVIUS: + # Cheap hack for forcing channel selection. + try: + mbus_reference = { + DataloggerSettings.DSMR_EXTRA_DEVICE_CHANNEL_1: obis_references.BELGIUM_MBUS1_METER_READING2, + DataloggerSettings.DSMR_EXTRA_DEVICE_CHANNEL_2: obis_references.BELGIUM_MBUS2_METER_READING2, + DataloggerSettings.DSMR_EXTRA_DEVICE_CHANNEL_3: obis_references.BELGIUM_MBUS3_METER_READING2, + DataloggerSettings.DSMR_EXTRA_DEVICE_CHANNEL_4: obis_references.BELGIUM_MBUS4_METER_READING2, + }[datalogger_settings.dsmr_extra_device_channel] + except KeyError: + mbus_reference = obis_references.BELGIUM_MBUS_WILDCARD_METER_READING2 + mapping.update( { - obis_references.BELGIUM_MBUS_WILDCARD_METER_READING2: SPLIT_GAS_FIELD, + mbus_reference: SPLIT_GAS_FIELD, } ) - if version == DataloggerSettings.DSMR_LUXEMBOURG_SMARTY: + if datalogger_settings.dsmr_version == DataloggerSettings.DSMR_LUXEMBOURG_SMARTY: mapping.update( { obis_references.ELECTRICITY_IMPORTED_TOTAL: "electricity_delivered_1", diff --git a/dsmr_datalogger/tests/datalogger/test_fluvius_multiple_gas_devices.py b/dsmr_datalogger/tests/datalogger/test_fluvius_multiple_gas_devices.py new file mode 100644 index 000000000..3b9dedea6 --- /dev/null +++ b/dsmr_datalogger/tests/datalogger/test_fluvius_multiple_gas_devices.py @@ -0,0 +1,124 @@ +from unittest import mock +from datetime import datetime +from decimal import Decimal + +from django.test import TestCase +from django.utils import timezone +import pytz + +from dsmr_backend.tests.mixins import InterceptCommandStdoutMixin +from dsmr_datalogger.models.reading import DsmrReading +from dsmr_datalogger.models.statistics import MeterStatistics +from dsmr_datalogger.models.settings import DataloggerSettings +from dsmr_datalogger.tests.datalogger.mixins import FakeDsmrReadingMixin + + +class TestDatalogger(FakeDsmrReadingMixin, InterceptCommandStdoutMixin, TestCase): + """Belgium Fluvius meter (polyphase) with two gas devices (one inactive).""" + + def setUp(self): + DataloggerSettings.get_solo() + DataloggerSettings.objects.all().update( + dsmr_version=DataloggerSettings.DSMR_BELGIUM_FLUVIUS, + dsmr_extra_device_channel=DataloggerSettings.DSMR_EXTRA_DEVICE_CHANNEL_1, + ) + + def _dsmr_dummy_data(self): + return [ + "/FLU5\253769484_A\r\n", + "\r\n", + "0-0:96.1.4(012345678901234567890123456789)\r\n", + "0-0:96.1.1(012345678901234567890123456789)\r\n", + "0-0:1.0.0(230119124638W)\r\n", + "1-0:1.8.1(004423.770*kWh)\r\n", + "1-0:1.8.2(002607.237*kWh)\r\n", + "1-0:2.8.1(001194.693*kWh)\r\n", + "1-0:2.8.2(000755.554*kWh)\r\n", + "0-0:96.14.0(0001)\r\n", + "1-0:1.4.0(00.000*kW)\r\n", + "1-0:1.6.0(230116090000W)(11.173*kW)\r\n", + "0-0:98.1.0(1)(1-0:1.6.0)(1-0:1.6.0)(230101000000W)(221230114500W)(13.603*kW)\r\n", + "1-0:1.7.0(00.000*kW)\r\n", + "1-0:2.7.0(00.204*kW)\r\n", + "1-0:21.7.0(00.000*kW)\r\n", + "1-0:41.7.0(00.177*kW)\r\n", + "1-0:61.7.0(00.109*kW)\r\n", + "1-0:22.7.0(00.491*kW)\r\n", + "1-0:42.7.0(00.000*kW)\r\n", + "1-0:62.7.0(00.000*kW)\r\n", + "1-0:32.7.0(234.3*V)\r\n", + "1-0:52.7.0(234.2*V)\r\n", + "1-0:72.7.0(234.3*V)\r\n", + "1-0:31.7.0(002.18*A)\r\n", + "1-0:51.7.0(000.93*A)\r\n", + "1-0:71.7.0(000.60*A)\r\n", + "0-0:96.3.10(1)\r\n", + "0-0:17.0.0(999.9*kW)\r\n", + "1-0:31.4.0(999*A)\r\n", + "0-0:96.13.0()\r\n", + # Active gas device + "0-1:24.1.0(003)\r\n", + "0-1:96.1.1(012345678901234567890123456789)\r\n", + "0-1:24.4.0(1)\r\n", + "0-1:24.2.3(230119124616W)(00734.607*m3)\r\n", + # Inactive/stale gas device + "0-2:24.1.0(003)\r\n", + "0-2:96.1.1(012345678901234567890123456789)\r\n", + "0-2:24.4.0(1)\r\n", + "0-2:24.2.3(230119124005W)(00000.000*m3)\r\n", + "!B315", + ] + + def test_reading_creation(self): + self.assertFalse(DsmrReading.objects.exists()) + self._fake_dsmr_reading() + self.assertTrue(DsmrReading.objects.exists()) + + @mock.patch("django.utils.timezone.now") + def test_reading_values(self, now_mock): + now_mock.return_value = timezone.make_aware(timezone.datetime(2023, 1, 20)) + self._fake_dsmr_reading() + self.assertTrue(DsmrReading.objects.exists()) + reading = DsmrReading.objects.get() + self.assertEqual( + reading.timestamp, datetime(2023, 1, 19, 11, 46, 38, tzinfo=pytz.UTC) + ) + self.assertEqual(reading.electricity_delivered_1, Decimal("4423.770")) + self.assertEqual(reading.electricity_returned_1, Decimal("1194.693")) + self.assertEqual(reading.electricity_delivered_2, Decimal("2607.237")) + self.assertEqual(reading.electricity_returned_2, Decimal("755.554")) + self.assertEqual(reading.electricity_currently_delivered, Decimal("0")) + self.assertEqual(reading.electricity_currently_returned, Decimal("0.204")) + self.assertEqual( + reading.extra_device_timestamp, + datetime(2023, 1, 19, 11, 46, 16, tzinfo=pytz.UTC), + ) + self.assertEqual(reading.extra_device_delivered, Decimal("734.607")) + self.assertEqual(reading.phase_voltage_l1, Decimal("234.3")) + self.assertEqual(reading.phase_voltage_l2, Decimal("234.2")) + self.assertEqual(reading.phase_voltage_l3, Decimal("234.3")) + self.assertEqual(reading.phase_power_current_l1, 2) + self.assertEqual(reading.phase_power_current_l2, 0) + self.assertEqual(reading.phase_power_current_l3, 0) + + meter_statistics = MeterStatistics.get_solo() + self.assertIsNone(meter_statistics.dsmr_version) + self.assertEqual(meter_statistics.electricity_tariff, 1) + self.assertEqual(meter_statistics.power_failure_count, None) + self.assertEqual(meter_statistics.long_power_failure_count, None) + self.assertEqual(meter_statistics.voltage_sag_count_l1, None) + self.assertEqual(meter_statistics.voltage_sag_count_l2, None) + self.assertEqual(meter_statistics.voltage_sag_count_l3, None) + self.assertEqual(meter_statistics.voltage_swell_count_l1, None) + self.assertEqual(meter_statistics.voltage_swell_count_l2, None) + self.assertEqual(meter_statistics.voltage_swell_count_l3, None) + + # @mock.patch("django.utils.timezone.now") + # def test_telegram_override_timestamp(self, now_mock): + # reading = self._reading_with_override_telegram_timestamp_active(now_mock) + # + # self.assertEqual( + # # CET > UTC. Minute marker rounded to hours. Because Fluvius may or may not communicate DSMR v5 in telegrams + # reading.extra_device_timestamp, + # datetime(2021, 1, 15, 11, 0, 0, 0, tzinfo=pytz.UTC), + # ) diff --git a/dsmrreader/locales/nl/LC_MESSAGES/django.mo b/dsmrreader/locales/nl/LC_MESSAGES/django.mo index 175e1cc9c..70dfdde65 100644 Binary files a/dsmrreader/locales/nl/LC_MESSAGES/django.mo and b/dsmrreader/locales/nl/LC_MESSAGES/django.mo differ diff --git a/dsmrreader/locales/nl/LC_MESSAGES/django.po b/dsmrreader/locales/nl/LC_MESSAGES/django.po index a3b868c9e..841d9c7f8 100644 --- a/dsmrreader/locales/nl/LC_MESSAGES/django.po +++ b/dsmrreader/locales/nl/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: DSMR-reader\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-01-09 22:12+0100\n" +"POT-Creation-Date: 2023-01-23 22:41+0100\n" "PO-Revision-Date: \n" "Last-Translator: Dennis Siemensma \n" "Language-Team: Dennis Siemensma \n" @@ -711,17 +711,38 @@ msgstr "België - Fluvius (fix voor gasmeter)" msgid "Luxembourg - Smarty (single tariff fix)" msgstr "Luxemburg - Smarty (fix voor enkel tarief)" +msgid "Auto (default)" +msgstr "Automatisch (standaard)" + +msgid "Belgium - Fluvius (channel 1)" +msgstr "België - Fluvius (kanaal 1)" + +msgid "Belgium - Fluvius (channel 2)" +msgstr "België - Fluvius (kanaal 2)" + +msgid "Belgium - Fluvius (channel 3)" +msgstr "België - Fluvius (kanaal 3)" + +msgid "Belgium - Fluvius (channel 4)" +msgstr "België - Fluvius (kanaal 4)" + msgid "Input method" msgstr "Uitleesmethode" msgid "Whether to read telegrams from a serial port or network socket." msgstr "Geeft aan of telegrammen uitgelezen worden via een seriële poort of netwerk socket." -msgid "DSMR version" -msgstr "DSMR-versie" +msgid "DSMR version/vendor" +msgstr "DSMR-versie/fabrikant" + +msgid "The DSMR version your meter supports or the vendor related to it. Version should be printed on meter." +msgstr "De DSMR-versie ondersteund door je meter, of de fabrikant. Versie staat meestal aangegeven op de meter." + +msgid "Extra device channel" +msgstr "Kanaal extra gekoppeld apparaat" -msgid "The DSMR version your meter supports. Version should be printed on meter." -msgstr "De DSMR-versie ondersteund door je meter. Versie staat meestal aangegeven op de meter." +msgid "Only use when your extra device is read incorrectly (e.g. gas). Also, only works with specific vendor(s)." +msgstr "Alleen gebruiken wanneer een extra gekoppeld apparaat (zoals een gasmeter) niet goed uitgelezen wordt. Werkt daarnaast alleen voor specifieke fabrikant(en)." msgid "For serial input: Serial port connected to smartmeter. E.g.: /dev/ttyUSB0" msgstr "Voor uitlezen via een seriële poort: De seriële poort verbonden met de slimme meter. Bijvoorbeeld: /dev/ttyUSB0" @@ -786,6 +807,9 @@ msgstr "Retentieconfiguratie" msgid "Timestamp indicating when the reading was taken" msgstr "Moment waarop de meting is gedaan" +msgid "DSMR version" +msgstr "DSMR-versie" + msgid "Tariff indicator electricity. The tariff indicator can be used to switch tariff dependent loads e.g boilers. This is responsibility of the P1 user." msgstr "Tariefindicatie. Dit kan gebruikt worden om te wisselen met tariefafhankelijke vraag, zoals bijvoorbeeld een boiler. Verantwoording is voor de gebruiker van de P1-poort."