From 7dcd4e2c04aae33c6e2543bc24c1ecd45673fc5a Mon Sep 17 00:00:00 2001 From: Arman Vartan Date: Mon, 26 Feb 2024 01:27:13 +0100 Subject: [PATCH 1/2] Feature: Support for second Victron MPPT charge controller this change adds support for a second Victron MPPT charge controller using a second serial connection. * Add device configuration for a second victron mppt * Update VedirectView for second victron mppt * Update MqttHandleVedirect for second victron mppt * Update MqttHandleVedirectHass for second victron mppt * Handle nonexisting victron controllers with optionals * Add bool-function to Battery and inherited classes, if uart port 2 is being used * Introduced a serial port manager. In order to prevent the battery and the Victron MPPT to use the same hw serial ports, this class keeps track of the used ports and their owners. --- include/Battery.h | 33 ++-- include/Configuration.h | 8 +- include/JkBmsController.h | 1 + include/MqttBattery.h | 33 ++-- include/MqttHandleVedirect.h | 8 +- include/MqttHandleVedirectHass.h | 14 +- include/PinMapping.h | 4 +- include/PylontechCanReceiver.h | 1 + include/SerialPortManager.h | 27 +++ include/VictronMppt.h | 6 +- include/VictronSmartShunt.h | 1 + include/WebApi_ws_vedirect_live.h | 6 +- .../VeDirectMpptController.cpp | 4 +- .../VeDirectMpptController.h | 4 +- src/Battery.cpp | 21 ++- src/JkBmsController.cpp | 4 + src/MqttBattery.cpp | 4 + src/MqttHandlVedirectHass.cpp | 91 +++++---- src/MqttHandleVedirect.cpp | 178 ++++++++++-------- src/PinMapping.cpp | 9 +- src/PylontechCanReceiver.cpp | 4 + src/SerialPortManager.cpp | 59 ++++++ src/VictronMppt.cpp | 53 ++++-- src/VictronSmartShunt.cpp | 4 + src/WebApi_device.cpp | 4 +- src/WebApi_ws_vedirect_live.cpp | 47 +++-- webapp/src/components/VedirectView.vue | 54 +++--- webapp/src/types/VedirectLiveDataStatus.ts | 14 +- 28 files changed, 458 insertions(+), 238 deletions(-) create mode 100644 include/SerialPortManager.h create mode 100644 src/SerialPortManager.cpp diff --git a/include/Battery.h b/include/Battery.h index b5f5ace63..2947dc9f6 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include #include #include #include @@ -9,28 +8,28 @@ #include "BatteryStats.h" class BatteryProvider { - public: - // returns true if the provider is ready for use, false otherwise - virtual bool init(bool verboseLogging) = 0; - - virtual void deinit() = 0; - virtual void loop() = 0; - virtual std::shared_ptr getStats() const = 0; +public: + // returns true if the provider is ready for use, false otherwise + virtual bool init(bool verboseLogging) = 0; + virtual void deinit() = 0; + virtual void loop() = 0; + virtual std::shared_ptr getStats() const = 0; + virtual bool usesHwPort2() = 0; }; class BatteryClass { - public: - void init(Scheduler&); - void updateSettings(); +public: + void init(Scheduler&); + void updateSettings(); - std::shared_ptr getStats() const; - private: - void loop(); + std::shared_ptr getStats() const; - Task _loopTask; +private: + void loop(); - mutable std::mutex _mutex; - std::unique_ptr _upProvider = nullptr; + Task _loopTask; + mutable std::mutex _mutex; + std::unique_ptr _upProvider = nullptr; }; extern BatteryClass Battery; diff --git a/include/Configuration.h b/include/Configuration.h index 14a056670..daa77916d 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -30,6 +30,8 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 +#define VICTRON_MAX_COUNT 2 + #define POWERMETER_MAX_PHASES 3 #define POWERMETER_MAX_HTTP_URL_STRLEN 1024 #define POWERMETER_MAX_USERNAME_STRLEN 64 @@ -198,7 +200,7 @@ struct CONFIG_T { bool HttpIndividualRequests; POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; } PowerMeter; - + struct { bool Enabled; bool VerboseLogging; @@ -225,7 +227,7 @@ struct CONFIG_T { float FullSolarPassThroughStartVoltage; float FullSolarPassThroughStopVoltage; } PowerLimiter; - + struct { bool Enabled; bool VerboseLogging; @@ -243,7 +245,7 @@ struct CONFIG_T { float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; - float Auto_Power_Upper_Power_Limit; + float Auto_Power_Upper_Power_Limit; } Huawei; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index 5399951d4..4221f9f4a 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,6 +19,7 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: enum class Status : unsigned { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 61df04500..e75e38f3c 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -5,23 +5,24 @@ #include class MqttBattery : public BatteryProvider { - public: - MqttBattery() = default; +public: + MqttBattery() = default; - bool init(bool verboseLogging) final; - void deinit() final; - void loop() final { return; } // this class is event-driven - std::shared_ptr getStats() const final { return _stats; } + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final { return; } // this class is event-driven + std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; - private: - bool _verboseLogging = false; - String _socTopic; - String _voltageTopic; - std::shared_ptr _stats = std::make_shared(); +private: + bool _verboseLogging = false; + String _socTopic; + String _voltageTopic; + std::shared_ptr _stats = std::make_shared(); - std::optional getFloat(std::string const& src, char const* topic); - void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); - void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + std::optional getFloat(std::string const& src, char const* topic); + void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); }; diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 571ee1e6a..c420d0884 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -4,6 +4,7 @@ #include "VeDirectMpptController.h" #include "Configuration.h" #include +#include #include #ifndef VICTRON_PIN_RX @@ -20,7 +21,7 @@ class MqttHandleVedirectClass { void forceUpdate(); private: void loop(); - VeDirectMpptController::veMpptStruct _kvFrame{}; + std::map _kvFrames; Task _loopTask; @@ -31,6 +32,9 @@ class MqttHandleVedirectClass { uint32_t _nextPublishFull = 1; bool _PublishFull; + + void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const; }; -extern MqttHandleVedirectClass MqttHandleVedirect; \ No newline at end of file +extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 577f08d6a..86d364cda 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -14,9 +14,15 @@ class MqttHandleVedirectHassClass { private: void loop(); void publish(const String& subtopic, const String& payload); - void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off); - void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); - void createDeviceInfo(JsonObject& object); + void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData); + void publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData); + void createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData); Task _loopTask; @@ -24,4 +30,4 @@ class MqttHandleVedirectHassClass { bool _updateForced = false; }; -extern MqttHandleVedirectHassClass MqttHandleVedirectHass; \ No newline at end of file +extern MqttHandleVedirectHassClass MqttHandleVedirectHass; diff --git a/include/PinMapping.h b/include/PinMapping.h index 4096e8072..6197b5a45 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -40,6 +40,8 @@ struct PinMapping_t { uint8_t display_reset; int8_t victron_tx; int8_t victron_rx; + int8_t victron_tx2; + int8_t victron_rx2; int8_t battery_rx; int8_t battery_rxen; int8_t battery_tx; @@ -63,7 +65,7 @@ class PinMappingClass { bool isValidCmt2300Config() const; bool isValidEthConfig() const; bool isValidHuaweiConfig() const; - + private: PinMapping_t _pinMapping; }; diff --git a/include/PylontechCanReceiver.h b/include/PylontechCanReceiver.h index 2b2b922d9..7b6404253 100644 --- a/include/PylontechCanReceiver.h +++ b/include/PylontechCanReceiver.h @@ -14,6 +14,7 @@ class PylontechCanReceiver : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: uint16_t readUnsignedInt16(uint8_t *data); diff --git a/include/SerialPortManager.h b/include/SerialPortManager.h new file mode 100644 index 000000000..495e70141 --- /dev/null +++ b/include/SerialPortManager.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class SerialPortManager { +public: + bool allocateMpptPort(int port); + bool allocateBatteryPort(int port); + void invalidateBatteryPort(); + void invalidateMpptPorts(); + +private: + enum Owner { + BATTERY, + MPPT + }; + + std::map allocatedPorts; + + bool allocatePort(uint8_t port, Owner owner); + void invalidate(Owner owner); + + static const char* print(Owner owner); +}; + +extern SerialPortManager PortManager; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 12d6bdf75..8ce4e8dea 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -5,6 +5,7 @@ #include #include "VeDirectMpptController.h" +#include "Configuration.h" #include class VictronMpptClass { @@ -16,12 +17,13 @@ class VictronMpptClass { void updateSettings(); bool isDataValid() const; + bool isDataValid(size_t idx) const; // returns the data age of all controllers, // i.e, the youngest data's age is returned. uint32_t getDataAgeMillis() const; - VeDirectMpptController::spData_t getData(size_t idx = 0) const; + std::optional getData(size_t idx = 0) const; // total output of all MPPT charge controllers in Watts int32_t getPowerOutputWatts() const; @@ -50,6 +52,8 @@ class VictronMpptClass { mutable std::mutex _mutex; using controller_t = std::unique_ptr; std::vector _controllers; + + bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort); }; extern VictronMpptClass VictronMppt; diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index ffb91ee5b..0c5c5d89f 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,6 +9,7 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: uint32_t _lastUpdate = 0; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 3e0b81aba..3b41aa79f 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -2,6 +2,7 @@ #pragma once #include "ArduinoJson.h" +#include "Configuration.h" #include #include #include @@ -14,6 +15,7 @@ class WebApiWsVedirectLiveClass { private: void generateJsonResponse(JsonVariant& root); + static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); @@ -22,7 +24,7 @@ class WebApiWsVedirectLiveClass { uint32_t _lastWsPublish = 0; uint32_t _dataAgeMillis = 0; - static constexpr uint16_t _responseSize = 1024 + 128; + static constexpr uint16_t _responseSize = VICTRON_MAX_COUNT * (1024 + 128); std::mutex _mutex; @@ -31,4 +33,4 @@ class WebApiWsVedirectLiveClass { Task _sendDataTask; void sendDataTaskCb(); -}; \ No newline at end of file +}; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 5b8d6afd7..4112510f4 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,9 +1,9 @@ #include #include "VeDirectMpptController.h" -void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1); + VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort); _spData = std::make_shared(); if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 158772373..08574252d 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -39,7 +39,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; - void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); + void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); bool isDataValid() const; // return true if data valid and not outdated struct veMpptStruct : veStruct { @@ -49,7 +49,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { double VPV; // panel voltage in V double IPV; // panel current in A (calculated) bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e. g. OFF or Bulk + uint8_t CS; // current state of operation e.g. OFF or Bulk uint8_t ERR; // error code uint32_t OR; // off reason uint32_t HSDS; // day sequence number 1...365 diff --git a/src/Battery.cpp b/src/Battery.cpp index 381fdc952..0ea2b979f 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,6 +5,7 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" +#include "SerialPortManager.h" BatteryClass Battery; @@ -38,6 +39,7 @@ void BatteryClass::updateSettings() _upProvider->deinit(); _upProvider = nullptr; } + PortManager.invalidateBatteryPort(); CONFIG_T& config = Configuration.get(); if (!config.Battery.Enabled) { return; } @@ -47,23 +49,32 @@ void BatteryClass::updateSettings() switch (config.Battery.Provider) { case 0: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 1: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 2: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 3: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; default: MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider); - break; + return; + } + + if(_upProvider->usesHwPort2()) { + if (!PortManager.allocateBatteryPort(2)) { + MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2); + _upProvider = nullptr; + return; + } + } + + if (!_upProvider->init(verboseLogging)) { + PortManager.invalidateBatteryPort(); + _upProvider = nullptr; } } diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 3f924030f..381b1a8eb 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -427,4 +427,8 @@ void Controller::processDataPoints(DataPointContainer const& dataPoints) } } +bool Controller::usesHwPort2() { + return true; +} + } /* namespace JkBms */ diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 03e141e2f..0ce073fe9 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -112,3 +112,7 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con *voltage, topic); } } + +bool MqttBattery::usesHwPort2() { + return false; +} diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index b176e9410..e3e0655dc 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -7,7 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" -#include "VictronMppt.h" +#include "VictronMppt.h" #include "Utils.h" MqttHandleVedirectHassClass MqttHandleVedirectHass; @@ -15,7 +15,7 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass; void MqttHandleVedirectHassClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); } @@ -55,43 +55,56 @@ void MqttHandleVedirectHassClass::publishConfig() if (!MqttSettings.getConnected()) { return; } - // ensure data is revieved from victron - if (!VictronMppt.isDataValid()) { - return; - } // device info - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF"); - publishSensor("MPPT serial number", "mdi:counter", "SER"); - publishSensor("MPPT firmware number", "mdi:counter", "FW"); - publishSensor("MPPT state of operation", "mdi:wrench", "CS"); - publishSensor("MPPT error code", "mdi:bell", "ERR"); - publishSensor("MPPT off reason", "mdi:wrench", "OR"); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT"); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d"); - - // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V"); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A"); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W"); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%"); - - // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V"); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A"); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W"); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh"); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh"); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W"); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh"); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W"); + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + // ensure data is received from victron + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData); + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData); + + // battery info + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData); + + // panel info + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData); + } yield(); } -void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement ) +void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -126,7 +139,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; @@ -143,9 +156,11 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off) +void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -178,16 +193,16 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) +void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData) { - auto spMpptData = VictronMppt.getData(); String serial = spMpptData->SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index af659f0d7..b363493f2 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -17,7 +17,7 @@ MqttHandleVedirectClass MqttHandleVedirect; void MqttHandleVedirectClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -41,10 +41,6 @@ void MqttHandleVedirectClass::loop() return; } - if (!VictronMppt.isDataValid()) { - return; - } - if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) { // determine if this cycle should publish full values or updates only if (_nextPublishFull <= _nextPublishUpdatesOnly) { @@ -62,82 +58,23 @@ void MqttHandleVedirectClass::loop() } #endif - auto spMpptData = VictronMppt.getData(); - String value; - String topic = "victron/"; - topic.concat(spMpptData->SER); - topic.concat("/"); - - if (_PublishFull || spMpptData->PID != _kvFrame.PID) - MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); - if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0) - MqttSettings.publish(topic + "SER", spMpptData->SER ); - if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0) - MqttSettings.publish(topic + "FW", spMpptData->FW); - if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD) - MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF"); - if (_PublishFull || spMpptData->CS != _kvFrame.CS) - MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); - if (_PublishFull || spMpptData->ERR != _kvFrame.ERR) - MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); - if (_PublishFull || spMpptData->OR != _kvFrame.OR) - MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); - if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT) - MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); - if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) { - value = spMpptData->HSDS; - MqttSettings.publish(topic + "HSDS", value); - } - if (_PublishFull || spMpptData->V != _kvFrame.V) { - value = spMpptData->V; - MqttSettings.publish(topic + "V", value); - } - if (_PublishFull || spMpptData->I != _kvFrame.I) { - value = spMpptData->I; - MqttSettings.publish(topic + "I", value); - } - if (_PublishFull || spMpptData->P != _kvFrame.P) { - value = spMpptData->P; - MqttSettings.publish(topic + "P", value); - } - if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) { - value = spMpptData->VPV; - MqttSettings.publish(topic + "VPV", value); - } - if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) { - value = spMpptData->IPV; - MqttSettings.publish(topic + "IPV", value); - } - if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) { - value = spMpptData->PPV; - MqttSettings.publish(topic + "PPV", value); - } - if (_PublishFull || spMpptData->E != _kvFrame.E) { - value = spMpptData->E; - MqttSettings.publish(topic + "E", value); - } - if (_PublishFull || spMpptData->H19 != _kvFrame.H19) { - value = spMpptData->H19; - MqttSettings.publish(topic + "H19", value); - } - if (_PublishFull || spMpptData->H20 != _kvFrame.H20) { - value = spMpptData->H20; - MqttSettings.publish(topic + "H20", value); - } - if (_PublishFull || spMpptData->H21 != _kvFrame.H21) { - value = spMpptData->H21; - MqttSettings.publish(topic + "H21", value); - } - if (_PublishFull || spMpptData->H22 != _kvFrame.H22) { - value = spMpptData->H22; - MqttSettings.publish(topic + "H22", value); - } - if (_PublishFull || spMpptData->H23 != _kvFrame.H23) { - value = spMpptData->H23; - MqttSettings.publish(topic + "H23", value); - } - if (!_PublishFull) { - _kvFrame = *spMpptData; + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; + publish_mppt_data(spMpptData, _kvFrame); + if (!_PublishFull) { + _kvFrames[spMpptData->SER] = *spMpptData; + } } // now calculate next points of time to publish @@ -165,4 +102,81 @@ void MqttHandleVedirectClass::loop() MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull); #endif } -} \ No newline at end of file +} + +void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const { + String value; + String topic = "victron/"; + topic.concat(spMpptData->SER); + topic.concat("/"); + + if (_PublishFull || spMpptData->PID != frame.PID) + MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); + if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0) + MqttSettings.publish(topic + "SER", spMpptData->SER ); + if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0) + MqttSettings.publish(topic + "FW", spMpptData->FW); + if (_PublishFull || spMpptData->LOAD != frame.LOAD) + MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF"); + if (_PublishFull || spMpptData->CS != frame.CS) + MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); + if (_PublishFull || spMpptData->ERR != frame.ERR) + MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); + if (_PublishFull || spMpptData->OR != frame.OR) + MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); + if (_PublishFull || spMpptData->MPPT != frame.MPPT) + MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); + if (_PublishFull || spMpptData->HSDS != frame.HSDS) { + value = spMpptData->HSDS; + MqttSettings.publish(topic + "HSDS", value); + } + if (_PublishFull || spMpptData->V != frame.V) { + value = spMpptData->V; + MqttSettings.publish(topic + "V", value); + } + if (_PublishFull || spMpptData->I != frame.I) { + value = spMpptData->I; + MqttSettings.publish(topic + "I", value); + } + if (_PublishFull || spMpptData->P != frame.P) { + value = spMpptData->P; + MqttSettings.publish(topic + "P", value); + } + if (_PublishFull || spMpptData->VPV != frame.VPV) { + value = spMpptData->VPV; + MqttSettings.publish(topic + "VPV", value); + } + if (_PublishFull || spMpptData->IPV != frame.IPV) { + value = spMpptData->IPV; + MqttSettings.publish(topic + "IPV", value); + } + if (_PublishFull || spMpptData->PPV != frame.PPV) { + value = spMpptData->PPV; + MqttSettings.publish(topic + "PPV", value); + } + if (_PublishFull || spMpptData->E != frame.E) { + value = spMpptData->E; + MqttSettings.publish(topic + "E", value); + } + if (_PublishFull || spMpptData->H19 != frame.H19) { + value = spMpptData->H19; + MqttSettings.publish(topic + "H19", value); + } + if (_PublishFull || spMpptData->H20 != frame.H20) { + value = spMpptData->H20; + MqttSettings.publish(topic + "H20", value); + } + if (_PublishFull || spMpptData->H21 != frame.H21) { + value = spMpptData->H21; + MqttSettings.publish(topic + "H21", value); + } + if (_PublishFull || spMpptData->H22 != frame.H22) { + value = spMpptData->H22; + MqttSettings.publish(topic + "H22", value); + } + if (_PublishFull || spMpptData->H23 != frame.H23) { + value = spMpptData->H23; + MqttSettings.publish(topic + "H23", value); + } +} diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index bf78e4ede..39eeca13b 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -181,9 +181,12 @@ PinMappingClass::PinMappingClass() _pinMapping.display_clk = DISPLAY_CLK; _pinMapping.display_cs = DISPLAY_CS; _pinMapping.display_reset = DISPLAY_RESET; - - _pinMapping.victron_tx = VICTRON_PIN_TX; + _pinMapping.victron_rx = VICTRON_PIN_RX; + _pinMapping.victron_tx = VICTRON_PIN_TX; + + _pinMapping.victron_rx2 = VICTRON_PIN_RX; + _pinMapping.victron_tx2 = VICTRON_PIN_TX; _pinMapping.battery_rx = BATTERY_PIN_RX; _pinMapping.battery_rxen = BATTERY_PIN_RXEN; @@ -259,6 +262,8 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; + _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; + _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index e19cff599..6f18fb4d8 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -266,6 +266,10 @@ bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit) return (value & (1 << bit)) >> bit; } +bool PylontechCanReceiver::usesHwPort2() { + return false; +} + #ifdef PYLONTECH_DUMMY void PylontechCanReceiver::dummyData() { diff --git a/src/SerialPortManager.cpp b/src/SerialPortManager.cpp new file mode 100644 index 000000000..e5db3395f --- /dev/null +++ b/src/SerialPortManager.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SerialPortManager.h" +#include "MessageOutput.h" + +#define MAX_CONTROLLERS 3 + +SerialPortManager PortManager; + +bool SerialPortManager::allocateBatteryPort(int port) +{ + return allocatePort(port, Owner::BATTERY); +} + +bool SerialPortManager::allocateMpptPort(int port) +{ + return allocatePort(port, Owner::MPPT); +} + +bool SerialPortManager::allocatePort(uint8_t port, Owner owner) +{ + if (port >= MAX_CONTROLLERS) { + MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port); + return false; + } + + return allocatedPorts.insert({port, owner}).second; +} + +void SerialPortManager::invalidateBatteryPort() +{ + invalidate(Owner::BATTERY); +} + +void SerialPortManager::invalidateMpptPorts() +{ + invalidate(Owner::MPPT); +} + +void SerialPortManager::invalidate(Owner owner) +{ + for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) { + if (it->second == owner) { + MessageOutput.printf("[SerialPortManager] Removing port = %d, owner = %s \r\n", it->first, print(owner)); + it = allocatedPorts.erase(it); + } else { + ++it; + } + } +} + +const char* SerialPortManager::print(Owner owner) +{ + switch (owner) { + case BATTERY: + return "BATTERY"; + case MPPT: + return "MPPT"; + } +} diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index c4dd0bd5a..fec0f67d3 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -3,13 +3,14 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" +#include "SerialPortManager.h" VictronMpptClass VictronMppt; void VictronMpptClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&VictronMpptClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -21,24 +22,41 @@ void VictronMpptClass::updateSettings() std::lock_guard lock(_mutex); _controllers.clear(); + PortManager.invalidateMpptPorts(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); - int8_t rx = pin.victron_rx; - int8_t tx = pin.victron_tx; - MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx); + int hwSerialPort = 1; + bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort); + if (initSuccess) { + hwSerialPort++; + } + + initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort); +} + +bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort) +{ + MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort); if (rx < 0) { - MessageOutput.println("[VictronMppt] invalid pin config"); - return; + MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx); + return false; + } + + if (!PortManager.allocateMpptPort(hwSerialPort)) { + MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n", + hwSerialPort); + return false; } auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging); + upController->init(rx, tx, &MessageOutput, logging, hwSerialPort); _controllers.push_back(std::move(upController)); + return true; } void VictronMpptClass::loop() @@ -54,13 +72,24 @@ bool VictronMpptClass::isDataValid() const { std::lock_guard lock(_mutex); - for (auto const& upController : _controllers) { + for (auto const& upController: _controllers) { if (!upController->isDataValid()) { return false; } } return !_controllers.empty(); } +bool VictronMpptClass::isDataValid(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { + return false; + } + + return _controllers[idx]->isDataValid(); +} + uint32_t VictronMpptClass::getDataAgeMillis() const { std::lock_guard lock(_mutex); @@ -81,17 +110,17 @@ uint32_t VictronMpptClass::getDataAgeMillis() const return age; } -VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const +std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", - idx, _controllers.size()); - return std::make_shared(); + idx, _controllers.size()); + return std::nullopt; } - return _controllers[idx]->getData(); + return std::optional{_controllers[idx]->getData()}; } int32_t VictronMpptClass::getPowerOutputWatts() const diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index 7b6da145a..6a3047763 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -34,3 +34,7 @@ void VictronSmartShunt::loop() _stats->updateFrom(VeDirectShunt.veFrame); _lastUpdate = VeDirectShunt.getLastUpdate(); } + +bool VictronSmartShunt::usesHwPort2() { + return true; +} diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index cc08dfaab..9ab8d4fa2 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -86,9 +86,11 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) led["brightness"] = config.Led_Single[i].Brightness; } - JsonObject victronPinObj = curPin.createNestedObject("victron"); + auto victronPinObj = curPin.createNestedObject("victron"); victronPinObj["rx"] = pin.victron_rx; victronPinObj["tx"] = pin.victron_tx; + victronPinObj["rx2"] = pin.victron_rx2; + victronPinObj["tx2"] = pin.victron_tx2; JsonObject batteryPinObj = curPin.createNestedObject("battery"); batteryPinObj["rx"] = pin.battery_rx; diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index df59172e8..cf598c88f 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -32,7 +32,7 @@ void WebApiWsVedirectLiveClass::init(AsyncWebServer& server, Scheduler& schedule _server->addHandler(&_ws); _ws.onEvent(std::bind(&WebApiWsVedirectLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - + scheduler.addTask(_wsCleanupTask); _wsCleanupTask.setCallback(std::bind(&WebApiWsVedirectLiveClass::wsCleanupTaskCb, this)); _wsCleanupTask.setIterations(TASK_FOREVER); @@ -61,13 +61,13 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() // we assume this loop to be running at least twice for every // update from a VE.Direct MPPT data producer, so _dataAgeMillis - // acutally grows in between updates. + // actually grows in between updates. auto lastDataAgeMillis = _dataAgeMillis; _dataAgeMillis = VictronMppt.getDataAgeMillis(); // Update on ve.direct change or at least after 10 seconds if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { - + try { std::lock_guard lock(_mutex); DynamicJsonDocument root(_responseSize); @@ -77,7 +77,7 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() String buffer; serializeJson(root, buffer); - + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { @@ -99,15 +99,36 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) { - auto spMpptData = VictronMppt.getData(); + root["vedirect"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; + const JsonArray &array = root["vedirect"].createNestedArray("devices"); + + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + const JsonObject &nested = array.createNestedObject(); + nested["age_critical"] = !VictronMppt.isDataValid(idx); + populateJson(nested, spMpptData); + } + + // power limiter state + root["dpl"]["PLSTATE"] = -1; + if (Configuration.get().PowerLimiter.Enabled) + root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); + root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); +} +void +WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { // device info - root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; - root["device"]["age_critical"] = !VictronMppt.isDataValid(); root["device"]["PID"] = spMpptData->getPidAsString(); root["device"]["SER"] = spMpptData->SER; root["device"]["FW"] = spMpptData->FW; - root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF"; + root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF"; root["device"]["CS"] = spMpptData->getCsAsString(); root["device"]["ERR"] = spMpptData->getErrAsString(); root["device"]["OR"] = spMpptData->getOrAsString(); @@ -115,7 +136,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["device"]["HSDS"]["v"] = spMpptData->HSDS; root["device"]["HSDS"]["u"] = "d"; - // battery info + // battery info root["output"]["P"]["v"] = spMpptData->P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; @@ -154,12 +175,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; - - // power limiter state - root["dpl"]["PLSTATE"] = -1; - if (Configuration.get().PowerLimiter.Enabled) - root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); - root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) @@ -199,4 +214,4 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); WebApi.sendTooManyRequests(request); } -} \ No newline at end of file +} diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 215ce80de..004911558 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -9,25 +9,25 @@ @@ -178,7 +178,7 @@ \ No newline at end of file + diff --git a/webapp/src/types/VedirectLiveDataStatus.ts b/webapp/src/types/VedirectLiveDataStatus.ts index f6635c4c3..c3117a07b 100644 --- a/webapp/src/types/VedirectLiveDataStatus.ts +++ b/webapp/src/types/VedirectLiveDataStatus.ts @@ -5,12 +5,22 @@ export interface DynamicPowerLimiter { PLLIMIT: number; } +export interface Vedirect { + data_age: 0; + devices: Array; +} + +export interface VedirectDevices { + age_critical: boolean; + device: VedirectDevice; + input: VedirectInput; + output: VedirectOutput; +} + export interface VedirectDevice { SER: string; PID: string; FW: string; - age_critical: boolean; - data_age: 0; LOAD: ValueObject; CS: ValueObject; MPPT: ValueObject; From c584184e56a80c2bd1f6c87d3dad31757e305f9f Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 16 Mar 2024 22:22:39 +0100 Subject: [PATCH 2/2] polish support for second VE.Direct MPPT charge controller * fix compiler warning in SerialPortManager.cpp: function must not return void * clean up and simplify implementation of usesHwPort2() * make const * overrides are final * default implementation returns false * implement in header, as the implementation is very simple * rename PortManager to SerialPortManager. as "PortManager" is too generic, the static instance of the serial port manager is renamed to "SerialPortManager". the class is therefore renamed to SerialPortManagerClass, which is in line with other (static) classes withing OpenDTU(-OnBattery). * implement separate data ages for MPPT charge controllers * make sure MPPT data and live data time out * do not use invalid data of MPPT controlers for calculations * add :key binding to v-for iterating over MPPT instances --- include/Battery.h | 2 +- include/JkBmsController.h | 2 +- include/MqttBattery.h | 1 - include/PylontechCanReceiver.h | 1 - include/SerialPortManager.h | 4 +- include/VictronMppt.h | 1 + include/VictronSmartShunt.h | 2 +- include/WebApi_ws_vedirect_live.h | 6 +-- .../VeDirectFrameHandler.cpp | 8 +-- src/Battery.cpp | 6 +-- src/JkBmsController.cpp | 4 -- src/MqttBattery.cpp | 4 -- src/PylontechCanReceiver.cpp | 4 -- src/SerialPortManager.cpp | 17 ++++--- src/VictronMppt.cpp | 18 ++++++- src/VictronSmartShunt.cpp | 4 -- src/WebApi_ws_vedirect_live.cpp | 50 +++++++++++-------- webapp/src/components/VedirectView.vue | 46 +++++++++++------ webapp/src/types/VedirectLiveDataStatus.ts | 10 ++-- 19 files changed, 103 insertions(+), 87 deletions(-) diff --git a/include/Battery.h b/include/Battery.h index 2947dc9f6..700c46c45 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -14,7 +14,7 @@ class BatteryProvider { virtual void deinit() = 0; virtual void loop() = 0; virtual std::shared_ptr getStats() const = 0; - virtual bool usesHwPort2() = 0; + virtual bool usesHwPort2() const { return false; } }; class BatteryClass { diff --git a/include/JkBmsController.h b/include/JkBmsController.h index 4221f9f4a..b21744d3f 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,7 +19,7 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() override; + bool usesHwPort2() const final { return true; } private: enum class Status : unsigned { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index e75e38f3c..7948019e3 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -12,7 +12,6 @@ class MqttBattery : public BatteryProvider { void deinit() final; void loop() final { return; } // this class is event-driven std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() override; private: bool _verboseLogging = false; diff --git a/include/PylontechCanReceiver.h b/include/PylontechCanReceiver.h index 7b6404253..2b2b922d9 100644 --- a/include/PylontechCanReceiver.h +++ b/include/PylontechCanReceiver.h @@ -14,7 +14,6 @@ class PylontechCanReceiver : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() override; private: uint16_t readUnsignedInt16(uint8_t *data); diff --git a/include/SerialPortManager.h b/include/SerialPortManager.h index 495e70141..4d4c2dabb 100644 --- a/include/SerialPortManager.h +++ b/include/SerialPortManager.h @@ -3,7 +3,7 @@ #include -class SerialPortManager { +class SerialPortManagerClass { public: bool allocateMpptPort(int port); bool allocateBatteryPort(int port); @@ -24,4 +24,4 @@ class SerialPortManager { static const char* print(Owner owner); }; -extern SerialPortManager PortManager; +extern SerialPortManagerClass SerialPortManager; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 8ce4e8dea..6e3cfdee1 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -22,6 +22,7 @@ class VictronMpptClass { // returns the data age of all controllers, // i.e, the youngest data's age is returned. uint32_t getDataAgeMillis() const; + uint32_t getDataAgeMillis(size_t idx) const; std::optional getData(size_t idx = 0) const; diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index 0c5c5d89f..42b65774e 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,7 +9,7 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() override; + bool usesHwPort2() const final { return true; } private: uint32_t _lastUpdate = 0; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 3b41aa79f..de3c9d6a7 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -14,7 +14,7 @@ class WebApiWsVedirectLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); + void generateJsonResponse(JsonVariant& root, bool fullUpdate); static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); @@ -22,8 +22,8 @@ class WebApiWsVedirectLiveClass { AsyncWebServer* _server; AsyncWebSocket _ws; - uint32_t _lastWsPublish = 0; - uint32_t _dataAgeMillis = 0; + uint32_t _lastFullPublish = 0; + uint32_t _dataAgeMillis[VICTRON_MAX_COUNT] = { 0 }; static constexpr uint16_t _responseSize = VICTRON_MAX_COUNT * (1024 + 128); std::mutex _mutex; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 8047fd125..64c1e5df0 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -281,13 +281,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { } bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { - if (_lastUpdate == 0) { - return false; - } - if (strlen(frame.SER) == 0) { - return false; - } - return true; + return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); } uint32_t VeDirectFrameHandler::getLastUpdate() const diff --git a/src/Battery.cpp b/src/Battery.cpp index 0ea2b979f..de05b03db 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -39,7 +39,7 @@ void BatteryClass::updateSettings() _upProvider->deinit(); _upProvider = nullptr; } - PortManager.invalidateBatteryPort(); + SerialPortManager.invalidateBatteryPort(); CONFIG_T& config = Configuration.get(); if (!config.Battery.Enabled) { return; } @@ -65,7 +65,7 @@ void BatteryClass::updateSettings() } if(_upProvider->usesHwPort2()) { - if (!PortManager.allocateBatteryPort(2)) { + if (!SerialPortManager.allocateBatteryPort(2)) { MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2); _upProvider = nullptr; return; @@ -73,7 +73,7 @@ void BatteryClass::updateSettings() } if (!_upProvider->init(verboseLogging)) { - PortManager.invalidateBatteryPort(); + SerialPortManager.invalidateBatteryPort(); _upProvider = nullptr; } } diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 381b1a8eb..3f924030f 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -427,8 +427,4 @@ void Controller::processDataPoints(DataPointContainer const& dataPoints) } } -bool Controller::usesHwPort2() { - return true; -} - } /* namespace JkBms */ diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 0ce073fe9..03e141e2f 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -112,7 +112,3 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con *voltage, topic); } } - -bool MqttBattery::usesHwPort2() { - return false; -} diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index 6f18fb4d8..e19cff599 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -266,10 +266,6 @@ bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit) return (value & (1 << bit)) >> bit; } -bool PylontechCanReceiver::usesHwPort2() { - return false; -} - #ifdef PYLONTECH_DUMMY void PylontechCanReceiver::dummyData() { diff --git a/src/SerialPortManager.cpp b/src/SerialPortManager.cpp index e5db3395f..c4eb8514a 100644 --- a/src/SerialPortManager.cpp +++ b/src/SerialPortManager.cpp @@ -4,19 +4,19 @@ #define MAX_CONTROLLERS 3 -SerialPortManager PortManager; +SerialPortManagerClass SerialPortManager; -bool SerialPortManager::allocateBatteryPort(int port) +bool SerialPortManagerClass::allocateBatteryPort(int port) { return allocatePort(port, Owner::BATTERY); } -bool SerialPortManager::allocateMpptPort(int port) +bool SerialPortManagerClass::allocateMpptPort(int port) { return allocatePort(port, Owner::MPPT); } -bool SerialPortManager::allocatePort(uint8_t port, Owner owner) +bool SerialPortManagerClass::allocatePort(uint8_t port, Owner owner) { if (port >= MAX_CONTROLLERS) { MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port); @@ -26,17 +26,17 @@ bool SerialPortManager::allocatePort(uint8_t port, Owner owner) return allocatedPorts.insert({port, owner}).second; } -void SerialPortManager::invalidateBatteryPort() +void SerialPortManagerClass::invalidateBatteryPort() { invalidate(Owner::BATTERY); } -void SerialPortManager::invalidateMpptPorts() +void SerialPortManagerClass::invalidateMpptPorts() { invalidate(Owner::MPPT); } -void SerialPortManager::invalidate(Owner owner) +void SerialPortManagerClass::invalidate(Owner owner) { for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) { if (it->second == owner) { @@ -48,7 +48,7 @@ void SerialPortManager::invalidate(Owner owner) } } -const char* SerialPortManager::print(Owner owner) +const char* SerialPortManagerClass::print(Owner owner) { switch (owner) { case BATTERY: @@ -56,4 +56,5 @@ const char* SerialPortManager::print(Owner owner) case MPPT: return "MPPT"; } + return "unknown"; } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index fec0f67d3..e39cf3aad 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -22,7 +22,7 @@ void VictronMpptClass::updateSettings() std::lock_guard lock(_mutex); _controllers.clear(); - PortManager.invalidateMpptPorts(); + SerialPortManager.invalidateMpptPorts(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } @@ -47,7 +47,7 @@ bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hw return false; } - if (!PortManager.allocateMpptPort(hwSerialPort)) { + if (!SerialPortManager.allocateMpptPort(hwSerialPort)) { MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n", hwSerialPort); return false; @@ -110,6 +110,15 @@ uint32_t VictronMpptClass::getDataAgeMillis() const return age; } +uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { return 0; } + + return millis() - _controllers[idx]->getLastUpdate(); +} + std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); @@ -128,6 +137,7 @@ int32_t VictronMpptClass::getPowerOutputWatts() const int32_t sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->P; } @@ -139,6 +149,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const int32_t sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->PPV; } @@ -150,6 +161,7 @@ double VictronMpptClass::getYieldTotal() const double sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->H19; } @@ -161,6 +173,7 @@ double VictronMpptClass::getYieldDay() const double sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->H20; } @@ -172,6 +185,7 @@ double VictronMpptClass::getOutputVoltage() const double min = -1; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } double volts = upController->getData()->V; if (min == -1) { min = volts; } min = std::min(min, volts); diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index 6a3047763..7b6da145a 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -34,7 +34,3 @@ void VictronSmartShunt::loop() _stats->updateFrom(VeDirectShunt.veFrame); _lastUpdate = VeDirectShunt.getLastUpdate(); } - -bool VictronSmartShunt::usesHwPort2() { - return true; -} diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index cf598c88f..b3bde5248 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -55,25 +55,28 @@ void WebApiWsVedirectLiveClass::wsCleanupTaskCb() void WebApiWsVedirectLiveClass::sendDataTaskCb() { // do nothing if no WS client is connected - if (_ws.count() == 0) { - return; - } - - // we assume this loop to be running at least twice for every - // update from a VE.Direct MPPT data producer, so _dataAgeMillis - // actually grows in between updates. - auto lastDataAgeMillis = _dataAgeMillis; - _dataAgeMillis = VictronMppt.getDataAgeMillis(); + if (_ws.count() == 0) { return; } // Update on ve.direct change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { + bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000)); + bool updateAvailable = false; + if (!fullUpdate) { + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + auto currentAgeMillis = VictronMppt.getDataAgeMillis(idx); + if (currentAgeMillis > 0 && currentAgeMillis < _dataAgeMillis[idx]) { + updateAvailable = true; + break; + } + } + } + if (fullUpdate || updateAvailable) { try { std::lock_guard lock(_mutex); DynamicJsonDocument root(_responseSize); if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { JsonVariant var = root; - generateJsonResponse(var); + generateJsonResponse(var, fullUpdate); String buffer; serializeJson(root, buffer); @@ -92,15 +95,17 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() } catch (const std::exception& exc) { MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); } + } - _lastWsPublish = millis(); + if (fullUpdate) { + _lastFullPublish = millis(); } } -void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool fullUpdate) { - root["vedirect"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; - const JsonArray &array = root["vedirect"].createNestedArray("devices"); + const JsonObject &array = root["vedirect"].createNestedObject("instances"); + root["vedirect"]["full_update"] = fullUpdate; for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { std::optional spOptMpptData = VictronMppt.getData(idx); @@ -108,10 +113,16 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) continue; } + auto lastDataAgeMillis = _dataAgeMillis[idx]; + _dataAgeMillis[idx] = VictronMppt.getDataAgeMillis(idx); + bool validAge = _dataAgeMillis[idx] > 0; + bool updateAvailable = _dataAgeMillis[idx] < lastDataAgeMillis; + if (!fullUpdate && !(validAge && updateAvailable)) { continue; } + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - const JsonObject &nested = array.createNestedObject(); - nested["age_critical"] = !VictronMppt.isDataValid(idx); + const JsonObject &nested = array.createNestedObject(spMpptData->SER); + nested["data_age_ms"] = _dataAgeMillis[idx]; populateJson(nested, spMpptData); } @@ -122,8 +133,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } -void -WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { +void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { // device info root["device"]["PID"] = spMpptData->getPidAsString(); root["device"]["SER"] = spMpptData->SER; @@ -202,7 +212,7 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize); auto& root = response->getRoot(); - generateJsonResponse(root); + generateJsonResponse(root, true/*fullUpdate*/); response->setLength(); request->send(response); diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 004911558..91efeaebc 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -9,11 +9,11 @@