diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index 979b5f0a..4c5fbd63 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install cpplint==1.6.1 + pip install cpplint - name: Linting run: | cpplint --repository=. --recursive \ diff --git a/README.md b/README.md index 0d44c596..7ccf9522 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,49 @@ -- [OpenDTU-OnBattery](#opendtu-onbattery) - - [What is OpenDTU-OnBattery](#what-is-opendtu-onbattery) - - [Documentation](#documentation) - - [State of the project](#state-of-the-project) - - [History of the project](#history-of-the-project) - - [Acknowledgments](#acknowledgments) - -# OpenDTU-OnBattery - -This is a fork of [OpenDTU](https://github.com/tbnobody/OpenDTU). - +[![OpenDTU-OnBattery Build](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml) +[![cpplint](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml) +[![Yarn Linting](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml) -[![OpenDTU-OnBattery Build](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml) -[![cpplint](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml) -[![Yarn Linting](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml) +- [OpenDTU-OnBattery](#opendtu-onbattery) + - [Getting Started](#getting-started) + - [Important Differences](#important-differences) + - [Documentation](#documentation) + - [Project State](#project-state) + - [Project History](#project-history) + - [Acknowledgments](#acknowledgments) -## What is OpenDTU-OnBattery +# OpenDTU-OnBattery -OpenDTU-OnBattery is an extension of the original OpenDTU to support battery -chargers, battery management systems (BMS) and power meters on a single ESP32. -With the help of a Dynamic Power Limiter, the power production can be adjusted -to the actual consumption. In this way, it is possible to implement a zero -export policy. +OpenDTU-OnBattery is a fork of [OpenDTU](https://github.com/tbnobody/OpenDTU), +which adds support for battery chargers, battery management systems (BMS), and +power meters on a single ESP32. Its Dynamic Power Limiter can adjust the +inverter's power production to the actual houshold consumption. In this way, it +is possible to implement a zero export policy. + +## Getting Started + +See the documentation to learn [what hardware](https://opendtu-onbattery.net/hardware/) +to acquire, how to [initialize](https://opendtu-onbattery.net/firmware/) it +with OpenDTU-OnBattery firmware, and how to +[configure](https://opendtu-onbattery.net/firmware/device_profiles/) +OpenDTU-OnBattery for your hardware. + +## Important Differences + +Generally speaking, OpenDTU-OnBattery and the upstream project are compatible +with each other, because OpenDTU-OnBattery mostly only extends the upstream +project. However, there are a few notable differences aside from the added functionality: + +* OpenDTU-OnBattery, due to its code footprint, cannot offer support for + over-the-air (OTA) updates on ESP32 with only 4MB of flash memory. Consult + the [documentation](https://opendtu-onbattery.net/firmware/howto/upgrade_8mb/#background) + to learn more. +* Unlike in the upstream project, you **must** compile the web application + yourself when attempting to build your own firmware blob. See the + [documentation](https://opendtu-onbattery.net/firmware/compile_webapp/) for + details. ## Documentation @@ -35,37 +54,40 @@ You may find additional helpful information in the project's community-maintained [Github Wiki](https://github.com/hoylabs/OpenDTU-OnBattery/wiki). -To find out what's new or improved have a look at the changelog of the +To find out what's new or improved have a look at the [releases](https://github.com/hoylabs/OpenDTU-OnBattery/releases). -## State of the project +## Project State OpenDTU-OnBattery is actively maintained. Please note that OpenDTU-OnBattery may change significantly during its development. Bug reports, comments, feature requests and pull requests are welcome! -## History of the project +## Project History The original OpenDTU project was started from [a discussion on -Mikrocontroller.net](https://www.mikrocontroller.net/topic/525778). It was the -goal to replace the original Hoymiles DTU (Telemetry Gateway) to avoid using -Hoymile's cloud. With a lot of reverse engineering the Hoymiles protocol was -decrypted and analyzed. +Mikrocontroller.net](https://www.mikrocontroller.net/topic/525778). The +original ambition was to replace the original Hoymiles DTU (Telemetry Gateway) +to avoid using Hoymile's cloud. With a lot of reverse engineering, the Hoymiles +protocol was decrypted and analyzed. -In the summer of 2022 @helgeerbe bought a Victron MPPT charge cntroller, and -didn't like the idea to set up a separate ESP32 to receive the charger's data. -He decided to fork OpenDTU and extend it with battery charger support and a -Dynamic Power Limiter. +In the summer of 2022 [@helgeerbe](https://github.com/helgeerbe) bought a +Victron MPPT charge controller, and didn't like the idea to set up a separate +ESP32 to receive the charger's data. He decided to fork OpenDTU and extend it +with battery charger support and a Dynamic Power Limiter. In early October 2024, the project moved to the newly founded GitHub organisation `hoylabs` and is since maintained by multiple community members. ## Acknowledgments -* Special thanks to Thomas Basler (@tbnobody), the author of the [upstream - project](https://github.com/tbnobody/OpenDTU), for hist continued effort! -* Thanks to @helgeerbe for starting OpenDTU-OnBattery and his dedication to the - project, as well as his trust in the current maintainers of the project, - which act as part of the `hoylabs` GitHub organisation. +* Special thanks to Thomas Basler ([@tbnobody](https://github.com/tbnobody)), + the author of the [upstream project](https://github.com/tbnobody/OpenDTU), + for his continued effort! +* Thanks to [@helgeerbe](https://github.com/helgeerbe) for starting + OpenDTU-OnBattery, for his dedication to the project, as well as for his + trust in the current maintainers of the project, which act as part of the + `hoylabs` GitHub organisation. * We like to thank all contributors. With your ideas and enhancements, you have - made OpenDTU-OnBattery much more than @helgeerbe originally had in mind. + made OpenDTU-OnBattery much more than + [@helgeerbe](https://github.com/helgeerbe) originally had in mind. diff --git a/include/BatteryStats.h b/include/BatteryStats.h index bcf7cedc..cfce8ea8 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -6,6 +6,7 @@ #include "AsyncJson.h" #include "Arduino.h" #include "JkBmsDataPoints.h" +#include "JbdBmsDataPoints.h" #include "VeDirectShuntController.h" #include @@ -159,7 +160,6 @@ class SBSBatteryStats : public BatteryStats { float _chargeVoltage; float _chargeCurrentLimitation; - float _dischargeCurrentLimitation; uint16_t _stateOfHealth; float _current; float _temperature; @@ -284,6 +284,35 @@ class JkBmsBatteryStats : public BatteryStats { uint32_t _cellVoltageTimestamp = 0; }; +class JbdBmsBatteryStats : public BatteryStats { + public: + void getLiveViewData(JsonVariant& root) const final { + getJsonData(root, false); + } + + void getInfoViewData(JsonVariant& root) const { + getJsonData(root, true); + } + + void mqttPublish() const final; + + uint32_t getMqttFullPublishIntervalMs() const final { return 60 * 1000; } + + void updateFrom(JbdBms::DataPointContainer const& dp); + + private: + void getJsonData(JsonVariant& root, bool verbose) const; + + JbdBms::DataPointContainer _dataPoints; + mutable uint32_t _lastMqttPublish = 0; + mutable uint32_t _lastFullMqttPublish = 0; + + uint16_t _cellMinMilliVolt = 0; + uint16_t _cellAvgMilliVolt = 0; + uint16_t _cellMaxMilliVolt = 0; + uint32_t _cellVoltageTimestamp = 0; +}; + class VictronSmartShuntStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final; diff --git a/include/Configuration.h b/include/Configuration.h index 3b99c38b..79d3e61c 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,9 +4,12 @@ #include "PinMapping.h" #include #include +#include +#include +#include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change +#define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -33,6 +36,7 @@ #define CHAN_MAX_NAME_STRLEN 31 #define DEV_MAX_MAPPING_NAME_STRLEN 63 +#define LOCALE_STRLEN 2 #define HTTP_REQUEST_MAX_URL_STRLEN 1024 #define HTTP_REQUEST_MAX_USERNAME_STRLEN 64 @@ -128,6 +132,43 @@ struct POWERMETER_HTTP_SML_CONFIG_T { }; using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; +struct POWERLIMITER_INVERTER_CONFIG_T { + uint64_t Serial; + bool IsGoverned; + bool IsBehindPowerMeter; + bool IsSolarPowered; + bool UseOverscalingToCompensateShading; + uint16_t LowerPowerLimit; + uint16_t UpperPowerLimit; +}; +using PowerLimiterInverterConfig = struct POWERLIMITER_INVERTER_CONFIG_T; + +struct POWERLIMITER_CONFIG_T { + bool Enabled; + bool VerboseLogging; + bool SolarPassThroughEnabled; + uint8_t SolarPassThroughLosses; + bool BatteryAlwaysUseAtNight; + int16_t TargetPowerConsumption; + uint16_t TargetPowerConsumptionHysteresis; + uint16_t BaseLoadLimit; + bool IgnoreSoc; + uint16_t BatterySocStartThreshold; + uint16_t BatterySocStopThreshold; + float VoltageStartThreshold; + float VoltageStopThreshold; + float VoltageLoadCorrectionFactor; + uint16_t FullSolarPassThroughSoc; + float FullSolarPassThroughStartVoltage; + float FullSolarPassThroughStopVoltage; + uint64_t InverterSerialForDcVoltage; + uint8_t InverterChannelIdForDcVoltage; + int8_t RestartHour; + uint16_t TotalUpperPowerLimit; + PowerLimiterInverterConfig Inverters[INV_MAX_COUNT]; +}; +using PowerLimiterConfig = struct POWERLIMITER_CONFIG_T; + enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 }; @@ -145,6 +186,8 @@ struct BATTERY_CONFIG_T { BatteryVoltageUnit MqttVoltageUnit; bool EnableDischargeCurrentLimit; float DischargeCurrentLimit; + float DischargeCurrentLimitBelowSoc; + float DischargeCurrentLimitBelowVoltage; bool UseBatteryReportedDischargeCurrentLimit; char MqttDischargeCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; char MqttDischargeCurrentJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; @@ -251,7 +294,7 @@ struct CONFIG_T { bool ScreenSaver; uint8_t Rotation; uint8_t Contrast; - uint8_t Language; + char Locale[LOCALE_STRLEN + 1]; struct { uint32_t Duration; uint8_t Mode; @@ -278,34 +321,7 @@ struct CONFIG_T { PowerMeterHttpSmlConfig HttpSml; } PowerMeter; - struct { - bool Enabled; - bool VerboseLogging; - bool SolarPassThroughEnabled; - uint8_t SolarPassThroughLosses; - bool BatteryAlwaysUseAtNight; - uint32_t Interval; - bool IsInverterBehindPowerMeter; - bool IsInverterSolarPowered; - bool UseOverscalingToCompensateShading; - uint64_t InverterId; - uint8_t InverterChannelId; - int32_t TargetPowerConsumption; - int32_t TargetPowerConsumptionHysteresis; - int32_t LowerPowerLimit; - int32_t BaseLoadLimit; - int32_t UpperPowerLimit; - bool IgnoreSoc; - uint32_t BatterySocStartThreshold; - uint32_t BatterySocStopThreshold; - float VoltageStartThreshold; - float VoltageStopThreshold; - float VoltageLoadCorrectionFactor; - int8_t RestartHour; - uint32_t FullSolarPassThroughSoc; - float FullSolarPassThroughStartVoltage; - float FullSolarPassThroughStopVoltage; - } PowerLimiter; + PowerLimiterConfig PowerLimiter; BatteryConfig Battery; @@ -331,11 +347,23 @@ struct CONFIG_T { class ConfigurationClass { public: - void init(); + void init(Scheduler& scheduler); bool read(); bool write(); void migrate(); - CONFIG_T& get(); + CONFIG_T const& get(); + + class WriteGuard { + public: + WriteGuard(); + CONFIG_T& getConfig(); + ~WriteGuard(); + + private: + std::unique_lock _lock; + }; + + WriteGuard getWriteGuard(); INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); @@ -347,6 +375,7 @@ public: static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target); + static void serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target); static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); @@ -354,6 +383,12 @@ public: static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target); + static void deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target); + +private: + void loop(); + + Task _loopTask; }; extern ConfigurationClass Configuration; diff --git a/include/DataPoints.h b/include/DataPoints.h new file mode 100644 index 00000000..91f19d92 --- /dev/null +++ b/include/DataPoints.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +using tCellVoltages = std::map; + +template +class DataPoint { + template class> + friend class DataPointContainer; + + public: + using tValue = std::variant; + + DataPoint() = delete; + + DataPoint(DataPoint const& other) + : _strLabel(other._strLabel) + , _strValue(other._strValue) + , _strUnit(other._strUnit) + , _value(other._value) + , _timestamp(other._timestamp) { } + + DataPoint(std::string const& strLabel, std::string const& strValue, + std::string const& strUnit, tValue value, uint32_t timestamp) + : _strLabel(strLabel) + , _strValue(strValue) + , _strUnit(strUnit) + , _value(std::move(value)) + , _timestamp(timestamp) { } + + std::string const& getLabelText() const { return _strLabel; } + std::string const& getValueText() const { return _strValue; } + std::string const& getUnitText() const { return _strUnit; } + uint32_t getTimestamp() const { return _timestamp; } + + bool operator==(DataPoint const& other) const { + return _value == other._value; + } + + private: + std::string _strLabel; + std::string _strValue; + std::string _strUnit; + tValue _value; + uint32_t _timestamp; +}; + +template std::string dataPointValueToStr(T const& v); + +template class Traits> +class DataPointContainer { + public: + DataPointContainer() = default; + + //template