Repository: hoylabs/OpenDTU-OnBattery
Pull request #1651: Use DataPoints for all PowerMeter Providers
Author: login: AndreasBoehm
Created: 2025-02-18T17:40:20Z | Merged: 2025-03-19T20:02:42Z

With this PR we switch all PowerMeter Providers to use DataPoints. This will allow us to easily build a HASS integration, show individual phase values in the LiveView and also to group inverters by phase within the DPL.

Additonaly i added reading the total power directly for 3-phase SDM powermeters as requested in issue https://github.com/hoylabs/OpenDTU-OnBattery/issues/765

### Tested Providers
- [X] HTTP (schlimmchen)
- [X] MQTT (schlimmchen)
- [x] SDM 1-Phase (schlimmchen)
- [ ] SDM 3-Phase
- [ ] SML via Serial
- [X] SML via HTTP (schlimmchen)
- [x] SMA HomeManager2  (schlimmchen)
- [X] Victron Modbus UDP (Sorbat)

### Test version
https://github.com/hoylabs/OpenDTU-OnBattery/actions/runs/13955474338


Diff:
diff --git a/include/Configuration.h b/include/Configuration.h
index c08a56bae..4800b8049 100644
--- a/include/Configuration.h
+++ b/include/Configuration.h
@@ -132,6 +132,12 @@ struct POWERMETER_HTTP_SML_CONFIG_T {
 };
 using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T;
 
+struct POWERMETER_UDP_VICTRON_CONFIG_T {
+    uint16_t PollingIntervalMs;
+    uint8_t IpAddress[4];
+};
+using PowerMeterUdpVictronConfig = struct POWERMETER_UDP_VICTRON_CONFIG_T;
+
 struct POWERLIMITER_INVERTER_CONFIG_T {
     uint64_t Serial;
     bool IsGoverned;
@@ -369,6 +375,7 @@ struct CONFIG_T {
         PowerMeterSerialSdmConfig SerialSdm;
         PowerMeterHttpJsonConfig HttpJson;
         PowerMeterHttpSmlConfig HttpSml;
+        PowerMeterUdpVictronConfig UdpVictron;
     } PowerMeter;
 
     PowerLimiterConfig PowerLimiter;
@@ -413,6 +420,7 @@ class ConfigurationClass {
     static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target);
     static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
     static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target);
+    static void serializePowerMeterUdpVictronConfig(PowerMeterUdpVictronConfig const& source, JsonObject& target);
     static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target);
     static void serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target);
     static void serializeGridChargerConfig(GridChargerConfig const& source, JsonObject& target);
@@ -424,6 +432,7 @@ class ConfigurationClass {
     static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target);
     static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
     static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
+    static void deserializePowerMeterUdpVictronConfig(JsonObject const& source, PowerMeterUdpVictronConfig& target);
     static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target);
     static void deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target);
     static void deserializeGridChargerConfig(JsonObject const& source, GridChargerConfig& target);
diff --git a/include/DataPoints.h b/include/DataPoints.h
index a8cfaf2ea..f19d37c9d 100644
--- a/include/DataPoints.h
+++ b/include/DataPoints.h
@@ -8,6 +8,7 @@
 #include <variant>
 #include <limits>
 #include <algorithm>
+#include <mutex>
 
 using tCellVoltages = std::map<uint8_t, uint16_t>;
 
@@ -60,8 +61,24 @@ class DataPointContainer {
     public:
         DataPointContainer() = default;
 
+        DataPointContainer(DataPointContainer const& other)
+        {
+            auto scopedLock = other.lock();
+            _dataPoints = other._dataPoints;
+        }
+
+        // allows to keep the container locked while adding multiple data points
+        // that are supposed to be coherent, and/or to ensure thread safety.
+        std::unique_lock<std::mutex> lock() const {
+            return std::unique_lock<std::mutex>(_mutex);
+        }
+
         template<Label L>
         void add(typename Traits<L>::type val) {
+            // no locking here! iff thread safety is required, use the lock()
+            // method in a scoped block in which this method is called.
+
+            _dataPoints.erase(L);
             _dataPoints.emplace(
                     L,
                     DataPoint(
@@ -81,6 +98,8 @@ class DataPointContainer {
 
         template<Label L>
         std::optional<DataPoint const> getDataPointFor() const {
+            auto scopedLock = lock();
+
             auto it = _dataPoints.find(L);
             if (it == _dataPoints.end()) { return std::nullopt; }
             return it->second;
@@ -101,6 +120,9 @@ class DataPointContainer {
         // existing data points in this instance.
         void updateFrom(DataPointContainer const& source)
         {
+            auto scopedLock = lock();
+            auto otherScopedLock = source.lock();
+
             for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
                 auto pos = _dataPoints.find(iter->first);
 
@@ -116,6 +138,8 @@ class DataPointContainer {
 
         uint32_t getLastUpdate() const
         {
+            auto scopedLock = lock();
+
             uint32_t now = millis();
             uint32_t diff = std::numeric_limits<uint32_t>::max()/2;
             for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
@@ -124,6 +148,12 @@ class DataPointContainer {
             return now - diff;
         }
 
+        void clear() {
+            auto scopedLock = lock();
+            _dataPoints.clear();
+        }
+
     private:
         tMap _dataPoints;
+        mutable std::mutex _mutex;
 };
diff --git a/include/powermeter/DataPoints.h b/include/powermeter/DataPoints.h
new file mode 100644
index 000000000..af94c5a31
--- /dev/null
+++ b/include/powermeter/DataPoints.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <cstdint>
+#include <DataPoints.h>
+
+namespace PowerMeters {
+
+enum class DataPointLabel {
+    PowerTotal,
+    PowerL1,
+    PowerL2,
+    PowerL3,
+    VoltageL1,
+    VoltageL2,
+    VoltageL3,
+    CurrentL1,
+    CurrentL2,
+    CurrentL3,
+    Import,
+    Export,
+};
+
+template<DataPointLabel> struct DataPointLabelTraits;
+
+#define LABEL_TRAIT(n, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
+    using type = float; \
+    static constexpr char const name[] = #n; \
+    static constexpr char const unit[] = u; \
+};
+
+LABEL_TRAIT(PowerTotal,      "W");
+LABEL_TRAIT(PowerL1,         "W");
+LABEL_TRAIT(PowerL2,         "W");
+LABEL_TRAIT(PowerL3,         "W");
+LABEL_TRAIT(VoltageL1,       "V");
+LABEL_TRAIT(VoltageL2,       "V");
+LABEL_TRAIT(VoltageL3,       "V");
+LABEL_TRAIT(CurrentL1,       "A");
+LABEL_TRAIT(CurrentL2,       "A");
+LABEL_TRAIT(CurrentL3,       "A");
+LABEL_TRAIT(Import,          "kWh");
+LABEL_TRAIT(Export,          "kWh");
+#undef LABEL_TRAIT
+
+} // namespace PowerMeters
+
+template class DataPointContainer<DataPoint<float>,
+                                  PowerMeters::DataPointLabel,
+                                  PowerMeters::DataPointLabelTraits>;
+
+namespace PowerMeters {
+    using DataPointContainer = DataPointContainer<DataPoint<float>, DataPointLabel, DataPointLabelTraits>;
+} // namespace PowerMeters
diff --git a/include/powermeter/Provider.h b/include/powermeter/Provider.h
index ca8c4995b..c4129ec06 100644
--- a/include/powermeter/Provider.h
+++ b/include/powermeter/Provider.h
@@ -2,7 +2,8 @@
 #pragma once
 
 #include <atomic>
-#include "Configuration.h"
+#include <Configuration.h>
+#include <powermeter/DataPoints.h>
 
 namespace PowerMeters {
 
@@ -17,17 +18,18 @@ class Provider {
         HTTP_JSON = 3,
         SERIAL_SML = 4,
         SMAHM2 = 5,
-        HTTP_SML = 6
+        HTTP_SML = 6,
+        UDP_VICTRON = 7
     };
 
     // returns true if the provider is ready for use, false otherwise
     virtual bool init() = 0;
 
     virtual void loop() = 0;
-    virtual float getPowerTotal() const = 0;
     virtual bool isDataValid() const;
 
-    uint32_t getLastUpdate() const { return _lastUpdate; }
+    float getPowerTotal() const;
+    uint32_t getLastUpdate() const { return _dataCurrent.getLastUpdate(); }
     void mqttLoop() const;
 
 protected:
@@ -36,19 +38,11 @@ class Provider {
         _verboseLogging = config.PowerMeter.VerboseLogging;
     }
 
-    void gotUpdate() { _lastUpdate = millis(); }
-
-    void mqttPublish(String const& topic, float const& value) const;
-
     bool _verboseLogging;
 
-private:
-    virtual void doMqttPublish() const = 0;
-
-    // gotUpdate() updates this variable potentially from a different thread
-    // than users that request to read this variable through getLastUpdate().
-    std::atomic<uint32_t> _lastUpdate = 0;
+    DataPointContainer _dataCurrent;
 
+private:
     mutable uint32_t _lastMqttPublish = 0;
 };
 
diff --git a/include/powermeter/json/http/Provider.h b/include/powermeter/json/http/Provider.h
index c29fbb518..c166760d6 100644
--- a/include/powermeter/json/http/Provider.h
+++ b/include/powermeter/json/http/Provider.h
@@ -26,12 +26,9 @@ class Provider : public ::PowerMeters::Provider {
 
     bool init() final;
     void loop() final;
-    float getPowerTotal() const final;
     bool isDataValid() const final;
-    void doMqttPublish() const final;
 
-    using power_values_t = std::array<float, POWERMETER_HTTP_JSON_MAX_VALUES>;
-    using poll_result_t = std::variant<power_values_t, String>;
+    using poll_result_t = std::variant<DataPointContainer, String>;
     poll_result_t poll();
 
 private:
@@ -43,9 +40,6 @@ class Provider : public ::PowerMeters::Provider {
 
     uint32_t _lastPoll = 0;
 
-    mutable std::mutex _valueMutex;
-    power_values_t _powerValues = {};
-
     std::array<std::unique_ptr<HttpGetter>, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters;
 
     TaskHandle_t _taskHandle = nullptr;
diff --git a/include/powermeter/json/mqtt/Provider.h b/include/powermeter/json/mqtt/Provider.h
index f81cb4cef..19d43dd76 100644
--- a/include/powermeter/json/mqtt/Provider.h
+++ b/include/powermeter/json/mqtt/Provider.h
@@ -19,25 +19,16 @@ class Provider : public ::PowerMeters::Provider {
 
     bool init() final;
     void loop() final { }
-    float getPowerTotal() const final;
 
 private:
     using MsgProperties = espMqttClientTypes::MessageProperties;
     void onMessage(MsgProperties const& properties, char const* topic,
             uint8_t const* payload, size_t len, size_t index,
-            size_t total, float* targetVariable, PowerMeterMqttValue const* cfg);
-
-    // we don't need to republish data received from MQTT
-    void doMqttPublish() const final { };
+            size_t total, uint8_t const phaseIndex, PowerMeterMqttValue const* cfg);
 
     PowerMeterMqttConfig const _cfg;
 
-    using power_values_t = std::array<float, POWERMETER_MQTT_MAX_VALUES>;
-    power_values_t _powerValues;
-
     std::vector<String> _mqttSubscriptions;
-
-    mutable std::mutex _mutex;
 };
 
 } // namespace PowerMeters::Json::Mqtt
diff --git a/include/powermeter/sdm/serial/Provider.h b/include/powermeter/sdm/serial/Provider.h
index 0019141ff..5c858f152 100644
--- a/include/powermeter/sdm/serial/Provider.h
+++ b/include/powermeter/sdm/serial/Provider.h
@@ -26,9 +26,7 @@ class Provider : public ::PowerMeters::Provider {
 
     bool init() final;
     void loop() final;
-    float getPowerTotal() const final;
     bool isDataValid() const final;
-    void doMqttPublish() const final;
 
 private:
     static void pollingLoopHelper(void* context);
@@ -41,17 +39,6 @@ class Provider : public ::PowerMeters::Provider {
 
     uint32_t _lastPoll = 0;
 
-    float _phase1Power = 0.0;
-    float _phase2Power = 0.0;
-    float _phase3Power = 0.0;
-    float _phase1Voltage = 0.0;
-    float _phase2Voltage = 0.0;
-    float _phase3Voltage = 0.0;
-    float _energyImport = 0.0;
-    float _energyExport = 0.0;
-
-    mutable std::mutex _valueMutex;
-
     std::unique_ptr<SoftwareSerial> _upSdmSerial = nullptr;
     std::unique_ptr<SDM> _upSdm = nullptr;
 
diff --git a/include/powermeter/sml/Provider.h b/include/powermeter/sml/Provider.h
index 59a192ee3..f1e05fc80 100644
--- a/include/powermeter/sml/Provider.h
+++ b/include/powermeter/sml/Provider.h
@@ -14,10 +14,6 @@
 namespace PowerMeters::Sml {
 
 class Provider : public ::PowerMeters::Provider {
-public:
-    float getPowerTotal() const final;
-    void doMqttPublish() const final;
-
 protected:
     explicit Provider(char const* user)
         : _user(user) { }
@@ -27,46 +23,29 @@ class Provider : public ::PowerMeters::Provider {
 
 private:
     std::string _user;
-    mutable std::mutex _mutex;
-
-    using values_t = struct {
-        std::optional<float> activePowerTotal = std::nullopt;
-        std::optional<float> activePowerL1 = std::nullopt;
-        std::optional<float> activePowerL2 = std::nullopt;
-        std::optional<float> activePowerL3 = std::nullopt;
-        std::optional<float> voltageL1 = std::nullopt;
-        std::optional<float> voltageL2 = std::nullopt;
-        std::optional<float> voltageL3 = std::nullopt;
-        std::optional<float> currentL1 = std::nullopt;
-        std::optional<float> currentL2 = std::nullopt;
-        std::optional<float> currentL3 = std::nullopt;
-        std::optional<float> energyImport = std::nullopt;
-        std::optional<float> energyExport = std::nullopt;
-    };
 
-    values_t _values;
-    values_t _cache;
+    DataPointContainer _dataInFlight;
 
     using OBISHandler = struct {
         uint8_t const OBIS[6];
         void (*decoder)(float&);
-        std::optional<float>* target;
+        DataPointLabel target;
         char const* name;
     };
 
     const std::list<OBISHandler> smlHandlerList{
-        {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerTotal, "active power total"},
-        {{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL1, "active power L1"},
-        {{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL2, "active power L2"},
-        {{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL3, "active power L3"},
-        {{0x01, 0x00, 0x20, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL1, "voltage L1"},
-        {{0x01, 0x00, 0x34, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL2, "voltage L2"},
-        {{0x01, 0x00, 0x48, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL3, "voltage L3"},
-        {{0x01, 0x00, 0x1f, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL1, "current L1"},
-        {{0x01, 0x00, 0x33, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL2, "current L2"},
-        {{0x01, 0x00, 0x47, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL3, "current L3"},
-        {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyImport, "energy import"},
-        {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyExport, "energy export"}
+        {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, DataPointLabel::PowerTotal, "PowerTotal"},
+        {{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, DataPointLabel::PowerL1, "PowerL1"},
+        {{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, DataPointLabel::PowerL2, "PowerL2"},
+        {{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, DataPointLabel::PowerL3, "PowerL3"},
+        {{0x01, 0x00, 0x20, 0x07, 0x00, 0xff}, &smlOBISVolt, DataPointLabel::VoltageL1, "VoltageL1"},
+        {{0x01, 0x00, 0x34, 0x07, 0x00, 0xff}, &smlOBISVolt, DataPointLabel::VoltageL2, "VoltageL2"},
+        {{0x01, 0x00, 0x48, 0x07, 0x00, 0xff}, &smlOBISVolt, DataPointLabel::VoltageL3, "VoltageL3"},
+        {{0x01, 0x00, 0x1f, 0x07, 0x00, 0xff}, &smlOBISAmpere, DataPointLabel::CurrentL1, "CurrentL1"},
+        {{0x01, 0x00, 0x33, 0x07, 0x00, 0xff}, &smlOBISAmpere, DataPointLabel::CurrentL2, "CurrentL2"},
+        {{0x01, 0x00, 0x47, 0x07, 0x00, 0xff}, &smlOBISAmpere, DataPointLabel::CurrentL3, "CurrentL3"},
+        {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, DataPointLabel::Import, "Import"},
+        {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, DataPointLabel::Export, "Export"}
     };
 };
 
diff --git a/include/powermeter/udp/smahm/Provider.h b/include/powermeter/udp/smahm/Provider.h
index 3abdd1c41..f1a7772e3 100644
--- a/include/powermeter/udp/smahm/Provider.h
+++ b/include/powermeter/udp/smahm/Provider.h
@@ -15,8 +15,6 @@ class Provider : public ::PowerMeters::Provider {
 
     bool init() final;
     void loop() final;
-    float getPowerTotal() const final { return _powerMeterPower; }
-    void doMqttPublish() const final;
 
 private:
     void Soutput(int kanal, int index, int art, int tarif,
@@ -24,10 +22,6 @@ class Provider : public ::PowerMeters::Provider {
 
     uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
 
-    float _powerMeterPower = 0.0;
-    float _powerMeterL1 = 0.0;
-    float _powerMeterL2 = 0.0;
-    float _powerMeterL3 = 0.0;
     uint32_t _previousMillis = 0;
     uint32_t _serial = 0;
 };
diff --git a/include/powermeter/udp/victron/Provider.h b/include/powermeter/udp/victron/Provider.h
new file mode 100644
index 000000000..c9a8e7e30
--- /dev/null
+++ b/include/powermeter/udp/victron/Provider.h
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024 Holger-Steffen Stapf
+ */
+#pragma once
+
+#include <cstdint>
+#include <Configuration.h>
+#include <powermeter/Provider.h>
+
+namespace PowerMeters::Udp::Victron {
+
+class Provider : public ::PowerMeters::Provider {
+public:
+    explicit Provider(PowerMeterUdpVictronConfig const& cfg);
+    ~Provider();
+
+    bool init() final;
+    void loop() final;
+
+private:
+    void sendModbusRequest();
+    void parseModbusResponse();
+
+    uint32_t _lastRequest = 0;
+    PowerMeterUdpVictronConfig _cfg;
+};
+
+} // namespace PowerMeters::Udp::Victron
diff --git a/src/Configuration.cpp b/src/Configuration.cpp
index 500618854..d0fa06ea7 100644
--- a/src/Configuration.cpp
+++ b/src/Configuration.cpp
@@ -118,6 +118,12 @@ void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfi
     serializeHttpRequestConfig(source.HttpRequest, target);
 }
 
+void ConfigurationClass::serializePowerMeterUdpVictronConfig(PowerMeterUdpVictronConfig const& source, JsonObject& target)
+{
+    target["polling_interval_ms"] = source.PollingIntervalMs;
+    target["ip_address"] = IPAddress(source.IpAddress).toString();
+}
+
 void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, JsonObject& target)
 {
     target["enabled"] = config.Battery.Enabled;
@@ -361,6 +367,9 @@ bool ConfigurationClass::write()
     JsonObject powermeter_http_sml = powermeter["http_sml"].to<JsonObject>();
     serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
 
+    JsonObject powermeter_udp_victron = powermeter["udp_victron"].to<JsonObject>();
+    serializePowerMeterUdpVictronConfig(config.PowerMeter.UdpVictron, powermeter_udp_victron);
+
     JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
     serializePowerLimiterConfig(config.PowerLimiter, powerlimiter);
 
@@ -463,6 +472,17 @@ void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& so
     deserializeHttpRequestConfig(source["http_request"], target.HttpRequest);
 }
 
+void ConfigurationClass::deserializePowerMeterUdpVictronConfig(JsonObject const& source, PowerMeterUdpVictronConfig& target)
+{
+    target.PollingIntervalMs = source["polling_interval_ms"] | POWERMETER_POLLING_INTERVAL * 1000;
+    IPAddress ip;
+    ip.fromString(source["ip_address"] | "");
+    target.IpAddress[0] = ip[0];
+    target.IpAddress[1] = ip[1];
+    target.IpAddress[2] = ip[2];
+    target.IpAddress[3] = ip[3];
+}
+
 void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target)
 {
     target.Enabled = source["enabled"] | BATTERY_ENABLED;
@@ -742,6 +762,8 @@ bool ConfigurationClass::read()
 
     deserializePowerMeterHttpSmlConfig(powermeter["http_sml"], config.PowerMeter.HttpSml);
 
+    deserializePowerMeterUdpVictronConfig(powermeter["udp_victron"], config.PowerMeter.UdpVictron);
+
     deserializePowerLimiterConfig(doc["powerlimiter"], config.PowerLimiter);
 
     deserializeBatteryConfig(doc["battery"], config.Battery);
diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp
index 8ffbbc22b..2d1eaf895 100644
--- a/src/WebApi_powermeter.cpp
+++ b/src/WebApi_powermeter.cpp
@@ -54,6 +54,9 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
     auto httpSml = root["http_sml"].to<JsonObject>();
     Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml);
 
+    auto udpVictron = root["udp_victron"].to<JsonObject>();
+    Configuration.serializePowerMeterUdpVictronConfig(config.PowerMeter.UdpVictron, udpVictron);
+
     WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
 }
 
@@ -149,6 +152,25 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
         }
     }
 
+    if (static_cast<::PowerMeters::Provider::Type>(root["source"].as<uint8_t>()) == ::PowerMeters::Provider::Type::UDP_VICTRON) {
+        JsonObject udpVictron = root["udp_victron"];
+        if (!udpVictron["ip_address"].is<String>()
+                || udpVictron["ip_address"].as<String>().length() == 0) {
+            retMsg["message"] = "IP address must not be empty!";
+            response->setLength();
+            request->send(response);
+            return;
+        }
+
+        if (!udpVictron["polling_interval_ms"].is<uint32_t>()
+                || udpVictron["polling_interval_ms"].as<uint32_t>() <= 0) {
+            retMsg["message"] = "Polling interval must be greater than 0 ms!";
+            response->setLength();
+            request->send(response);
+            return;
+        }
+    }
+
     {
         auto guard = Configuration.getWriteGuard();
         auto& config = guard.getConfig();
@@ -167,6 +189,9 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
 
         Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as<JsonObject>(),
                 config.PowerMeter.HttpSml);
+
+        Configuration.deserializePowerMeterUdpVictronConfig(root["udp_victron"].as<JsonObject>(),
+                config.PowerMeter.UdpVictron);
     }
 
     WebApi.writeConfig(retMsg);
@@ -198,14 +223,16 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request
     auto upMeter = std::make_unique<::PowerMeters::Json::Http::Provider>(*powerMeterConfig);
     upMeter->init();
     auto res = upMeter->poll();
-    using values_t = ::PowerMeters::Json::Http::Provider::power_values_t;
+    using values_t = ::PowerMeters::DataPointContainer;
     if (std::holds_alternative<values_t>(res)) {
         retMsg["type"] = "success";
-        auto vals = std::get<values_t>(res);
-        auto pos = snprintf(response, sizeof(response), "Result: %5.2fW", vals[0]);
-        for (size_t i = 1; i < vals.size(); ++i) {
-            if (!powerMeterConfig->Values[i].Enabled) { continue; }
-            pos += snprintf(response + pos, sizeof(response) - pos, ", %5.2fW", vals[i]);
+        auto const& vals = std::get<values_t>(res);
+        auto iter = vals.cbegin();
+        auto pos = snprintf(response, sizeof(response), "Result: %sW", iter->second.getValueText().c_str());
+        ++iter;
+        while (iter != vals.cend()) {
+            pos += snprintf(response + pos, sizeof(response) - pos, ", %sW", iter->second.getValueText().c_str());
+            ++iter;
         }
         snprintf(response + pos, sizeof(response) - pos, ", Total: %5.2f", upMeter->getPowerTotal());
     } else {
diff --git a/src/powermeter/Controller.cpp b/src/powermeter/Controller.cpp
index 409108741..4d9448c38 100644
--- a/src/powermeter/Controller.cpp
+++ b/src/powermeter/Controller.cpp
@@ -7,6 +7,7 @@
 #include <powermeter/sml/http/Provider.h>
 #include <powermeter/sml/serial/Provider.h>
 #include <powermeter/udp/smahm/Provider.h>
+#include <powermeter/udp/victron/Provider.h>
 
 PowerMeters::Controller PowerMeter;
 
@@ -56,6 +57,9 @@ void Controller::updateSettings()
         case Provider::Type::HTTP_SML:
             _upProvider = std::make_unique<::PowerMeters::Sml::Http::Provider>(pmcfg.HttpSml);
             break;
+        case Provider::Type::UDP_VICTRON:
+            _upProvider = std::make_unique<::PowerMeters::Udp::Victron::Provider>(pmcfg.UdpVictron);
+            break;
     }
 
     if (!_upProvider->init()) {
@@ -91,6 +95,7 @@ void Controller::loop()
     _upProvider->loop();
 
     auto const& pmcfg = Configuration.get().PowerMeter;
+    // we don't need to republish data received from MQTT
     if (pmcfg.Source == static_cast<uint8_t>(Provider::Type::MQTT)) { return; }
     _upProvider->mqttLoop();
 }
diff --git a/src/powermeter/Provider.cpp b/src/powermeter/Provider.cpp
index 6c0bfeef7..c961ed03b 100644
--- a/src/powermeter/Provider.cpp
+++ b/src/powermeter/Provider.cpp
@@ -6,12 +6,17 @@ namespace PowerMeters {
 
 bool Provider::isDataValid() const
 {
-    return _lastUpdate > 0 && ((millis() - _lastUpdate) < (30 * 1000));
+    return getLastUpdate() > 0 && ((millis() - getLastUpdate()) < (30 * 1000));
 }
 
-void Provider::mqttPublish(String const& topic, float const& value) const
+float Provider::getPowerTotal() const
 {
-    MqttSettings.publish("powermeter/" + topic, String(value));
+    auto oPowerTotal = _dataCurrent.get<DataPointLabel::PowerTotal>();
+    if (oPowerTotal) { return *oPowerTotal; }
+
+    return _dataCurrent.get<DataPointLabel::PowerL1>().value_or(0.0f)
+        + _dataCurrent.get<DataPointLabel::PowerL2>().value_or(0.0f)
+        + _dataCurrent.get<DataPointLabel::PowerL3>().value_or(0.0f);
 }
 
 void Provider::mqttLoop() const
@@ -21,11 +26,31 @@ void Provider::mqttLoop() const
     if (!isDataValid()) { return; }
 
     auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
-    if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; }
-
-    mqttPublish("powertotal", getPowerTotal());
-
-    doMqttPublish();
+    if ((getLastUpdate() - _lastMqttPublish) > halfOfAllMillis) { return; }
+
+    // based on getPowerTotal() as we can not be sure that the PowerTotal value is set for all providers
+    MqttSettings.publish("powermeter/powertotal", String(getPowerTotal()));
+
+#define PUB(l, t) \
+    { \
+        auto oDataPoint = _dataCurrent.get<DataPointLabel::l>(); \
+        if (oDataPoint) { \
+            MqttSettings.publish("powermeter/" t, String(*oDataPoint)); \
+        } \
+    }
+
+    PUB(PowerL1, "power1");
+    PUB(PowerL2, "power2");
+    PUB(PowerL3, "power3");
+    PUB(VoltageL1, "voltage1");
+    PUB(VoltageL2, "voltage2");
+    PUB(VoltageL3, "voltage3");
+    PUB(CurrentL1, "current1");
+    PUB(CurrentL2, "current2");
+    PUB(CurrentL3, "current3");
+    PUB(Import, "import");
+    PUB(Export, "export");
+#undef PUB
 
     _lastMqttPublish = millis();
 }
diff --git a/src/powermeter/json/http/Provider.cpp b/src/powermeter/json/http/Provider.cpp
index 9ce349414..4724e3308 100644
--- a/src/powermeter/json/http/Provider.cpp
+++ b/src/powermeter/json/http/Provider.cpp
@@ -100,14 +100,11 @@ void Provider::pollingLoop()
         }
 
         MessageOutput.printf("[PowerMeters::Json::Http] New total: %.2f\r\n", getPowerTotal());
-
-        gotUpdate();
     }
 }
 
 Provider::poll_result_t Provider::poll()
 {
-    power_values_t cache;
     JsonDocument jsonResponse;
 
     auto prefixedError = [](uint8_t idx, char const* err) -> String {
@@ -120,7 +117,6 @@ Provider::poll_result_t Provider::poll()
         auto const& cfg = _cfg.Values[i];
 
         if (!cfg.Enabled) {
-            cache[i] = 0.0;
             continue;
         }
 
@@ -150,33 +146,43 @@ Provider::poll_result_t Provider::poll()
         }
 
         // this value is supposed to be in Watts and positive if energy is consumed
-        cache[i] = pathResolutionResult.first;
+        float newValue = pathResolutionResult.first;
 
         switch (cfg.PowerUnit) {
             case Unit_t::MilliWatts:
-                cache[i] /= 1000;
+                newValue /= 1000;
                 break;
             case Unit_t::KiloWatts:
-                cache[i] *= 1000;
+                newValue *= 1000;
                 break;
             default:
                 break;
         }
 
-        if (cfg.SignInverted) { cache[i] *= -1; }
-    }
+        if (cfg.SignInverted) { newValue *= -1; }
 
-    std::unique_lock<std::mutex> lock(_valueMutex);
-    _powerValues = cache;
-    return cache;
-}
+        {
+            auto scopedLock = _dataCurrent.lock();
+            switch (i) {
+                case 0:
+                    _dataCurrent.add<DataPointLabel::PowerL1>(newValue);
+                    break;
 
-float Provider::getPowerTotal() const
-{
-    float sum = 0.0;
-    std::unique_lock<std::mutex> lock(_valueMutex);
-    for (auto v: _powerValues) { sum += v; }
-    return sum;
+                case 1:
+                    _dataCurrent.add<DataPointLabel::PowerL2>(newValue);
+                    break;
+
+                case 2:
+                    _dataCurrent.add<DataPointLabel::PowerL3>(newValue);
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    }
+
+    return _dataCurrent;
 }
 
 bool Provider::isDataValid() const
@@ -185,12 +191,4 @@ bool Provider::isDataValid() const
     return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000));
 }
 
-void Provider::doMqttPublish() const
-{
-    std::unique_lock<std::mutex> lock(_valueMutex);
-    mqttPublish("power1", _powerValues[0]);
-    mqttPublish("power2", _powerValues[1]);
-    mqttPublish("power3", _powerValues[2]);
-}
-
 } // namespace PowerMeters::Json::Http
diff --git a/src/powermeter/json/mqtt/Provider.cpp b/src/powermeter/json/mqtt/Provider.cpp
index 26d373de2..9b38b9f91 100644
--- a/src/powermeter/json/mqtt/Provider.cpp
+++ b/src/powermeter/json/mqtt/Provider.cpp
@@ -9,8 +9,7 @@ namespace PowerMeters::Json::Mqtt {
 
 bool Provider::init()
 {
-    auto subscribe = [this](PowerMeterMqttValue const& val, float* targetVariable) {
-        *targetVariable = 0;
+    auto subscribe = [this](PowerMeterMqttValue const& val, uint8_t phaseIndex) {
         char const* topic = val.Topic;
         if (strlen(topic) == 0) { return; }
         MqttSettings.subscribe(topic, 0,
@@ -18,13 +17,13 @@ bool Provider::init()
                     this, std::placeholders::_1, std::placeholders::_2,
                     std::placeholders::_3, std::placeholders::_4,
                     std::placeholders::_5, std::placeholders::_6,
-                    targetVariable, &val)
+                    phaseIndex, &val)
                 );
         _mqttSubscriptions.push_back(topic);
     };
 
-    for (size_t i = 0; i < _powerValues.size(); ++i) {
-        subscribe(_cfg.Values[i], &_powerValues[i]);
+    for (uint8_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) {
+        subscribe(_cfg.Values[i], i);
     }
 
     return _mqttSubscriptions.size() > 0;
@@ -38,7 +37,7 @@ Provider::~Provider()
 
 void Provider::onMessage(Provider::MsgProperties const& properties,
         char const* topic, uint8_t const* payload, size_t len, size_t index,
-        size_t total, float* targetVariable, PowerMeterMqttValue const* cfg)
+        size_t total, uint8_t const phaseIndex, PowerMeterMqttValue const* cfg)
 {
     auto extracted = Utils::getNumericValueFromMqttPayload<float>("PowerMeters::Json::Mqtt",
             std::string(reinterpret_cast<const char*>(payload), len), topic,
@@ -63,24 +62,29 @@ void Provider::onMessage(Provider::MsgProperties const& properties,
     if (cfg->SignInverted) { newValue *= -1; }
 
     {
-        std::lock_guard<std::mutex> l(_mutex);
-        *targetVariable = newValue;
+        auto scopedLock = _dataCurrent.lock();
+        switch (phaseIndex) {
+            case 0:
+                _dataCurrent.add<DataPointLabel::PowerL1>(newValue);
+                break;
+
+            case 1:
+                _dataCurrent.add<DataPointLabel::PowerL2>(newValue);
+                break;
+
+            case 2:
+                _dataCurrent.add<DataPointLabel::PowerL3>(newValue);
+                break;
+
+            default:
+                break;
+        }
     }
 
     if (_verboseLogging) {
         MessageOutput.printf("[PowerMeters::Json::Mqtt] Topic '%s': new value: %5.2f, "
                 "total: %5.2f\r\n", topic, newValue, getPowerTotal());
     }
-
-    gotUpdate();
-}
-
-float Provider::getPowerTotal() const
-{
-    float sum = 0.0;
-    std::unique_lock<std::mutex> lock(_mutex);
-    for (auto v: _powerValues) { sum += v; }
-    return sum;
 }
 
 } // namespace PowerMeters::Json::Mqtt
diff --git a/src/powermeter/sdm/serial/Provider.cpp b/src/powermeter/sdm/serial/Provider.cpp
index 0cc438468..517f4c077 100644
--- a/src/powermeter/sdm/serial/Provider.cpp
+++ b/src/powermeter/sdm/serial/Provider.cpp
@@ -68,34 +68,12 @@ void Provider::loop()
             stackSize, this, 1/*prio*/, &_taskHandle);
 }
 
-float Provider::getPowerTotal() const
-{
-    std::lock_guard<std::mutex> l(_valueMutex);
-    return _phase1Power + _phase2Power + _phase3Power;
-}
-
 bool Provider::isDataValid() const
 {
     uint32_t age = millis() - getLastUpdate();
     return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000));
 }
 
-void Provider::doMqttPublish() const
-{
-    std::lock_guard<std::mutex> l(_valueMutex);
-    mqttPublish("power1", _phase1Power);
-    mqttPublish("voltage1", _phase1Voltage);
-    mqttPublish("import", _energyImport);
-    mqttPublish("export", _energyExport);
-
-    if (_phases == Phases::Three) {
-        mqttPublish("power2", _phase2Power);
-        mqttPublish("power3", _phase3Power);
-        mqttPublish("voltage2", _phase2Voltage);
-        mqttPublish("voltage3", _phase3Voltage);
-    }
-}
-
 void Provider::pollingLoopHelper(void* context)
 {
     auto pInstance = static_cast<Provider*>(context);
@@ -171,6 +149,7 @@ void Provider::pollingLoop()
         // reading takes a "very long" time as each readVal() is a synchronous
         // exchange of serial messages. cache the values and write later to
         // enforce consistent values.
+        std::optional<float> oTotalPower = std::nullopt;
         float phase1Power = 0.0;
         float phase2Power = 0.0;
         float phase3Power = 0.0;
@@ -180,6 +159,13 @@ void Provider::pollingLoop()
         float energyImport = 0.0;
         float energyExport = 0.0;
 
+        if (_phases == Phases::Three) {
+            float totalPower = 0.0;
+            if (readValue(lock, SDM_TOTAL_SYSTEM_POWER, totalPower)) {
+                oTotalPower = totalPower;
+            }
+        }
+
         bool success = readValue(lock, SDM_PHASE_1_POWER, phase1Power) &&
             readValue(lock, SDM_PHASE_1_VOLTAGE, phase1Voltage) &&
             readValue(lock, SDM_IMPORT_ACTIVE_ENERGY, energyImport) &&
@@ -195,20 +181,25 @@ void Provider::pollingLoop()
         if (!success) { continue; }
 
         {
-            std::lock_guard<std::mutex> l(_valueMutex);
-            _phase1Power = static_cast<float>(phase1Power);
-            _phase2Power = static_cast<float>(phase2Power);
-            _phase3Power = static_cast<float>(phase3Power);
-            _phase1Voltage = static_cast<float>(phase1Voltage);
-            _phase2Voltage = static_cast<float>(phase2Voltage);
-            _phase3Voltage = static_cast<float>(phase3Voltage);
-            _energyImport = static_cast<float>(energyImport);
-            _energyExport = static_cast<float>(energyExport);
+            auto scopedLock = _dataCurrent.lock();
+
+            _dataCurrent.add<DataPointLabel::PowerL1>(phase1Power);
+            _dataCurrent.add<DataPointLabel::VoltageL1>(phase1Voltage);
+            _dataCurrent.add<DataPointLabel::Import>(energyImport);
+            _dataCurrent.add<DataPointLabel::Export>(energyExport);
+
+            if (_phases == Phases::Three) {
+                if (oTotalPower.has_value()) {
+                    _dataCurrent.add<DataPointLabel::PowerTotal>(*oTotalPower);
+                }
+                _dataCurrent.add<DataPointLabel::PowerL2>(phase2Power);
+                _dataCurrent.add<DataPointLabel::PowerL3>(phase3Power);
+                _dataCurrent.add<DataPointLabel::VoltageL2>(phase2Voltage);
+                _dataCurrent.add<DataPointLabel::VoltageL3>(phase3Voltage);
+            }
         }
 
         MessageOutput.printf("[PowerMeters::Sdm::Serial] TotalPower: %5.2f\r\n", getPowerTotal());
-
-        gotUpdate();
     }
 }
 
diff --git a/src/powermeter/sml/Provider.cpp b/src/powermeter/sml/Provider.cpp
index 86cce573a..ce2c73da9 100644
--- a/src/powermeter/sml/Provider.cpp
+++ b/src/powermeter/sml/Provider.cpp
@@ -4,38 +4,10 @@
 
 namespace PowerMeters::Sml {
 
-float Provider::getPowerTotal() const
-{
-    std::lock_guard<std::mutex> l(_mutex);
-    if (_values.activePowerTotal.has_value()) { return *_values.activePowerTotal; }
-    return 0;
-}
-
-void Provider::doMqttPublish() const
-{
-#define PUB(t, m) \
-    if (_values.m.has_value()) { mqttPublish(t, *_values.m); }
-
-    std::lock_guard<std::mutex> l(_mutex);
-    PUB("power1", activePowerL1);
-    PUB("power2", activePowerL2);
-    PUB("power3", activePowerL3);
-    PUB("voltage1", voltageL1);
-    PUB("voltage2", voltageL2);
-    PUB("voltage3", voltageL3);
-    PUB("current1", currentL1);
-    PUB("current2", currentL2);
-    PUB("current3", currentL3);
-    PUB("import", energyImport);
-    PUB("export", energyExport);
-
-#undef PUB
-}
-
 void Provider::reset()
 {
     smlReset();
-    _cache = { std::nullopt };
+    _dataInFlight.clear();
 }
 
 void Provider::processSmlByte(uint8_t byte)
@@ -53,13 +25,63 @@ void Provider::processSmlByte(uint8_t byte)
                             _user.c_str(), handler.name, helper);
                 }
 
-                std::lock_guard<std::mutex> l(_mutex);
-                *handler.target = helper;
+                switch (handler.target)
+                {
+                case DataPointLabel::PowerTotal:
+                    _dataInFlight.add<DataPointLabel::PowerTotal>(helper);
+                    break;
+
+                case DataPointLabel::PowerL1:
+                    _dataInFlight.add<DataPointLabel::PowerL1>(helper);
+                    break;
+
+                case DataPointLabel::PowerL2:
+                    _dataInFlight.add<DataPointLabel::PowerL2>(helper);
+                    break;
+
+                case DataPointLabel::PowerL3:
+                    _dataInFlight.add<DataPointLabel::PowerL3>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL1:
+                    _dataInFlight.add<DataPointLabel::VoltageL1>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL2:
+                    _dataInFlight.add<DataPointLabel::VoltageL2>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL3:
+                    _dataInFlight.add<DataPointLabel::VoltageL3>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL1:
+                    _dataInFlight.add<DataPointLabel::CurrentL1>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL2:
+                    _dataInFlight.add<DataPointLabel::CurrentL2>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL3:
+                    _dataInFlight.add<DataPointLabel::CurrentL3>(helper);
+                    break;
+
+                case DataPointLabel::Import:
+                    _dataInFlight.add<DataPointLabel::Import>(helper);
+                    break;
+
+                case DataPointLabel::Export:
+                    _dataInFlight.add<DataPointLabel::Export>(helper);
+                    break;
+
+                default:
+                    break;
+                }
             }
             break;
         case SML_FINAL:
-            gotUpdate();
-            _values = _cache;
+            _dataCurrent.updateFrom(_dataInFlight);
             reset();
             MessageOutput.printf("[%s] TotalPower: %5.2f\r\n",
                     _user.c_str(), getPowerTotal());
diff --git a/src/powermeter/sml/http/Provider.cpp b/src/powermeter/sml/http/Provider.cpp
index cf41572b5..79a0ca09e 100644
--- a/src/powermeter/sml/http/Provider.cpp
+++ b/src/powermeter/sml/http/Provider.cpp
@@ -82,8 +82,6 @@ void Provider::pollingLoop()
             MessageOutput.printf("[PowerMeters::Sml::Http] %s\r\n", res.c_str());
             continue;
         }
-
-        gotUpdate();
     }
 }
 
diff --git a/src/powermeter/udp/smahm/Provider.cpp b/src/powermeter/udp/smahm/Provider.cpp
index 5df24801b..8663236ed 100644
--- a/src/powermeter/udp/smahm/Provider.cpp
+++ b/src/powermeter/udp/smahm/Provider.cpp
@@ -5,7 +5,7 @@
 #include <powermeter/udp/smahm/Provider.h>
 #include <Arduino.h>
 #include <WiFiUdp.h>
-#include "MessageOutput.h"
+#include <MessageOutput.h>
 
 namespace PowerMeters::Udp::SmaHM {
 
@@ -36,13 +36,6 @@ Provider::~Provider()
     SMAUdp.stop();
 }
 
-void Provider::doMqttPublish() const
-{
-    mqttPublish("power1", _powerMeterL1);
-    mqttPublish("power2", _powerMeterL2);
-    mqttPublish("power3", _powerMeterL3);
-}
-
 uint8_t* Provider::decodeGroup(uint8_t* offset, uint16_t grouplen)
 {
     float Pbezug = 0;
@@ -132,16 +125,24 @@ uint8_t* Provider::decodeGroup(uint8_t* offset, uint16_t grouplen)
             }
 
             if (count == 8) {
-                _powerMeterPower = Pbezug - Peinspeisung;
-                _powerMeterL1 = BezugL1 - EinspeisungL1;
-                _powerMeterL2 = BezugL2 - EinspeisungL2;
-                _powerMeterL3 = BezugL3 - EinspeisungL3;
-                Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp);
+                auto powerTotal = Pbezug - Peinspeisung;
+                auto powerL1 = BezugL1 - EinspeisungL1;
+                auto powerL2 = BezugL2 - EinspeisungL2;
+                auto powerL3 = BezugL3 - EinspeisungL3;
+
+                {
+                    auto scopedLock = _dataCurrent.lock();
+                    _dataCurrent.add<DataPointLabel::PowerTotal>(powerTotal);
+                    _dataCurrent.add<DataPointLabel::PowerL1>(powerL1);
+                    _dataCurrent.add<DataPointLabel::PowerL2>(powerL2);
+                    _dataCurrent.add<DataPointLabel::PowerL3>(powerL3);
+                }
+
+                Soutput(kanal, index, art, tarif, "Leistung", powerTotal, timestamp);
+                Soutput(kanal, index, art, tarif, "Leistung L1", powerL1, timestamp);
+                Soutput(kanal, index, art, tarif, "Leistung L2", powerL2, timestamp);
+                Soutput(kanal, index, art, tarif, "Leistung L3", powerL3, timestamp);
                 count = 0;
-                gotUpdate();
             }
 
             continue;
diff --git a/src/powermeter/udp/victron/Provider.cpp b/src/powermeter/udp/victron/Provider.cpp
new file mode 100644
index 000000000..3baac3ebd
--- /dev/null
+++ b/src/powermeter/udp/victron/Provider.cpp
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024 Holger-Steffen Stapf
+ */
+#include <powermeter/udp/victron/Provider.h>
+#include <Arduino.h>
+#include <WiFiUdp.h>
+#include <MessageOutput.h>
+
+namespace PowerMeters::Udp::Victron {
+
+static constexpr unsigned int modbusPort = 502;  // local port to listen on
+
+// we only send one request which spans all registers we want to read
+static constexpr uint16_t sTransactionId = 0xDEAD; // arbitrary value
+static constexpr uint8_t sUnitId = 0x01;
+static constexpr uint8_t sFunctionCode = 0x03; // read holding registers
+static constexpr uint16_t sRegisterAddress = 0x3032;
+static constexpr uint16_t sRegisterCount = 0x005A;
+
+static WiFiUDP VictronUdp;
+
+Provider::Provider(PowerMeterUdpVictronConfig const& cfg)
+    : _cfg(cfg)
+{
+}
+
+bool Provider::init()
+{
+    VictronUdp.begin(modbusPort);
+    return true;
+}
+
+Provider::~Provider()
+{
+    VictronUdp.stop();
+}
+
+void Provider::sendModbusRequest()
+{
+    auto interval = _cfg.PollingIntervalMs;
+
+    uint32_t currentMillis = millis();
+
+    if (currentMillis - _lastRequest < interval) { return; }
+
+    std::vector<uint8_t> payload;
+
+    payload.push_back(sTransactionId >> 8);
+    payload.push_back(sTransactionId & 0xFF);
+
+    // protocol ID
+    payload.push_back(0x00);
+    payload.push_back(0x00);
+
+    // length
+    payload.push_back(0x00);
+    payload.push_back(0x06);
+
+    payload.push_back(sUnitId);
+    payload.push_back(sFunctionCode);
+    payload.push_back(sRegisterAddress >> 8);
+    payload.push_back(sRegisterAddress & 0xFF);
+    payload.push_back(sRegisterCount >> 8);
+    payload.push_back(sRegisterCount & 0xFF);
+
+    VictronUdp.beginPacket(_cfg.IpAddress, modbusPort);
+    VictronUdp.write(payload.data(), payload.size());
+    VictronUdp.endPacket();
+
+    _lastRequest = currentMillis;
+}
+
+static float readInt16(uint8_t** buffer, uint8_t factor)
+{
+    uint8_t* p = *buffer;
+    int16_t value = (p[0] << 8) | p[1];
+    *buffer += 2;
+    return static_cast<float>(value) / factor;
+}
+
+static float readInt32(uint8_t** buffer, uint8_t factor)
+{
+    uint8_t* p = *buffer;
+    int32_t value = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
+    *buffer += 4;
+    return static_cast<float>(value) / factor;
+}
+
+static float readUint32(uint8_t** buffer, uint8_t factor)
+{
+    uint8_t* p = *buffer;
+    uint32_t value = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
+    *buffer += 4;
+    return static_cast<float>(value) / factor;
+}
+
+void Provider::parseModbusResponse()
+{
+    int packetSize = VictronUdp.parsePacket();
+    if (!packetSize) { return; }
+
+    uint8_t buffer[256];
+    VictronUdp.read(buffer, sizeof(buffer));
+
+    uint8_t* p = buffer;
+
+    if (_verboseLogging) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] received %d bytes:", packetSize);
+
+        for (int i = 0; i < packetSize; i++) {
+            if (i % 16 == 0) {
+                MessageOutput.print("\r\n");
+            }
+            MessageOutput.printf("%02X ", buffer[i]);
+        }
+
+        MessageOutput.print("\r\n");
+    }
+
+    uint16_t transactionId = (p[0] << 8) | p[1];
+    p += 2;
+
+    if (transactionId != sTransactionId) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] invalid transaction ID: %04X\r\n", transactionId);
+        return;
+    }
+
+    uint16_t protocolId = (p[0] << 8) | p[1];
+    p += 2;
+
+    if (protocolId != 0x0000) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] invalid protocol ID: %04X\r\n", protocolId);
+        return;
+    }
+
+    uint16_t length = (p[0] << 8) | p[1];
+    p += 2;
+
+    uint16_t expectedLength = (sRegisterCount * 2) + 3;
+    if (length != expectedLength) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] unexpected length: %04X, "
+            "expected %04X\r\n", length, expectedLength);
+        return;
+    }
+
+    uint8_t unitId = p[0];
+    p += 1;
+
+    if (unitId != sUnitId) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] unexpected unit ID: %02X, "
+            "expected %02X\r\n", unitId, sUnitId);
+        return;
+    }
+
+    uint8_t functionCode = p[0];
+    p += 1;
+
+    if (functionCode != sFunctionCode) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] unexpected function code: %02X, "
+            "expected %02X\r\n", functionCode, sFunctionCode);
+        return;
+    }
+
+    uint8_t byteCount = p[0];
+    p += 1;
+
+    uint8_t expectedByteCount = sRegisterCount * 2;
+    if (byteCount != expectedByteCount) {
+        MessageOutput.printf("[PowerMeters::Udp::Victron] unexpected byte count: %02X, "
+            "expected %02X\r\n", byteCount, expectedByteCount);
+        return;
+    }
+
+    using Label = ::PowerMeters::DataPointLabel;
+
+    auto scopedLock = _dataCurrent.lock();
+
+    p += 2; // skip register 0x3032 (AC frequency)
+    p += 2; // skip register 0x3033 (PEN voltage)
+
+    _dataCurrent.add<Label::Import>(readUint32(&p, 100)); // 0x3034f
+    _dataCurrent.add<Label::Export>(readUint32(&p, 100)); // 0x3036f
+    p += 16; // jump to register 0x3040
+    _dataCurrent.add<Label::VoltageL1>(readInt16(&p, 100)); // 0x3040
+    _dataCurrent.add<Label::CurrentL1>(readInt16(&p, 100)); // 0x3041
+    p += 12; // jump to register 0x3048
+    _dataCurrent.add<Label::VoltageL2>(readInt16(&p, 100)); // 0x3048
+    _dataCurrent.add<Label::CurrentL2>(readInt16(&p, 100)); // 0x3049
+    p += 12; // jump to register 0x3050
+    _dataCurrent.add<Label::VoltageL3>(readInt16(&p, 100)); // 0x3050
+    _dataCurrent.add<Label::CurrentL3>(readInt16(&p, 100)); // 0x3051
+    p += 92; // jump from 0x3052 to 0x3080 (0x2E registers = 92 bytes)
+    _dataCurrent.add<Label::PowerTotal>(readInt32(&p, 1)); // 0x3080f
+    _dataCurrent.add<Label::PowerL1>(readInt32(&p, 1)); // 0x3082f
+    p += 4; // jump to 0x3086
+    _dataCurrent.add<Label::PowerL2>(readInt32(&p, 1)); // 0x3086f
+    p += 4; // jump to 0x308A
+    _dataCurrent.add<Label::PowerL3>(readInt32(&p, 1)); // 0x308Af
+}
+
+void Provider::loop()
+{
+    sendModbusRequest();
+    parseModbusResponse();
+}
+
+} // namespace PowerMeters::Udp::Victron
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json
index 9fa003f3c..95be0ed24 100644
--- a/webapp/src/locales/de.json
+++ b/webapp/src/locales/de.json
@@ -633,11 +633,13 @@
         "typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)",
         "typeSMAHM2": "SMA Homemanager 2.0",
         "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)",
+        "typeUDP_VICTRON": "Victron VM-3P75CT (Modbus UDP)",
         "MqttValue": "Konfiguration Wert {valueNumber}",
         "MqttTopic": "MQTT Topic",
         "mqttJsonPath": "Optional: JSON-Pfad",
         "SDM": "SDM-Stromzähler Konfiguration",
         "sdmaddress": "Modbus Adresse",
+        "ipAddress": "IP-Adresse",
         "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration",
         "httpIndividualRequests": "Individuelle HTTP(S) Anfragen pro Wert",
         "urlExamplesHeading": "Beispiele für URLs",
@@ -654,7 +656,8 @@
         "testHttpJsonRequest": "HTTP(S)-Anfrage(n) senden und Antwort(en) verarbeiten",
         "testHttpSmlHeader": "Konfiguration testen",
         "testHttpSmlRequest": "HTTP(S)-Anfrage senden und Antwort verarbeiten",
-        "HTTP_SML": "HTTP(S) + SML - Konfiguration"
+        "HTTP_SML": "HTTP(S) + SML - Konfiguration",
+        "UDP_VICTRON": "Victron VM-3P75CT (Modbus UDP) - Konfiguration"
     },
     "httprequestsettings": {
         "url": "URL",
diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json
index 289b21732..e951f6b2a 100644
--- a/webapp/src/locales/en.json
+++ b/webapp/src/locales/en.json
@@ -635,11 +635,13 @@
         "typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)",
         "typeSMAHM2": "SMA Homemanager 2.0",
         "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)",
+        "typeUDP_VICTRON": "Victron VM-3P75CT (Modbus UDP)",
         "MqttValue": "Value {valueNumber} Configuration",
         "mqttJsonPath": "Optional: JSON Path",
         "MqttTopic": "MQTT Topic",
         "SDM": "SDM-Power Meter Parameter",
         "sdmaddress": "Modbus Address",
+        "ipAddress": "IP Address",
         "HTTP": "HTTP(S) + JSON - General configuration",
         "httpIndividualRequests": "Individual HTTP(S) requests per value",
         "urlExamplesHeading": "URL Examples",
@@ -656,7 +658,8 @@
         "testHttpJsonRequest": "Send HTTP(S) request(s) and process response(s)",
         "testHttpSmlHeader": "Test Configuration",
         "testHttpSmlRequest": "Send HTTP(S) request and process response",
-        "HTTP_SML": "Configuration"
+        "HTTP_SML": "Configuration",
+        "UDP_VICTRON": "Configuration"
     },
     "httprequestsettings": {
         "url": "URL",
diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts
index 111da3cb0..8d7053e22 100644
--- a/webapp/src/types/PowerMeterConfig.ts
+++ b/webapp/src/types/PowerMeterConfig.ts
@@ -35,6 +35,11 @@ export interface PowerMeterHttpSmlConfig {
     http_request: HttpRequestConfig;
 }
 
+export interface PowerMeterUdpVictronConfig {
+    polling_interval_ms: number;
+    ip_address: string;
+}
+
 export interface PowerMeterConfig {
     enabled: boolean;
     verbose_logging: boolean;
@@ -44,4 +49,5 @@ export interface PowerMeterConfig {
     serial_sdm: PowerMeterSerialSdmConfig;
     http_json: PowerMeterHttpJsonConfig;
     http_sml: PowerMeterHttpSmlConfig;
+    udp_victron: PowerMeterUdpVictronConfig;
 }
diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue
index 554791567..6779e2a13 100644
--- a/webapp/src/views/PowerMeterAdminView.vue
+++ b/webapp/src/views/PowerMeterAdminView.vue
@@ -277,6 +277,29 @@
                         </BootstrapAlert>
                     </CardElement>
                 </template>
+
+                <template v-if="powerMeterConfigList.source === 7">
+                    <CardElement :text="$t('powermeteradmin.UDP_VICTRON')" textVariant="text-bg-primary" add-space>
+                        <InputElement
+                            :label="$t('powermeteradmin.pollingInterval')"
+                            v-model="udpVictronPollIntervalSeconds"
+                            type="number"
+                            min="0.5"
+                            max="15.0"
+                            step="0.1"
+                            :postfix="$t('powermeteradmin.seconds')"
+                            wide
+                        />
+
+                        <InputElement
+                            :label="$t('powermeteradmin.ipAddress')"
+                            v-model="powerMeterConfigList.udp_victron.ip_address"
+                            type="text"
+                            maxlength="15"
+                            wide
+                        />
+                    </CardElement>
+                </template>
             </template>
 
             <FormFooter @reload="getPowerMeterConfig" />
@@ -316,6 +339,7 @@ export default defineComponent({
                 { key: 4, value: this.$t('powermeteradmin.typeSML') },
                 { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') },
                 { key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') },
+                { key: 7, value: this.$t('powermeteradmin.typeUDP_VICTRON') },
             ],
             unitTypeList: [
                 { key: 1, value: 'mW' },
@@ -340,6 +364,16 @@ export default defineComponent({
     created() {
         this.getPowerMeterConfig();
     },
+    computed: {
+        udpVictronPollIntervalSeconds: {
+            get(): number {
+                return this.powerMeterConfigList.udp_victron.polling_interval_ms / 1000;
+            },
+            set(value: number) {
+                this.powerMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
+            },
+        },
+    },
     methods: {
         getPowerMeterConfig() {
             this.dataLoading = true;


Reviews:
user: login: schlimmchen
body: It seems to me that you broke thread-safety.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-11T21:15:54Z
user: login: schlimmchen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T08:10:26Z
user: login: schlimmchen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T08:10:30Z
user: login: schlimmchen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T08:10:37Z
user: login: schlimmchen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T19:48:38Z

Review comments:
diff_hunk: @@ -18,26 +18,17 @@ class Provider : public ::PowerMeters::Provider {
     ~Provider();
 
     bool init() final;
-    void loop() final { }
-    float getPowerTotal() const final;
+    void loop() final { };
path: include/powermeter/json/mqtt/Provider.h
user: login: schlimmchen
body: spurious semicolon.
created_at: 2025-03-11T20:33:21Z
author_association: MEMBER
diff_hunk: @@ -62,25 +61,32 @@ void Provider::onMessage(Provider::MsgProperties const& properties,
 
     if (cfg->SignInverted) { newValue *= -1; }
 
+    auto dataInFlight = DataPointContainer();
+
+    switch (phaseIndex)
     {
-        std::lock_guard<std::mutex> l(_mutex);
-        *targetVariable = newValue;
+    case 0:
+        dataInFlight.add<DataPointLabel::PowerL1>(newValue);
+        break;
+
+    case 1:
+        dataInFlight.add<DataPointLabel::PowerL2>(newValue);
+        break;
+
+    case 2:
+        dataInFlight.add<DataPointLabel::PowerL3>(newValue);
+        break;
+
+    default:
+        break;
     }
 
+    _dataCurrent.updateFrom(dataInFlight);
+
path: src/powermeter/json/mqtt/Provider.cpp
user: login: schlimmchen
body: I don't see why we need `dataInFlight`. Add the new value to `_dataCurrent` directly.
created_at: 2025-03-11T20:44:47Z
author_association: MEMBER
diff_hunk: @@ -50,16 +22,66 @@ void Provider::processSmlByte(uint8_t byte)
 
                 if (_verboseLogging) {
                     MessageOutput.printf("[%s] decoded %s to %.2f\r\n",
-                            _user.c_str(), handler.name, helper);
+                            _user.c_str(), "", helper); // TODO(andreasboehm): get name via label from traits
path: src/powermeter/sml/Provider.cpp
user: login: schlimmchen
body: Nope, unfortunately not. This is one of the reasons I desperately want to have the code generation going, which will also generate stuff like this which is available at runtime. You won't be able to catch the label name using the traits here without a switch-case over `handler.name`, as the label is assigned to a variable and not used as a type here...

As a workaround, you should add the text to the handler struct and out a "TODO" there: "remove and use runtime info on labels once available"
created_at: 2025-03-11T20:51:59Z
author_association: MEMBER
diff_hunk: @@ -50,16 +22,66 @@ void Provider::processSmlByte(uint8_t byte)
 
                 if (_verboseLogging) {
                     MessageOutput.printf("[%s] decoded %s to %.2f\r\n",
-                            _user.c_str(), handler.name, helper);
+                            _user.c_str(), "", helper); // TODO(andreasboehm): get name via label from traits
                 }
 
-                std::lock_guard<std::mutex> l(_mutex);
-                *handler.target = helper;
+                switch (handler.target)
+                {
+                case DataPointLabel::PowerTotal:
+                    _dataInFlight.add<DataPointLabel::PowerTotal>(helper);
+                    break;
+
+                case DataPointLabel::PowerL1:
+                    _dataInFlight.add<DataPointLabel::PowerL1>(helper);
+                    break;
+
+                case DataPointLabel::PowerL2:
+                    _dataInFlight.add<DataPointLabel::PowerL2>(helper);
+                    break;
+
+                case DataPointLabel::PowerL3:
+                    _dataInFlight.add<DataPointLabel::PowerL3>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL1:
+                    _dataInFlight.add<DataPointLabel::VoltageL1>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL2:
+                    _dataInFlight.add<DataPointLabel::VoltageL2>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL3:
+                    _dataInFlight.add<DataPointLabel::VoltageL3>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL1:
+                    _dataInFlight.add<DataPointLabel::CurrentL1>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL2:
+                    _dataInFlight.add<DataPointLabel::CurrentL2>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL3:
+                    _dataInFlight.add<DataPointLabel::CurrentL3>(helper);
+                    break;
+
+                case DataPointLabel::Import:
+                    _dataInFlight.add<DataPointLabel::Import>(helper);
+                    break;
+
+                case DataPointLabel::Export:
+                    _dataInFlight.add<DataPointLabel::Export>(helper);
+                    break;
+
+                default:
+                    break;
+                }
             }
             break;
         case SML_FINAL:
-            gotUpdate();
-            _values = _cache;
+            _dataCurrent.updateFrom(_dataInFlight);
path: src/powermeter/sml/Provider.cpp
user: login: schlimmchen
body: Again, I don't see why you shouldn't add the data points to the final container directly.
created_at: 2025-03-11T20:52:59Z
author_association: MEMBER
diff_hunk: @@ -132,16 +125,23 @@ uint8_t* Provider::decodeGroup(uint8_t* offset, uint16_t grouplen)
             }
 
             if (count == 8) {
-                _powerMeterPower = Pbezug - Peinspeisung;
-                _powerMeterL1 = BezugL1 - EinspeisungL1;
-                _powerMeterL2 = BezugL2 - EinspeisungL2;
-                _powerMeterL3 = BezugL3 - EinspeisungL3;
-                Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp);
+                auto powerTotal = Pbezug - Peinspeisung;
+                auto powerL1 = BezugL1 - EinspeisungL1;
+                auto powerL2 = BezugL2 - EinspeisungL2;
+                auto powerL3 = BezugL3 - EinspeisungL3;
+
+                auto dataInFlight = DataPointContainer();
+                dataInFlight.add<DataPointLabel::PowerTotal>(powerTotal);
+                dataInFlight.add<DataPointLabel::PowerL1>(powerL1);
+                dataInFlight.add<DataPointLabel::PowerL2>(powerL2);
+                dataInFlight.add<DataPointLabel::PowerL3>(powerL3);
+                _dataCurrent.updateFrom(dataInFlight);
path: src/powermeter/udp/smahm/Provider.cpp
user: login: schlimmchen
body: This is the most obvious of them all, I guess. What made you chose to use a temporary container here?
created_at: 2025-03-11T21:08:10Z
author_association: MEMBER
diff_hunk: @@ -43,9 +40,6 @@ class Provider : public ::PowerMeters::Provider {
 
     uint32_t _lastPoll = 0;
 
-    mutable std::mutex _valueMutex;
path: include/powermeter/json/http/Provider.h
user: login: schlimmchen
body: I am confused that you remove multiple mutexes without adding anything in this PR to re-establish thread-safety. Am I missing something? I think thread-safety is a neat feature for DataPointContainers, so let's make the whole thing thread-safe. Should I assist?
created_at: 2025-03-11T21:11:25Z
author_association: MEMBER
diff_hunk: @@ -132,16 +125,23 @@ uint8_t* Provider::decodeGroup(uint8_t* offset, uint16_t grouplen)
             }
 
             if (count == 8) {
-                _powerMeterPower = Pbezug - Peinspeisung;
-                _powerMeterL1 = BezugL1 - EinspeisungL1;
-                _powerMeterL2 = BezugL2 - EinspeisungL2;
-                _powerMeterL3 = BezugL3 - EinspeisungL3;
-                Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp);
-                Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp);
+                auto powerTotal = Pbezug - Peinspeisung;
+                auto powerL1 = BezugL1 - EinspeisungL1;
+                auto powerL2 = BezugL2 - EinspeisungL2;
+                auto powerL3 = BezugL3 - EinspeisungL3;
+
+                auto dataInFlight = DataPointContainer();
+                dataInFlight.add<DataPointLabel::PowerTotal>(powerTotal);
+                dataInFlight.add<DataPointLabel::PowerL1>(powerL1);
+                dataInFlight.add<DataPointLabel::PowerL2>(powerL2);
+                dataInFlight.add<DataPointLabel::PowerL3>(powerL3);
+                _dataCurrent.updateFrom(dataInFlight);
path: src/powermeter/udp/smahm/Provider.cpp
user: login: schlimmchen
body: I understand now: 9dac7d31 Now that updating values in-place is possible, let's do that instead of using a temporary container just to be able to use `updateFrom()`.
created_at: 2025-03-19T08:10:25Z
author_association: MEMBER
diff_hunk: @@ -50,16 +22,66 @@ void Provider::processSmlByte(uint8_t byte)
 
                 if (_verboseLogging) {
                     MessageOutput.printf("[%s] decoded %s to %.2f\r\n",
-                            _user.c_str(), handler.name, helper);
+                            _user.c_str(), "", helper); // TODO(andreasboehm): get name via label from traits
                 }
 
-                std::lock_guard<std::mutex> l(_mutex);
-                *handler.target = helper;
+                switch (handler.target)
+                {
+                case DataPointLabel::PowerTotal:
+                    _dataInFlight.add<DataPointLabel::PowerTotal>(helper);
+                    break;
+
+                case DataPointLabel::PowerL1:
+                    _dataInFlight.add<DataPointLabel::PowerL1>(helper);
+                    break;
+
+                case DataPointLabel::PowerL2:
+                    _dataInFlight.add<DataPointLabel::PowerL2>(helper);
+                    break;
+
+                case DataPointLabel::PowerL3:
+                    _dataInFlight.add<DataPointLabel::PowerL3>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL1:
+                    _dataInFlight.add<DataPointLabel::VoltageL1>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL2:
+                    _dataInFlight.add<DataPointLabel::VoltageL2>(helper);
+                    break;
+
+                case DataPointLabel::VoltageL3:
+                    _dataInFlight.add<DataPointLabel::VoltageL3>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL1:
+                    _dataInFlight.add<DataPointLabel::CurrentL1>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL2:
+                    _dataInFlight.add<DataPointLabel::CurrentL2>(helper);
+                    break;
+
+                case DataPointLabel::CurrentL3:
+                    _dataInFlight.add<DataPointLabel::CurrentL3>(helper);
+                    break;
+
+                case DataPointLabel::Import:
+                    _dataInFlight.add<DataPointLabel::Import>(helper);
+                    break;
+
+                case DataPointLabel::Export:
+                    _dataInFlight.add<DataPointLabel::Export>(helper);
+                    break;
+
+                default:
+                    break;
+                }
             }
             break;
         case SML_FINAL:
-            gotUpdate();
-            _values = _cache;
+            _dataCurrent.updateFrom(_dataInFlight);
path: src/powermeter/sml/Provider.cpp
user: login: schlimmchen
body: I understand now: 9dac7d31 Now that updating values in-place is possible, let's do that instead of using a temporary container just to be able to use `updateFrom()`.
created_at: 2025-03-19T08:10:30Z
author_association: MEMBER
diff_hunk: @@ -62,25 +61,32 @@ void Provider::onMessage(Provider::MsgProperties const& properties,
 
     if (cfg->SignInverted) { newValue *= -1; }
 
+    auto dataInFlight = DataPointContainer();
+
+    switch (phaseIndex)
     {
-        std::lock_guard<std::mutex> l(_mutex);
-        *targetVariable = newValue;
+    case 0:
+        dataInFlight.add<DataPointLabel::PowerL1>(newValue);
+        break;
+
+    case 1:
+        dataInFlight.add<DataPointLabel::PowerL2>(newValue);
+        break;
+
+    case 2:
+        dataInFlight.add<DataPointLabel::PowerL3>(newValue);
+        break;
+
+    default:
+        break;
     }
 
+    _dataCurrent.updateFrom(dataInFlight);
+
path: src/powermeter/json/mqtt/Provider.cpp
user: login: schlimmchen
body: I understand now: 9dac7d31 Now that updating values in-place is possible, let's do that instead of using a temporary container just to be able to use `updateFrom()`.
created_at: 2025-03-19T08:10:37Z
author_association: MEMBER
diff_hunk: @@ -21,11 +26,31 @@ void Provider::mqttLoop() const
     if (!isDataValid()) { return; }
 
     auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
-    if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; }
-
-    mqttPublish("powertotal", getPowerTotal());
-
-    doMqttPublish();
+    if ((getLastUpdate() - _lastMqttPublish) > halfOfAllMillis) { return; }
+
+    // based on getPowerTotal() as we can not be sure that the PowerTotal value is set for all providers
+    MqttSettings.publish("powermeter/powerTotal", String(getPowerTotal()));
path: src/powermeter/Provider.cpp
user: login: schlimmchen
body: This is a regression: `powerTotal != powertotal`.
created_at: 2025-03-19T19:48:38Z
author_association: MEMBER

PR comments:
user: login: schlimmchen
created_at: 2025-02-18T18:50:01Z
body: Man, this is awesome :heart: As the power meters basically all provide the same stuff, we finally have a common baseline for all of them. There is one catch: If one providers delivers values that are unique to it, there has to be a data label for it even though only it uses the value. I think that should be fine.

> I also renamed 'Values' to 'Phases' int the web ui to make this change kind of visible.

That I actually don't like... For one that we are mixing concerns/features/changes. More importantly, I changed the name from "phases" to "values" for a reason: They are only values, the readings are not always associated with phases. Example: We had the guy who has a power meter which delivers a power value in one JSON path when importing energy, and in another path when exporting energy. We told him to configure two values, one for each JSON path/value, with the one inverted.

Why do you think the usability improves when the wording is "phases"?
author_association: MEMBER
user: login: AndreasBoehm
created_at: 2025-02-18T18:53:39Z
body: It felt better for me to setup phases instead of values. Thinking about it, the only real usecase is that if we want to show L1, L2, L3 power in the liveview with exactly those labels it will match with the configuration view, but besides that there is actually no need to call it phases.

Let me take back that change.
author_association: MEMBER
user: login: AndreasBoehm
created_at: 2025-02-18T18:57:24Z
body: Values are back 🔙 
author_association: MEMBER
user: login: schlimmchen
created_at: 2025-03-19T08:11:31Z
body: I added 9dac7d31 to this PR/branch and will now rebase it onto current development branch.
author_association: MEMBER
user: login: cerise21
created_at: 2025-03-20T14:55:23Z
body: Das SDM630 habe ich mit https://github.com/hoylabs/OpenDTU-OnBattery/actions/runs/13955474338 getestet. Die Zählerwerte kommen im eingestellten Intervall (2-3s) bei den Live-Daten an. 
Ab und zu kommen für manche Register CRC-Fehler oder timeouts beim Lesen; aber das hat wahrscheinlich mit meiner Installation zu tun.
author_association: NONE
user: login: schlimmchen
created_at: 2025-03-20T19:52:29Z
body: Danke für die Rückmeldung!

Und CRC Fehler erkennen bzw. Timeouts sind doch "okay", hauptsache keinen Müll dekodieren.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-20T04:12:14Z
body: This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion or issue for related concerns.
author_association: NONE