Prepare Release 2024.11.20

merge development into master
This commit is contained in:
Bernhard Kirchen 2024-11-20 22:57:58 +01:00 committed by GitHub
commit 81b1e6e158
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
173 changed files with 7950 additions and 3022 deletions

View File

@ -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 \

View File

@ -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)
<!---
disabled while "create release badge" action is broken, see .github/build.yml
![GitHub tag (latest SemVer)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/helgeerbe/68b47cc8c8994d04ab3a4fa9d8aee5e6/raw/openDTUcoreRelease.json)
--->
[![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.

View File

@ -6,6 +6,7 @@
#include "AsyncJson.h"
#include "Arduino.h"
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "VeDirectShuntController.h"
#include <cfloat>
@ -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;

View File

@ -4,9 +4,12 @@
#include "PinMapping.h"
#include <cstdint>
#include <ArduinoJson.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <condition_variable>
#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<std::mutex> _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;

119
include/DataPoints.h Normal file
View File

@ -0,0 +1,119 @@
#pragma once
#include <Arduino.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
using tCellVoltages = std::map<uint8_t, uint16_t>;
template<typename... V>
class DataPoint {
template<typename, typename L, template<L> class>
friend class DataPointContainer;
public:
using tValue = std::variant<V...>;
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<typename T> std::string dataPointValueToStr(T const& v);
template<typename DataPoint, typename Label, template<Label> class Traits>
class DataPointContainer {
public:
DataPointContainer() = default;
//template<Label L> using Traits = LabelTraits<L>;
template<Label L>
void add(typename Traits<L>::type val) {
_dataPoints.emplace(
L,
DataPoint(
Traits<L>::name,
dataPointValueToStr(val),
Traits<L>::unit,
typename DataPoint::tValue(std::move(val)),
millis()
)
);
}
// make sure add() is only called with the type expected for the
// respective label, no implicit conversions allowed.
template<Label L, typename T>
void add(T) = delete;
template<Label L>
std::optional<DataPoint const> getDataPointFor() const {
auto it = _dataPoints.find(L);
if (it == _dataPoints.end()) { return std::nullopt; }
return it->second;
}
template<Label L>
std::optional<typename Traits<L>::type> get() const {
auto optionalDataPoint = getDataPointFor<L>();
if (!optionalDataPoint.has_value()) { return std::nullopt; }
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
}
using tMap = std::unordered_map<Label, DataPoint const>;
typename tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
typename tMap::const_iterator cend() const { return _dataPoints.cend(); }
// copy all data points from source into this instance, overwriting
// existing data points in this instance.
void updateFrom(DataPointContainer const& source)
{
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
auto pos = _dataPoints.find(iter->first);
if (pos != _dataPoints.end()) {
// do not update existing data points with the same value
if (pos->second == iter->second) { continue; }
_dataPoints.erase(pos);
}
_dataPoints.insert(*iter);
}
}
private:
tMap _dataPoints;
};

View File

@ -40,7 +40,7 @@ public:
void setContrast(const uint8_t contrast);
void setStatus(const bool turnOn);
void setOrientation(const uint8_t rotation = DISPLAY_ROTATION);
void setLanguage(const uint8_t language);
void setLocale(const String& locale);
void setDiagramMode(DiagramMode_t mode);
void setStartupDisplay();
@ -65,7 +65,7 @@ private:
DisplayType_t _display_type = DisplayType_t::None;
DiagramMode_t _diagram_mode = DiagramMode_t::Off;
uint8_t _display_language = DISPLAY_LANGUAGE;
String _display_language = DISPLAY_LOCALE;
uint8_t _mExtra;
const uint16_t _period = 1000;
const uint16_t _interval = 60000; // interval at which to power save (milliseconds)
@ -73,6 +73,17 @@ private:
char _fmtText[32];
bool _isLarge = false;
uint8_t _lineOffsets[5];
String _i18n_offline;
String _i18n_yield_today_kwh;
String _i18n_yield_today_wh;
String _i18n_date_format;
String _i18n_current_power_kw;
String _i18n_current_power_w;
String _i18n_meter_power_w;
String _i18n_meter_power_kw;
String _i18n_yield_total_mwh;
String _i18n_yield_total_kwh;
};
extern DisplayGraphicClass Display;

36
include/I18n.h Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
#include <WString.h>
#include <list>
struct LanguageInfo_t {
String code;
String name;
String filename;
};
class I18nClass {
public:
I18nClass();
void init(Scheduler& scheduler);
std::list<LanguageInfo_t> getAvailableLanguages();
String getFilenameByLocale(const String& locale) const;
void readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& meter_power_w, String& meter_power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh);
private:
void readLangPacks();
void readConfig(String file);
std::list<LanguageInfo_t> _availLanguages;
};
extern I18nClass I18n;

View File

@ -0,0 +1,87 @@
#pragma once
#include <memory>
#include <vector>
#include <frozen/string.h>
#include "Battery.h"
#include "JbdBmsDataPoints.h"
#include "JbdBmsSerialMessage.h"
#include "JbdBmsController.h"
namespace JbdBms {
class Controller : public BatteryProvider {
public:
Controller() = default;
bool init(bool verboseLogging) final;
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
static char constexpr _serialPortOwner[] = "JBD BMS";
#ifdef JBDBMS_DUMMY_SERIAL
std::unique_ptr<DummySerial> _upSerial;
#else
std::unique_ptr<HardwareSerial> _upSerial;
#endif
enum class Status : unsigned {
Initializing,
Timeout,
WaitingForPollInterval,
HwSerialNotAvailableForWrite,
BusyReading,
RequestSent,
FrameCompleted
};
frozen::string const& getStatusText(Status status);
void announceStatus(Status status);
void sendRequest(uint8_t pollInterval);
void rxData(uint8_t inbyte);
void reset();
void frameComplete();
void processDataPoints(DataPointContainer const& dataPoints);
enum class Interface : unsigned {
Invalid,
Uart,
Transceiver
};
Interface getInterface() const;
enum class ReadState : unsigned {
Idle,
WaitingForFrameStart,
FrameStartReceived, // 1 Byte: 0xDD
StateReceived,
CommandCodeReceived,
ReadingDataContent,
DataContentReceived,
ReadingCheckSum,
CheckSumReceived,
};
ReadState _readState;
void setReadState(ReadState state) {
_readState = state;
}
bool _verboseLogging = true;
int8_t _rxEnablePin = -1;
int8_t _txEnablePin = -1;
Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0;
uint32_t _lastRequest = 0;
uint8_t _dataLength = 0;
JbdBms::SerialResponse::tData _buffer = {};
std::shared_ptr<JbdBmsBatteryStats> _stats =
std::make_shared<JbdBmsBatteryStats>();
};
} /* namespace JbdBms */

118
include/JbdBmsDataPoints.h Normal file
View File

@ -0,0 +1,118 @@
#pragma once
#include <Arduino.h>
#include <map>
#include <frozen/map.h>
#include <frozen/string.h>
#include "DataPoints.h"
namespace JbdBms {
#define JBD_PROTECTION_STATUS(fnc) \
fnc(CellOverVoltage, (1<<0)) \
fnc(CellUnderVoltage, (1<<1)) \
fnc(PackOverVoltage, (1<<2)) \
fnc(PackUnderVoltage, (1<<3)) \
fnc(ChargingOverTemperature, (1<<4)) \
fnc(ChargingLowTemperature, (1<<5)) \
fnc(DischargingOverTemperature, (1<<6)) \
fnc(DischargingLowTemperature, (1<<7)) \
fnc(ChargingOverCurrent, (1<<8)) \
fnc(DischargeOverCurrent, (1<<9)) \
fnc(ShortCircuit, (1<<10)) \
fnc(IcFrontEndError, (1<<11)) \
fnc(MosSotwareLock, (1<<12)) \
fnc(Reserved1, (1<<13)) \
fnc(Reserved2, (1<<14)) \
fnc(Reserved3, (1<<15))
enum class AlarmBits : uint16_t {
#define ALARM_ENUM(name, value) name = value,
JBD_PROTECTION_STATUS(ALARM_ENUM)
#undef ALARM_ENUM
};
static const frozen::map<AlarmBits, frozen::string, 16> AlarmBitTexts = {
#define ALARM_TEXT(name, value) { AlarmBits::name, #name },
JBD_PROTECTION_STATUS(ALARM_TEXT)
#undef ALARM_TEXT
};
enum class DataPointLabel : uint8_t {
CellsMilliVolt,
BatteryTempOneCelsius,
BatteryTempTwoCelsius,
BatteryVoltageMilliVolt,
BatteryCurrentMilliAmps,
BatterySoCPercent,
BatteryTemperatureSensorAmount,
BatteryCycles,
BatteryCellAmount,
AlarmsBitmask,
BalancingEnabled,
CellAmountSetting,
BatteryCapacitySettingAmpHours,
BatteryChargeEnabled,
BatteryDischargeEnabled,
DateOfManufacturing,
BmsSoftwareVersion,
BmsHardwareVersion,
ActualBatteryCapacityAmpHours
};
using tCells = tCellVoltages;
template<DataPointLabel> struct DataPointLabelTraits;
#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
using type = t; \
static constexpr char const name[] = #n; \
static constexpr char const unit[] = u; \
};
/**
* the types associated with the labels are the types for the respective data
* points in the JbdBms::DataPoint class. they are *not* always equal to the
* type used in the serial message.
*
* it is unfortunate that we have to repeat all enum values here to define the
* traits. code generation could help here (labels are defined in a single
* source of truth and this code is generated -- no typing errors, etc.).
* however, the compiler will complain if an enum is misspelled or traits are
* defined for a removed enum, so we will notice. it will also complain when a
* trait is missing and if a data point for a label without traits is added to
* the DataPointContainer class, because the traits must be available then.
* even though this is tedious to maintain, human errors will be caught.
*/
LABEL_TRAIT(CellsMilliVolt, tCells, "mV");
LABEL_TRAIT(BatteryTempOneCelsius, int16_t, "°C");
LABEL_TRAIT(BatteryTempTwoCelsius, int16_t, "°C");
LABEL_TRAIT(BatteryVoltageMilliVolt, uint32_t, "mV");
LABEL_TRAIT(BatteryCurrentMilliAmps, int32_t, "mA");
LABEL_TRAIT(BatterySoCPercent, uint8_t, "%");
LABEL_TRAIT(BatteryTemperatureSensorAmount, uint8_t, "");
LABEL_TRAIT(BatteryCycles, uint16_t, "");
LABEL_TRAIT(BatteryCellAmount, uint16_t, "");
LABEL_TRAIT(AlarmsBitmask, uint16_t, "");
LABEL_TRAIT(BalancingEnabled, bool, "");
LABEL_TRAIT(CellAmountSetting, uint8_t, "");
LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah");
LABEL_TRAIT(BatteryChargeEnabled, bool, "");
LABEL_TRAIT(BatteryDischargeEnabled, bool, "");
LABEL_TRAIT(DateOfManufacturing, std::string, "");
LABEL_TRAIT(BmsSoftwareVersion, std::string, "");
LABEL_TRAIT(BmsHardwareVersion, std::string, "");
LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah");
#undef LABEL_TRAIT
} /* namespace JbdBms */
using JbdBmsDataPoint = DataPoint<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, JbdBms::tCells>;
template class DataPointContainer<JbdBmsDataPoint, JbdBms::DataPointLabel, JbdBms::DataPointLabelTraits>;
namespace JbdBms {
using DataPointContainer = DataPointContainer<JbdBmsDataPoint, DataPointLabel, DataPointLabelTraits>;
} /* namespace JbdBms */

View File

@ -0,0 +1,96 @@
#pragma once
#include <utility>
#include <vector>
#include <Arduino.h>
#include "JbdBmsDataPoints.h"
namespace JbdBms {
// Only valid for receiving messages
class SerialMessage {
public:
using tData = std::vector<uint8_t>;
SerialMessage() = delete;
enum class Command : uint8_t {
Init = 0x00,
ReadBasicInformation = 0x03,
ReadCellVoltages = 0x04,
ReadHardwareVersionNumber = 0x05,
ControlMosInstruction = 0xE1,
};
uint8_t getStartMarker() const { return _raw[0]; }
virtual Command getCommand() const = 0;
uint8_t getDataLength() const { return _raw[3]; }
uint16_t getChecksum() const { return get<uint16_t>(_raw.cend()-3); }
uint8_t getEndMarker() const { return *(_raw.cend()-1); }
void printMessage();
bool isValid() const;
uint8_t const* data() { return _raw.data(); }
size_t size() { return _raw.size(); }
static constexpr uint8_t startMarker = 0xDD;
static constexpr uint8_t endMarker = 0x77;
protected:
template <typename... Args>
explicit SerialMessage(Args&&... args) : _raw(std::forward<Args>(args)...) { }
template<typename T, typename It> T get(It&& pos) const;
template<typename It> bool getBool(It&& pos) const;
template<typename It> int16_t getTemperature(It&& pos) const;
template<typename It> std::string getString(It&& pos, size_t len, bool replaceZeroes = false) const;
template<typename It> std::string getProductionDate(It&& pos) const;
template<typename T> void set(tData::iterator const& pos, T val);
uint16_t calcChecksum() const;
void updateChecksum();
tData _raw;
JbdBms::DataPointContainer _dp;
};
class SerialResponse : public SerialMessage {
public:
enum class Status : uint8_t {
Ok = 0x00,
Error = 0x80
};
using tData = SerialMessage::tData;
explicit SerialResponse(tData&& raw);
Command getCommand() const { return static_cast<Command>(_raw[1]); }
Status getStatus() const { return static_cast<Status>(_raw[2]); }
bool isValid() const;
DataPointContainer const& getDataPoints() const { return _dp; }
};
class SerialCommand : public SerialMessage {
public:
enum class Status : uint8_t {
Read = 0xA5,
Write = 0x5A,
};
explicit SerialCommand(Status status, Command cmd);
Status getStatus() const { return static_cast<Status>(_raw[1]); }
Command getCommand() const { return static_cast<Command>(_raw[2]); }
static Command getLastCommand() { return _lastCmd; }
bool isValid() const;
private:
static Command _lastCmd;
};
} /* namespace JbdBms */

View File

@ -5,13 +5,12 @@
#include <frozen/string.h>
#include "Battery.h"
#include "JkBmsDataPoints.h"
#include "JkBmsSerialMessage.h"
#include "JkBmsDummy.h"
//#define JKBMS_DUMMY_SERIAL
class DataPointContainer;
namespace JkBms {
class Controller : public BatteryProvider {

View File

@ -2,13 +2,11 @@
#include <Arduino.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <frozen/map.h>
#include <frozen/string.h>
#include "DataPoints.h"
namespace JkBms {
#define ALARM_BITS(fnc) \
@ -121,7 +119,7 @@ enum class DataPointLabel : uint8_t {
ProtocolVersion = 0xc0
};
using tCells = std::map<uint8_t, uint16_t>;
using tCells = tCellVoltages;
template<DataPointLabel> struct DataPointLabelTraits;
@ -206,99 +204,13 @@ LABEL_TRAIT(ProductId, std::string, "");
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
#undef LABEL_TRAIT
class DataPoint {
friend class DataPointContainer;
public:
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, tCells>;
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<typename T> std::string dataPointValueToStr(T const& v);
class DataPointContainer {
public:
DataPointContainer() = default;
using Label = DataPointLabel;
template<Label L> using Traits = JkBms::DataPointLabelTraits<L>;
template<Label L>
void add(typename Traits<L>::type val) {
_dataPoints.emplace(
L,
DataPoint(
Traits<L>::name,
dataPointValueToStr(val),
Traits<L>::unit,
DataPoint::tValue(std::move(val)),
millis()
)
);
}
// make sure add() is only called with the type expected for the
// respective label, no implicit conversions allowed.
template<Label L, typename T>
void add(T) = delete;
template<Label L>
std::optional<DataPoint const> getDataPointFor() const {
auto it = _dataPoints.find(L);
if (it == _dataPoints.end()) { return std::nullopt; }
return it->second;
}
template<Label L>
std::optional<typename Traits<L>::type> get() const {
auto optionalDataPoint = getDataPointFor<L>();
if (!optionalDataPoint.has_value()) { return std::nullopt; }
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
}
using tMap = std::unordered_map<Label, DataPoint const>;
tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
tMap::const_iterator cend() const { return _dataPoints.cend(); }
// copy all data points from source into this instance, overwriting
// existing data points in this instance.
void updateFrom(DataPointContainer const& source);
private:
tMap _dataPoints;
};
} /* namespace JkBms */
using JkBmsDataPoint = DataPoint<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, JkBms::tCells>;
template class DataPointContainer<JkBmsDataPoint, JkBms::DataPointLabel, JkBms::DataPointLabelTraits>;
namespace JkBms {
using DataPointContainer = DataPointContainer<JkBmsDataPoint, DataPointLabel, DataPointLabelTraits>;
} /* namespace JkBms */

View File

@ -2,9 +2,11 @@
#pragma once
#include "Configuration.h"
#include "PowerLimiterInverter.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <atomic>
#include <deque>
#include <memory>
#include <functional>
#include <optional>
@ -18,6 +20,8 @@
class PowerLimiterClass {
public:
PowerLimiterClass() = default;
enum class Status : unsigned {
Initializing,
DisabledByConfig,
@ -25,25 +29,19 @@ public:
WaitingForValidTimestamp,
PowerMeterPending,
InverterInvalid,
InverterChanged,
InverterOffline,
InverterCommandsDisabled,
InverterLimitPending,
InverterPowerCmdPending,
InverterDevInfoPending,
InverterCmdPending,
ConfigReload,
InverterStatsPending,
CalculatedLimitBelowMinLimit,
FullSolarPassthrough,
UnconditionalSolarPassthrough,
NoVeDirect,
NoEnergy,
HuaweiPsu,
Stable,
};
void init(Scheduler& scheduler);
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
void triggerReloadingConfig() { _reloadConfigFlag = true; }
uint8_t getInverterUpdateTimeouts() const;
uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; }
int32_t getInverterOutput() { return _lastExpectedInverterOutput; }
bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; }
enum class Mode : unsigned {
@ -54,54 +52,54 @@ public:
void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; }
void calcNextInverterRestart();
bool usesBatteryPoweredInverter();
bool isGovernedInverterProducing();
private:
void loop();
Task _loopTask;
int32_t _lastRequestedPowerLimit = 0;
bool _shutdownPending = false;
std::optional<uint32_t> _oInverterStatsMillis = std::nullopt;
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
std::optional<int32_t> _oTargetPowerLimitWatts = std::nullopt;
std::optional<bool> _oTargetPowerState = std::nullopt;
std::atomic<bool> _reloadConfigFlag = true;
uint16_t _lastExpectedInverterOutput = 0;
Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0;
uint32_t _lastCalculation = 0;
static constexpr uint32_t _calculationBackoffMsDefault = 128;
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr;
std::deque<std::unique_ptr<PowerLimiterInverter>> _inverters;
bool _batteryDischargeEnabled = false;
bool _nighttimeDischarging = false;
uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis()
uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart
std::pair<bool, uint32_t> _nextInverterRestart = { false, 0 };
bool _fullSolarPassThroughEnabled = false;
bool _verboseLogging = true;
uint8_t _inverterUpdateTimeouts = 0;
frozen::string const& getStatusText(Status status);
void announceStatus(Status status);
bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); }
void reloadConfig();
std::pair<float, char const*> getInverterDcVoltage();
float getBatteryVoltage(bool log = false);
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
bool canUseDirectSolarPower();
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, int32_t batteryPowerLimit, bool batteryPower);
bool updateInverter();
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarPower();
int32_t getBatteryDischargeLimit();
uint16_t solarDcToInverterAc(uint16_t dcPower);
void fullSolarPassthrough(PowerLimiterClass::Status reason);
int16_t calcHouseholdConsumption();
using inverter_filter_t = std::function<bool(PowerLimiterInverter const&)>;
uint16_t updateInverterLimits(uint16_t powerRequested, inverter_filter_t filter, std::string const& filterExpression);
uint16_t calcBatteryAllowance(uint16_t powerRequested);
bool updateInverters();
uint16_t getSolarPassthroughPower();
std::optional<uint16_t> getBatteryDischargeLimit();
float getBatteryInvertersOutputAcWatts();
float getLoadCorrectedVoltage();
bool testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> compare);
bool isStartThresholdReached();
bool isStopThresholdReached();
bool isBelowStopThreshold();
bool useFullSolarPassthrough();
void calcNextInverterRestart();
bool isFullSolarPassthroughActive();
};
extern PowerLimiterClass PowerLimiter;

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerLimiterInverter.h"
class PowerLimiterBatteryInverter : public PowerLimiterInverter {
public:
PowerLimiterBatteryInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
uint16_t getMaxReductionWatts(bool allowStandby) const final;
uint16_t getMaxIncreaseWatts() const final;
uint16_t applyReduction(uint16_t reduction, bool allowStandby) final;
uint16_t applyIncrease(uint16_t increase) final;
uint16_t standby() final;
bool isSolarPowered() const final { return false; }
private:
void setAcOutput(uint16_t expectedOutputWatts) final;
};

View File

@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <Hoymiles.h>
#include <optional>
#include <memory>
class PowerLimiterInverter {
public:
static std::unique_ptr<PowerLimiterInverter> create(bool verboseLogging, PowerLimiterInverterConfig const& config);
// send command(s) to inverter to reach desired target state (limit and
// production). return true if an update is pending, i.e., if the target
// state is NOT yet reached, false otherwise.
bool update();
// returns the timestamp of the oldest stats received for this inverter
// *after* its last command completed. return std::nullopt if new stats
// are pending after the last command completed.
std::optional<uint32_t> getLatestStatsMillis() const;
// the amount of times an update command issued to the inverter timed out
uint8_t getUpdateTimeouts() const { return _updateTimeouts; }
// maximum amount of AC power the inverter is able to produce
// (not regarding the configured upper power limit)
uint16_t getInverterMaxPowerWatts() const;
// maximum amount of AC power the inverter is allowed to produce as per
// upper power limit (additionally restricted by inverter's absolute max)
uint16_t getConfiguredMaxPowerWatts() const;
uint16_t getCurrentOutputAcWatts() const;
// this differs from current output power if new limit was assigned
uint16_t getExpectedOutputAcWatts() const;
// the maximum reduction of power output the inverter
// can achieve with or withouth going into standby.
virtual uint16_t getMaxReductionWatts(bool allowStandby) const = 0;
// the maximum increase of power output the inverter can achieve
// (is expected to achieve), possibly coming out of standby.
virtual uint16_t getMaxIncreaseWatts() const = 0;
// change the target limit such that the requested change becomes effective
// on the expected AC power output. returns the change in the range
// [0..reduction] that will become effective (once update() returns false).
virtual uint16_t applyReduction(uint16_t reduction, bool allowStandby) = 0;
virtual uint16_t applyIncrease(uint16_t increase) = 0;
// stop producing AC power. returns the change in power output
// that will become effective (once update() returns false).
virtual uint16_t standby() = 0;
// wake the inverter from standby and set it to produce
// as much power as permissible by its upper power limit.
void setMaxOutput();
void restart();
float getDcVoltage(uint8_t input);
bool isSendingCommandsEnabled() const { return _spInverter->getEnableCommands(); }
bool isReachable() const { return _spInverter->isReachable(); }
bool isProducing() const { return _spInverter->isProducing(); }
uint64_t getSerial() const { return _config.Serial; }
char const* getSerialStr() const { return _serialStr; }
bool isBehindPowerMeter() const { return _config.IsBehindPowerMeter; }
virtual bool isSolarPowered() const = 0;
void debug() const;
protected:
PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
// returns false if the inverter cannot participate
// in achieving the requested change in power output
bool isEligible() const;
uint16_t getCurrentLimitWatts() const;
void setTargetPowerLimitWatts(uint16_t power) { _oTargetPowerLimitWatts = power; }
void setTargetPowerState(bool enable) { _oTargetPowerState = enable; }
void setExpectedOutputAcWatts(uint16_t power) { _expectedOutputAcWatts = power; }
// copied to avoid races with web UI
PowerLimiterInverterConfig _config;
// Hoymiles lib inverter instance
std::shared_ptr<InverterAbstract> _spInverter = nullptr;
bool _verboseLogging;
char _logPrefix[32];
private:
virtual void setAcOutput(uint16_t expectedOutputWatts) = 0;
char _serialStr[16];
// track (target) state
uint8_t _updateTimeouts = 0;
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
std::optional<uint16_t> _oTargetPowerLimitWatts = std::nullopt;
std::optional<bool> _oTargetPowerState = std::nullopt;
mutable std::optional<uint32_t> _oStatsMillis = std::nullopt;
// the expected AC output (possibly is different from the target limit)
uint16_t _expectedOutputAcWatts = 0;
};

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerLimiterInverter.h"
class PowerLimiterSolarInverter : public PowerLimiterInverter {
public:
PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
uint16_t getMaxReductionWatts(bool allowStandby) const final;
uint16_t getMaxIncreaseWatts() const final;
uint16_t applyReduction(uint16_t reduction, bool allowStandby) final;
uint16_t applyIncrease(uint16_t increase) final;
uint16_t standby() final;
bool isSolarPowered() const final { return true; }
private:
uint16_t scaleLimit(uint16_t expectedOutputWatts);
void setAcOutput(uint16_t expectedOutputWatts) final;
};

View File

@ -2,6 +2,7 @@
#pragma once
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <cstdint>
#include <utility>
@ -12,6 +13,8 @@ public:
static int getTimezoneOffset();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles();
static String generateMd5FromFile(String file);
static void skipBom(File& f);
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>

View File

@ -42,6 +42,17 @@ public:
// minimum of all MPPT charge controllers' output voltages in V
float getOutputVoltage() const;
// returns the state of operation from the first available controller
std::optional<uint8_t> getStateOfOperation() const;
// returns the requested value from the first available controller in mV
enum class MPPTVoltage : uint8_t {
ABSORPTION = 0,
FLOAT = 1,
BATTERY = 2
};
std::optional<float> getVoltage(MPPTVoltage kindOf) const;
private:
void loop();
VictronMpptClass(VictronMpptClass const& other) = delete;

View File

@ -2,14 +2,15 @@
#pragma once
#include "WebApi_battery.h"
#include "WebApi_config.h"
#include "WebApi_device.h"
#include "WebApi_devinfo.h"
#include "WebApi_dtu.h"
#include "WebApi_errors.h"
#include "WebApi_eventlog.h"
#include "WebApi_file.h"
#include "WebApi_firmware.h"
#include "WebApi_gridprofile.h"
#include "WebApi_i18n.h"
#include "WebApi_inverter.h"
#include "WebApi_limit.h"
#include "WebApi_maintenance.h"
@ -55,13 +56,14 @@ private:
AsyncWebServer _server;
WebApiBatteryClass _webApiBattery;
WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu;
WebApiEventlogClass _webApiEventlog;
WebApiFileClass _webApiFile;
WebApiFirmwareClass _webApiFirmware;
WebApiGridProfileClass _webApiGridprofile;
WebApiI18nClass _webApiI18n;
WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit;
WebApiMaintenanceClass _webApiMaintenance;

View File

@ -1,17 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiConfigClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onConfigGet(AsyncWebServerRequest* request);
void onConfigDelete(AsyncWebServerRequest* request);
void onConfigListGet(AsyncWebServerRequest* request);
void onConfigUploadFinish(AsyncWebServerRequest* request);
void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

View File

@ -18,9 +18,10 @@ enum WebApiError {
DtuInvalidCmtFrequency,
DtuInvalidCmtCountry,
ConfigBase = 3000,
ConfigNotDeleted,
ConfigSuccess,
FileBase = 3000,
FileNotDeleted,
FileSuccess,
FileDeleteSuccess,
InverterBase = 4000,
InverterSerialZero,

18
include/WebApi_file.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiFileClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onFileGet(AsyncWebServerRequest* request);
void onFileDelete(AsyncWebServerRequest* request);
void onFileDeleteAll(AsyncWebServerRequest* request);
void onFileListGet(AsyncWebServerRequest* request);
void onFileUploadFinish(AsyncWebServerRequest* request);
void onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

14
include/WebApi_i18n.h Normal file
View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiI18nClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onI18nLanguages(AsyncWebServerRequest* request);
void onI18nLanguage(AsyncWebServerRequest* request);
};

View File

@ -102,7 +102,7 @@
#define DISPLAY_SCREENSAVER true
#define DISPLAY_ROTATION 2U
#define DISPLAY_CONTRAST 60U
#define DISPLAY_LANGUAGE 0U
#define DISPLAY_LOCALE "en"
#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL)
#define DISPLAY_DIAGRAM_MODE 1U
@ -112,6 +112,8 @@
#define MAX_INVERTER_LIMIT 2250
#define LANG_PACK_SUFFIX ".lang.json"
// values specific to downstream project OpenDTU-OnBattery start here:
#define VEDIRECT_ENABLED false
#define VEDIRECT_VERBOSE_LOGGING false
@ -128,18 +130,16 @@
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false
#define POWERLIMITER_INVERTER_ID 0ULL
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_IGNORE_SOC false
#define POWERLIMITER_IGNORE_SOC true
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
@ -156,6 +156,8 @@
#define BATTERY_JKBMS_POLLING_INTERVAL 5
#define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false
#define BATTERY_DISCHARGE_CURRENT_LIMIT 0.0
#define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC 100.0
#define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_VOLTAGE 60.0
#define BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT false
#define HUAWEI_ENABLED false

9
lang/README.md Normal file
View File

@ -0,0 +1,9 @@
# Language Packs
This folder contains language packs for OpenDTU which can be uploaded to the
device using the "Config Management" function.
Select "Language Pack" in the restore section, select a `.json` file containing
your language and press "Restore". Afterwards all language selection drop down
menues contain the new language.
Create a pull to request to share your own language pack (or corrections) with the community.

690
lang/es.lang.json Normal file
View File

@ -0,0 +1,690 @@
{
"meta": {
"name": "Español",
"code": "es"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Apagado",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "Hoy: %4.0f Wh",
"yield_today_kwh": "Hoy: %.1f kWh",
"yield_total_kwh": "Total: %.1f kWh",
"yield_total_mwh": "Total: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Vista en directo",
"Settings": "Ajustes",
"NetworkSettings": "Ajustes de Red",
"NTPSettings": "Ajustes NTP",
"MQTTSettings": "Ajustes MQTT",
"InverterSettings": "Ajustes Inversor",
"SecuritySettings": "Ajustes Seguridad",
"DTUSettings": "Ajustes DTU",
"DeviceManager": "Administrador Dispositivos",
"ConfigManagement": "Gestión configuración",
"FirmwareUpgrade": "Actualización Firmware",
"DeviceReboot": "Reinicio Dispositivo",
"Info": "Info",
"System": "Sistema",
"Network": "Red",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Consola",
"About": "Acerca",
"Logout": "Logout",
"Login": "Login"
},
"base": {
"Loading": "Cargando...",
"Reload": "Recargar",
"Cancel": "Cancelar",
"Save": "Guardar",
"Refreshing": "Refrescando",
"Pull": "Tira hacia abajo para refrescar",
"Release": "Soltar para refrescar",
"Close": "Cerrar"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Oscuro",
"Light": "Claro",
"Auto": "Automático"
},
"apiresponse": {
"1001": "¡Opciones guardadas!",
"1002": "No se encontraron valores",
"1003": "Datos demasiado grandes",
"1004": "Fallo al procesar los datos",
"1005": "Faltan valores",
"1006": "Fallo en la escritura",
"2001": "¡El número de serie no puede ser cero!",
"2002": "Intervalo de Poll interval debe ser mayor que cero!",
"2003": "Configuración de potencia incorrecta!",
"2004": "La frecuencia debe estar entre {min} y {max} kHz y debe ser un múltiplo de 250 kHz!",
"2005": "Modelo desconocido! Por favor, informe el \"Modelo de pieza de hardware\" y el modelo (por ejemplo, HM-350) como un problema en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"3001": "No se eliminó nada",
"3002": "Configuración borrada. Reinicio en curso...",
"4001": "@:apiresponse.2001",
"4002": "El nombre debe tener entre 1 y {max} caracteres de longitud!",
"4003": "Solo se admiten {max} inversores!",
"4004": "Inversor creado!",
"4005": "ID no válido especificado",
"4006": "Cantidad de canales máxima incorrecta dada!",
"4007": "Inversor modificado!",
"4008": "Inversor eliminado!",
"4009": "Orden de inversores guardado!",
"5001": "@:apiresponse.2001",
"5002": "Límite debe estar entre 1 y {max}!",
"5003": "Tipo incorrecto especificado!",
"5004": "Inversor incorrecto especificado!",
"6001": "Reinicio desencadenado!",
"6002": "Reinicio cancelado!",
"7001": "¡El servidor MQTT debe tener entre 1 y {max} caracteres de longitud!",
"7002": "¡El nombre de usuario debe no tener más de {max} caracteres!",
"7003": "¡La contraseña debe no tener más de {max} caracteres!",
"7004": "¡El tema debe tener entre 1 y {max} caracteres de longitud!",
"7005": "¡El tema no debe contener caracteres de espacio!",
"7006": "¡El tema debe terminar con barra inclinada (/)!",
"7007": "¡El puerto debe ser un número entre 1 y 65535!",
"7008": "¡El certificado debe tener entre 1 y {max} caracteres de longitud!",
"7009": "¡El tema LWT debe tener entre 1 y {max} caracteres de longitud!",
"7010": "¡El tema LWT no debe contener caracteres de espacio!",
"7011": "¡El valor LWT en línea debe tener entre 1 y {max} caracteres de longitud!",
"7012": "¡El valor LWT fuera de línea debe tener entre 1 y {max} caracteres de longitud!",
"7013": "¡El intervalo de publicación debe ser un número entre {min} y {max}!",
"7014": "¡El tema Hass debe tener entre 1 y {max} caracteres de longitud!",
"7015": "¡El tema Hass no debe contener caracteres de espacio!",
"7016": "¡La QoS LWT no debe ser mayor que {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "¡La dirección IP no es válida!",
"8002": "¡La máscara de red no es válida!",
"8003": "¡El gateway no es válido!",
"8004": "¡La dirección IP del servidor DNS 1 no es válida!",
"8005": "¡La dirección IP del servidor DNS 2 no es válida!",
"8006": "¡El valor de tiempo de espera del punto de acceso administrativo es inválido!",
"9001": "¡El servidor NTP debe tener entre 1 y {max} caracteres de longitud!",
"9002": "¡La zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9003": "¡La descripción de la zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9004": "¡El año debe ser un número entre {min} y {max}!",
"9005": "¡El mes debe ser un número entre {min} y {max}!",
"9006": "¡El día debe ser un número entre {min} y {max}!",
"9007": "¡La hora debe ser un número entre {min} y {max}!",
"9008": "¡Los minutos deben ser un número entre {min} y {max}!",
"9009": "¡Los segundos deben ser un número entre {min} y {max}!",
"9010": "¡Hora actualizada!",
"10001": "¡La contraseña debe tener entre 8 y {max} caracteres de longitud!",
"10002": "¡Autenticación exitosa!",
"11001": "¡@:apiresponse.2001",
"11002": "¡@:apiresponse:5004",
"12001": "¡El perfil debe tener entre 1 y {max} caracteres de longitud!"
},
"home": {
"LiveData": "Datos en Vivo",
"SerialNumber": "Número de Serie: ",
"CurrentLimit": "Límite de Corriente: ",
"DataAge": "Edad de los Datos: ",
"Seconds": "{val} segundos",
"ShowSetInverterLimit": "Ver / Establecer Límite del Inversor",
"TurnOnOff": "Encender/Apagar el Inversor",
"ShowInverterInfo": "Ver Información del Inversor",
"ShowEventlog": "Ver Registro de Eventos",
"UnreadMessages": "mensajes sin leer",
"Loading": "@:base.Cargando",
"EventLog": "Registro de Eventos",
"InverterInfo": "Información del Inversor",
"LimitSettings": "Configuración de Límites",
"LastLimitSetStatus": "Último Estado de Configuración del Límite:",
"SetLimit": "Establecer Límite:",
"Relative": "Relativo (%)",
"Absolute": "Absoluto (W)",
"LimitHint": "<b>Consejo:</b> Si establece el límite como un valor absoluto, la visualización del valor actual solo se actualizará después de ~4 minutos.",
"SetPersistent": "Establecer Límite Permanente",
"SetNonPersistent": "Establecer Límite No Permanente",
"PowerSettings": "Configuración de Energía",
"LastPowerSetStatus": "Último Estado de Configuración de Energía:",
"TurnOn": "Encender",
"TurnOff": "Apagar",
"Restart": "Reiniciar",
"Failure": "Fallo",
"Pending": "Pendiente",
"Ok": "Aceptar",
"Unknown": "Desconocido",
"ShowGridProfile": "Ver Perfil de la Red",
"GridProfile": "Perfil de la Red",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Iniciar",
"Stop": "Parar",
"Id": "ID",
"Message": "Mensaje"
},
"devinfo": {
"NoInfo": "Sin información disponible",
"NoInfoLong": "No se ha recibido ningún dato válido del inversor hasta ahora. Todavía estamos intentando...",
"UnknownModel": "¡Modelo desconocido! Por favor, informe el \"Número de parte de hardware\" y el modelo (por ejemplo, HM-350) como un problema <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"Serial": "Número de serie",
"ProdYear": "Año de producción",
"ProdWeek": "Semana de producción",
"Model": "Modelo",
"DetectedMaxPower": "Potencia máxima detectada",
"BootloaderVersion": "Versión del cargador de arranque",
"FirmwareVersion": "Versión del firmware",
"FirmwareBuildDate": "Fecha de construcción del firmware",
"HardwarePartNumber": "Número de parte de hardware",
"HardwareVersion": "Versión de hardware"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nombre",
"Version": "Versión",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Apoyar el desarrollo",
"GridprofileSupportLong": "Por favor, consulte <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">aquí</a> para obtener más información."
},
"systeminfo": {
"SystemInfo": "Información del sistema",
"VersionError": "Error al obtener información de la versión",
"VersionNew": "¡Nueva versión disponible! ¡Mostrar cambios!",
"VersionOk": "¡Actualizado!"
},
"firmwareinfo": {
"FirmwareInformation": "Información del firmware",
"Hostname": "Hostname",
"SdkVersion": "Versión del SDK",
"ConfigVersion": "Versión de la configuración",
"FirmwareVersion": "Versión del firmware / Hash de Git",
"PioEnv": "Entorno PIO",
"FirmwareVersionHint": "Haga clic aquí para mostrar información sobre su versión actual",
"FirmwareUpdate": "Actualización de firmware",
"FirmwareUpdateHint": "Haga clic aquí para ver las diferencias entre su versión y la última versión",
"FrmwareUpdateAllow": "Al activar la comprobación de actualización, se envía una solicitud a GitHub.com cada vez que se llama a la página para recuperar la versión actualmente disponible. Si no está de acuerdo con esto, deje esta función desactivada.",
"ResetReason0": "Razón de reinicio CPU 0",
"ResetReason1": "Razón de reinicio CPU 1",
"ConfigSaveCount": "Contador de guardado de configuración",
"Uptime": "Tiempo de actividad",
"UptimeValue": "0 días {time} | 1 día {time} | {count} días {time}"
},
"hardwareinfo": {
"HardwareInformation": "Información del hardware",
"ChipModel": "Modelo de chip",
"ChipRevision": "Revisión de chip",
"ChipCores": "Núcleos del chip",
"CpuFrequency": "Frecuencia de la CPU",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Información de la memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libre",
"Used": "Usado",
"Size": "Tamaño",
"Heap": "Montón",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Boceto"
},
"heapdetails": {
"HeapDetails": "Detalles del montón",
"TotalFree": "Total libre",
"LargestFreeBlock": "Bloque libre contiguo más grande",
"MaxUsage": "Uso máximo desde el inicio",
"Fragmentation": "Nivel de fragmentación"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Información de la radio",
"Status": "Estado de {module}",
"ChipStatus": "Estado del chip de {module}",
"ChipType": "Tipo de chip de {module}",
"Connected": "conectado",
"NotConnected": "no conectado",
"Configured": "configurado",
"NotConfigured": "no configurado",
"Unknown": "Desconocido"
},
"networkinfo": {
"NetworkInformation": "Información de la red"
},
"wifistationinfo": {
"WifiStationInfo": "Información de WiFi (Estación)",
"Status": "Estado",
"Enabled": "habilitado",
"Disabled": "deshabilitado",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Calidad",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Información de WiFi (Punto de acceso)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "# Estaciones"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaz de red ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de red",
"DefaultGateway": "Puerta de enlace predeterminada",
"Dns": "DNS {num}",
"MacAddress": "Dirección MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaz de red (Punto de acceso)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Información de NTP",
"ConfigurationSummary": "Resumen de configuración",
"Server": "Servidor",
"Timezone": "Zona horaria",
"TimezoneDescription": "Descripción de la zona horaria",
"CurrentTime": "Hora actual",
"Status": "Estado",
"Synced": "sincronizado",
"NotSynced": "no sincronizado",
"LocalTime": "Hora local",
"Sunrise": "Amanecer",
"Sunset": "Atardecer",
"NotAvailable": "No disponible",
"Mode": "Modo",
"Day": "Día",
"Night": "Noche"
},
"mqttinfo": {
"MqttInformation": "Información de MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Habilitado",
"Disabled": "Deshabilitado",
"Server": "@:ntpinfo.Server",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de usuario",
"BaseTopic": "Tema base",
"PublishInterval": "Intervalo de publicación",
"Seconds": "{sec} segundos",
"CleanSession": "Bandera CleanSession",
"Retain": "Retener",
"Tls": "TLS",
"RootCertifcateInfo": "Información del certificado raíz de CA",
"TlsCertLogin": "Iniciar sesión con certificado TLS",
"ClientCertifcateInfo": "Información del Certificado del Cliente",
"HassSummary": "Resumen de la Configuración de Descubrimiento Automático MQTT de Home Assistant",
"Expire": "Expirar",
"IndividualPanels": "Paneles Individuales",
"RuntimeSummary": "Resumen de Tiempo de Ejecución",
"ConnectionStatus": "Estado de Conexión",
"Connected": "conectado",
"Disconnected": "desconectado"
},
"console": {
"Console": "Consola",
"VirtualDebugConsole": "Consola de Depuración Virtual",
"EnableAutoScroll": "Habilitar Desplazamiento Automático",
"ClearConsole": "Limpiar Consola",
"CopyToClipboard": "Copiar al Portapapeles"
},
"inverterchannelinfo": {
"String": "Cadena {num}",
"Phase": "Fase {num}",
"General": "General"
},
"invertertotalinfo": {
"TotalYieldTotal": "Total de Rendimiento Acumulado",
"TotalYieldDay": "Total de Rendimiento del Día",
"TotalPower": "Potencia Total"
},
"inverterchannelproperty": {
"Power": "Potencia",
"Voltage": "Voltaje",
"Current": "Corriente",
"Power DC": "Potencia DC",
"YieldDay": "Rendimiento del Día",
"YieldTotal": "Rendimiento Total",
"Frequency": "Frecuencia",
"Temperature": "Temperatura",
"PowerFactor": "Factor de Potencia",
"ReactivePower": "Potencia Reactiva",
"Efficiency": "Eficiencia",
"Irradiation": "Irradiación"
},
"maintenancereboot": {
"DeviceReboot": "Reinicio del Dispositivo",
"PerformReboot": "Realizar Reinicio",
"Reboot": "¡Reiniciar!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Reiniciar OpenDTU",
"RebootQuestion": "¿Realmente desea reiniciar el dispositivo?",
"RebootHint": "<b>Nota:</b> Normalmente no es necesario realizar un reinicio manual. OpenDTU realiza cualquier reinicio necesario (por ejemplo, después de una actualización de firmware) automáticamente. También se adoptan configuraciones sin reiniciar. Si necesita reiniciar debido a un error, considere informarlo en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Configuración de DTU",
"DtuConfiguration": "Configuración de DTU",
"Serial": "Serial",
"SerialHint": "Tanto el inversor como el DTU tienen un número de serie. El número de serie del DTU se genera aleatoriamente en el primer inicio y generalmente no es necesario cambiarlo.",
"PollInterval": "Intervalo de Sondeo",
"Seconds": "Segundos",
"NrfPaLevel": "Potencia de Transmisión NRF24",
"CmtPaLevel": "Potencia de Transmisión CMT2300A",
"NrfPaLevelHint": "Utilizado para inversores HM. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtPaLevelHint": "Utilizado para inversores HMS/HMT. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtCountry": "Región/País CMT2300A",
"CmtCountryHint": "Cada país tiene asignaciones de frecuencia diferentes.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "América del Norte ({min}MHz - {max}MHz)",
"country_2": "Brasil ({min}MHz - {max}MHz)",
"CmtFrequency": "Frecuencia CMT2300A",
"CmtFrequencyHint": "¡Asegúrese de utilizar solo frecuencias permitidas en el país respectivo! Después de un cambio de frecuencia, puede tardar hasta 15 minutos en establecer una conexión.",
"CmtFrequencyWarning": "La frecuencia seleccionada está fuera del rango permitido en su región/país seleccionado. Asegúrese de que esta selección no infrinja ninguna regulación local.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Mínimo ({db} dBm)",
"Low": "Bajo ({db} dBm)",
"High": "Alto ({db} dBm)",
"Max": "Máximo ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Configuración de Seguridad",
"AdminPassword": "Contraseña de Administrador",
"Password": "Contraseña",
"RepeatPassword": "Repetir Contraseña",
"PasswordHint": "<b>Consejo:</b> La contraseña de administrador se utiliza para acceder a esta interfaz web (usuario 'admin'), pero también para conectarse al dispositivo cuando está en modo AP. Debe tener 8 a 64 caracteres.",
"Permissions": "Permisos",
"ReadOnly": "Permitir acceso de solo lectura a la interfaz web sin contraseña"
},
"ntpadmin": {
"NtpSettings": "Configuración de NTP",
"NtpConfiguration": "Configuración de NTP",
"TimeServer": "Servidor de Tiempo",
"TimeServerHint": "El valor predeterminado es adecuado siempre que OpenDTU tenga acceso directo a Internet.",
"Timezone": "Zona Horaria",
"TimezoneConfig": "Configuración de Zona Horaria",
"LocationConfiguration": "Configuración de Ubicación",
"Longitude": "Longitud",
"Latitude": "Latitud",
"SunSetType": "Tipo de Atardecer",
"SunSetTypeHint": "Afecta al cálculo día/noche. Puede tardar hasta un minuto en aplicarse el nuevo tipo.",
"OFFICIAL": "Amanecer estándar (90.8°)",
"NAUTICAL": "Amanecer náutico (102°)",
"CIVIL": "Amanecer civil (96°)",
"ASTONOMICAL": "Amanecer astronómico (108°)",
"ManualTimeSynchronization": "Sincronización Manual del Tiempo",
"CurrentOpenDtuTime": "Hora Actual de OpenDTU",
"CurrentLocalTime": "Hora Local Actual",
"SynchronizeTime": "Sincronizar Tiempo",
"SynchronizeTimeHint": "<b>Consejo:</b> Puede utilizar la sincronización manual del tiempo para establecer la hora actual de OpenDTU si no hay un servidor NTP disponible. Pero tenga en cuenta que en caso de un ciclo de energía, se perderá la hora. Además, tenga en cuenta que la precisión del tiempo se verá gravemente afectada, ya que no se puede resincronizar regularmente y el microcontrolador ESP32 no tiene un reloj en tiempo real."
},
"networkadmin": {
"NetworkSettings": "Configuración de Red",
"WifiConfiguration": "Configuración de WiFi",
"WifiSsid": "SSID de WiFi",
"WifiPassword": "Contraseña de WiFi",
"Hostname": "Nombre de Host",
"HostnameHint": "<b>Consejo:</b> El texto <span class=\"font-monospace\">%06X</span> se remplazará con los últimos 6 dígitos del ChipID de ESP en formato hexadecimal.",
"EnableDhcp": "Habilitar DHCP",
"StaticIpConfiguration": "Configuración de IP Estática",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de Red",
"DefaultGateway": "Puerta de Enlace Predeterminada",
"Dns": "Servidor DNS {num}",
"AdminAp": "Configuración de WiFi (Punto de Acceso de Administrador)",
"ApTimeout": "Tiempo de espera del Punto de Acceso",
"ApTimeoutHint": "Tiempo que se mantiene abierto el Punto de Acceso. Un valor de 0 significa infinito.",
"Minutes": "minutos",
"EnableMdns": "Habilitar mDNS",
"MdnsSettings": "Configuración de mDNS"
},
"mqttadmin": {
"MqttSettings": "Configuración de MQTT",
"MqttConfiguration": "Configuración de MQTT",
"EnableMqtt": "Habilitar MQTT",
"EnableHass": "Habilitar Descubrimiento Automático MQTT de Home Assistant",
"MqttBrokerParameter": "Parámetros del Broker MQTT",
"Hostname": "Nombre de Host",
"HostnameHint": "Nombre de host o dirección IP",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de Usuario",
"UsernameHint": "Nombre de usuario, dejar vacío para conexión anónima",
"Password": "Contraseña",
"PasswordHint": "Contraseña, dejar vacío para conexión anónima",
"BaseTopic": "Tema Base",
"BaseTopicHint": "Tema base, se antepondrá a todos los temas publicados (por ejemplo, inverter/)",
"PublishInterval": "Intervalo de Publicación",
"Seconds": "segundos",
"CleanSession": "Habilitar Bandera CleanSession",
"EnableRetain": "Habilitar Bandera Retain",
"EnableTls": "Habilitar TLS",
"RootCa": "Certificado Raíz CA (predeterminado Letsencrypt)",
"TlsCertLoginEnable": "Habilitar Inicio de Sesión con Certificado TLS",
"ClientCert": "Certificado del Cliente TLS",
"ClientKey": "Clave del Cliente TLS",
"LwtParameters": "Parámetros de LWT",
"LwtTopic": "Tema de LWT",
"LwtTopicHint": "Tema de LWT, se añadirá al tema base",
"LwtOnline": "Mensaje de LWT en línea",
"LwtOnlineHint": "Mensaje que se publicará en el tema de LWT cuando esté en línea",
"LwtOffline": "Mensaje de LWT fuera de línea",
"LwtOfflineHint": "Mensaje que se publicará en el tema de LWT cuando esté fuera de línea",
"LwtQos": "QoS (Calidad de Servicio)",
"QOS0": "0 (Como máximo una vez)",
"QOS1": "1 (Al menos una vez)",
"QOS2": "2 (Exactamente una vez)",
"HassParameters": "Parámetros de Descubrimiento Automático MQTT de Home Assistant",
"HassPrefixTopic": "Tema de Prefijo",
"HassPrefixTopicHint": "El prefijo para el tema de descubrimiento",
"HassRetain": "Habilitar Bandera Retain",
"HassExpire": "Habilitar Expiración",
"HassIndividual": "Paneles Individuales"
},
"inverteradmin": {
"InverterSettings": "Configuración del Inversor",
"AddInverter": "Agregar un nuevo Inversor",
"Serial": "Serial",
"Name": "Nombre",
"Add": "Agregar",
"AddHint": "<b>Consejo:</b> Puede configurar parámetros adicionales después de haber creado el inversor. Use el ícono de lápiz en la lista de inversores.",
"InverterList": "Lista de Inversores",
"Status": "Estado",
"Send": "Enviar",
"Receive": "Recibir",
"StatusHint": "<b>Consejo:</b> El inversor se alimenta con su entrada de CC. Si no hay sol, el inversor está apagado. Aún se pueden enviar solicitudes.",
"Type": "Tipo",
"Action": "Acción",
"SaveOrder": "Guardar orden",
"DeleteInverter": "Eliminar inversor",
"EditInverter": "Editar inversor",
"General": "General",
"String": "Cadena",
"Advanced": "Avanzado",
"InverterSerial": "Serial del Inversor:",
"InverterName": "Nombre del Inversor:",
"InverterNameHint": "Aquí puede especificar un nombre personalizado para su inversor.",
"InverterStatus": "Recibir / Enviar",
"PollEnable": "Sondear datos del inversor",
"PollEnableNight": "Sondear datos del inversor por la noche",
"CommandEnable": "Enviar comandos",
"CommandEnableNight": "Enviar comandos por la noche",
"StringName": "Nombre de cadena {num}:",
"StringNameHint": "Aquí puede especificar un nombre personalizado para el puerto respectivo de su inversor.",
"StringMaxPower": "Potencia máxima de cadena {num}:",
"StringMaxPowerHint": "Ingrese la potencia máxima de los paneles solares conectados.",
"StringYtOffset": "Compensación total de rendimiento de cadena {num}:",
"StringYtOffsetHint": "Esta compensación se aplica al valor total de rendimiento leído del inversor. Esto se puede usar para ajustar el rendimiento total del inversor a cero si se utiliza un inversor usado. Pero aún puede intentar sondear datos.",
"InverterHint": "*) Ingrese W<sub>p</sub> del canal para calcular la irradiación.",
"ReachableThreshold": "Umbral de Alcanzabilidad",
"ReachableThresholdHint": "Define cuántas solicitudes se permiten fallar hasta que el inversor se considere no alcanzable.",
"ZeroRuntime": "Datos de tiempo cero",
"ZeroRuntimeHint": "Datos de tiempo cero (sin datos de rendimiento) si el inversor se vuelve inalcanzable.",
"ZeroDay": "Rendimiento diario cero a medianoche",
"ZeroDayHint": "Esto solo funciona si el inversor es inalcanzable. Si se leen datos del inversor, se usarán sus valores. (El reinicio solo ocurre en el ciclo de energía)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "¿Está seguro de que desea eliminar el inversor \"{name}\" con número de serie {serial}?",
"Delete": "Eliminar",
"YieldDayCorrection": "Corrección de Rendimiento Diario",
"YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche"
},
"fileadmin": {
"ConfigManagement": "Gestión de Configuración",
"BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración",
"BackupConfig": "Copia de seguridad del archivo de configuración",
"Backup": "Copia de seguridad",
"Restore": "Restaurar",
"NoFileSelected": "Ningún archivo seleccionado",
"RestoreHeader": "Restaurar: Restaurar el Archivo de Configuración",
"Back": "Atrás",
"UploadSuccess": "Carga Exitosa",
"RestoreHint": "<b>Nota:</b> Esta operación reemplaza el archivo de configuración con la configuración restaurada y reinicia OpenDTU para aplicar todas las configuraciones.",
"ResetHeader": "Inicializar: Realizar Restablecimiento de Fábrica",
"FactoryResetButton": "Restaurar Configuraciones Predeterminadas de Fábrica",
"ResetHint": "<b>Nota:</b> Haga clic en Restaurar Configuraciones Predeterminadas de Fábrica para restaurar e inicializar las configuraciones predeterminadas de fábrica y reiniciar.",
"FactoryReset": "Restablecimiento de Fábrica",
"ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?",
"ResetConfirm": "Restablecimiento de Fábrica",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Iniciar Sesión",
"SystemLogin": "Inicio de Sesión en el Sistema",
"Username": "Nombre de Usuario",
"UsernameRequired": "Se requiere el nombre de usuario",
"Password": "Contraseña",
"PasswordRequired": "Se requiere la contraseña",
"LoginButton": "Iniciar Sesión"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Actualización de Firmware",
"Loading": "@:base.Loading",
"OtaError": "Error OTA",
"Back": "Atrás",
"Retry": "Reintentar",
"OtaStatus": "Estado OTA",
"OtaSuccess": "La carga de firmware fue exitosa. El dispositivo se reinició automáticamente. Cuando el dispositivo vuelva a ser accesible, la interfaz se recargará automáticamente.",
"FirmwareUpload": "Carga de Firmware",
"UploadProgress": "Progreso de Carga"
},
"about": {
"AboutOpendtu": "Acerca de OpenDTU",
"Documentation": "Documentation",
"DocumentationBody": "The firmware and hardware documentation can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origen del Proyecto",
"ProjectOriginBody1": "Este proyecto se inició a partir de <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">esta discusión. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "El protocolo de Hoymiles fue descifrado mediante los esfuerzos voluntarios de muchos participantes. OpenDTU, entre otros, se desarrolló basado en este trabajo. El proyecto está bajo una Licencia de Código Abierto (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">Licencia Pública General de GNU versión 2</a>).",
"ProjectOriginBody3": "El software se desarrolló según nuestro mejor conocimiento y creencia. Sin embargo, no se acepta ninguna responsabilidad por un mal funcionamiento o pérdida de garantía del inversor.",
"ProjectOriginBody4": "OpenDTU está disponible de forma gratuita. Si pagaste dinero por el software, probablemente te estafaron.",
"NewsUpdates": "Noticias y Actualizaciones",
"NewsUpdatesBody": "Las nuevas actualizaciones se pueden encontrar en Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Reporte de Errores",
"ErrorReportingBody": "Por favor, informa problemas utilizando la función proporcionada por <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discusión",
"DiscussionBody": "Discute con nosotros en <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "No se pudo conectar a un módulo de radio configurado. Por favor, verifica la conexión.",
"TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.",
"TimeSyncLink": "Por favor, verifica la configuración de tu hora.",
"DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.",
"DefaultPasswordLink": "Por favor, cambia la contraseña."
},
"deviceadmin": {
"DeviceManager": "Administrador de Dispositivos",
"ParseError": "Error de análisis en 'pin_mapping.json': {error}",
"PinAssignment": "Configuración de Conexión",
"SelectedProfile": "Perfil Seleccionado",
"DefaultProfile": "(Configuraciones predeterminadas)",
"ProfileHint": "Tu dispositivo puede dejar de responder si seleccionas un perfil incompatible. En este caso, debes realizar una eliminación a través de la interfaz serial.",
"Display": "Pantalla",
"PowerSafe": "Habilitar Ahorro de Energía",
"PowerSafeHint": "Apaga la pantalla si no hay un inversor produciendo.",
"Screensaver": "Habilitar Protector de Pantalla",
"ScreensaverHint": "Mueve la pantalla un poco en cada actualización para evitar el quemado. (Útil especialmente para pantallas OLED)",
"DiagramMode": "Modo de Diagrama",
"off": "Apagar",
"small": "Pequeño",
"fullscreen": "Pantalla Completa",
"DiagramDuration": "Duración del Diagrama",
"DiagramDurationHint": "El período de tiempo que se muestra en el diagrama.",
"Seconds": "Segundos",
"Contrast": "Contraste ({contrast})",
"Rotation": "Rotación",
"rot0": "Sin rotación",
"rot90": "Rotación de 90 grados",
"rot180": "Rotación de 180 grados",
"rot270": "Rotación de 270 grados",
"DisplayLanguage": "Idioma de la Pantalla",
"en": "Inglés",
"de": "Alemán",
"fr": "Francés",
"Leds": "LEDs",
"EqualBrightness": "Brillo Equitativo",
"LedBrightness": "Brillo del LED {led} ({brightness})"
},
"pininfo": {
"Category": "Categoría",
"Name": "Nombre",
"Number": "Número",
"ValueSelected": "Seleccionado",
"ValueActive": "Activo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
}
}
}

690
lang/it.lang.json Normal file
View File

@ -0,0 +1,690 @@
{
"meta": {
"name": "Italiano",
"code": "it"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Offline",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "oggi: %4.0f Wh",
"yield_today_kwh": "oggi: %.1f kWh",
"yield_total_kwh": "totale: %.1f kWh",
"yield_total_mwh": "totale: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Dati in tempo reale",
"Settings": "Impostazioni",
"NetworkSettings": "Impostazioni di rete",
"NTPSettings": "Impostazioni NTP",
"MQTTSettings": "Impostazioni MQTT",
"InverterSettings": "Impostazioni Inverter",
"SecuritySettings": "Impostazioni di Sicurezza",
"DTUSettings": "Impostazioni DTU",
"DeviceManager": "Gestione Dispositivi",
"ConfigManagement": "Gestione Configurazione",
"FirmwareUpgrade": "Aggiornamento Firmware",
"DeviceReboot": "Riavvio DTU",
"Info": "Info",
"System": "Sistema",
"Network": "Rete",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Console",
"About": "Informazioni DTU",
"Logout": "Esci",
"Login": "Login"
},
"base": {
"Loading": "Caricamento...",
"Reload": "Ricarica",
"Cancel": "Cancella",
"Save": "Salva",
"Refreshing": "Aggiorna",
"Pull": "Trascina in basso per aggiornare",
"Release": "Rilascia per aggiornare",
"Close": "Chiudi"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Scuro",
"Light": "Chiaro",
"Auto": "Automatico"
},
"apiresponse": {
"1001": "Settings saved!",
"1002": "No values found!",
"1003": "Data too large!",
"1004": "Failed to parse data!",
"1005": "Values are missing!",
"1006": "Write failed!",
"2001": "Serial cannot be zero!",
"2002": "Poll interval must be greater zero!",
"2003": "Invalid power level setting!",
"2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!",
"2005": "Invalid country selection!",
"3001": "Not deleted anything!",
"3002": "Configuration resettet. Rebooting now...",
"4001": "@:apiresponse.2001",
"4002": "Name must between 1 and {max} characters long!",
"4003": "Only {max} inverters are supported!",
"4004": "Inverter created!",
"4005": "Invalid ID specified!",
"4006": "Invalid amount of max channel setting given!",
"4007": "Inverter changed!",
"4008": "Inverter deleted!",
"4009": "Inverter order saved!",
"5001": "@:apiresponse.2001",
"5002": "Limit must between 1 and {max}!",
"5003": "Invalid type specified!",
"5004": "Invalid inverter specified!",
"6001": "Reboot triggered!",
"6002": "Reboot cancled!",
"7001": "MQTT Server must between 1 and {max} characters long!",
"7002": "Username must not longer then {max} characters!",
"7003": "Password must not longer then {max} characters!",
"7004": "Topic must not longer then {max} characters!",
"7005": "Topic must not contain space characters!",
"7006": "Topic must end with slash (/)!",
"7007": "Port must be a number between 1 and 65535!",
"7008": "Certificate must not longer then {max} characters!",
"7009": "LWT topic must not longer then {max} characters!",
"7010": "LWT topic must not contain space characters!",
"7011": "LWT online value must not longer then {max} characters!",
"7012": "LWT offline value must not longer then {max} characters!",
"7013": "Publish interval must be a number between {min} and {max}!",
"7014": "Hass topic must not longer then {max} characters!",
"7015": "Hass topic must not contain space characters!",
"7016": "LWT QOS must not greater then {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "IP address is invalid!",
"8002": "Netmask is invalid!",
"8003": "Gateway is invalid!",
"8004": "DNS Server IP 1 is invalid!",
"8005": "DNS Server IP 2 is invalid!",
"8006": "Administrative AccessPoint Timeout value is invalid",
"9001": "NTP Server must between 1 and {max} characters long!",
"9002": "Timezone must between 1 and {max} characters long!",
"9003": "Timezone description must between 1 and {max} characters long!",
"9004": "Year must be a number between {min} and {max}!",
"9005": "Month must be a number between {min} and {max}!",
"9006": "Day must be a number between {min} and {max}!",
"9007": "Hour must be a number between {min} and {max}!",
"9008": "Minute must be a number between {min} and {max}!",
"9009": "Second must be a number between {min} and {max}!",
"9010": "Time updated!",
"10001": "Password must between 8 and {max} characters long!",
"10002": "Authentication successful!",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004",
"12001": "Profil must between 1 and {max} characters long!"
},
"home": {
"LiveData": "Dati in tempo reale",
"SerialNumber": "Numero seriale: ",
"CurrentLimit": "Limite attuale: ",
"DataAge": "Aggiornamento Dati: ",
"Seconds": "{val} secondi",
"ShowSetInverterLimit": "Mostra / Imposta Limite di Potenza",
"TurnOnOff": "Accendi/Spegni Inverter",
"ShowInverterInfo": "Mostra info Inverter",
"ShowEventlog": "Mostra Log Eventi",
"UnreadMessages": "msg non letti",
"Loading": "@:base.Loading",
"EventLog": "Log Eventi",
"InverterInfo": "Info Inverter",
"LimitSettings": "Impostazioni Limite Potenza",
"LastLimitSetStatus": "Stato ultimo limite impostato:",
"SetLimit": "Imposta Limite a:",
"Relative": "Percentuale (%)",
"Absolute": "Assoluto (W)",
"LimitHint": "<b>Nota:</b> Se imposti il limite assoluto, il valore sul display sarà aggiornato dopo circa 4 minuti.",
"SetPersistent": "Imposta Limite in Modo Persistente",
"SetNonPersistent": "Imposta Limite Temporaneamente",
"PowerSettings": "Impostazioni Potenza",
"LastPowerSetStatus": "Ultimo Stato dell'Inverter:",
"TurnOn": "Accendi Inverter",
"TurnOff": "Spegni Inverter",
"Restart": "Riavvia Inverter",
"Failure": "Fallito",
"Pending": "In Attesa",
"Ok": "Ok",
"Unknown": "Sconosciuto",
"ShowGridProfile": "Mostra Settaggi Inverter",
"GridProfile": "Settaggi Inverter",
"LoadingInverter": "In attesa dei dati... (puo' richiedere fino a 10 secondi)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Inizio",
"Stop": "Fine",
"Id": "ID",
"Message": "Messaggio"
},
"devinfo": {
"NoInfo": "Informazioni non disponibili",
"NoInfoLong": "Ancora nessuna informazione dall'inverter. Sto riprovando...",
"UnknownModel": "Modello sconosciuto! Per favore fornisci \"Hardware Part Number\" ed il modello (esempio HM-350) in una Issue su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">GitHub</a>.",
"Serial": "Seriale",
"ProdYear": "Produzione Annua",
"ProdWeek": "Produzione Settimanale",
"Model": "Modello",
"DetectedMaxPower": "Rilevata potenza massima",
"BootloaderVersion": "Versione Bootloader",
"FirmwareVersion": "Versione Firmware",
"FirmwareBuildDate": "Data Firmware",
"HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nome",
"Version": "Versione",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Supporto sviluppatori",
"GridprofileSupportLong": "Clicca <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">qui</a> per ulteriori informazioni."
},
"systeminfo": {
"SystemInfo": "Info Sistema",
"VersionError": "Errore ricezione della versione",
"VersionNew": "Nuova versione disponibile! Mostra aggiornamenti!",
"VersionOk": "Già aggiornato!"
},
"firmwareinfo": {
"FirmwareInformation": "Info Firmware",
"Hostname": "Hostname",
"SdkVersion": "SDK Version",
"ConfigVersion": "Config Version",
"FirmwareVersion": "Firmware Version / Git Hash",
"PioEnv": "PIO Environment",
"FirmwareVersionHint": "Click here to show information about your current version",
"FirmwareUpdate": "Firmware Update",
"FirmwareUpdateHint": "Click here to view the changes between your version and the latest version",
"FrmwareUpdateAllow": "By activating the update check, a request is sent to GitHub.com each time the page is called up to retrieve the currently available version. If you do not agree with this, leave this function deactivated.",
"ResetReason0": "Reset Reason CPU 0",
"ResetReason1": "Reset Reason CPU 1",
"ConfigSaveCount": "Config save count",
"Uptime": "Uptime",
"UptimeValue": "0 days {time} | 1 day {time} | {count} days {time}"
},
"hardwareinfo": {
"HardwareInformation": "Info Hardware",
"ChipModel": "Chip Model",
"ChipRevision": "Chip Revision",
"ChipCores": "Chip Cores",
"CpuFrequency": "CPU Frequency",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Info Memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libera",
"Used": "Usata",
"Size": "Dimensione",
"Heap": "Heap",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Sketch"
},
"heapdetails": {
"HeapDetails": "Dettagli memoria Heap",
"TotalFree": "Libera totale",
"LargestFreeBlock": "Blocco contiguo libero più grande",
"MaxUsage": "Massima utilizzata dall'avvio",
"Fragmentation": "Livello frammentazione"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Info Transceiver Radio",
"Status": "{module} Stato",
"ChipStatus": "{module} Chip Stato",
"ChipType": "{module} Chip Tipo",
"Connected": "connesso",
"NotConnected": "non connesso",
"Configured": "configurato",
"NotConfigured": "no configurato",
"Unknown": "Sconosciuto"
},
"networkinfo": {
"NetworkInformation": "Informazioni Rete"
},
"wifistationinfo": {
"WifiStationInfo": "Info WiFi (Station)",
"Status": "Stato",
"Enabled": "abilitato",
"Disabled": "disabilitato",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Qualità",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Info WiFi (Access Point)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "Numero Stazioni"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaccia di Rete ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Gateway",
"Dns": "DNS {num}",
"MacAddress": "Indirizzo MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaccia di Rete (Access Point)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Informazioni NTP",
"ConfigurationSummary": "Riepilogo Configurazione",
"Server": "Server",
"Timezone": "Timezone",
"TimezoneDescription": "Descrizione Timezone",
"CurrentTime": "Data/Ora attuale",
"Status": "Stato",
"Synced": "sincronizzata",
"NotSynced": "non sincronizzata",
"LocalTime": "Ora Locale",
"Sunrise": "Alba",
"Sunset": "Tramonto",
"NotAvailable": "Non Disponibile",
"Mode": "Modalità",
"Day": "Giorno",
"Night": "Notte"
},
"mqttinfo": {
"MqttInformation": "Informazioni MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Server": "@:ntpinfo.Server",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"BaseTopic": "Topic Base",
"PublishInterval": "Intervallo Publish",
"Seconds": "{sec} secondi",
"CleanSession": "CleanSession",
"Retain": "Retain",
"Tls": "TLS",
"RootCertifcateInfo": "Info Certificato Root CA",
"TlsCertLogin": "Entra con Certificato TLS",
"ClientCertifcateInfo": "Info Certificato Client",
"HassSummary": "Riepilogo Configurazione Home Assistant MQTT Auto Discovery",
"Expire": "Scade",
"IndividualPanels": "Pannello Individuale",
"RuntimeSummary": "Riepilogo Runtime",
"ConnectionStatus": "Stato Connessione",
"Connected": "connesso",
"Disconnected": "disconnesso"
},
"console": {
"Console": "Console",
"VirtualDebugConsole": "Virtual Debug Console",
"EnableAutoScroll": "Abilita AutoScroll",
"ClearConsole": "Pulisci Console",
"CopyToClipboard": "Copia nella clipboard"
},
"inverterchannelinfo": {
"String": "Stringa {num}",
"Phase": "Fase {num}",
"General": "Generale"
},
"invertertotalinfo": {
"TotalYieldTotal": "Totale Energia",
"TotalYieldDay": "Energia Giornaliera",
"TotalPower": "Potenza Totale"
},
"inverterchannelproperty": {
"Power": "Potenza",
"Voltage": "Tensione",
"Current": "Corrente",
"Power DC": "PotenzaDC",
"YieldDay": "EnergiaOggi",
"YieldTotal": "EnergiaTotale",
"Frequency": "Frequenza",
"Temperature": "Temperatura",
"PowerFactor": "FattorePotenza",
"ReactivePower": "PotenzaReattiva",
"Efficiency": "Efficienza",
"Irradiation": "Irragiamento"
},
"maintenancereboot": {
"DeviceReboot": "Riavvio DTU",
"PerformReboot": "Fai il riavvio",
"Reboot": "Riavvio!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Riavvio OpenDTU",
"RebootQuestion": "Vuoi veramente riavvia il DTU?",
"RebootHint": "<b>Nota:</b> Normalmente non serve riavviare OpenDTU, in quanto esegue automaticamente il ravvio quando necessario (ad esempio dopo aggiornamento firmware). Modifiche alla configurazione vengono apprese subito, senza richiedere riavvio. Se devi riavviare a causa di un errore, ti preghiamo di segnalarcelo cliccando su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Impostazioni DTU",
"DtuConfiguration": "Configurazione DTU",
"Serial": "Seriale",
"SerialHint": "Sia il DTU che l'inverter hanno un numero seriale. Il numero seriale del DTU è generato casualmente al primo avvio e normalmente non serve modificarlo.",
"PollInterval": "Intervallo Interrogazione",
"Seconds": "Secondi",
"NrfPaLevel": "Potenza Trasmettitore NRF24",
"CmtPaLevel": "Potenza Trasmettitore CMT2300A",
"NrfPaLevelHint": "Usato per inverter HM. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtPaLevelHint": "Usato per inverter HMS/HMT. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtCountry": "CMT2300A Zona/Paese",
"CmtCountryHint": "Ogni zona ha una differente allocazione di frequenze utilizzabili.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "Nord America ({min}MHz - {max}MHz)",
"country_2": "Brasile ({min}MHz - {max}MHz)",
"CmtFrequency": "Frequenza CMT2300A",
"CmtFrequencyHint": "Fai attenzione ad usare solo frequenze ammesse nel tuo Paese! Dopo la modifica frequenza, servono fino a 15 minuti affinché la connessione si ristabilisca.",
"CmtFrequencyWarning": "La frequenza selezionata è fuori dal range selezionato dal tuo Paese. Verifica che la frequenza selezionata non violi le normative del tuo Paese.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minima ({db} dBm)",
"Low": "Bassa ({db} dBm)",
"High": "Alta ({db} dBm)",
"Max": "Massima ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Impostazioni di Sicurezza",
"AdminPassword": "Password Admin",
"Password": "Password",
"RepeatPassword": "Ripeti Password",
"PasswordHint": "<b>Nota:</b> La password di amministrazione viene utilizzata non solo per accedere a questa interfaccia web (con user 'admin'), ma anche per connettersi al dispositivo in modalità AP. Deve avere da 8 a 64 caratteri.",
"Permissions": "Permessi",
"ReadOnly": "Permetti accessi web in sola lettura senza richiedere la password"
},
"ntpadmin": {
"NtpSettings": "Impostazioni NTP (Data / Ora)",
"NtpConfiguration": "Configurazione NTP",
"TimeServer": "Server NTP",
"TimeServerHint": "Puoi lasciare il valore di default, nel caso in cui OpenDTU abbia accesso ad internet.",
"Timezone": "Timezone",
"TimezoneConfig": "Timezone Config",
"LocationConfiguration": "Configurazione Posizione",
"Longitude": "Longitudine",
"Latitude": "Latitudine",
"SunSetType": "Tipo di Alba",
"SunSetTypeHint": "Influenza il calcolo dell'ora di Alba/Tramonto. Dopo la conferma, è richiesto fino ad un minuto perché la modifica venga applicata.",
"OFFICIAL": "Standard dawn (90.8°)",
"NAUTICAL": "Nautical dawn (102°)",
"CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)",
"ManualTimeSynchronization": "Sincronizzazione Manuale Data/Ora",
"CurrentOpenDtuTime": "Ora OpenDTU attuale",
"CurrentLocalTime": "Ora Locale attuale",
"SynchronizeTime": "Sincronizza Data/Ora",
"SynchronizeTimeHint": "<b>Nota:</b> Puoi usare la sincronizzazione manuale per impostare Data/Ora nel caso che non sia disponibile un server NTP. In questo caso la data/ora viene persa in caso di mancata alimentazione. Inoltre, con la sincronizzazione manuale ci sarà una progressiva deriva della Data/Ora in quanto l'ESP32 non ha un Real Time Clock interno."
},
"networkadmin": {
"NetworkSettings": "Impostazioni di Rete",
"WifiConfiguration": "Configurazione WiFi",
"WifiSsid": "WiFi SSID",
"WifiPassword": "WiFi Password",
"Hostname": "Hostname",
"HostnameHint": "<b>Nota:</b> Il testo <span class=\"font-monospace\">%06X</span> sarà rimpiazzato con le ultime 6 cifre del ChipID dell'ESP32 in formato esadecimale.",
"EnableDhcp": "Abilita DHCP",
"StaticIpConfiguration": "Configurazione IP Statico",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Default Gateway",
"Dns": "DNS Server {num}",
"AdminAp": "Configurazione WiFi (Admin AccessPoint)",
"ApTimeout": "Timeout AccessPoint",
"ApTimeoutHint": "Tempo in cui la modalità AccessPoint rimarrà attiva. 0=per sempre.",
"Minutes": "minuti",
"EnableMdns": "Abilita mDNS",
"MdnsSettings": "Configurazione mDNS"
},
"mqttadmin": {
"MqttSettings": "Impostazioni MQTT",
"MqttConfiguration": "Configurazione MQTT",
"EnableMqtt": "Abilita MQTT",
"EnableHass": "Abilita Home Assistant MQTT Auto Discovery",
"MqttBrokerParameter": "Parametri Broker MQTT",
"Hostname": "Hostname",
"HostnameHint": "Hostname o Indirizzo IP",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"UsernameHint": "Username, lascia vuoto per connessione anonima",
"Password": "Password",
"PasswordHint": "Password, lascia vuota per connessione anonima",
"BaseTopic": "Topic Base",
"BaseTopicHint": "Topic Base, prefisso da aggiungere (ad esempio inverter/)",
"PublishInterval": "Intervallo pubblicazione",
"Seconds": "secondi",
"CleanSession": "Abilita CleanSession",
"EnableRetain": "Abilita Retain",
"EnableTls": "Abilita TLS",
"RootCa": "CA-Root-Certificate (default Letsencrypt)",
"TlsCertLoginEnable": "Abilita Login con certificato TLS",
"ClientCert": "TLS Client-Certificate",
"ClientKey": "TLS Client-Key",
"LwtParameters": "Parametri LWT",
"LwtTopic": "Topic LWT",
"LwtTopicHint": "Topic LWT, da aggiungere al Topic Base",
"LwtOnline": "Messaggio 'Online0 LWT",
"LwtOnlineHint": "Messaggio pubblicato quando online",
"LwtOffline": "Messaggio 'Offline' LWT",
"LwtOfflineHint": "Messaggio che sarà pubblicato quando offline",
"LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (Al massimo una volta)",
"QOS1": "1 (Almeno una volta)",
"QOS2": "2 (Esattamente una volta)",
"HassParameters": "Parametri Home Assistant MQTT Auto Discovery",
"HassPrefixTopic": "Prefisso Topic",
"HassPrefixTopicHint": "Prefisso per Topic autodiscovery",
"HassRetain": "Abilita Retain",
"HassExpire": "Abilita Scadenza",
"HassIndividual": "Pannelli Individuale"
},
"inverteradmin": {
"InverterSettings": "Impostazioni Inverter",
"AddInverter": "Aggiungi nuovo Inverter",
"Serial": "Seriale",
"Name": "Nome",
"Add": "Aggiungi",
"AddHint": "<b>Nota:</b> Potrai aggiungere ulteriori parametri dopo aver creato l'inverter, cliccando sull'icona 'Matita' nella lista inverter.",
"InverterList": "Lista Inverter",
"Status": "Stato",
"Send": "Invia",
"Receive": "Riceve",
"StatusHint": "<b>Nota:</b> L'inverter viene alimentato dal fotovoltaico. Durante la notte, l'inverter risulterà spento. Le richieste potranno comunque essere trasmesse.",
"Type": "Tipo",
"Action": "Azione",
"SaveOrder": "Salva ordine",
"DeleteInverter": "Rimuovi inverter",
"EditInverter": "Modifica inverter",
"General": "Generale",
"String": "Stringa",
"Advanced": "Avanzate",
"InverterSerial": "Seriale Inverter:",
"InverterName": "Nome Inverter:",
"InverterNameHint": "Puoi specificare un nome qualsiasi da assegnare all'inverter.",
"InverterStatus": "Riceve / Invia",
"PollEnable": "Interroga inverter",
"PollEnableNight": "Interroga inverter di notte",
"CommandEnable": "Invia comandi",
"CommandEnableNight": "Invia comandi di notte",
"StringName": "Nome stringa {num}:",
"StringNameHint": "Qui puoi specificare un nome qualsiasi per la porta dell'inverter o per il pannello fotovoltaico collegato.",
"StringMaxPower": "Massima potenza stringa {num}:",
"StringMaxPowerHint": "Inserisci la potenza massima associata ai panelli fotovoltaici collegati a questa stringa.",
"StringYtOffset": "Offset Energia totale per la stringa {num}:",
"StringYtOffsetHint": "Questo offset viene utilizzato per azzerare il contatore qualora venga usato un inverter usato.",
"InverterHint": "*) Inserisci la potenza W<sub>p</sub> dei pannelli fotovoltaici collegati alla stringa: servirà per calcolare l'irragiamento.",
"ReachableThreshold": "Reachable Threshold",
"ReachableThresholdHint": "Definisce il numero di richieste fallite prima che l'inverter sia considerato irraggiungibile.",
"ZeroRuntime": "Azzera dati in tempo reale",
"ZeroRuntimeHint": "Azzera i dati in tempo reale (tranne l'Energia) se l'inverter diventa irraggiunbile.",
"ZeroDay": "Azzera dati energia alla mezzanotte",
"ZeroDayHint": "Questo vale se l'inverter risulta irraggiungibile. Se l'inverter risponde anche di notte, verranno mostrati i suoi valori. (Il Reset si verifica al riavvio)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "Sicuro di voler rimuovere l'inverter \"{name}\" con numero seriale {serial}?",
"Delete": "Rimuovi",
"YieldDayCorrection": "Correzione energia giornaliera",
"YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte"
},
"fileadmin": {
"ConfigManagement": "Configurazione Gestione",
"BackupHeader": "Backup: Configurazione File Backup",
"BackupConfig": "Esegui il backup del file",
"Backup": "Backup",
"Restore": "Ripristina",
"NoFileSelected": "Nessun file selezionato",
"RestoreHeader": "Ripristina: Ripristina File Configurazione",
"Back": "Indietro",
"UploadSuccess": "Invio File con successo",
"RestoreHint": "<b>Nota:</b> questa operazione rimpiazza la configurazione con quella contenuta nel file, e poi riavvia automaticamente OpenDTU per applicare la nuova configurazione.",
"ResetHeader": "Inizializza: Esegui il Factory Reset",
"FactoryResetButton": "Ripristina Configurazione Factory-Default",
"ResetHint": "<b>Nota:</b> Clicca 'Ripristina Configurazione Factory-Default' per stabilire le impostazioni di fabbrica e riavviare automaticamente OpenDTU.",
"FactoryReset": "Factory Reset",
"ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?",
"ResetConfirm": "Factory Reset!",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Login",
"SystemLogin": "System Login",
"Username": "Username",
"UsernameRequired": "Inserisci Username",
"Password": "Password",
"PasswordRequired": "Inserisci Password",
"LoginButton": "Login"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Aggiornamento Firmware",
"Loading": "@:base.Loading",
"OtaError": "Errore aggiornamento OTA",
"Back": "Indietro",
"Retry": "Riprova",
"OtaStatus": "Stato OTA",
"OtaSuccess": "Aggiornamento firmware eseguito con successo. Il dispositivo si riavvierà automaticamente. Quando sarà nuovamente disponibile, l'interfacca sarà ricaricata automaticamente.",
"FirmwareUpload": "Invia Firmware",
"UploadProgress": "Upload in corso"
},
"about": {
"AboutOpendtu": "About OpenDTU",
"Documentation": "Documentazione",
"DocumentationBody": "La documentazione firmware e hardware sono disponibili qui: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origine Progetto",
"ProjectOriginBody1": "Questo progetto è partito da <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">questa discussione. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "Il protocollo Hoymiles è stato decriptato grazie al contributo volontario di molti programmatori. OpenDTU, fra gli altri, è stato sviluppato grazie a questo lavoro. Il progetto è distribuito con Licenza Open Source (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
"ProjectOriginBody3": "Il software è stato sviluppato con le nostre migliori conoscenze e convinzioni. Tuttavia, non si assume alcuna responsabilità per malfunzionamenti o perdita di garanzia dell'inverter.",
"ProjectOriginBody4": "OpenDTU è disponibile gratuitamente. Se hai pagato per questo software, probabilmente sei stato truffato.",
"NewsUpdates": "Novità e Aggiornamenti",
"NewsUpdatesBody": "Nuovi aggiornamenti sono disponibili su Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Segnalazione Errori",
"ErrorReportingBody": "Per favore segnala eventuali problemi utilizzando le funzionalità della piattaforma <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discussioni",
"DiscussionBody": "Puoi avviare una discussione con noi su <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "Non è possibile dialogare con il modulo radio selezionato. Controlla i collegamenti alla radio.",
"TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.",
"TimeSyncLink": "Controlla le impostazioni Data/Ora.",
"DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.",
"DefaultPasswordLink": "Per favore cambia la password."
},
"deviceadmin": {
"DeviceManager": "Device-Manager",
"ParseError": "Parse error in 'pin_mapping.json': {error}",
"PinAssignment": "Impostazioni Connessione",
"SelectedProfile": "Profilo selezionato",
"DefaultProfile": "(Impostazioni di Default)",
"ProfileHint": "Il tuo dispositivo potrebbe smettere di rispondere selezionando un profilo incompatibile. In questo caso, dovrai eseguire una cancellazione collegandoti all'interfaccia seriale.",
"Display": "Display",
"PowerSafe": "Abilita Risparmio Energetico",
"PowerSafeHint": "Spegni il display se l'inverter non produce.",
"Screensaver": "Abilita Screensaver",
"ScreensaverHint": "Muove il testo nel display per prevenire danneggiamento pixel. (Utile in caso di display OLED)",
"DiagramMode": "Modalità grafica",
"off": "Off",
"small": "Small",
"fullscreen": "Fullscreen",
"DiagramDuration": "Durata grafico",
"DiagramDurationHint": "Periodo che viene mostrato nel grafico.",
"Seconds": "Secondi",
"Contrast": "Contrasto ({contrast})",
"Rotation": "Rotazione",
"rot0": "Nessuna rotazione",
"rot90": "Rotazione 90 gradi",
"rot180": "Rotazione 180 gradi",
"rot270": "Rotazione 270 gradi",
"DisplayLanguage": "Linuga Display",
"en": "English",
"de": "German",
"fr": "French",
"Leds": "LEDs",
"EqualBrightness": "Equalizza luminosità",
"LedBrightness": "LED {led}, Luminosità ({brightness})"
},
"pininfo": {
"Category": "Categoria",
"Name": "Nome",
"Number": "Numero",
"ValueSelected": "Selezionato",
"ValueActive": "Attivo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
}
}
}

View File

@ -85,13 +85,13 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons
float ActivePowerControlCommand::getLimit() const
{
const float l = (((uint16_t)_payload[12] << 8) | _payload[13]);
const float l = (static_cast<uint16_t>(_payload[12]) << 8) | _payload[13];
return l / 10;
}
PowerLimitControlType ActivePowerControlCommand::getType()
{
return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]);
return (PowerLimitControlType)((static_cast<uint16_t>(_payload[14]) << 8) | _payload[15]);
}
void ActivePowerControlCommand::gotTimeout()

View File

@ -35,8 +35,8 @@ DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t route
void DevControlCommand::udpateCRC(const uint8_t len)
{
const uint16_t crc = crc16(&_payload[10], len);
_payload[10 + len] = (uint8_t)(crc >> 8);
_payload[10 + len + 1] = (uint8_t)(crc);
_payload[10 + len] = static_cast<uint8_t>(crc >> 8);
_payload[10 + len + 1] = static_cast<uint8_t>(crc);
}
bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

@ -63,10 +63,10 @@ uint8_t MultiDataCommand::getDataType() const
void MultiDataCommand::setTime(const time_t time)
{
_payload[12] = (uint8_t)(time >> 24);
_payload[13] = (uint8_t)(time >> 16);
_payload[14] = (uint8_t)(time >> 8);
_payload[15] = (uint8_t)(time);
_payload[12] = static_cast<uint8_t>(time >> 24);
_payload[13] = static_cast<uint8_t>(time >> 16);
_payload[14] = static_cast<uint8_t>(time >> 8);
_payload[15] = static_cast<uint8_t>(time);
udpateCRC();
}
@ -112,8 +112,8 @@ bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t
void MultiDataCommand::udpateCRC()
{
const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password
_payload[24] = (uint8_t)(crc >> 8);
_payload[25] = (uint8_t)(crc);
_payload[24] = static_cast<uint8_t>(crc >> 8);
_payload[25] = static_cast<uint8_t>(crc);
}
uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

@ -29,6 +29,10 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A }
};
HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
@ -53,3 +57,13 @@ uint8_t HERF_1CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HERF_1CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HERF_1CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -36,6 +36,11 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_B }
};
HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
@ -60,3 +65,13 @@ uint8_t HERF_2CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HERF_2CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HERF_2CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -28,6 +28,10 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A }
};
HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {};
@ -52,3 +56,13 @@ uint8_t HMS_1CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMS_1CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMS_1CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -11,4 +11,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -28,6 +28,10 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A }
};
HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {};
@ -52,3 +56,13 @@ uint8_t HMS_1CHv2::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMS_1CHv2::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMS_1CHv2::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -11,4 +11,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -35,6 +35,11 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_B }
};
HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {};
@ -59,3 +64,13 @@ uint8_t HMS_2CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMS_2CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMS_2CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -11,4 +11,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -49,6 +49,13 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_B },
{ CH2, MPPT_C },
{ CH3, MPPT_D }
};
HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {};
@ -73,3 +80,13 @@ uint8_t HMS_4CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMS_4CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMS_4CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -58,6 +58,13 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_A },
{ CH2, MPPT_B },
{ CH3, MPPT_B }
};
HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial)
: HMT_Abstract(radio, serial) {};
@ -82,3 +89,13 @@ uint8_t HMT_4CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMT_4CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMT_4CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -72,6 +72,15 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_A },
{ CH2, MPPT_B },
{ CH3, MPPT_B },
{ CH4, MPPT_C },
{ CH5, MPPT_C }
};
HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial)
: HMT_Abstract(radio, serial) {};
@ -96,3 +105,13 @@ uint8_t HMT_6CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HMT_6CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HMT_6CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -28,6 +28,10 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A }
};
HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
@ -36,10 +40,10 @@ bool HM_1CH::isValidSerial(const uint64_t serial)
// serial >= 0x112100000000 && serial <= 0x1121ffffffff
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) {
if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x12) {
return true;
}
@ -65,3 +69,13 @@ uint8_t HM_1CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HM_1CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HM_1CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -11,4 +11,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -36,6 +36,11 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_B }
};
HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
@ -44,10 +49,10 @@ bool HM_2CH::isValidSerial(const uint64_t serial)
// serial >= 0x114100000000 && serial <= 0x1141ffffffff
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) {
if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x14) {
return true;
}
@ -73,3 +78,13 @@ uint8_t HM_2CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HM_2CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HM_2CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -49,6 +49,13 @@ static const byteAssign_t byteAssignment[] = {
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
static const channelMetaData_t channelMetaData[] = {
{ CH0, MPPT_A },
{ CH1, MPPT_A },
{ CH2, MPPT_B },
{ CH3, MPPT_B }
};
HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
@ -57,10 +64,10 @@ bool HM_4CH::isValidSerial(const uint64_t serial)
// serial >= 0x116100000000 && serial <= 0x1161ffffffff
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) {
if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x16) {
return true;
}
@ -86,3 +93,13 @@ uint8_t HM_4CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
const channelMetaData_t* HM_4CH::getChannelMetaData() const
{
return channelMetaData;
}
uint8_t HM_4CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

View File

@ -10,4 +10,6 @@ public:
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
};

View File

@ -50,13 +50,13 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force)
if (!force) {
if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
if ((uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) {
if (static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt)) {
return false;
}
}
}
_lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG);
_lastAlarmLogCnt = static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG));
time_t now;
time(&now);

View File

@ -14,8 +14,8 @@ InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial)
char serial_buff[sizeof(uint64_t) * 8 + 1];
snprintf(serial_buff, sizeof(serial_buff), "%0x%08x",
((uint32_t)((serial >> 32) & 0xFFFFFFFF)),
((uint32_t)(serial & 0xFFFFFFFF)));
static_cast<uint32_t>((serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(serial & 0xFFFFFFFF));
_serialString = serial_buff;
_alarmLogParser.reset(new AlarmLogParser());
@ -298,3 +298,35 @@ void InverterAbstract::resetRadioStats()
{
RadioStats = {};
}
std::vector<ChannelNum_t> InverterAbstract::getChannelsDC() const
{
std::vector<ChannelNum_t> l;
for (uint8_t i = 0; i < getChannelMetaDataSize(); i++) {
l.push_back(getChannelMetaData()[i].ch);
}
return l;
}
std::vector<MpptNum_t> InverterAbstract::getMppts() const
{
std::vector<MpptNum_t> l;
for (uint8_t i = 0; i < getChannelMetaDataSize(); i++) {
auto m = getChannelMetaData()[i].mppt;
if (l.end() == std::find(l.begin(), l.end(), m)){
l.push_back(m);
}
}
return l;
}
std::vector<ChannelNum_t> InverterAbstract::getChannelsDCByMppt(const MpptNum_t mppt) const
{
std::vector<ChannelNum_t> l;
for (uint8_t i = 0; i < getChannelMetaDataSize(); i++) {
if (getChannelMetaData()[i].mppt == mppt) {
l.push_back(getChannelMetaData()[i].ch);
}
}
return l;
}

View File

@ -24,6 +24,20 @@ enum {
FRAGMENT_OK = 0
};
enum MpptNum_t {
MPPT_A = 0,
MPPT_B,
MPPT_C,
MPPT_D,
MPPT_CNT
};
// additional meta data per input channel
typedef struct {
ChannelNum_t ch; // channel 0 - 5
MpptNum_t mppt; // mppt a - d (0 - 3)
} channelMetaData_t;
#define MAX_RF_FRAGMENT_COUNT 13
class CommandAbstract;
@ -40,6 +54,9 @@ public:
virtual const byteAssign_t* getByteAssignment() const = 0;
virtual uint8_t getByteAssignmentSize() const = 0;
virtual const channelMetaData_t* getChannelMetaData() const = 0;
virtual uint8_t getChannelMetaDataSize() const = 0;
bool isProducing();
bool isReachable();
@ -112,6 +129,10 @@ public:
StatisticsParser* Statistics();
SystemConfigParaParser* SystemConfigPara();
std::vector<MpptNum_t> getMppts() const;
std::vector<ChannelNum_t> getChannelsDC() const;
std::vector<ChannelNum_t> getChannelsDCByMppt(const MpptNum_t mppt) const;
protected:
HoymilesRadio* _radio;

View File

@ -243,7 +243,7 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
HOY_SEMAPHORE_TAKE();
const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1];
const uint32_t wcode = static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset]) << 8 | _payloadAlarmLog[entryStartOffset + 1];
uint32_t startTimeOffset = 0;
if (((wcode >> 13) & 0x01) == 1) {
startTimeOffset = 12 * 60 * 60;
@ -255,8 +255,8 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
}
entry.MessageId = _payloadAlarmLog[entryStartOffset + 1];
entry.StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset;
entry.EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]);
entry.StartTime = ((static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 4]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset;
entry.EndTime = (static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 6]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 7]);
HOY_SEMAPHORE_GIVE();

View File

@ -61,6 +61,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02
{ { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01
{ { 0x10, 0x21, 0x21, ALL }, 700, "HMS-700-2T" }, // 00
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00
{ { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00
{ { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01
@ -149,7 +150,7 @@ void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate)
uint16_t DevInfoParser::getFwBuildVersion() const
{
HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1];
const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1];
HOY_SEMAPHORE_GIVE();
return ret;
}
@ -158,13 +159,13 @@ time_t DevInfoParser::getFwBuildDateTime() const
{
struct tm timeinfo = {};
HOY_SEMAPHORE_TAKE();
timeinfo.tm_year = ((((uint16_t)_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900;
timeinfo.tm_year = ((static_cast<uint16_t>(_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900;
timeinfo.tm_mon = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1;
timeinfo.tm_mday = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100;
timeinfo.tm_mon = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1;
timeinfo.tm_mday = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100;
timeinfo.tm_hour = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100;
timeinfo.tm_min = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100;
timeinfo.tm_hour = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100;
timeinfo.tm_min = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100;
HOY_SEMAPHORE_GIVE();
return timegm(&timeinfo);
@ -181,7 +182,7 @@ String DevInfoParser::getFwBuildDateTimeStr() const
uint16_t DevInfoParser::getFwBootloaderVersion() const
{
HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9];
const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9];
HOY_SEMAPHORE_GIVE();
return ret;
}
@ -189,11 +190,11 @@ uint16_t DevInfoParser::getFwBootloaderVersion() const
uint32_t DevInfoParser::getHwPartNumber() const
{
HOY_SEMAPHORE_TAKE();
const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3];
const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5];
const uint16_t hwpn_h = (static_cast<uint16_t>(_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3];
const uint16_t hwpn_l = (static_cast<uint16_t>(_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5];
HOY_SEMAPHORE_GIVE();
return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l);
return (static_cast<uint32_t>(hwpn_h) << 16) | static_cast<uint32_t>(hwpn_l);
}
String DevInfoParser::getHwVersion() const

View File

@ -443,7 +443,7 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
for (uint8_t val_id = 0; val_id < section_size; val_id++) {
auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition);
float value = (int16_t)((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]);
float value = static_cast<int16_t>((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]);
value /= itemDefinition.Divider;
GridProfileItem_t v;

View File

@ -45,7 +45,7 @@ void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t*
float SystemConfigParaParser::getLimitPercent() const
{
HOY_SEMAPHORE_TAKE();
const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0;
const float ret = ((static_cast<uint16_t>(_payload[2]) << 8) | _payload[3]) / 10.0;
HOY_SEMAPHORE_GIVE();
return ret;
}
@ -53,8 +53,8 @@ float SystemConfigParaParser::getLimitPercent() const
void SystemConfigParaParser::setLimitPercent(const float value)
{
HOY_SEMAPHORE_TAKE();
_payload[2] = ((uint16_t)(value * 10)) >> 8;
_payload[3] = ((uint16_t)(value * 10));
_payload[2] = static_cast<uint16_t>(value * 10) >> 8;
_payload[3] = static_cast<uint16_t>(value * 10);
HOY_SEMAPHORE_GIVE();
}

View File

@ -143,6 +143,17 @@ frozen::string const& veStruct::getPidAsString() const
*/
uint32_t veStruct::getFwVersionAsInteger() const
{
if (strlen(firmwareVer_FW) == 0) {
if (strlen(firmwareVer_FWE) == 0) { return 0; }
// the firmware version from the FWE field may be preceeded by a zero
// for padding as per VE.Direct protocol, which is fine for strtoul()
// when we use a fixed base. however, the postfix (2 chars) might be
// numeric as well to indicate a beta release, which we must not parse.
std::string strVer(firmwareVer_FWE, strlen(firmwareVer_FWE) - 2);
return static_cast<uint32_t>(strtoul(strVer.c_str(), nullptr, 10));
}
char const* strVersion = firmwareVer_FW;
// VE.Direct protocol manual states that the first char can be a non-digit,
@ -157,6 +168,34 @@ uint32_t veStruct::getFwVersionAsInteger() const
*/
String veStruct::getFwVersionFormatted() const
{
if (strlen(firmwareVer_FW) == 0 && strlen(firmwareVer_FWE) == 0) {
return "n/a";
}
if (strlen(firmwareVer_FWE) > 0) {
char const* strVersion = firmwareVer_FWE;
// the firmware version from the FWE field may be preceeded by a zero
// for padding as per VE.Direct protocol.
while (strVersion[0] == '0') { ++strVersion; }
String res(strVersion[0]);
strVersion++;
res += ".";
res += strVersion[0];
strVersion++;
res += strVersion[0];
strVersion++;
String suffix(strVersion);
suffix.toUpperCase();
if (suffix == "FF") { return res; }
res += "-beta-";
res += suffix;
return res;
}
char const* strVersion = firmwareVer_FW;
char rc = 0;
@ -191,7 +230,7 @@ frozen::string const& veMpptStruct::getCsAsString() const
{ 0, "OFF" },
{ 2, "Fault" },
{ 3, "Bulk" },
{ 4, "Absorbtion" },
{ 4, "Absorption" },
{ 5, "Float" },
{ 7, "Equalize (manual)" },
{ 245, "Starting-up" },
@ -287,18 +326,27 @@ frozen::string const& VeDirectHexData::getResponseAsString() const
frozen::string const& VeDirectHexData::getRegisterAsString() const
{
using Register = VeDirectHexRegister;
static constexpr frozen::map<Register, frozen::string, 11> values = {
static constexpr frozen::map<Register, frozen::string, 20> values = {
{ Register::DeviceMode, "Device Mode" },
{ Register::DeviceState, "Device State" },
{ Register::RemoteControlUsed, "Remote Control Used" },
{ Register::PanelVoltage, "Panel Voltage" },
{ Register::PanelPower, "Panel Power" },
{ Register::ChargerVoltage, "Charger Voltage" },
{ Register::ChargerCurrent, "Charger Current" },
{ Register::NetworkTotalDcInputPower, "Network Total DC Input Power" },
{ Register::ChargeControllerTemperature, "Charger Controller Temperature" },
{ Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" },
{ Register::NetworkInfo, "Network Info" },
{ Register::NetworkMode, "Network Mode" },
{ Register::NetworkStatus, "Network Status" }
{ Register::NetworkStatus, "Network Status" },
{ Register::BatteryAbsorptionVoltage, "Battery Absorption Voltage" },
{ Register::BatteryFloatVoltage, "Battery Float Voltage" },
{ Register::TotalChargeCurrent, "Total Charge Current" },
{ Register::ChargeStateElapsedTime, "Charge State Elapsed Time" },
{ Register::BatteryVoltageSense, "Battery Voltage Sense" },
{ Register::LoadCurrent, "Load current" },
{ Register::LoadOutputVoltage, "Load Output Voltage" }
};
return getAsString(values, addr);

View File

@ -11,6 +11,8 @@ typedef struct {
uint16_t productID_PID = 0; // product id
char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number
char firmwareVer_FW[VE_MAX_VALUE_LEN]; // firmware release number
// some devices use "FWE" instead of "FW" for the firmware version.
char firmwareVer_FWE[VE_MAX_VALUE_LEN]; // firmware release number (alternative field)
uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV
int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative)
float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average)
@ -26,8 +28,6 @@ struct veMpptStruct : veStruct {
uint32_t panelVoltage_VPV_mV; // panel voltage in mV
uint32_t panelCurrent_mA; // panel current in mA (calculated)
int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used)
uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output)
bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk
uint8_t errorCode_ERR; // error code
uint32_t offReason_OR; // off reason
@ -38,6 +38,15 @@ struct veMpptStruct : veStruct {
uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh
uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W
// these are optional values communicated through the TEXT protocol. the pair's first
// value is the timestamp the respective info was last received. if it is
// zero, the value is deemed invalid. the timestamp is reset if no current
// value could be retrieved.
std::pair<uint32_t, bool> loadOutputState_LOAD; // physical load output or virtual load output state (on if battery voltage
// reaches upper limit, off if battery reaches lower limit)
std::pair<uint32_t, uint32_t> loadCurrent_IL_mA; // Load current in mA (Available only for models with a physical load output)
std::pair<uint32_t, bool> relayState_RELAY; // relay alarm state. On=true, Off=false
// these are values communicated through the HEX protocol. the pair's first
// value is the timestamp the respective info was last received. if it is
// zero, the value is deemed invalid. the timestamp is reset if no current
@ -45,6 +54,8 @@ struct veMpptStruct : veStruct {
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
std::pair<uint32_t, uint32_t> BatteryAbsorptionMilliVolt;
std::pair<uint32_t, uint32_t> BatteryFloatMilliVolt;
std::pair<uint32_t, uint8_t> NetworkInfo;
std::pair<uint32_t, uint8_t> NetworkMode;
std::pair<uint32_t, uint8_t> NetworkStatus;
@ -121,7 +132,9 @@ enum class VeDirectHexRegister : uint16_t {
DeviceState = 0x0201,
RemoteControlUsed = 0x0202,
PanelVoltage = 0xEDBB,
PanelPower = 0xEDBC,
ChargerVoltage = 0xEDD5,
ChargerCurrent = 0xEDD7,
NetworkTotalDcInputPower = 0x2027,
ChargeControllerTemperature = 0xEDDB,
SmartBatterySenseTemperature = 0xEDEC,
@ -129,7 +142,14 @@ enum class VeDirectHexRegister : uint16_t {
NetworkMode = 0x200E,
NetworkStatus = 0x200F,
HistoryTotal = 0x104F,
HistoryMPPTD30 = 0x10BE
HistoryMPPTD30 = 0x10BE,
BatteryAbsorptionVoltage = 0xEDF7,
BatteryFloatVoltage = 0xEDF6,
TotalChargeCurrent = 0x2013,
ChargeStateElapsedTime= 0x2007,
BatteryVoltageSense = 0x2002,
LoadCurrent = 0xEDAD,
LoadOutputVoltage = 0xEDA9
};
struct VeDirectHexData {

View File

@ -249,10 +249,18 @@ void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::stri
}
if (name == "FW") {
_tmpFrame.firmwareVer_FWE[0] = '\0';
strncpy(_tmpFrame.firmwareVer_FW, value.c_str(), sizeof(_tmpFrame.firmwareVer_FW));
return;
}
// some devices use "FWE" instead of "FW" for the firmware version.
if (name == "FWE") {
_tmpFrame.firmwareVer_FW[0] = '\0';
strncpy(_tmpFrame.firmwareVer_FWE, value.c_str(), sizeof(_tmpFrame.firmwareVer_FWE));
return;
}
if (name == "V") {
_tmpFrame.batteryVoltage_V_mV = atol(value.c_str());
return;

View File

@ -27,12 +27,13 @@ public:
bool isDataValid() const; // return true if data valid and not outdated
T const& getData() const { return _tmpFrame; }
bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0);
bool isStateIdle() const { return (_state == State::IDLE); }
protected:
VeDirectFrameHandler();
void init(char const* who, int8_t rx, int8_t tx, Print* msgOut,
bool verboseLogging, uint8_t hwSerialPort);
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembled hex response
bool _verboseLogging;
Print* _msgOut;

View File

@ -8,7 +8,7 @@ HexHandler.cpp
* 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter.
* 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function
* void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data)
* to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler()
* to handle the received hex messages. All hex messages will be forwarded to function hexDataHandler()
* 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits.
*
* 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages
@ -63,9 +63,9 @@ static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
* disassembleHexData()
* analysis the hex message and extract: response, address, flags and value/text
* buffer: pointer to message (ascii hex little endian format)
* data: disassembeled message
* return: true = successful disassembeld, false = hex sum fault or message
* do not aligin with VE.Diekt syntax
* data: disassembled message
* return: true = successful disassembled, false = hex sum fault or message
* do not align with VE.Direct syntax
*/
template<typename T>
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
@ -164,14 +164,14 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) {
* addr: register address, default 0
* value: value to write into a register, default 0
* valsize: size of the value, 8, 16 or 32 bit, default 0
* return: true = message assembeld and send, false = it was not possible to put the message together
* return: true = message assembled and send, false = it was not possible to put the message together
* SAMPLE: ping command: sendHexCommand(PING),
* read total DC input power sendHexCommand(GET, 0xEDEC)
* set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16)
*
* WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will
* lead to early failure.
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf"
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Victron doc "BlueSolar-HEX-protocol.pdf"
*/
template<typename T>
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {

View File

@ -22,11 +22,18 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut,
bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
{
if (name == "IL") {
_tmpFrame.loadCurrent_IL_mA = atol(value.c_str());
_tmpFrame.loadCurrent_IL_mA.second = atol(value.c_str());
_tmpFrame.loadCurrent_IL_mA.first = millis();
return true;
}
if (name == "LOAD") {
_tmpFrame.loadOutputState_LOAD = (value == "ON");
_tmpFrame.loadOutputState_LOAD.second = (value == "ON");
_tmpFrame.loadOutputState_LOAD.first = millis();
return true;
}
if (name == "RELAY") {
_tmpFrame.relayState_RELAY.second = (value == "ON");
_tmpFrame.relayState_RELAY.first = millis();
return true;
}
if (name == "CS") {
@ -97,51 +104,46 @@ void VeDirectMpptController::frameValidEvent() {
}
// calculation of the MPPT efficiency
float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f;
float loadCurrent = (_tmpFrame.loadCurrent_IL_mA.first > 0) ? _tmpFrame.loadCurrent_IL_mA.second / 1000.0f : 0.0f;
float totalPower_W = (loadCurrent + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV / 1000.0f;
if (_tmpFrame.panelPower_PPV_W > 0) {
_efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W);
_tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage();
} else {
_tmpFrame.mpptEfficiency_Percent = 0.0f;
}
if (!_canSend) { return; }
// Copy from the "VE.Direct Protocol" documentation
// For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the
// charger periodically sends human readable (TEXT) data to the serial port. For firmware
// versions v1.53 and above, the charger always periodically sends TEXT data to the serial port.
// --> We just use hex commandes for firmware >= 1.53 to keep text messages alive
if (_tmpFrame.getFwVersionAsInteger() < 153) { return; }
using Command = VeDirectHexCommand;
using Register = VeDirectHexRegister;
sendHexCommand(Command::GET, Register::ChargeControllerTemperature);
sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature);
sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower);
#ifdef PROCESS_NETWORK_STATE
sendHexCommand(Command::GET, Register::NetworkInfo);
sendHexCommand(Command::GET, Register::NetworkMode);
sendHexCommand(Command::GET, Register::NetworkStatus);
#endif // PROCESS_NETWORK_STATE
}
void VeDirectMpptController::loop()
{
// First we send HEX-Commands (timing improvement)
if (isHexCommandPossible()) {
sendNextHexCommandFromQueue();
}
// Second we read Text- and HEX-Messages
VeDirectFrameHandler::loop();
// Note: Room for improvement, longer data valid time for slow changing values?
auto resetTimestamp = [this](auto& pair) {
if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) {
pair.first = 0;
}
};
// Check if optional TEXT-Data is outdated
resetTimestamp(_tmpFrame.loadOutputState_LOAD);
resetTimestamp(_tmpFrame.loadCurrent_IL_mA);
resetTimestamp(_tmpFrame.relayState_RELAY);
// Third we check if HEX-Data is outdated
if (!isHexCommandPossible()) { return; }
resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts);
resetTimestamp(_tmpFrame.BatteryFloatMilliVolt);
resetTimestamp(_tmpFrame.BatteryAbsorptionMilliVolt);
#ifdef PROCESS_NETWORK_STATE
resetTimestamp(_tmpFrame.NetworkInfo);
@ -153,8 +155,8 @@ void VeDirectMpptController::loop()
/*
* hexDataHandler()
* analyse the content of VE.Direct hex messages
* Handels the received hex data from the MPPT
* analyze the content of VE.Direct hex messages
* handles the received hex data from the MPPT
*/
bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
if (data.rsp != VeDirectHexResponse::GET &&
@ -162,6 +164,11 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
auto regLog = static_cast<uint16_t>(data.addr);
// we check whether the answer matches a previously asked query
if ((data.rsp == VeDirectHexResponse::GET) && (data.addr == _hexQueue[_sendQueueNr]._hexRegister)) {
_sendTimeout = 0;
}
switch (data.addr) {
case VeDirectHexRegister::ChargeControllerTemperature:
_tmpFrame.MpptTemperatureMilliCelsius =
@ -215,6 +222,29 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
return true;
break;
case VeDirectHexRegister::BatteryAbsorptionVoltage:
_tmpFrame.BatteryAbsorptionMilliVolt =
{ millis(), static_cast<uint32_t>(data.value) * 10 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: MPPT Absorption Voltage (0x%04X): %.2fV\r\n",
_logId, regLog,
_tmpFrame.BatteryAbsorptionMilliVolt.second / 1000.0);
}
return true;
break;
case VeDirectHexRegister::BatteryFloatVoltage:
_tmpFrame.BatteryFloatMilliVolt =
{ millis(), static_cast<uint32_t>(data.value) * 10 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: MPPT Float Voltage (0x%04X): %.2fV\r\n",
_logId, regLog,
_tmpFrame.BatteryFloatMilliVolt.second / 1000.0);
}
return true;
break;
#ifdef PROCESS_NETWORK_STATE
case VeDirectHexRegister::NetworkInfo:
_tmpFrame.NetworkInfo =
@ -257,3 +287,66 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
return false;
}
/*
* isHexCommandPossible()
* return: true = yes we can use Hex-Commands
*/
bool VeDirectMpptController::isHexCommandPossible(void) {
// Copy from the "VE.Direct Protocol" documentation
// For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the
// charger periodically sends human readable (TEXT) data to the serial port. For firmware
// versions v1.53 and above, the charger always periodically sends TEXT data to the serial port.
// --> We just use hex commands for firmware >= 1.53 to keep text messages alive
return (_canSend && (_tmpFrame.getFwVersionAsInteger() >= 153));
}
/*
* sendNextHexCommandFromQueue()
* send one Hex Commands from the Hex Command Queue
* handles the received hex data from the MPPT
*/
void VeDirectMpptController::sendNextHexCommandFromQueue(void) {
// It seems some commands get lost if we send to fast the next command.
// maybe we produce an overflow on the MPPT receive buffer or we have to
// wait for the MPPT answer before we can send the next command. We only
// send a new query in VE.Direct idle state and if no query is pending. In
// case we do not get an answer we send the next query from the queue after
// a timeout of 500ms. NOTE: _sendTimeout will be set to 0 after receiving
// an answer, see function hexDataHandler().
auto millisTime = millis();
if (isStateIdle() && ((millisTime - _hexQueue[_sendQueueNr]._lastSendTime) > _sendTimeout)) {
// we do 2 loops, first for high prio commands and second for low prio commands
bool prio = true;
for (auto idy = 0; idy < 2; ++idy) {
// we start searching the queue with the next queue index
auto idx = _sendQueueNr + 1;
if (idx >= _hexQueue.size()) { idx = 0; }
do {
// we check if it is time to send the command again
if (((prio && (_hexQueue[idx]._readPeriod == HIGH_PRIO_COMMAND)) ||
(!prio && (_hexQueue[idx]._readPeriod != HIGH_PRIO_COMMAND))) &&
(millisTime - _hexQueue[idx]._lastSendTime) > (_hexQueue[idx]._readPeriod * 1000)) {
sendHexCommand(VeDirectHexCommand::GET, _hexQueue[idx]._hexRegister);
_hexQueue[idx]._lastSendTime = millisTime;
// we need this information to check if we get an answer, see hexDataHandler()
_sendTimeout = 500;
_sendQueueNr = idx;
return;
}
++idx;
if (idx == _hexQueue.size()) { idx = 0; }
} while (idx != _sendQueueNr);
prio = false; // second loop for low prio commands
}
}
}

View File

@ -36,6 +36,12 @@ private:
size_t _count;
};
struct VeDirectHexQueue {
VeDirectHexRegister _hexRegister; // hex register
uint8_t _readPeriod; // time period in sec until we send the command again
uint32_t _lastSendTime; // time stamp in milli sec of last send
};
class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
public:
VeDirectMpptController() = default;
@ -51,5 +57,18 @@ private:
bool hexDataHandler(VeDirectHexData const &data) final;
bool processTextDataDerived(std::string const& name, std::string const& value) final;
void frameValidEvent() final;
void sendNextHexCommandFromQueue(void);
bool isHexCommandPossible(void);
MovingAverage<float, 5> _efficiency;
uint32_t _sendTimeout = 0; // timeout until we send the next command from the queue
size_t _sendQueueNr = 0; // actual queue position;
// for slow changing values we use a send time period of 4 sec
#define HIGH_PRIO_COMMAND 1
std::array<VeDirectHexQueue, 5> _hexQueue { VeDirectHexRegister::NetworkTotalDcInputPower, HIGH_PRIO_COMMAND, 0,
VeDirectHexRegister::ChargeControllerTemperature, 4, 0,
VeDirectHexRegister::SmartBatterySenseTemperature, 4, 0,
VeDirectHexRegister::BatteryFloatVoltage, 4, 0,
VeDirectHexRegister::BatteryAbsorptionVoltage, 4, 0 };
};

View File

@ -130,4 +130,4 @@ def esp32_create_combined_bin(source, target, env):
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
env.AddPostAction("buildprog", esp32_create_combined_bin)

View File

@ -41,10 +41,10 @@ build_unflags =
-std=gnu++11
lib_deps =
mathieucarbou/ESPAsyncWebServer @ 3.3.16
mathieucarbou/ESPAsyncWebServer @ 3.3.22
bblanchon/ArduinoJson @ 7.2.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.9
nrf24/RF24 @ 1.4.10
olikraus/U8g2 @ 2.36.2
buelowp/sunset @ 1.1.7
arkhipenko/TaskScheduler @ 3.8.5

View File

@ -4,6 +4,7 @@
#include "PylontechCanReceiver.h"
#include "SBSCanReceiver.h"
#include "JkBmsController.h"
#include "JbdBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "PytesCanReceiver.h"
@ -41,7 +42,7 @@ void BatteryClass::updateSettings()
_upProvider = nullptr;
}
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
bool verboseLogging = config.Battery.VerboseLogging;
@ -65,6 +66,9 @@ void BatteryClass::updateSettings()
case 5:
_upProvider = std::make_unique<SBSCanReceiver>();
break;
case 6:
_upProvider = std::make_unique<JbdBms::Controller>();
break;
default:
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
return;
@ -86,19 +90,31 @@ void BatteryClass::loop()
float BatteryClass::getDischargeCurrentLimit()
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!config.Battery.EnableDischargeCurrentLimit) { return FLT_MAX; }
auto dischargeCurrentLimit = config.Battery.DischargeCurrentLimit;
auto dischargeCurrentValid = dischargeCurrentLimit > 0.0f;
auto dischargeCurrentLimitValid = dischargeCurrentLimit > 0.0f;
auto dischargeCurrentLimitBelowSoc = config.Battery.DischargeCurrentLimitBelowSoc;
auto dischargeCurrentLimitBelowVoltage = config.Battery.DischargeCurrentLimitBelowVoltage;
auto statsSoCValid = getStats()->getSoCAgeSeconds() <= 60 && !config.PowerLimiter.IgnoreSoc;
auto statsSoC = statsSoCValid ? getStats()->getSoC() : 100.0; // fail open so we use voltage instead
auto statsVoltageValid = getStats()->getVoltageAgeSeconds() <= 60;
auto statsVoltage = statsVoltageValid ? getStats()->getVoltage() : 0.0; // fail closed
auto statsCurrentLimit = getStats()->getDischargeCurrentLimit();
auto statsLimitValid = config.Battery.UseBatteryReportedDischargeCurrentLimit
&& statsCurrentLimit >= 0.0f
&& getStats()->getDischargeCurrentLimitAgeSeconds() <= 60;
if (statsLimitValid && dischargeCurrentValid) {
if (statsSoC > dischargeCurrentLimitBelowSoc && statsVoltage > dischargeCurrentLimitBelowVoltage) {
// Above SoC and Voltage thresholds, ignore custom limit.
// Battery-provided limit will still be applied.
dischargeCurrentLimitValid = false;
}
if (statsLimitValid && dischargeCurrentLimitValid) {
// take the lowest limit
return min(statsCurrentLimit, dischargeCurrentLimit);
}
@ -107,7 +123,7 @@ float BatteryClass::getDischargeCurrentLimit()
return statsCurrentLimit;
}
if (dischargeCurrentValid) {
if (dischargeCurrentLimitValid) {
return dischargeCurrentLimit;
}

View File

@ -5,6 +5,7 @@
#include "Configuration.h"
#include "MqttSettings.h"
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "MqttSettings.h"
template<typename T>
@ -163,7 +164,6 @@ void SBSBatteryStats::getLiveViewData(JsonVariant& root) const
// values go into the "Status" card of the web application
addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1);
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
@ -337,6 +337,79 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
}
}
void JbdBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
{
BatteryStats::getLiveViewData(root);
using Label = JbdBms::DataPointLabel;
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
auto voltage = static_cast<float>(*oVoltage) / 1000;
addLiveViewValue(root, "power", current * voltage , "W", 2);
}
auto oBatteryChargeEnabled = _dataPoints.get<Label::BatteryChargeEnabled>();
if (oBatteryChargeEnabled.has_value()) {
addLiveViewTextValue(root, "chargeEnabled", (*oBatteryChargeEnabled?"yes":"no"));
}
auto oBatteryDischargeEnabled = _dataPoints.get<Label::BatteryDischargeEnabled>();
if (oBatteryDischargeEnabled.has_value()) {
addLiveViewTextValue(root, "dischargeEnabled", (*oBatteryDischargeEnabled?"yes":"no"));
}
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperatureOne.has_value()) {
addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0);
}
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
if (oTemperatureTwo.has_value()) {
addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0);
}
if (_cellVoltageTimestamp > 0) {
addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
}
auto oBalancingEnabled = _dataPoints.get<Label::BalancingEnabled>();
if (oBalancingEnabled.has_value()) {
addLiveViewTextInSection(root, "cells", "balancingActive", (*oBalancingEnabled?"yes":"no"));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
#define ISSUE(t, x) \
auto x = *oAlarms & static_cast<uint16_t>(JbdBms::AlarmBits::x); \
addLiveView##t(root, "JbdBmsIssue"#x, x > 0);
//ISSUE(Warning, LowCapacity);
ISSUE(Alarm, CellOverVoltage);
ISSUE(Alarm, CellUnderVoltage);
ISSUE(Alarm, PackOverVoltage);
ISSUE(Alarm, PackUnderVoltage);
ISSUE(Alarm, ChargingOverTemperature);
ISSUE(Alarm, ChargingLowTemperature);
ISSUE(Alarm, DischargingOverTemperature);
ISSUE(Alarm, DischargingLowTemperature);
ISSUE(Alarm, ChargingOverCurrent);
ISSUE(Alarm, DischargeOverCurrent);
ISSUE(Alarm, ShortCircuit);
ISSUE(Alarm, IcFrontEndError);
ISSUE(Alarm, MosSotwareLock);
ISSUE(Alarm, Reserved1);
ISSUE(Alarm, Reserved2);
ISSUE(Alarm, Reserved3);
#undef ISSUE
}
}
void BatteryStats::mqttLoop()
{
auto& config = Configuration.get();
@ -419,7 +492,6 @@ void SBSBatteryStats::mqttPublish() const
MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage));
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation));
MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation));
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
MqttSettings.publish("battery/current", String(_current));
MqttSettings.publish("battery/temperature", String(_temperature));
@ -573,6 +645,66 @@ void JkBmsBatteryStats::mqttPublish() const
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
void JbdBmsBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
using Label = JbdBms::DataPointLabel;
static std::vector<Label> mqttSkip = {
Label::CellsMilliVolt, // complex data format
Label::BatteryVoltageMilliVolt, // already published by base class
Label::BatterySoCPercent // already published by base class
};
// regularly publish all topics regardless of whether or not their value changed
bool neverFullyPublished = _lastFullMqttPublish == 0;
bool intervalElapsed = _lastFullMqttPublish + getMqttFullPublishIntervalMs() < millis();
bool fullPublish = neverFullyPublished || intervalElapsed;
for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
// skip data points that did not change since last published
if (!fullPublish && iter->second.getTimestamp() < _lastMqttPublish) { continue; }
auto skipMatch = std::find(mqttSkip.begin(), mqttSkip.end(), iter->first);
if (skipMatch != mqttSkip.end()) { continue; }
String topic((std::string("battery/") + iter->second.getLabelText()).c_str());
MqttSettings.publish(topic, iter->second.getValueText().c_str());
}
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value() && (fullPublish || _cellVoltageTimestamp > _lastMqttPublish)) {
unsigned idx = 1;
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
String topic("battery/Cell");
topic += String(idx);
topic += "MilliVolt";
MqttSettings.publish(topic, String(iter->second));
++idx;
}
MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
MqttSettings.publish("battery/CellAvgMilliVolt", String(_cellAvgMilliVolt));
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
for (auto iter = JbdBms::AlarmBitTexts.begin(); iter != JbdBms::AlarmBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/alarms/") + iter->second.data(), value);
}
}
_lastMqttPublish = millis();
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
{
using Label = JkBms::DataPointLabel;
@ -650,6 +782,62 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
_lastUpdate = millis();
}
void JbdBmsBatteryStats::updateFrom(JbdBms::DataPointContainer const& dp)
{
using Label = JbdBms::DataPointLabel;
setManufacturer("JBDBMS");
auto oSoCValue = dp.get<Label::BatterySoCPercent>();
if (oSoCValue.has_value()) {
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
BatteryStats::setSoC(*oSoCValue, 0/*precision*/,
oSoCDataPoint->getTimestamp());
}
auto oVoltage = dp.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value()) {
auto oVoltageDataPoint = dp.getDataPointFor<Label::BatteryVoltageMilliVolt>();
BatteryStats::setVoltage(static_cast<float>(*oVoltage) / 1000,
oVoltageDataPoint->getTimestamp());
}
auto oCurrent = dp.get<Label::BatteryCurrentMilliAmps>();
if (oCurrent.has_value()) {
auto oCurrentDataPoint = dp.getDataPointFor<Label::BatteryCurrentMilliAmps>();
BatteryStats::setCurrent(static_cast<float>(*oCurrent) / 1000, 2/*precision*/,
oCurrentDataPoint->getTimestamp());
}
_dataPoints.updateFrom(dp);
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value()) {
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
if (iter == oCellVoltages->cbegin()) {
_cellMinMilliVolt = _cellAvgMilliVolt = _cellMaxMilliVolt = iter->second;
continue;
}
_cellMinMilliVolt = std::min(_cellMinMilliVolt, iter->second);
_cellAvgMilliVolt = (_cellAvgMilliVolt + iter->second) / 2;
_cellMaxMilliVolt = std::max(_cellMaxMilliVolt, iter->second);
}
_cellVoltageTimestamp = millis();
}
auto oSoftwareVersion = _dataPoints.get<Label::BmsSoftwareVersion>();
if (oSoftwareVersion.has_value()) {
_fwversion = oSoftwareVersion->c_str();
}
auto oHardwareVersion = _dataPoints.get<Label::BmsHardwareVersion>();
if (oHardwareVersion.has_value()) {
_hwversion = oHardwareVersion->c_str();
}
_lastUpdate = millis();
}
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());

View File

@ -12,8 +12,17 @@
CONFIG_T config;
void ConfigurationClass::init()
static std::condition_variable sWriterCv;
static std::mutex sWriterMutex;
static unsigned sWriterCount = 0;
void ConfigurationClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&ConfigurationClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
memset(&config, 0x0, sizeof(config));
}
@ -88,12 +97,74 @@ void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, Jso
target["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit;
target["enable_discharge_current_limit"] = config.Battery.EnableDischargeCurrentLimit;
target["discharge_current_limit"] = config.Battery.DischargeCurrentLimit;
target["discharge_current_limit_below_soc"] = config.Battery.DischargeCurrentLimitBelowSoc;
target["discharge_current_limit_below_voltage"] = config.Battery.DischargeCurrentLimitBelowVoltage;
target["use_battery_reported_discharge_current_limit"] = config.Battery.UseBatteryReportedDischargeCurrentLimit;
target["mqtt_discharge_current_topic"] = config.Battery.MqttDischargeCurrentTopic;
target["mqtt_discharge_current_json_path"] = config.Battery.MqttDischargeCurrentJsonPath;
target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit;
}
void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target)
{
char serialBuffer[sizeof(uint64_t) * 8 + 1];
auto serialStr = [&serialBuffer](uint64_t const& serial) -> String {
snprintf(serialBuffer, sizeof(serialBuffer), "%0x%08x",
static_cast<uint32_t>((serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(serial & 0xFFFFFFFF));
return String(serialBuffer);
};
// we want a representation of our floating-point value in the JSON that
// uses the least amount of decimal digits possible to convey the value that
// is actually represented by the float. this is no easy task. ArduinoJson
// does this for us, however, it does it as expected only for variables of
// type double. this is probably because it assumes all floating-point
// values to have the precision of a double (64 bits), so it prints the
// respective number of siginificant decimals, which are too many if the
// actual value is a float (32 bits).
auto roundedFloat = [](float val) -> double {
return static_cast<int>(val * 100 + (val > 0 ? 0.5 : -0.5)) / 100.0;
};
target["enabled"] = source.Enabled;
target["verbose_logging"] = source.VerboseLogging;
target["solar_passthrough_enabled"] = source.SolarPassThroughEnabled;
target["solar_passthrough_losses"] = source.SolarPassThroughLosses;
target["battery_always_use_at_night"] = source.BatteryAlwaysUseAtNight;
target["target_power_consumption"] = source.TargetPowerConsumption;
target["target_power_consumption_hysteresis"] = source.TargetPowerConsumptionHysteresis;
target["base_load_limit"] = source.BaseLoadLimit;
target["ignore_soc"] = source.IgnoreSoc;
target["battery_soc_start_threshold"] = source.BatterySocStartThreshold;
target["battery_soc_stop_threshold"] = source.BatterySocStopThreshold;
target["voltage_start_threshold"] = roundedFloat(source.VoltageStartThreshold);
target["voltage_stop_threshold"] = roundedFloat(source.VoltageStopThreshold);
target["voltage_load_correction_factor"] = source.VoltageLoadCorrectionFactor;
target["full_solar_passthrough_soc"] = source.FullSolarPassThroughSoc;
target["full_solar_passthrough_start_voltage"] = roundedFloat(source.FullSolarPassThroughStartVoltage);
target["full_solar_passthrough_stop_voltage"] = roundedFloat(source.FullSolarPassThroughStopVoltage);
target["inverter_serial_for_dc_voltage"] = serialStr(source.InverterSerialForDcVoltage);
target["inverter_channel_id_for_dc_voltage"] = source.InverterChannelIdForDcVoltage;
target["inverter_restart_hour"] = source.RestartHour;
target["total_upper_power_limit"] = source.TotalUpperPowerLimit;
JsonArray inverters = target["inverters"].to<JsonArray>();
for (size_t i = 0; i < INV_MAX_COUNT; ++i) {
PowerLimiterInverterConfig const& s = source.Inverters[i];
if (s.Serial == 0ULL) { break; }
JsonObject t = inverters.add<JsonObject>();
t["serial"] = serialStr(s.Serial);
t["is_governed"] = s.IsGoverned;
t["is_behind_power_meter"] = s.IsBehindPowerMeter;
t["is_solar_powered"] = s.IsSolarPowered;
t["use_overscaling_to_compensate_shading"] = s.UseOverscalingToCompensateShading;
t["lower_power_limit"] = s.LowerPowerLimit;
t["upper_power_limit"] = s.UpperPowerLimit;
}
}
bool ConfigurationClass::write()
{
File f = LittleFS.open(CONFIG_FILENAME, "w");
@ -190,7 +261,7 @@ bool ConfigurationClass::write()
display["screensaver"] = config.Display.ScreenSaver;
display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language;
display["locale"] = config.Display.Locale;
display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode;
@ -248,32 +319,7 @@ bool ConfigurationClass::write()
serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
powerlimiter["enabled"] = config.PowerLimiter.Enabled;
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
powerlimiter["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
powerlimiter["interval"] = config.PowerLimiter.Interval;
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
powerlimiter["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading;
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
powerlimiter["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold;
powerlimiter["voltage_stop_threshold"] = config.PowerLimiter.VoltageStopThreshold;
powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
powerlimiter["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage;
powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage;
serializePowerLimiterConfig(config.PowerLimiter, powerlimiter);
JsonObject battery = doc["battery"].to<JsonObject>();
serializeBatteryConfig(config.Battery, battery);
@ -311,7 +357,7 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source,
JsonObject source_http_config = source["http_request"];
// http request parameters of HTTP/JSON power meter were previously stored
// alongside other settings. TODO(schlimmchen): remove in early 2025.
// alongside other settings. TODO(schlimmchen): remove in mid 2025.
if (source_http_config.isNull()) { source_http_config = source; }
strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url));
@ -383,15 +429,68 @@ void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, Batt
target.MqttVoltageUnit = source["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
target.EnableDischargeCurrentLimit = source["enable_discharge_current_limit"] | BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT;
target.DischargeCurrentLimit = source["discharge_current_limit"] | BATTERY_DISCHARGE_CURRENT_LIMIT;
target.DischargeCurrentLimitBelowSoc = source["discharge_current_limit_below_soc"] | BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC;
target.DischargeCurrentLimitBelowVoltage = source["discharge_current_limit_below_voltage"] | BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_VOLTAGE;
target.UseBatteryReportedDischargeCurrentLimit = source["use_battery_reported_discharge_current_limit"] | BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT;
strlcpy(target.MqttDischargeCurrentTopic, source["mqtt_discharge_current_topic"] | "", sizeof(config.Battery.MqttDischargeCurrentTopic));
strlcpy(target.MqttDischargeCurrentJsonPath, source["mqtt_discharge_current_json_path"] | "", sizeof(config.Battery.MqttDischargeCurrentJsonPath));
target.MqttAmperageUnit = source["mqtt_amperage_unit"] | BatteryAmperageUnit::Amps;
}
void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target)
{
auto serialBin = [](String const& input) -> uint64_t {
return strtoll(input.c_str(), NULL, 16);
};
target.Enabled = source["enabled"] | POWERLIMITER_ENABLED;
target.VerboseLogging = source["verbose_logging"] | VERBOSE_LOGGING;
target.SolarPassThroughEnabled = source["solar_passthrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
target.SolarPassThroughLosses = source["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
target.BatteryAlwaysUseAtNight = source["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
target.TargetPowerConsumption = source["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
target.TargetPowerConsumptionHysteresis = source["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
target.BaseLoadLimit = source["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
target.IgnoreSoc = source["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
target.BatterySocStartThreshold = source["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
target.BatterySocStopThreshold = source["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
target.VoltageStartThreshold = source["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
target.VoltageStopThreshold = source["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
target.VoltageLoadCorrectionFactor = source["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
target.FullSolarPassThroughSoc = source["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC;
target.FullSolarPassThroughStartVoltage = source["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE;
target.FullSolarPassThroughStopVoltage = source["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE;
target.InverterSerialForDcVoltage = serialBin(source["inverter_serial_for_dc_voltage"] | String("0"));
target.InverterChannelIdForDcVoltage = source["inverter_channel_id_for_dc_voltage"] | POWERLIMITER_INVERTER_CHANNEL_ID;
target.RestartHour = source["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR;
target.TotalUpperPowerLimit = source["total_upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
JsonArray inverters = source["inverters"].as<JsonArray>();
for (size_t i = 0; i < INV_MAX_COUNT; ++i) {
PowerLimiterInverterConfig& inv = target.Inverters[i];
JsonObject s = inverters[i];
inv.Serial = serialBin(s["serial"] | String("0")); // 0 marks inverter slot as unused
inv.IsGoverned = s["is_governed"] | false;
inv.IsBehindPowerMeter = s["is_behind_power_meter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
inv.IsSolarPowered = s["is_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
inv.UseOverscalingToCompensateShading = s["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
inv.LowerPowerLimit = s["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
inv.UpperPowerLimit = s["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
}
}
bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
Utils::skipBom(f);
// skip Byte Order Mask (BOM). valid JSON docs always start with '{' or '['.
while (f.available() > 0) {
int c = f.peek();
if (c == '{' || c == '[') { break; }
f.read();
}
JsonDocument doc;
@ -522,7 +621,7 @@ bool ConfigurationClass::read()
config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER;
config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE;
strlcpy(config.Display.Locale, display["locale"] | DISPLAY_LOCALE, sizeof(config.Display.Locale));
config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE;
@ -570,7 +669,7 @@ bool ConfigurationClass::read()
deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt);
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025.
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["mqtt_topic_powermeter_1"].isNull()) {
auto& values = config.PowerMeter.Mqtt.Values;
strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic));
@ -581,7 +680,7 @@ bool ConfigurationClass::read()
deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm);
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025.
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["sdmaddress"].isNull()) {
config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"];
}
@ -593,7 +692,7 @@ bool ConfigurationClass::read()
deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml);
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025.
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["http_phases"].isNull()) {
auto& target = config.PowerMeter.HttpJson;
@ -613,33 +712,48 @@ bool ConfigurationClass::read()
}
JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter.Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | powerlimiter["solar_passtrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; // solar_passthrough_losses was previously saved as solar_passtrough_losses. Be nice and also try mistyped key.
config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
config.PowerLimiter.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
config.PowerLimiter.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
config.PowerLimiter.VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
config.PowerLimiter.VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
config.PowerLimiter.RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR;
config.PowerLimiter.FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC;
config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE;
config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE;
deserializePowerLimiterConfig(powerlimiter, config.PowerLimiter);
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) {
config.PowerLimiter.BatteryAlwaysUseAtNight = true; // convert legacy setting
}
if (!powerlimiter["solar_passtrough_enabled"].isNull()) {
// solar_passthrough_enabled was previously saved as
// solar_passtrough_enabled. be nice and also try misspelled key.
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"].as<bool>();
}
if (!powerlimiter["solar_passtrough_losses"].isNull()) {
// solar_passthrough_losses was previously saved as
// solar_passtrough_losses. be nice and also try misspelled key.
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passtrough_losses"].as<uint8_t>();
}
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in mid 2025.
if (!powerlimiter["inverter_id"].isNull()) {
config.PowerLimiter.InverterChannelIdForDcVoltage = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
auto& inv = config.PowerLimiter.Inverters[0];
uint64_t previousInverterSerial = powerlimiter["inverter_id"].as<uint64_t>();
if (previousInverterSerial < INV_MAX_COUNT) {
// we previously had an index (not a serial) saved as inverter_id.
previousInverterSerial = config.Inverter[inv.Serial].Serial; // still 0 if no inverters configured
}
inv.Serial = previousInverterSerial;
config.PowerLimiter.InverterSerialForDcVoltage = previousInverterSerial;
inv.IsGoverned = true;
inv.IsBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
inv.IsSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
inv.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
inv.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
inv.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.TotalUpperPowerLimit = inv.UpperPowerLimit;
config.PowerLimiter.Inverters[1].Serial = 0;
}
deserializeBatteryConfig(doc["battery"], config.Battery);
@ -658,6 +772,20 @@ bool ConfigurationClass::read()
config.Huawei.Auto_Power_Target_Power_Consumption = huawei["target_power_consumption"] | HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION;
f.close();
// Check for default DTU serial
MessageOutput.print("Check for default DTU serial... ");
if (config.Dtu.Serial == DTU_SERIAL) {
MessageOutput.print("generate serial based on ESP chip id: ");
const uint64_t dtuId = Utils::generateDtuSerial();
MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
static_cast<uint32_t>((dtuId >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(dtuId & 0xFFFFFFFF));
config.Dtu.Serial = dtuId;
write();
}
MessageOutput.println("done");
return true;
}
@ -723,6 +851,22 @@ void ConfigurationClass::migrate()
}
}
if (config.Cfg.Version < 0x00011d00) {
JsonObject device = doc["device"];
JsonObject display = device["display"];
switch (display["language"] | 0U) {
case 0U:
strlcpy(config.Display.Locale, "en", sizeof(config.Display.Locale));
break;
case 1U:
strlcpy(config.Display.Locale, "de", sizeof(config.Display.Locale));
break;
case 2U:
strlcpy(config.Display.Locale, "fr", sizeof(config.Display.Locale));
break;
}
}
f.close();
config.Cfg.Version = CONFIG_VERSION;
@ -730,11 +874,16 @@ void ConfigurationClass::migrate()
read();
}
CONFIG_T& ConfigurationClass::get()
CONFIG_T const& ConfigurationClass::get()
{
return config;
}
ConfigurationClass::WriteGuard ConfigurationClass::getWriteGuard()
{
return WriteGuard();
}
INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
{
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
@ -779,4 +928,30 @@ void ConfigurationClass::deleteInverterById(const uint8_t id)
}
}
void ConfigurationClass::loop()
{
std::unique_lock<std::mutex> lock(sWriterMutex);
if (sWriterCount == 0) { return; }
sWriterCv.notify_all();
sWriterCv.wait(lock, [] { return sWriterCount == 0; });
}
CONFIG_T& ConfigurationClass::WriteGuard::getConfig()
{
return config;
}
ConfigurationClass::WriteGuard::WriteGuard()
: _lock(sWriterMutex)
{
sWriterCount++;
sWriterCv.wait(_lock);
}
ConfigurationClass::WriteGuard::~WriteGuard() {
sWriterCount--;
if (sWriterCount == 0) { sWriterCv.notify_all(); }
}
ConfigurationClass Configuration;

View File

@ -1,8 +1,6 @@
#include <stdio.h>
#include "JkBmsDataPoints.h"
namespace JkBms {
#include "DataPoints.h"
static char conversionBuffer[16];
@ -30,7 +28,7 @@ std::string dataPointValueToStr(bool const& v) {
}
template<>
std::string dataPointValueToStr(tCells const& v) {
std::string dataPointValueToStr(tCellVoltages const& v) {
std::string res;
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
res += "(";
@ -44,20 +42,3 @@ std::string dataPointValueToStr(tCells const& v) {
res += ")";
return std::move(res);
}
void DataPointContainer::updateFrom(DataPointContainer const& source)
{
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
auto pos = _dataPoints.find(iter->first);
if (pos != _dataPoints.end()) {
// do not update existing data points with the same value
if (pos->second == iter->second) { continue; }
_dataPoints.erase(pos);
}
_dataPoints.insert(*iter);
}
}
} /* namespace JkBms */

View File

@ -4,6 +4,7 @@
*/
#include "Display_Graphic.h"
#include "Datastore.h"
#include "I18n.h"
#include "PowerMeter.h"
#include "Configuration.h"
#include <NetworkSettings.h>
@ -18,18 +19,11 @@ std::map<DisplayType_t, std::function<U8G2*(uint8_t, uint8_t, uint8_t, uint8_t)>
{ DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } },
};
// Language defintion, respect order in languages[] and translation lists
// Language defintion, respect order in translation lists
#define I18N_LOCALE_EN 0
#define I18N_LOCALE_DE 1
#define I18N_LOCALE_FR 2
// Languages supported. Note: the order is important and must match locale_translations.h
const uint8_t languages[] = {
I18N_LOCALE_EN,
I18N_LOCALE_DE,
I18N_LOCALE_FR
};
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
@ -171,9 +165,38 @@ void DisplayGraphicClass::setOrientation(const uint8_t rotation)
calcLineHeights();
}
void DisplayGraphicClass::setLanguage(const uint8_t language)
void DisplayGraphicClass::setLocale(const String& locale)
{
_display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE;
_display_language = locale;
uint8_t idx = I18N_LOCALE_EN;
if (locale == "de") {
idx = I18N_LOCALE_DE;
} else if (locale == "fr") {
idx = I18N_LOCALE_FR;
}
_i18n_date_format = i18n_date_format[idx];
_i18n_offline = i18n_offline[idx];
_i18n_current_power_w = i18n_current_power_w[idx];
_i18n_current_power_kw = i18n_current_power_kw[idx];
_i18n_meter_power_w = i18n_meter_power_w[idx];
_i18n_meter_power_kw = i18n_meter_power_kw[idx];
_i18n_yield_today_wh = i18n_yield_today_wh[idx];
_i18n_yield_today_kwh = i18n_yield_today_kwh[idx];
_i18n_yield_total_kwh = i18n_yield_total_kwh[idx];
_i18n_yield_total_mwh = i18n_yield_total_mwh[idx];
I18n.readDisplayStrings(locale,
_i18n_date_format,
_i18n_offline,
_i18n_current_power_w,
_i18n_current_power_kw,
_i18n_meter_power_w,
_i18n_meter_power_kw,
_i18n_yield_today_wh,
_i18n_yield_today_kwh,
_i18n_yield_total_kwh,
_i18n_yield_total_mwh);
}
void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode)
@ -230,9 +253,9 @@ void DisplayGraphicClass::loop()
if (showText) {
const float watts = Datastore.getTotalAcPowerEnabled();
if (watts > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000);
snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_kw.c_str(), watts / 1000);
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts);
snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_w.c_str(), watts);
}
printText(_fmtText, 0);
}
@ -242,7 +265,7 @@ void DisplayGraphicClass::loop()
//=====> Offline ===========
else {
printText(i18n_offline[_display_language], 0);
printText(_i18n_offline.c_str(), 0);
// check if it's time to enter power saving mode
if (millis() - _previousMillis >= (_interval * 2)) {
displayPowerSave = enablePowerSafe;
@ -254,16 +277,16 @@ void DisplayGraphicClass::loop()
// Daily production
float wattsToday = Datastore.getTotalAcYieldDayEnabled();
if (wattsToday >= 10000) {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000);
snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_kwh.c_str(), wattsToday / 1000);
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday);
snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_wh.c_str(), wattsToday);
}
printText(_fmtText, 1);
// Total production
const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal);
auto const format = (wattsTotal >= 1000) ? _i18n_yield_total_mwh : _i18n_yield_total_kwh;
snprintf(_fmtText, sizeof(_fmtText), format.c_str(), wattsTotal);
printText(_fmtText, 2);
//=====> IP or Date-Time ========
@ -273,7 +296,7 @@ void DisplayGraphicClass::loop()
} else {
// Get current time
time_t now = time(nullptr);
strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now));
strftime(_fmtText, sizeof(_fmtText), _i18n_date_format.c_str(), localtime(&now));
printText(_fmtText, 3);
}
}
@ -296,9 +319,9 @@ void DisplayGraphicClass::loop()
auto acPower = PowerMeter.getPowerTotal();
if (acPower > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
snprintf(_fmtText, sizeof(_fmtText), _i18n_meter_power_kw.c_str(), (acPower / 1000));
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_w[_display_language], acPower);
snprintf(_fmtText, sizeof(_fmtText), _i18n_meter_power_w.c_str(), acPower);
}
printText(_fmtText, 2);

View File

@ -83,7 +83,7 @@ HttpRequestResult HttpGetter::performGetRequest()
// depth at https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
// in conclusion: we cannot rely on _upHttpClient->begin(*wifiClient, url) to resolve
// IP adresses. have to do it manually.
IPAddress ipaddr((uint32_t)0);
IPAddress ipaddr(static_cast<uint32_t>(0));
if (!ipaddr.fromString(_host)) {
// host is not an IP address, so try to resolve the name to an address.

View File

@ -356,25 +356,12 @@ void HuaweiCanClass::loop()
_autoPowerEnabledCounter = 10;
}
// Check if inverter used by the power limiter is active
std::shared_ptr<InverterAbstract> inverter =
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);
if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
// we previously had an index saved as InverterId. fall back to the
// respective positional lookup if InverterId is not a known serial.
inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
}
if (inverter != nullptr) {
if(inverter->isProducing()) {
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
// Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus
_autoModeBlockedTillMillis = millis() + 1000;
MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n");
return;
}
if (PowerLimiter.isGovernedInverterProducing()) {
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
// Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus
_autoModeBlockedTillMillis = millis() + 1000;
MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n");
return;
}
if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis &&

167
src/I18n.cpp Normal file
View File

@ -0,0 +1,167 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "I18n.h"
#include "MessageOutput.h"
#include "Utils.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
I18nClass I18n;
I18nClass::I18nClass()
{
}
void I18nClass::init(Scheduler& scheduler)
{
readLangPacks();
}
std::list<LanguageInfo_t> I18nClass::getAvailableLanguages()
{
return _availLanguages;
}
String I18nClass::getFilenameByLocale(const String& locale) const
{
auto it = std::find_if(_availLanguages.begin(), _availLanguages.end(), [locale](const LanguageInfo_t& elem) {
return elem.code == locale;
});
if (it != _availLanguages.end()) {
return it->filename;
} else {
return String();
}
}
void I18nClass::readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& meter_power_w, String& meter_power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh)
{
auto filename = getFilenameByLocale(locale);
if (filename == "") {
return;
}
JsonDocument filter;
filter["display"] = true;
File f = LittleFS.open(filename, "r", false);
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter));
if (error) {
MessageOutput.printf("Failed to read file %s\r\n", filename.c_str());
f.close();
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
auto displayData = doc["display"];
if (displayData["date_format"].as<String>() != "null") {
date_format = displayData["date_format"].as<String>();
}
if (displayData["offline"].as<String>() != "null") {
offline = displayData["offline"].as<String>();
}
if (displayData["power_w"].as<String>() != "null") {
power_w = displayData["power_w"].as<String>();
}
if (displayData["power_kw"].as<String>() != "null") {
power_kw = displayData["power_kw"].as<String>();
}
if (displayData["meter_power_w"].as<String>() != "null") {
meter_power_w = displayData["meter_power_w"].as<String>();
}
if (displayData["meter_power_kw"].as<String>() != "null") {
meter_power_kw = displayData["meter_power_kw"].as<String>();
}
if (displayData["yield_today_wh"].as<String>() != "null") {
yield_today_wh = displayData["yield_today_wh"].as<String>();
}
if (displayData["yield_today_kwh"].as<String>() != "null") {
yield_today_kwh = displayData["yield_today_kwh"].as<String>();
}
if (displayData["yield_total_kwh"].as<String>() != "null") {
yield_total_kwh = displayData["yield_total_kwh"].as<String>();
}
if (displayData["yield_total_mwh"].as<String>() != "null") {
yield_total_mwh = displayData["yield_total_mwh"].as<String>();
}
f.close();
}
void I18nClass::readLangPacks()
{
auto root = LittleFS.open("/");
auto file = root.getNextFileName();
while (file != "") {
if (file.endsWith(LANG_PACK_SUFFIX)) {
MessageOutput.printf("Read File %s\r\n", file.c_str());
readConfig(file);
}
file = root.getNextFileName();
}
root.close();
}
void I18nClass::readConfig(String file)
{
JsonDocument filter;
filter["meta"] = true;
File f = LittleFS.open(file, "r", false);
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter));
if (error) {
MessageOutput.printf("Failed to read file %s\r\n", file.c_str());
f.close();
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
LanguageInfo_t lang;
lang.code = String(doc["meta"]["code"] | "");
lang.name = String(doc["meta"]["name"] | "");
lang.filename = file;
if (lang.code != "" && lang.name != "") {
_availLanguages.push_back(lang);
} else {
MessageOutput.printf("Invalid meta data\r\n");
}
f.close();
}

View File

@ -63,10 +63,10 @@ void InverterSettingsClass::init(Scheduler& scheduler)
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) {
MessageOutput.print(" Adding inverter: ");
MessageOutput.print(config.Inverter[i].Serial, HEX);
MessageOutput.print(" - ");
MessageOutput.print(config.Inverter[i].Name);
MessageOutput.printf(" Adding inverter: %0" PRIx32 "%08" PRIx32 " - %s",
static_cast<uint32_t>((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(config.Inverter[i].Serial & 0xFFFFFFFF),
config.Inverter[i].Name);
auto inv = Hoymiles.addInverter(
config.Inverter[i].Name,
config.Inverter[i].Serial);

274
src/JbdBmsController.cpp Normal file
View File

@ -0,0 +1,274 @@
#include <Arduino.h>
#include <numeric>
#include "Configuration.h"
#include "HardwareSerial.h"
#include "PinMapping.h"
#include "MessageOutput.h"
#include "JbdBmsDataPoints.h"
#include "JbdBmsController.h"
#include "JbdBmsSerialMessage.h"
#include "SerialPortManager.h"
#include <frozen/map.h>
namespace JbdBms {
bool Controller::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;
std::string ifcType = "transceiver";
if (Interface::Transceiver != getInterface()) { ifcType = "TTL-UART"; }
MessageOutput.printf("[JBD BMS] Initialize %s interface...\r\n", ifcType.c_str());
const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[JBD BMS] rx = %d, rxen = %d, tx = %d, txen = %d\r\n",
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.println("[JBD BMS] Invalid RX/TX pin config");
return false;
}
#ifdef JBDBMS_DUMMY_SERIAL
_upSerial = std::make_unique<DummySerial>();
#else
auto oHwSerialPort = SerialPortManager.allocatePort(_serialPortOwner);
if (!oHwSerialPort) { return false; }
_upSerial = std::make_unique<HardwareSerial>(*oHwSerialPort);
#endif
_upSerial->end(); // make sure the UART will be re-initialized
_upSerial->begin(9600, SERIAL_8N1, pin.battery_rx, pin.battery_tx);
_upSerial->flush();
if (Interface::Transceiver != getInterface()) { return true; }
_rxEnablePin = pin.battery_rxen;
_txEnablePin = pin.battery_txen;
if (_rxEnablePin < 0 || _txEnablePin < 0) {
MessageOutput.println("[JBD BMS] Invalid transceiver pin config");
return false;
}
pinMode(_rxEnablePin, OUTPUT);
pinMode(_txEnablePin, OUTPUT);
return true;
}
void Controller::deinit()
{
_upSerial->end();
if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); }
if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); }
SerialPortManager.freePort(_serialPortOwner);
}
Controller::Interface Controller::getInterface() const
{
auto const& config = Configuration.get();
if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; }
if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; }
return Interface::Invalid;
}
frozen::string const& Controller::getStatusText(Controller::Status status)
{
static constexpr frozen::string missing = "programmer error: missing status text";
static constexpr frozen::map<Status, frozen::string, 6> texts = {
{ Status::Timeout, "timeout wating for response from BMS" },
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
{ Status::BusyReading, "busy waiting for or reading a message from the BMS" },
{ Status::RequestSent, "request for data sent" },
{ Status::FrameCompleted, "a whole frame was received" }
};
auto iter = texts.find(status);
if (iter == texts.end()) { return missing; }
return iter->second;
}
void Controller::announceStatus(Controller::Status status)
{
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
MessageOutput.printf("[%11.3f] JBD BMS: %s\r\n",
static_cast<double>(millis())/1000, getStatusText(status).data());
_lastStatus = status;
_lastStatusPrinted = millis();
}
void Controller::sendRequest(uint8_t pollInterval)
{
if (ReadState::Idle != _readState) {
return announceStatus(Status::BusyReading);
}
if ((millis() - _lastRequest) < pollInterval * 1000) {
return announceStatus(Status::WaitingForPollInterval);
}
if (!_upSerial->availableForWrite()) {
return announceStatus(Status::HwSerialNotAvailableForWrite);
}
SerialCommand::Command cmd;
switch (SerialCommand::getLastCommand()) {
case SerialCommand::Command::Init: // read only once
cmd = SerialCommand::Command::ReadHardwareVersionNumber;
break;
case SerialCommand::Command::ReadBasicInformation:
cmd = SerialCommand::Command::ReadCellVoltages;
break;
case SerialCommand::Command::ReadCellVoltages:
cmd = SerialCommand::Command::ReadBasicInformation;
break;
default:
cmd = SerialCommand::Command::ReadBasicInformation;
break;
}
SerialCommand readCmd(SerialCommand::Status::Read, cmd);
if (Interface::Transceiver == getInterface()) {
digitalWrite(_rxEnablePin, HIGH); // disable reception (of our own data)
digitalWrite(_txEnablePin, HIGH); // enable transmission
}
_upSerial->write(readCmd.data(), readCmd.size());
if (Interface::Transceiver == getInterface()) {
_upSerial->flush();
digitalWrite(_rxEnablePin, LOW); // enable reception
digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus)
}
_lastRequest = millis();
setReadState(ReadState::WaitingForFrameStart);
return announceStatus(Status::RequestSent);
}
void Controller::loop()
{
auto const& config = Configuration.get();
uint8_t pollInterval = config.Battery.JkBmsPollingInterval;
while (_upSerial->available()) {
rxData(_upSerial->read());
}
sendRequest(pollInterval);
if (millis() > _lastRequest + 2 * pollInterval * 1000 + 250) {
reset();
return announceStatus(Status::Timeout);
}
}
void Controller::rxData(uint8_t inbyte)
{
_buffer.push_back(inbyte);
switch(_readState) {
case ReadState::Idle: // unsolicited message from BMS
case ReadState::WaitingForFrameStart:
if (inbyte == SerialMessage::startMarker) {
return setReadState(ReadState::FrameStartReceived);
}
break;
case ReadState::FrameStartReceived:
return setReadState(ReadState::StateReceived);
break;
case ReadState::StateReceived:
return setReadState(ReadState::CommandCodeReceived);
break;
case ReadState::CommandCodeReceived:
_dataLength = inbyte;
if (_dataLength == 0) {
return setReadState(ReadState::DataContentReceived);
}
return setReadState(ReadState::ReadingDataContent);
break;
case ReadState::ReadingDataContent:
_dataLength--;
if (_dataLength == 0) {
return setReadState(ReadState::DataContentReceived);
}
return setReadState(ReadState::ReadingDataContent);
break;
case ReadState::DataContentReceived:
return setReadState(ReadState::ReadingCheckSum);
break;
case ReadState::ReadingCheckSum:
return setReadState(ReadState::CheckSumReceived);
break;
case ReadState::CheckSumReceived:
if (inbyte == SerialMessage::endMarker) {
return frameComplete();
}
MessageOutput.printf("[JBD BMS] Invalid Frame: end marker not found.");
MessageOutput.println();
return reset();
break;
}
reset();
}
void Controller::reset()
{
_buffer.clear();
return setReadState(ReadState::Idle);
}
void Controller::frameComplete()
{
announceStatus(Status::FrameCompleted);
if (_verboseLogging) {
double ts = static_cast<double>(millis())/1000;
MessageOutput.printf("[%11.3f] JBD BMS: raw data (%d Bytes):",
ts, _buffer.size());
for (size_t ctr = 0; ctr < _buffer.size(); ++ctr) {
if (ctr % 16 == 0) {
MessageOutput.printf("\r\n[%11.3f] JBD BMS:", ts);
}
MessageOutput.printf(" %02x", _buffer[ctr]);
}
MessageOutput.println();
}
auto pResponse = std::make_unique<SerialResponse>(std::move(_buffer));
if (pResponse->isValid()) {
processDataPoints(pResponse->getDataPoints());
} // if invalid, error message has been produced by SerialResponse c'tor
reset();
}
void Controller::processDataPoints(DataPointContainer const& dataPoints)
{
_stats->updateFrom(dataPoints);
if (!_verboseLogging) { return; }
auto iter = dataPoints.cbegin();
while ( iter != dataPoints.cend() ) {
MessageOutput.printf("[%11.3f] JBD BMS: %s: %s%s\r\n",
static_cast<double>(iter->second.getTimestamp())/1000,
iter->second.getLabelText().c_str(),
iter->second.getValueText().c_str(),
iter->second.getUnitText().c_str());
++iter;
}
}
} /* namespace JbdBms */

272
src/JbdBmsSerialMessage.cpp Normal file
View File

@ -0,0 +1,272 @@
#include <numeric>
#include <sstream>
#include <iomanip>
#include "JbdBmsSerialMessage.h"
#include "MessageOutput.h"
namespace JbdBms {
SerialCommand::SerialCommand(SerialCommand::Status status, SerialCommand::Command cmd)
: SerialMessage(7, 0x00) // frame length 7 bytes initialized with zeros
{
set(_raw.begin(), startMarker);
set(_raw.begin() + 1, static_cast<uint8_t>(status));
set(_raw.begin() + 2, static_cast<uint8_t>(cmd));
set(_raw.begin() + 3, static_cast<uint16_t>(0x00)); // frame length
updateChecksum();
set(_raw.end() - 1, endMarker);
_lastCmd = cmd;
}
SerialCommand::Command SerialCommand::_lastCmd = SerialCommand::Command::Init;
using Label = JbdBms::DataPointLabel;
template<Label L> using Traits = DataPointLabelTraits<L>;
SerialResponse::SerialResponse(tData&& raw)
: SerialMessage(std::move(raw))
{
if (!isValid()) { return; }
auto pos = _raw.cbegin() + 4; // start of data content
auto end = pos + getDataLength(); // end of data content
if (pos < end) {
if (getCommand() == Command::ReadBasicInformation) {
_dp.add<Label::BatteryVoltageMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10); // Total voltage
_dp.add<Label::BatteryCurrentMilliAmps>(static_cast<int32_t>(get<int16_t>(pos)) * 10); // Current
_dp.add<Label::ActualBatteryCapacityAmpHours>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10 / 1000); // remaining capacity
_dp.add<Label::BatteryCapacitySettingAmpHours>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10 / 1000); // nominal capacity
_dp.add<Label::BatteryCycles>(get<uint16_t>(pos));
_dp.add<Label::DateOfManufacturing >(getProductionDate(pos));
bool balancingEnabled = false;
balancingEnabled |= static_cast<bool>(get<uint16_t>(pos)); // Equilibrium
balancingEnabled |= static_cast<bool>(get<uint16_t>(pos)); // Equilibrium_High
_dp.add<Label::BalancingEnabled>(balancingEnabled);
_dp.add<Label::AlarmsBitmask>(get<uint16_t>(pos)); // Protection status
uint8_t softwareVersion = get<uint8_t>(pos);
uint8_t digitOne = softwareVersion & 0x0F;
uint8_t digitTwo = softwareVersion >> 4;
char buffer[6];
snprintf(buffer, sizeof(buffer), "%d.%d", digitOne, digitTwo);
_dp.add<Label::BmsSoftwareVersion>(std::string(buffer)); // Software version
_dp.add<Label::BatterySoCPercent>(get<uint8_t>(pos)); // RSOC
uint8_t fetControl = get<uint8_t>(pos); // FET control status
const uint8_t chargingMask = (1 << 0);
const uint8_t dischargingMask = (1 << 1);
bool fetChargeEnabled = static_cast<bool>(fetControl & chargingMask);
bool fetDischargeEnabled = static_cast<bool>(fetControl & dischargingMask);
_dp.add<Label::BatteryChargeEnabled>(fetChargeEnabled);
_dp.add<Label::BatteryDischargeEnabled>(fetDischargeEnabled);
_dp.add<Label::BatteryCellAmount>(static_cast<uint16_t>(get<uint8_t>(pos))); // number of battery strings
_dp.add<Label::BatteryTemperatureSensorAmount>(get<uint8_t>(pos)); // number of ntc
_dp.add<Label::BatteryTempOneCelsius>(getTemperature(pos)); // ntc temperature one
_dp.add<Label::BatteryTempTwoCelsius>(getTemperature(pos)); // ntc temperature two
}
else if (getCommand() == Command::ReadCellVoltages)
{
uint8_t cellAmount = getDataLength() / 2;
std::map<uint8_t, uint16_t> voltages;
for (size_t cellCounter = 0; cellCounter < cellAmount; ++cellCounter) {
uint8_t idx = cellCounter;
auto cellMilliVolt = get<uint16_t>(pos);
voltages[idx] = cellMilliVolt;
}
_dp.add<Label::CellsMilliVolt>(voltages);
}
else if (getCommand() == Command::ReadHardwareVersionNumber)
{
_dp.add<Label::BmsHardwareVersion>(getString(pos, getDataLength()));
}
else if (getCommand() == Command::ControlMosInstruction)
{
/* Response doesn't contain any data content */
}
}
}
/**
* NOTE that this function moves the iterator by the amount of bytes read.
*/
template<typename T, typename It>
T SerialMessage::get(It&& pos) const
{
// add easy-to-understand error message when called with non-const iter,
// as compiler generated error message is hard to understand.
using ItNoRef = typename std::remove_reference<It>::type;
using PtrType = typename std::iterator_traits<ItNoRef>::pointer;
using ValueType = typename std::remove_pointer<PtrType>::type;
static_assert(std::is_const<ValueType>::value, "get() must be called with a const_iterator");
// avoid out-of-bound read
if (std::distance(pos, _raw.cend()) < sizeof(T)) { return 0; }
T res = 0;
for (unsigned i = 0; i < sizeof(T); ++i) {
res |= static_cast<T>(*(pos++)) << (sizeof(T)-1-i)*8;
}
return res;
}
template<typename It>
bool SerialMessage::getBool(It&& pos) const
{
uint8_t raw = get<uint8_t>(pos);
return raw > 0;
}
template<typename It>
int16_t SerialMessage::getTemperature(It&& pos) const
{
// raw in 0.1K
uint16_t raw = get<uint16_t>(pos);
return static_cast<int16_t>(raw - 2731) / 10;
}
template<typename It>
std::string SerialMessage::getProductionDate(It&& pos) const
{
// E.g. 0x2068 = 08.03.2016
// the date is the lowest 5: 0x2028 & 0x1f = 8 means the date;
// month (0x2068>>5) & 0x0f = 0x03 means March;
// the year is 2000+ (0x2068>>9) = 2000 + 0x10 =2016;
uint16_t raw = get<uint16_t>(pos);
uint16_t day = raw & 0x1f;
uint16_t month = (raw>>5) & 0x0f;
uint16_t year = 2000 + (raw>>9);
std::ostringstream oss;
oss << std::setw(2) << std::setfill('0') << day << "." << std::setw(2) << std::setfill('0') << month << "." << std::setw(4) << std::setfill('0') << year;
return oss.str();
}
template<typename It>
std::string SerialMessage::getString(It&& pos, size_t len, bool replaceZeroes) const
{
// avoid out-of-bound read
len = std::min<size_t>(std::distance(pos, _raw.cend()), len);
auto start = pos;
pos += len;
if (replaceZeroes) {
std::vector<uint8_t> copy(start, pos);
for (auto& c : copy) {
if (c == 0) { c = 0x20; } // replace by ASCII space
}
return std::string(copy.cbegin(), copy.cend());
}
return std::string(start, pos);
}
template<typename T>
void SerialMessage::set(tData::iterator const& pos, T val)
{
// avoid out-of-bound write
if (std::distance(pos, _raw.end()) < sizeof(T)) { return; }
for (unsigned i = 0; i < sizeof(T); ++i) {
*(pos+i) = static_cast<uint8_t>(val >> (sizeof(T)-1-i)*8);
}
}
uint16_t SerialMessage::calcChecksum() const
{
return (~std::accumulate(_raw.cbegin()+2, _raw.cend()-3, 0) + 0x01);
}
void SerialMessage::updateChecksum()
{
set(_raw.end()-3, calcChecksum());
}
void SerialMessage::printMessage() {
double ts = static_cast<double>(millis())/1000;
MessageOutput.printf("[%11.3f] JBD BMS: raw message (%d Bytes):",
ts, _raw.size());
for (size_t ctr = 0; ctr < _raw.size(); ++ctr) {
MessageOutput.printf(" %02x", _raw[ctr]);
}
MessageOutput.println();
}
bool SerialMessage::isValid() const {
uint8_t const actualStartMarker = getStartMarker();
if (actualStartMarker != startMarker) {
MessageOutput.printf("JbdBms::SerialMessage: invalid start marker 0x%02x, expected 0x%02x\r\n",
actualStartMarker, startMarker);
return false;
}
uint16_t const dataLength = getDataLength();
uint16_t const dataLengthExpected = _raw.size() - 7;
if (dataLength != _raw.size() - 7) {
MessageOutput.printf("JbdBms::SerialMessage: unexpected data length 0x%04x, expected 0x%04x\r\n",
dataLength, dataLengthExpected);
return false;
}
uint8_t const actualEndMarker = getEndMarker();
if (actualEndMarker != endMarker) {
MessageOutput.printf("JbdBms::SerialMessage: invalid end marker 0x%02x, expected 0x%02x\r\n",
actualEndMarker, endMarker);
return false;
}
uint16_t const actualChecksum = getChecksum();
uint16_t const expectedChecksum = calcChecksum();
if (actualChecksum != expectedChecksum) {
MessageOutput.printf("JbdBms::SerialMessage: invalid checksum 0x%04x, expected 0x%04x\r\n",
actualChecksum, expectedChecksum);
return false;
}
return true;
}
bool SerialResponse::isValid() const {
if(!SerialMessage::isValid()) {return false;}
Status const actualStatus = getStatus();
if (actualStatus != Status::Ok) {
MessageOutput.printf("JbdBms::SerialMessage: invalid status 0x%02x, expected 0x%02x\r\n",
static_cast<uint32_t>(actualStatus),
static_cast<uint32_t>(Status::Ok));
return false;
}
return true;
}
bool SerialCommand::isValid() const {
if(!SerialMessage::isValid()) {return false;}
Status const actualStatus = getStatus();
if (actualStatus != Status::Read || actualStatus != Status::Write) {
MessageOutput.printf("JbdBms::SerialMessage: invalid status 0x%02x, expected 0x%02x or 0x%02x\r\n",
static_cast<uint32_t>(actualStatus),
static_cast<uint32_t>(Status::Read),
static_cast<uint32_t>(Status::Write));
return false;
}
return true;
}
} /* namespace JbdBms */

View File

@ -68,7 +68,7 @@ void Controller::deinit()
Controller::Interface Controller::getInterface() const
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; }
if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; }
return Interface::Invalid;
@ -141,7 +141,7 @@ void Controller::sendRequest(uint8_t pollInterval)
void Controller::loop()
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
uint8_t pollInterval = config.Battery.JkBmsPollingInterval;
while (_upSerial->available()) {

View File

@ -21,7 +21,7 @@ void MqttHandleBatteryHassClass::init(Scheduler& scheduler)
void MqttHandleBatteryHassClass::loop()
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
@ -214,6 +214,43 @@ void MqttHandleBatteryHassClass::loop()
publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");
break;
case 6: // JBD BMS
// caption icon topic dev. class state class unit
publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V");
publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A");
publishSensor("Cell Min Voltage", NULL, "CellMinMilliVolt", "voltage", "measurement", "mV");
publishSensor("Cell Average Voltage", NULL, "CellAvgMilliVolt", "voltage", "measurement", "mV");
publishSensor("Cell Max Voltage", NULL, "CellMaxMilliVolt", "voltage", "measurement", "mV");
publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV");
publishSensor("Battery Temperature 1", "mdi:thermometer", "BatteryTempOneCelsius", "temperature", "measurement", "°C");
publishSensor("Battery Temperature 2", "mdi:thermometer", "BatteryTempTwoCelsius", "temperature", "measurement", "°C");
publishSensor("Alarms Bitmask", NULL, "AlarmsBitmask");
publishSensor("Charge Cycles", "mdi:counter", "BatteryCycles");
publishSensor("Available Capacity", NULL, "ActualBatteryCapacityAmpHours");
publishBinarySensor("Charging enabled", "mdi:battery-arrow-up", "BatteryChargeEnabled", "yes", "no");
publishBinarySensor("Discharging enabled", "mdi:battery-arrow-down", "BatteryDischargeEnabled", "yes", "no");
publishBinarySensor("Balancing enabled", "mdi:scale-balance", "BatteryBalancingEnabled", "yes", "no");
#define PBS(a, b, c) publishBinarySensor("Alarm: " a, "mdi:" b, "alarms/" c, "1", "0")
PBS("Cell Overvoltage", "battery-alert", "CellOverVoltage");
PBS("Cell Undervoltage", "battery-alert", "CellUnderVoltage");
PBS("Pack Overvoltage", "fuse-alert", "PackOverVoltage");
PBS("Pack Undervoltage", "fuse-alert", "PackUnderVoltage");
PBS("Charging Overtemperature", "thermometer-alert", "ChargingOverTemperature");
PBS("Charging Undertemperature", "thermometer-alert", "ChargingLowTemperature");
PBS("Discharging Overtemperature", "thermometer-alert", "DischargingOverTemperature");
PBS("Discharging Undertemperature", "thermometer-alert", "DischargingLowTemperature");
PBS("Charging Overcurrent", "fuse-alert", "ChargingOverCurrent");
PBS("Discharging Overcurrent", "fuse-alert", "DischargeOverCurrent");
PBS("Short Circuit", "fuse-alert", "ShortCircuit");
PBS("IC Frontend Error", "battery-alert-variant-outline", "IcFrontEndError");
PBS("MOS Software Lock", "battery-alert-variant-outline", "MosSoftwareLock");
PBS("Reserved 1", "battery-alert-variant-outline", "Reserved1");
PBS("Reserved 2", "battery-alert-variant-outline", "Reserved2");
PBS("Reserved 3", "battery-alert-variant-outline", "Reserved3");
#undef PBS
break;
}
@ -323,7 +360,7 @@ void MqttHandleBatteryHassClass::createDeviceInfo(JsonObject& object)
{
object["name"] = "Battery(" + serial + ")";
auto& config = Configuration.get();
auto const& config = Configuration.get();
if (config.Battery.Provider == 1) {
object["name"] = "JK BMS (" + Battery.getStats()->getManufacturer() + ")";
}

View File

@ -65,7 +65,7 @@ void MqttHandlePowerLimiterClass::loop()
{
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
const CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!config.PowerLimiter.Enabled) {
_mqttCallbacks.clear();
@ -87,15 +87,15 @@ void MqttHandlePowerLimiterClass::loop()
auto val = static_cast<unsigned>(PowerLimiter.getMode());
MqttSettings.publish("powerlimiter/status/mode", String(val));
MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit));
MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.TotalUpperPowerLimit));
MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption));
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
// no thresholds are relevant for setups without a battery
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
if (!PowerLimiter.usesBatteryPoweredInverter()) { return; }
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
@ -118,8 +118,6 @@ void MqttHandlePowerLimiterClass::loop()
void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
CONFIG_T& config = Configuration.get();
std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
try {
@ -132,30 +130,35 @@ void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, con
}
const int intValue = static_cast<int>(payload_val);
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
if (command == MqttPowerLimiterCommand::Mode) {
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
using Mode = PowerLimiterClass::Mode;
Mode mode = static_cast<Mode>(intValue);
if (mode == Mode::UnconditionalFullSolarPassthrough) {
MessageOutput.println("Power limiter unconditional full solar PT");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
} else if (mode == Mode::Disabled) {
MessageOutput.println("Power limiter disabled (override)");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Disabled));
} else if (mode == Mode::Normal) {
MessageOutput.println("Power limiter normal operation");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Normal));
} else {
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
}
return;
}
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
switch (command) {
case MqttPowerLimiterCommand::Mode:
{
using Mode = PowerLimiterClass::Mode;
Mode mode = static_cast<Mode>(intValue);
if (mode == Mode::UnconditionalFullSolarPassthrough) {
MessageOutput.println("Power limiter unconditional full solar PT");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
} else if (mode == Mode::Disabled) {
MessageOutput.println("Power limiter disabled (override)");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Disabled));
} else if (mode == Mode::Normal) {
MessageOutput.println("Power limiter normal operation");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Normal));
} else {
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
}
return;
}
// handled separately above to avoid locking two mutexes
break;
case MqttPowerLimiterCommand::BatterySoCStartThreshold:
if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; }
MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue);
@ -192,9 +195,9 @@ void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, con
config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val;
break;
case MqttPowerLimiterCommand::UpperPowerLimit:
if (config.PowerLimiter.UpperPowerLimit == intValue) { return; }
MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue);
config.PowerLimiter.UpperPowerLimit = intValue;
if (config.PowerLimiter.TotalUpperPowerLimit == intValue) { return; }
MessageOutput.printf("Setting total upper power limit to: %d W\r\n", intValue);
config.PowerLimiter.TotalUpperPowerLimit = intValue;
break;
case MqttPowerLimiterCommand::TargetPowerConsumption:
if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; }

View File

@ -9,6 +9,7 @@
#include "NetworkSettings.h"
#include "MessageOutput.h"
#include "Utils.h"
#include "PowerLimiter.h"
#include "__compiled_constants.h"
MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
@ -64,7 +65,7 @@ void MqttHandlePowerLimiterHassClass::publishConfig()
publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode");
if (config.PowerLimiter.IsInverterSolarPowered) {
if (!PowerLimiter.usesBatteryPoweredInverter()) {
return;
}

View File

@ -35,7 +35,7 @@ void MqttHandleVedirectClass::forceUpdate()
void MqttHandleVedirectClass::loop()
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!MqttSettings.getConnected() || !config.Vedirect.Enabled) {
return;
@ -110,8 +110,10 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da
PUBLISH(productID_PID, "PID", currentData.getPidAsString().data());
PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER);
PUBLISH(firmwareVer_FW, "FWI", currentData.getFwVersionAsInteger());
PUBLISH(firmwareVer_FW, "FWF", currentData.getFwVersionFormatted());
PUBLISH(firmwareVer_FW, "FW", currentData.firmwareVer_FW);
PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF"));
PUBLISH(firmwareVer_FWE, "FWE", currentData.firmwareVer_FWE);
PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data());
PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data());
PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data());
@ -136,8 +138,13 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da
MqttSettings.publish(topic + t, String(val)); \
}
PUBLISH_OPT(relayState_RELAY, "RELAY", currentData.relayState_RELAY.second ? "ON" : "OFF");
PUBLISH_OPT(loadOutputState_LOAD, "LOAD", currentData.loadOutputState_LOAD.second ? "ON" : "OFF");
PUBLISH_OPT(loadCurrent_IL_mA, "IL", currentData.loadCurrent_IL_mA.second / 1000.0);
PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0);
PUBLISH_OPT(BatteryAbsorptionMilliVolt, "BatteryAbsorption", currentData.BatteryAbsorptionMilliVolt.second / 1000.0);
PUBLISH_OPT(BatteryFloatMilliVolt, "BatteryFloat", currentData.BatteryFloatMilliVolt.second / 1000.0);
PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
#undef PUBLILSH_OPT
}

View File

@ -63,9 +63,11 @@ void MqttHandleVedirectHassClass::publishConfig()
auto optMpptData = VictronMppt.getData(idx);
if (!optMpptData.has_value()) { continue; }
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData);
publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware version integer", "mdi:counter", "FWI", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware version formatted", "mdi:counter", "FWF", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware version FW", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware version FWE", "mdi:counter", "FWE", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData);
@ -88,6 +90,17 @@ void MqttHandleVedirectHassClass::publishConfig()
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData);
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData);
// optional info, provided only if the charge controller delivers the information
if (optMpptData->relayState_RELAY.first != 0) {
publishBinarySensor("MPPT error relay state", "mdi:electric-switch", "RELAY", "ON", "OFF", *optMpptData);
}
if (optMpptData->loadOutputState_LOAD.first != 0) {
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData);
}
if (optMpptData->loadCurrent_IL_mA.first != 0) {
publishSensor("MPPT load current", NULL, "IL", "current", "measurement", "A", *optMpptData);
}
// optional info, provided only if TX is connected to charge controller
if (optMpptData->NetworkTotalDcInputPowerMilliWatts.first != 0) {
publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData);
@ -95,6 +108,12 @@ void MqttHandleVedirectHassClass::publishConfig()
if (optMpptData->MpptTemperatureMilliCelsius.first != 0) {
publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "°C", *optMpptData);
}
if (optMpptData->BatteryAbsorptionMilliVolt.first != 0) {
publishSensor("Battery absorption voltage", "mdi:battery-charging-90", "BatteryAbsorption", "voltage", "measurement", "V", *optMpptData);
}
if (optMpptData->BatteryFloatMilliVolt.first != 0) {
publishSensor("Battery float voltage", "mdi:battery-charging-100", "BatteryFloat", "voltage", "measurement", "V", *optMpptData);
}
if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) {
publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "°C", *optMpptData);
}

View File

@ -92,8 +92,7 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
{
if (_verboseLogging) {
MessageOutput.print("Received MQTT message on topic: ");
MessageOutput.println(topic);
MessageOutput.printf("Received MQTT message on topic: %s\r\n", topic);
}
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);

View File

@ -4,6 +4,7 @@
*/
#include "PinMapping.h"
#include "MessageOutput.h"
#include "Utils.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <string.h>
@ -321,6 +322,8 @@ bool PinMappingClass::init(const String& deviceMapping)
return false;
}
Utils::skipBom(f);
JsonDocument doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, f);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
#include "PowerLimiterBatteryInverter.h"
PowerLimiterBatteryInverter::PowerLimiterBatteryInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: PowerLimiterInverter(verboseLogging, config) { }
uint16_t PowerLimiterBatteryInverter::getMaxReductionWatts(bool allowStandby) const
{
if (!isEligible()) { return 0; }
if (!isProducing()) { return 0; }
if (allowStandby) { return getCurrentOutputAcWatts(); }
if (getCurrentOutputAcWatts() <= _config.LowerPowerLimit) { return 0; }
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterBatteryInverter::getMaxIncreaseWatts() const
{
if (!isEligible()) { return 0; }
if (!isProducing()) {
return getConfiguredMaxPowerWatts();
}
// this should not happen for battery-powered inverters, but we want to
// be robust in case something else set a limit on the inverter (or in
// case we did something wrong...).
if (getCurrentLimitWatts() >= getConfiguredMaxPowerWatts()) { return 0; }
// we must not substract the current AC output here, but the current
// limit value, so we avoid trying to produce even more even if the
// inverter is already at the maximum limit value (the actual AC
// output may be less than the inverter's current power limit).
return getConfiguredMaxPowerWatts() - getCurrentLimitWatts();
}
uint16_t PowerLimiterBatteryInverter::applyReduction(uint16_t reduction, bool allowStandby)
{
if (!isEligible()) { return 0; }
if (reduction == 0) { return 0; }
auto low = std::min(getCurrentLimitWatts(), getCurrentOutputAcWatts());
if (low <= _config.LowerPowerLimit) {
if (allowStandby) {
standby();
return std::min(reduction, getCurrentOutputAcWatts());
}
return 0;
}
if ((getCurrentLimitWatts() - _config.LowerPowerLimit) >= reduction) {
setAcOutput(getCurrentLimitWatts() - reduction);
return reduction;
}
if (allowStandby) {
standby();
return std::min(reduction, getCurrentOutputAcWatts());
}
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterBatteryInverter::applyIncrease(uint16_t increase)
{
if (!isEligible()) { return 0; }
if (increase == 0) { return 0; }
// do not wake inverter up if it would produce too much power
if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; }
auto baseline = getCurrentLimitWatts();
// battery-powered inverters in standby can have an arbitrary limit, yet
// the baseline is 0 in case we are about to wake it up from standby.
if (!isProducing()) { baseline = 0; }
auto actualIncrease = std::min(increase, getMaxIncreaseWatts());
setAcOutput(baseline + actualIncrease);
return actualIncrease;
}
uint16_t PowerLimiterBatteryInverter::standby()
{
setTargetPowerState(false);
setExpectedOutputAcWatts(0);
return getCurrentOutputAcWatts();
}
void PowerLimiterBatteryInverter::setAcOutput(uint16_t expectedOutputWatts)
{
// make sure to enforce the lower and upper bounds
expectedOutputWatts = std::min(expectedOutputWatts, getConfiguredMaxPowerWatts());
expectedOutputWatts = std::max(expectedOutputWatts, _config.LowerPowerLimit);
setExpectedOutputAcWatts(expectedOutputWatts);
setTargetPowerLimitWatts(expectedOutputWatts);
setTargetPowerState(true);
}

View File

@ -0,0 +1,311 @@
#include "RestartHelper.h"
#include "MessageOutput.h"
#include "PowerLimiterInverter.h"
#include "PowerLimiterBatteryInverter.h"
#include "PowerLimiterSolarInverter.h"
std::unique_ptr<PowerLimiterInverter> PowerLimiterInverter::create(
bool verboseLogging, PowerLimiterInverterConfig const& config)
{
std::unique_ptr<PowerLimiterInverter> upInverter;
if (config.IsSolarPowered) {
upInverter = std::make_unique<PowerLimiterSolarInverter>(verboseLogging, config);
}
else {
upInverter = std::make_unique<PowerLimiterBatteryInverter>(verboseLogging, config);
}
if (nullptr == upInverter->_spInverter) { return nullptr; }
return std::move(upInverter);
}
PowerLimiterInverter::PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: _config(config)
, _verboseLogging(verboseLogging)
{
_spInverter = Hoymiles.getInverterBySerial(config.Serial);
if (!_spInverter) { return; }
snprintf(_serialStr, sizeof(_serialStr), "%0x%08x",
static_cast<uint32_t>((config.Serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(config.Serial & 0xFFFFFFFF));
snprintf(_logPrefix, sizeof(_logPrefix), "[DPL inverter %s]:", _serialStr);
}
bool PowerLimiterInverter::isEligible() const
{
if (!isReachable() || !isSendingCommandsEnabled()) { return false; }
// after startup, the limit effective at the inverter is not known. the
// respective message to request this info is only sent after a significant
// backoff (4 minutes). this is to avoid error messages to appear in the
// inverter's event log. we will wait until the current limit is known.
if (getCurrentLimitWatts() == 0) { return false; }
// the model-dependent maximum AC power output is only known after the
// first DevInfoSimpleCommand succeeded. we desperately need this info, so
// the inverter is not eligible until this value is known.
if (getInverterMaxPowerWatts() == 0) { return false; }
return true;
}
bool PowerLimiterInverter::update()
{
auto reset = [this]() -> bool {
_oTargetPowerState = std::nullopt;
_oTargetPowerLimitWatts = std::nullopt;
_oUpdateStartMillis = std::nullopt;
return false;
};
// do not reset _updateTimeouts below if no state change requested
if (!_oTargetPowerState.has_value() && !_oTargetPowerLimitWatts.has_value()) {
return reset();
}
if (!_oUpdateStartMillis.has_value()) {
_oUpdateStartMillis = millis();
}
if ((millis() - *_oUpdateStartMillis) > 30 * 1000) {
++_updateTimeouts;
MessageOutput.printf("%s timeout (%d in succession), "
"state transition pending: %s, limit pending: %s\r\n",
_logPrefix, _updateTimeouts,
(_oTargetPowerState.has_value()?"yes":"no"),
(_oTargetPowerLimitWatts.has_value()?"yes":"no"));
// NOTE that this is not always 5 minutes, since this counts timeouts,
// not absolute time. after any timeout, an update cycle ends. a new
// timeout can only happen after starting a new update cycle, which in
// turn is only started if the DPL did calculate a new limit, which in
// turn does not happen while the inverter is unreachable, no matter
// how long (a whole night) that might be.
if (_updateTimeouts >= 10) {
MessageOutput.printf("%s issuing restart command after update "
"timed out repeatedly\r\n", _logPrefix);
_spInverter->sendRestartControlRequest();
}
if (_updateTimeouts >= 20) {
MessageOutput.printf("%s restarting system since inverter is "
"unresponsive\r\n", _logPrefix);
RestartHelper.triggerRestart();
}
return reset();
}
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
auto switchPowerState = [this](bool transitionOn) -> bool {
// no power state transition requested at all
if (!_oTargetPowerState.has_value()) { return false; }
// the transition that may be started is not the one which is requested
if (transitionOn != *_oTargetPowerState) { return false; }
// wait for pending power command(s) to complete
auto lastPowerCommandState = _spInverter->PowerCommand()->getLastPowerCommandSuccess();
if (CMD_PENDING == lastPowerCommandState) {
return true;
}
// we need to wait for statistics that are more recent than
// the last power update command to reliably use isProducing()
auto lastPowerCommandMillis = _spInverter->PowerCommand()->getLastUpdateCommand();
auto lastStatisticsMillis = _spInverter->Statistics()->getLastUpdate();
if ((lastStatisticsMillis - lastPowerCommandMillis) > halfOfAllMillis) { return true; }
if (isProducing() != *_oTargetPowerState) {
MessageOutput.printf("%s %s inverter...\r\n", _logPrefix,
((*_oTargetPowerState)?"Starting":"Stopping"));
_spInverter->sendPowerControlRequest(*_oTargetPowerState);
return true;
}
_oTargetPowerState = std::nullopt; // target power state reached
return false;
};
// we use a lambda function here to be able to use return statements,
// which allows to avoid if-else-indentions and improves code readability
auto updateLimit = [this]() -> bool {
// no limit update requested at all
if (!_oTargetPowerLimitWatts.has_value()) { return false; }
// wait for pending limit command(s) to complete
auto lastLimitCommandState = _spInverter->SystemConfigPara()->getLastLimitCommandSuccess();
if (CMD_PENDING == lastLimitCommandState) {
return true;
}
float newRelativeLimit = static_cast<float>(*_oTargetPowerLimitWatts * 100) / getInverterMaxPowerWatts();
// if no limit command is pending, the SystemConfigPara does report the
// current limit, as the answer by the inverter to a limit command is
// the canonical source that updates the known current limit.
auto currentRelativeLimit = _spInverter->SystemConfigPara()->getLimitPercent();
// we assume having exclusive control over the inverter. if the last
// limit command was successful and sent after we started the last
// update cycle, we should assume *our* requested limit was set.
uint32_t lastLimitCommandMillis = _spInverter->SystemConfigPara()->getLastUpdateCommand();
if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis &&
CMD_OK == lastLimitCommandState) {
MessageOutput.printf("%s actual limit is %.1f %% (%.0f W "
"respectively), effective %d ms after update started, "
"requested were %.1f %%\r\n",
_logPrefix, currentRelativeLimit,
(currentRelativeLimit * getInverterMaxPowerWatts() / 100),
(lastLimitCommandMillis - *_oUpdateStartMillis),
newRelativeLimit);
if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) {
MessageOutput.printf("%s NOTE: expected limit of %.1f %% "
"and actual limit of %.1f %% mismatch by more than 2 %%, "
"is the DPL in exclusive control over the inverter?\r\n",
_logPrefix, newRelativeLimit, currentRelativeLimit);
}
_oTargetPowerLimitWatts = std::nullopt;
return false;
}
MessageOutput.printf("%s sending limit of %.1f %% (%.0f W "
"respectively), max output is %d W\r\n", _logPrefix,
newRelativeLimit, (newRelativeLimit * getInverterMaxPowerWatts() / 100),
getInverterMaxPowerWatts());
_spInverter->sendActivePowerControlRequest(newRelativeLimit,
PowerLimitControlType::RelativNonPersistent);
return true;
};
// disable power production as soon as possible.
// setting the power limit is less important once the inverter is off.
if (switchPowerState(false)) { return true; }
if (updateLimit()) { return true; }
// enable power production only after setting the desired limit
if (switchPowerState(true)) { return true; }
_updateTimeouts = 0;
return reset();
}
std::optional<uint32_t> PowerLimiterInverter::getLatestStatsMillis() const
{
uint32_t now = millis();
// concerns both power limits and start/stop/restart commands and is
// only updated if a respective response was received from the inverter
auto lastUpdateCmdAge = std::min(
now - _spInverter->SystemConfigPara()->getLastUpdateCommand(),
now - _spInverter->PowerCommand()->getLastUpdateCommand()
);
// we use _oStatsMillis to persist a stats update timestamp, as we are
// looking for the single oldest inverter stats which is still younger than
// the last update command. we shall not just return the actual youngest
// stats timestamp if newer stats arrived while no update command was sent
// in the meantime.
if (_oStatsMillis && lastUpdateCmdAge < (now - *_oStatsMillis)) {
_oStatsMillis.reset();
}
if (!_oStatsMillis) {
auto lastStatsMillis = _spInverter->Statistics()->getLastUpdate();
auto lastStatsAge = now - lastStatsMillis;
if (lastStatsAge > lastUpdateCmdAge) {
return std::nullopt;
}
_oStatsMillis = lastStatsMillis;
}
return _oStatsMillis;
}
uint16_t PowerLimiterInverter::getInverterMaxPowerWatts() const
{
return _spInverter->DevInfo()->getMaxPower();
}
uint16_t PowerLimiterInverter::getConfiguredMaxPowerWatts() const
{
return std::min(getInverterMaxPowerWatts(), _config.UpperPowerLimit);
}
uint16_t PowerLimiterInverter::getCurrentOutputAcWatts() const
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
}
uint16_t PowerLimiterInverter::getExpectedOutputAcWatts() const
{
if (!_oTargetPowerLimitWatts && !_oTargetPowerState) {
// the inverter's output will not change due to commands being sent
return getCurrentOutputAcWatts();
}
return _expectedOutputAcWatts;
}
void PowerLimiterInverter::setMaxOutput()
{
_oTargetPowerState = true;
setAcOutput(getConfiguredMaxPowerWatts());
}
void PowerLimiterInverter::restart()
{
_spInverter->sendRestartControlRequest();
}
float PowerLimiterInverter::getDcVoltage(uint8_t input)
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_DC,
static_cast<ChannelNum_t>(input), FLD_UDC);
}
uint16_t PowerLimiterInverter::getCurrentLimitWatts() const
{
auto currentLimitPercent = _spInverter->SystemConfigPara()->getLimitPercent();
return static_cast<uint16_t>(currentLimitPercent * getInverterMaxPowerWatts() / 100);
}
void PowerLimiterInverter::debug() const
{
if (!_verboseLogging) { return; }
MessageOutput.printf(
"%s\r\n"
" %s-powered, %s %d W\r\n"
" lower/current/upper limit: %d/%d/%d W, output capability: %d W\r\n"
" sending commands %s, %s, %s\r\n"
" max reduction production/standby: %d/%d W, max increase: %d W\r\n"
" target limit/output/state: %i W (%s)/%d W/%s, %d update timeouts\r\n",
_logPrefix,
(isSolarPowered()?"solar":"battery"),
(isProducing()?"producing":"standing by at"), getCurrentOutputAcWatts(),
_config.LowerPowerLimit, getCurrentLimitWatts(), _config.UpperPowerLimit,
getInverterMaxPowerWatts(),
(isSendingCommandsEnabled()?"enabled":"disabled"),
(isReachable()?"reachable":"offline"),
(isEligible()?"eligible":"disqualified"),
getMaxReductionWatts(false), getMaxReductionWatts(true), getMaxIncreaseWatts(),
(_oTargetPowerLimitWatts.has_value()?*_oTargetPowerLimitWatts:-1),
(_oTargetPowerLimitWatts.has_value()?"update":"unchanged"),
getExpectedOutputAcWatts(),
(_oTargetPowerState.has_value()?(*_oTargetPowerState?"production":"standby"):"unchanged"),
getUpdateTimeouts()
);
}

View File

@ -0,0 +1,248 @@
#include "MessageOutput.h"
#include "PowerLimiterSolarInverter.h"
PowerLimiterSolarInverter::PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: PowerLimiterInverter(verboseLogging, config) { }
uint16_t PowerLimiterSolarInverter::getMaxReductionWatts(bool) const
{
if (!isEligible()) { return 0; }
auto low = std::min(getCurrentLimitWatts(), getCurrentOutputAcWatts());
if (low <= _config.LowerPowerLimit) { return 0; }
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::getMaxIncreaseWatts() const
{
if (!isEligible()) { return 0; }
// the maximum increase possible for this inverter
int16_t maxTotalIncrease = getConfiguredMaxPowerWatts() - getCurrentOutputAcWatts();
if (!isProducing()) {
// the inverter is not producing, we don't know how much we can increase
// the power, so we return the maximum possible increase
return maxTotalIncrease;
}
auto pStats = _spInverter->Statistics();
std::vector<MpptNum_t> dcMppts = _spInverter->getMppts();
size_t dcTotalMppts = dcMppts.size();
float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF) / 100;
// 98% of the expected power is good enough
auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * 0.98;
size_t dcNonShadedMppts = 0;
auto nonShadedMpptACPowerSum = 0.0;
for (auto& m : dcMppts) {
float mpptPowerAC = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);
for (auto& c : mpptChnls) {
mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
}
if (mpptPowerAC >= expectedAcPowerPerMppt) {
nonShadedMpptACPowerSum += mpptPowerAC;
dcNonShadedMppts++;
}
}
if (dcNonShadedMppts == 0) {
// all mppts are shaded, we can't increase the power
return 0;
}
if (dcNonShadedMppts == dcTotalMppts) {
// no MPPT is shaded, we assume that we can increase the power by the maximum
return maxTotalIncrease;
}
int16_t maxPowerPerMppt = getConfiguredMaxPowerWatts() / dcTotalMppts;
int16_t currentPowerPerNonShadedMppt = nonShadedMpptACPowerSum / dcNonShadedMppts;
int16_t maxIncreasePerNonShadedMppt = maxPowerPerMppt - currentPowerPerNonShadedMppt;
// maximum increase based on the non-shaded mppts
return maxIncreasePerNonShadedMppt * dcNonShadedMppts;
}
uint16_t PowerLimiterSolarInverter::applyReduction(uint16_t reduction, bool)
{
if (!isEligible()) { return 0; }
if (reduction == 0) { return 0; }
if ((getCurrentOutputAcWatts() - _config.LowerPowerLimit) >= reduction) {
setAcOutput(getCurrentOutputAcWatts() - reduction);
return reduction;
}
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::applyIncrease(uint16_t increase)
{
if (!isEligible()) { return 0; }
if (increase == 0) { return 0; }
// do not wake inverter up if it would produce too much power
if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; }
// the limit for solar-powered inverters might be scaled, so we use the
// current output as the baseline. solar-powered inverters in standby have
// no output (baseline is zero).
auto baseline = getCurrentOutputAcWatts();
auto actualIncrease = std::min(increase, getMaxIncreaseWatts());
setAcOutput(baseline + actualIncrease);
return actualIncrease;
}
uint16_t PowerLimiterSolarInverter::standby()
{
// solar-powered inverters are never actually put into standby (by the
// DPL), but only set to the configured lower power limit instead.
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::scaleLimit(uint16_t expectedOutputWatts)
{
// prevent scaling if inverter is not producing, as input channels are not
// producing energy and hence are detected as not-producing, causing
// unreasonable scaling.
if (!isProducing()) { return expectedOutputWatts; }
auto pStats = _spInverter->Statistics();
std::vector<ChannelNum_t> dcChnls = _spInverter->getChannelsDC();
std::vector<MpptNum_t> dcMppts = _spInverter->getMppts();
size_t dcTotalChnls = dcChnls.size();
size_t dcTotalMppts = dcMppts.size();
// if there is only one MPPT available, there is nothing we can do
if (dcTotalMppts <= 1) { return expectedOutputWatts; }
// test for a reasonable power limit that allows us to assume that an input
// channel with little energy is actually not producing, rather than
// producing very little due to the very low limit.
if (getCurrentLimitWatts() < dcTotalChnls * 10) { return expectedOutputWatts; }
// overscalling allows us to compensate for shaded panels by increasing the
// total power limit, if the inverter is solar powered.
if (_config.UseOverscalingToCompensateShading) {
auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF);
// fall back to hoymiles peak efficiency as per datasheet if inverter
// is currently not producing (efficiency is zero in that case)
inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967;
// 98% of the expected power is good enough
auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * 0.98;
if (_verboseLogging) {
MessageOutput.printf("%s expected AC power per MPPT %.0f W\r\n",
_logPrefix, expectedAcPowerPerMppt);
}
size_t dcShadedMppts = 0;
auto shadedChannelACPowerSum = 0.0;
for (auto& m : dcMppts) {
float mpptPowerAC = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);
for (auto& c : mpptChnls) {
mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
}
if (mpptPowerAC < expectedAcPowerPerMppt) {
dcShadedMppts++;
shadedChannelACPowerSum += mpptPowerAC;
}
if (_verboseLogging) {
MessageOutput.printf("%s MPPT-%c AC power %.0f W\r\n",
_logPrefix, m + 'a', mpptPowerAC);
}
}
// no shading or the shaded channels provide more power than what
// we currently need.
if (dcShadedMppts == 0 || shadedChannelACPowerSum >= expectedOutputWatts) {
return expectedOutputWatts;
}
if (dcShadedMppts == dcTotalMppts) {
// keep the currentLimit when:
// - all channels are shaded
// - currentLimit >= expectedOutputWatts
// - we get the expected AC power or less and
if (getCurrentLimitWatts() >= expectedOutputWatts &&
inverterOutputAC <= expectedOutputWatts) {
if (_verboseLogging) {
MessageOutput.printf("%s all mppts are shaded, "
"keeping the current limit of %d W\r\n",
_logPrefix, getCurrentLimitWatts());
}
return getCurrentLimitWatts();
} else {
return expectedOutputWatts;
}
}
size_t dcNonShadedMppts = dcTotalMppts - dcShadedMppts;
uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedMppts * dcTotalMppts;
if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; }
if (_verboseLogging) {
MessageOutput.printf("%s %d/%d mppts are shaded, scaling %d W\r\n",
_logPrefix, dcShadedMppts, dcTotalMppts, overScaledLimit);
}
return overScaledLimit;
}
size_t dcProdMppts = 0;
for (auto& m : dcMppts) {
float dcPowerMppt = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);
for (auto& c : mpptChnls) {
dcPowerMppt += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
}
if (dcPowerMppt > 2.0 * mpptChnls.size()) {
dcProdMppts++;
}
}
if (dcProdMppts == 0 || dcProdMppts == dcTotalMppts) { return expectedOutputWatts; }
uint16_t scaled = expectedOutputWatts / dcProdMppts * dcTotalMppts;
MessageOutput.printf("%s %d/%d mppts are producing, scaling from %d to "
"%d W\r\n", _logPrefix, dcProdMppts, dcTotalMppts,
expectedOutputWatts, scaled);
return scaled;
}
void PowerLimiterSolarInverter::setAcOutput(uint16_t expectedOutputWatts)
{
setExpectedOutputAcWatts(expectedOutputWatts);
setTargetPowerLimitWatts(scaleLimit(expectedOutputWatts));
setTargetPowerState(true);
}

View File

@ -74,10 +74,10 @@ void SBSCanReceiver::onMessage(twai_message_t rx_message)
case 0x640: {
_stats->_chargeCurrentLimitation = (this->readSignedInt24(rx_message.data + 3) * 0.001);
_stats->_dischargeCurrentLimitation = (this->readSignedInt24(rx_message.data)) * 0.001;
_stats->setDischargeCurrentLimit(this->readSignedInt24(rx_message.data) * 0.001, millis());
if (_verboseLogging) {
MessageOutput.printf("[SBS Unipower] 1600 Currents %f, %f \r\n", _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation);
MessageOutput.printf("[SBS Unipower] 1600 Currents %f, %f \r\n", _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit());
}
break;
}
@ -144,7 +144,7 @@ void SBSCanReceiver::dummyData()
_stats->setSoC(42, 0/*precision*/, millis());
_stats->_chargeVoltage = dummyFloat(50);
_stats->_chargeCurrentLimitation = dummyFloat(33);
_stats->_dischargeCurrentLimitation = dummyFloat(12);
_stats->setDischargeCurrentLimit(dummyFloat(12), millis());
_stats->_stateOfHealth = 99;
_stats->setVoltage(48.67, millis());
_stats->_current = dummyFloat(-1);

View File

@ -7,6 +7,8 @@
#include "Utils.h"
#include <Arduino.h>
#define CALC_UNIQUE_ID (((timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday) << 1 | timeinfo.tm_isdst)
SunPositionClass SunPosition;
SunPositionClass::SunPositionClass()
@ -57,7 +59,7 @@ bool SunPositionClass::checkRecalcDayChanged() const
time(&now);
localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms
const uint32_t ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday;
const uint32_t ymd = CALC_UNIQUE_ID;
return _lastSunPositionCalculatedYMD != ymd;
}
@ -67,7 +69,7 @@ void SunPositionClass::updateSunData()
struct tm timeinfo;
const bool gotLocalTime = getLocalTime(&timeinfo, 5);
_lastSunPositionCalculatedYMD = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday;
_lastSunPositionCalculatedYMD = CALC_UNIQUE_ID;
setDoRecalc(false);
if (!gotLocalTime) {

View File

@ -7,6 +7,7 @@
#include "MessageOutput.h"
#include "PinMapping.h"
#include <LittleFS.h>
#include <MD5Builder.h>
uint32_t Utils::getChipId()
{
@ -81,6 +82,48 @@ void Utils::removeAllFiles()
}
}
String Utils::generateMd5FromFile(String file)
{
if (!LittleFS.exists(file)) {
return String();
}
File f = LittleFS.open(file, "r");
if (!file) {
return String();
}
MD5Builder md5;
md5.begin();
// Read the file in chunks to avoid using too much memory
uint8_t buffer[512];
while (f.available()) {
size_t bytesRead = f.read(buffer, sizeof(buffer) / sizeof(buffer[0]));
md5.add(buffer, bytesRead);
}
// Finalize and calculate the MD5 hash
md5.calculate();
f.close();
return md5.toString();
}
void Utils::skipBom(File& f)
{
// skip Byte Order Mask (BOM). valid JSON docs always start with '{' or '['.
while (f.available() > 0) {
int c = f.peek();
if (c == '{' || c == '[') {
break;
}
f.read();
}
}
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>
std::optional<T> getFromString(char const* val);

View File

@ -27,7 +27,7 @@ void VictronMpptClass::updateSettings()
}
_serialPortOwners.clear();
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (!config.Vedirect.Enabled) { return; }
const PinMapping_t& pin = PinMapping.get();
@ -226,3 +226,39 @@ float VictronMpptClass::getOutputVoltage() const
return min;
}
std::optional<uint8_t> VictronMpptClass::getStateOfOperation() const
{
for (const auto& upController : _controllers) {
if (upController->isDataValid()) {
return upController->getData().currentState_CS;
}
}
return std::nullopt;
}
std::optional<float> VictronMpptClass::getVoltage(MPPTVoltage kindOf) const
{
for (const auto& upController : _controllers) {
switch (kindOf) {
case MPPTVoltage::ABSORPTION: {
auto const& absorptionVoltage = upController->getData().BatteryAbsorptionMilliVolt;
if (absorptionVoltage.first > 0) { return absorptionVoltage.second; }
break;
}
case MPPTVoltage::FLOAT: {
auto const& floatVoltage = upController->getData().BatteryFloatMilliVolt;
if (floatVoltage.first > 0) { return floatVoltage.second; }
break;
}
case MPPTVoltage::BATTERY: {
auto const& batteryVoltage = upController->getData().batteryVoltage_V_mV;
if (upController->isDataValid()) { return batteryVoltage; }
break;
}
}
}
return std::nullopt;
}

View File

@ -15,13 +15,14 @@ WebApiClass::WebApiClass()
void WebApiClass::init(Scheduler& scheduler)
{
_webApiConfig.init(_server, scheduler);
_webApiDevice.init(_server, scheduler);
_webApiDevInfo.init(_server, scheduler);
_webApiDtu.init(_server, scheduler);
_webApiEventlog.init(_server, scheduler);
_webApiFile.init(_server, scheduler);
_webApiFirmware.init(_server, scheduler);
_webApiGridprofile.init(_server, scheduler);
_webApiI18n.init(_server, scheduler);
_webApiInverter.init(_server, scheduler);
_webApiLimit.init(_server, scheduler);
_webApiMaintenance.init(_server, scheduler);
@ -58,7 +59,7 @@ void WebApiClass::reload()
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (request->authenticate(AUTH_USERNAME, config.Security.Password)) {
return true;
}
@ -76,7 +77,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request)
{
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) {
return true;
} else {

View File

@ -153,7 +153,7 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
root["enabled"] = config.Huawei.Enabled;
root["verbose_logging"] = config.Huawei.VerboseLogging;
@ -200,19 +200,22 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
return;
}
CONFIG_T& config = Configuration.get();
config.Huawei.Enabled = root["enabled"].as<bool>();
config.Huawei.VerboseLogging = root["verbose_logging"];
config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as<uint32_t>();
config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as<bool>();
config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as<bool>();
config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as<bool>();
config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as<float>();
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"];
config.Huawei.Auto_Power_Target_Power_Consumption = root["target_power_consumption"];
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.Huawei.Enabled = root["enabled"].as<bool>();
config.Huawei.VerboseLogging = root["verbose_logging"];
config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as<uint32_t>();
config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as<bool>();
config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as<bool>();
config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as<bool>();
config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as<float>();
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"];
config.Huawei.Auto_Power_Target_Power_Consumption = root["target_power_consumption"];
}
WebApi.writeConfig(retMsg);
@ -226,6 +229,8 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
yield();
ESP.restart();
auto const& config = Configuration.get();
const PinMapping_t& pin = PinMapping.get();
// Properly turn this on
if (config.Huawei.Enabled) {

View File

@ -33,7 +33,7 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
AsyncJsonResponse* response = new AsyncJsonResponse();
auto root = response->getRoot().as<JsonObject>();
auto& config = Configuration.get();
auto const& config = Configuration.get();
ConfigurationClass::serializeBatteryConfig(config.Battery, root);
@ -70,8 +70,11 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
return;
}
auto& config = Configuration.get();
ConfigurationClass::deserializeBatteryConfig(root.as<JsonObject>(), config.Battery);
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
ConfigurationClass::deserializeBatteryConfig(root.as<JsonObject>(), config.Battery);
}
WebApi.writeConfig(retMsg);

View File

@ -86,7 +86,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["power_safe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver;
display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language;
display["locale"] = config.Display.Locale;
display["diagramduration"] = config.Display.Diagram.Duration;
display["diagrammode"] = config.Display.Diagram.Mode;
@ -158,29 +158,34 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
return;
}
CONFIG_T& config = Configuration.get();
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping));
config.Display.Rotation = root["display"]["rotation"].as<uint8_t>();
config.Display.PowerSafe = root["display"]["power_safe"].as<bool>();
config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>();
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
config.Display.Language = root["display"]["language"].as<uint8_t>();
config.Display.Diagram.Duration = root["display"]["diagramduration"].as<uint32_t>();
config.Display.Diagram.Mode = root["display"]["diagrammode"].as<DiagramMode_t>();
strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping));
config.Display.Rotation = root["display"]["rotation"].as<uint8_t>();
config.Display.PowerSafe = root["display"]["power_safe"].as<bool>();
config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>();
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
strlcpy(config.Display.Locale, root["display"]["locale"].as<String>().c_str(), sizeof(config.Display.Locale));
config.Display.Diagram.Duration = root["display"]["diagramduration"].as<uint32_t>();
config.Display.Diagram.Mode = root["display"]["diagrammode"].as<DiagramMode_t>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness);
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness);
}
}
auto const& config = Configuration.get();
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
Display.setOrientation(config.Display.Rotation);
Display.enablePowerSafe = config.Display.PowerSafe;
Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language);
Display.setLocale(config.Display.Locale);
Display.Diagram().updatePeriod();
WebApi.writeConfig(retMsg);

View File

@ -27,7 +27,7 @@ void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler)
void WebApiDtuClass::applyDataTaskCb()
{
// Execute stuff in main thread to avoid busy SPI bus
CONFIG_T& config = Configuration.get();
auto const& config = Configuration.get();
Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel);
Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel);
Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial);
@ -50,8 +50,8 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
// DTU Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32,
((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)),
((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF)));
static_cast<uint32_t>((config.Dtu.Serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(config.Dtu.Serial & 0xFFFFFFFF));
root["serial"] = buffer;
root["pollinterval"] = config.Dtu.PollInterval;
root["verbose_logging"] = config.Dtu.VerboseLogging;
@ -155,15 +155,17 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
return;
}
CONFIG_T& config = Configuration.get();
config.Dtu.Serial = serial;
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
config.Dtu.VerboseLogging = root["verbose_logging"].as<bool>();
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
config.Dtu.Cmt.CountryMode = root["cmt_country"].as<CountryModeId_t>();
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.Dtu.Serial = serial;
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
config.Dtu.VerboseLogging = root["verbose_logging"].as<bool>();
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
config.Dtu.Cmt.CountryMode = root["cmt_country"].as<CountryModeId_t>();
}
WebApi.writeConfig(retMsg);

Some files were not shown because too many files have changed in this diff Show More