Prepare Release 2024-08-18

merge development into master
This commit is contained in:
Bernhard Kirchen 2024-08-18 12:10:12 +02:00 committed by GitHub
commit 5a6fe9d174
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
244 changed files with 12552 additions and 8154 deletions

View File

@ -5,11 +5,18 @@ body:
- type: markdown
attributes:
value: >
### ✋ **This is bug tracker, not a support forum**
### ⚠️ Please remember: issues are for *bugs*
That is, something you believe affects every single user of OpenDTU, not just you. If you're not sure, start with one of the other options below.
- type: markdown
attributes:
value: |
#### Have a question? 👉 [Start a new discussion](https://github.com/tbnobody/OpenDTU/discussions/new) or [ask in chat](https://discord.gg/WzhxEY62mB).
If something isn't working right, you have questions or need help, [**get in touch on the Discussions**](https://github.com/tbnobody/OpenDTU/discussions).
#### Before opening an issue, please double check:
Please quickly search existing issues first before submitting a bug.
- [Documentation](https://www.opendtu.solar).
- [The FAQs](https://www.opendtu.solar/firmware/faq/).
- [Existing issues and discussions](https://github.com/tbnobody/OpenDTU/search?q=&type=issues).
- type: textarea
id: what-happened
attributes:
@ -65,4 +72,17 @@ body:
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
required: false
- type: checkboxes
id: required-checks
attributes:
label: Please confirm the following
options:
- label: I believe this issue is a bug that affects all users of OpenDTU, not something specific to my installation.
required: true
- label: I have already searched for relevant existing issues and discussions before opening this report.
required: true
- label: I have updated the title field above with a concise description.
required: true
- label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported
required: true

View File

@ -1,4 +1,4 @@
name: OpenDTU-onBattery Build
name: OpenDTU-OnBattery Build
on:
push:
@ -8,7 +8,7 @@ on:
branches:
- master
- development
tags-ignore:
tags-ignore:
- 'v**'
pull_request:
paths-ignore:
@ -120,6 +120,7 @@ jobs:
name: opendtu-onbattery-${{ matrix.environment }}
path: |
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
!.pio/build/generic_esp32_4mb_no_ota/opendtu-onbattery-generic_esp32_4mb_no_ota.bin
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
release:
@ -130,14 +131,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get tags
run: git fetch --force --tags origin
- name: Get openDTU core release
run: |
echo "OPEN_DTU_CORE_RELEASE=$(git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | grep 'refs/tags/v' | tail -1 | sed 's#.*/##' | sed 's/ .*//')" >> $GITHUB_ENV
- name: Create openDTU-core-release-Badge
uses: schneegans/dynamic-badges-action@v1.6.0
with:

View File

@ -22,6 +22,34 @@
"clk_mode": 0
}
},
{
"name": "WT32-ETH01 with SH1106",
"links": [
{"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"}
],
"nrf24": {
"miso": 4,
"mosi": 2,
"clk": 32,
"irq": 33,
"en": 14,
"cs": 15
},
"eth": {
"enabled": true,
"phy_addr": 1,
"power": 16,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 0
},
"display": {
"type": 3,
"data": 5,
"clk": 17
}
},
{
"name": "WT32-ETH01 with SSD1306",
"links": [
@ -78,4 +106,4 @@
"clk": 17
}
}
]
]

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Battery.h"
#include <driver/twai.h>
#include <Arduino.h>
class BatteryCanReceiver : public BatteryProvider {
public:
bool init(bool verboseLogging, char const* providerName);
void deinit() final;
void loop() final;
virtual void onMessage(twai_message_t rx_message) = 0;
protected:
uint8_t readUnsignedInt8(uint8_t *data);
uint16_t readUnsignedInt16(uint8_t *data);
int16_t readSignedInt16(uint8_t *data);
uint32_t readUnsignedInt32(uint8_t *data);
float scaleValue(int16_t value, float factor);
bool getBit(uint8_t value, uint8_t bit);
bool _verboseLogging = true;
private:
char const* _providerName = "Battery CAN";
};

View File

@ -14,33 +14,37 @@ class BatteryStats {
public:
String const& getManufacturer() const { return _manufacturer; }
// the last time *any* datum was updated
// the last time *any* data was updated
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
bool updateAvailable(uint32_t since) const;
uint8_t getSoC() const { return _soc; }
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
uint8_t getSoCPrecision() const { return _socPrecision; }
float getVoltage() const { return _voltage; }
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }
float getChargeCurrent() const { return _current; };
uint8_t getChargeCurrentPrecision() const { return _currentPrecision; }
// convert stats to JSON for web application live view
virtual void getLiveViewData(JsonVariant& root) const;
void mqttLoop();
// the interval at which all battery datums will be re-published, even
// the interval at which all battery data will be re-published, even
// if they did not change. used to calculate Home Assistent expiration.
virtual uint32_t getMqttFullPublishIntervalMs() const;
bool isSoCValid() const { return _lastUpdateSoC > 0; }
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }
bool isCurrentValid() const { return _lastUpdateCurrent > 0; }
// returns true if the battery reached a critically low voltage/SoC,
// such that it is in need of charging to prevent degredation.
virtual bool getImmediateChargingRequest() const { return false; };
virtual float getChargeCurrent() const { return 0; };
virtual float getChargeCurrentLimitation() const { return FLT_MAX; };
protected:
@ -57,9 +61,16 @@ class BatteryStats {
_lastUpdateVoltage = _lastUpdate = timestamp;
}
void setCurrent(float current, uint8_t precision, uint32_t timestamp) {
_current = current;
_currentPrecision = precision;
_lastUpdateCurrent = _lastUpdate = timestamp;
}
String _manufacturer = "unknown";
String _hwversion = "";
String _fwversion = "";
String _serial = "";
uint32_t _lastUpdate = 0;
private:
@ -69,6 +80,12 @@ class BatteryStats {
uint32_t _lastUpdateSoC = 0;
float _voltage = 0; // total battery pack voltage
uint32_t _lastUpdateVoltage = 0;
// total current into (positive) or from (negative)
// the battery, i.e., the charging current
float _current = 0;
uint8_t _currentPrecision = 0; // decimal places
uint32_t _lastUpdateCurrent = 0;
};
class PylontechBatteryStats : public BatteryStats {
@ -78,7 +95,6 @@ class PylontechBatteryStats : public BatteryStats {
void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final;
bool getImmediateChargingRequest() const { return _chargeImmediately; } ;
float getChargeCurrent() const { return _current; } ;
float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ;
private:
@ -89,9 +105,6 @@ class PylontechBatteryStats : public BatteryStats {
float _chargeCurrentLimitation;
float _dischargeCurrentLimitation;
uint16_t _stateOfHealth;
// total current into (positive) or from (negative)
// the battery, i.e., the charging current
float _current;
float _temperature;
bool _alarmOverCurrentDischarge;
@ -115,6 +128,80 @@ class PylontechBatteryStats : public BatteryStats {
bool _chargeImmediately;
};
class PytesBatteryStats : public BatteryStats {
friend class PytesCanReceiver;
public:
void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final;
float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ;
private:
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }
void updateSerial() {
if (!_serialPart1.isEmpty() && !_serialPart2.isEmpty()) {
_serial = _serialPart1 + _serialPart2;
}
}
String _serialPart1 = "";
String _serialPart2 = "";
float _chargeVoltageLimit;
float _chargeCurrentLimit;
float _dischargeVoltageLimit;
float _dischargeCurrentLimit;
uint16_t _stateOfHealth;
float _temperature;
uint16_t _cellMinMilliVolt;
uint16_t _cellMaxMilliVolt;
float _cellMinTemperature;
float _cellMaxTemperature;
String _cellMinVoltageName;
String _cellMaxVoltageName;
String _cellMinTemperatureName;
String _cellMaxTemperatureName;
uint8_t _moduleCountOnline;
uint8_t _moduleCountOffline;
uint8_t _moduleCountBlockingCharge;
uint8_t _moduleCountBlockingDischarge;
uint16_t _totalCapacity;
uint16_t _availableCapacity;
float _chargedEnergy = -1;
float _dischargedEnergy = -1;
bool _alarmUnderVoltage;
bool _alarmOverVoltage;
bool _alarmOverCurrentCharge;
bool _alarmOverCurrentDischarge;
bool _alarmUnderTemperature;
bool _alarmOverTemperature;
bool _alarmUnderTemperatureCharge;
bool _alarmOverTemperatureCharge;
bool _alarmInternalFailure;
bool _alarmCellImbalance;
bool _warningLowVoltage;
bool _warningHighVoltage;
bool _warningHighChargeCurrent;
bool _warningHighDischargeCurrent;
bool _warningLowTemperature;
bool _warningHighTemperature;
bool _warningLowTemperatureCharge;
bool _warningHighTemperatureCharge;
bool _warningInternalFailure;
bool _warningCellImbalance;
};
class JkBmsBatteryStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final {
@ -152,7 +239,6 @@ class VictronSmartShuntStats : public BatteryStats {
void updateFrom(VeDirectShuntController::data_t const& shuntData);
private:
float _current;
float _temperature;
bool _tempPresent;
uint8_t _chargeCycles;
@ -180,7 +266,7 @@ class MqttBatteryStats : public BatteryStats {
// we do NOT publish the same data under a different topic.
void mqttPublish() const final { }
// if the voltage is subscribed to at all, it alone does not warrant a
// card in the live view, since the SoC is already displayed at the top
// we don't need a card in the liveview, since the SoC and
// voltage (if available) is already displayed at the top.
void getLiveViewData(JsonVariant& root) const final { }
};

View File

@ -3,6 +3,7 @@
#include "PinMapping.h"
#include <cstdint>
#include <ArduinoJson.h>
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change
@ -16,6 +17,7 @@
#define NTP_MAX_TIMEZONEDESCR_STRLEN 50
#define MQTT_MAX_HOSTNAME_STRLEN 128
#define MQTT_MAX_CLIENTID_STRLEN 64
#define MQTT_MAX_USERNAME_STRLEN 64
#define MQTT_MAX_PASSWORD_STRLEN 64
#define MQTT_MAX_TOPIC_STRLEN 256
@ -30,14 +32,16 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define POWERMETER_MAX_PHASES 3
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
#define POWERMETER_MAX_USERNAME_STRLEN 64
#define POWERMETER_MAX_PASSWORD_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
#define POWERMETER_HTTP_TIMEOUT 1000
#define HTTP_REQUEST_MAX_URL_STRLEN 1024
#define HTTP_REQUEST_MAX_USERNAME_STRLEN 64
#define HTTP_REQUEST_MAX_PASSWORD_STRLEN 64
#define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64
#define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256
#define POWERMETER_MQTT_MAX_VALUES 3
#define POWERMETER_HTTP_JSON_MAX_VALUES 3
#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256
#define BATTERY_JSON_MAX_PATH_STRLEN 128
struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower;
@ -56,26 +60,73 @@ struct INVERTER_CONFIG_T {
uint8_t ReachableThreshold;
bool ZeroRuntimeDataIfUnrechable;
bool ZeroYieldDayOnMidnight;
bool ClearEventlogOnMidnight;
bool YieldDayCorrection;
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
};
struct POWERMETER_HTTP_PHASE_CONFIG_T {
struct HTTP_REQUEST_CONFIG_T {
char Url[HTTP_REQUEST_MAX_URL_STRLEN + 1];
enum Auth { None, Basic, Digest };
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
bool Enabled;
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
Auth AuthType;
char Username[POWERMETER_MAX_USERNAME_STRLEN +1];
char Password[POWERMETER_MAX_USERNAME_STRLEN +1];
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
char Username[HTTP_REQUEST_MAX_USERNAME_STRLEN + 1];
char Password[HTTP_REQUEST_MAX_PASSWORD_STRLEN + 1];
char HeaderKey[HTTP_REQUEST_MAX_HEADER_KEY_STRLEN + 1];
char HeaderValue[HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN + 1];
uint16_t Timeout;
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
};
using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T;
struct POWERMETER_MQTT_VALUE_T {
char Topic[MQTT_MAX_TOPIC_STRLEN + 1];
char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1];
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
Unit PowerUnit;
bool SignInverted;
};
using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T;
using PowerMeterMqttValue = struct POWERMETER_MQTT_VALUE_T;
struct POWERMETER_MQTT_CONFIG_T {
PowerMeterMqttValue Values[POWERMETER_MQTT_MAX_VALUES];
};
using PowerMeterMqttConfig = struct POWERMETER_MQTT_CONFIG_T;
struct POWERMETER_SERIAL_SDM_CONFIG_T {
uint32_t Address;
uint32_t PollingInterval;
};
using PowerMeterSerialSdmConfig = struct POWERMETER_SERIAL_SDM_CONFIG_T;
struct POWERMETER_HTTP_JSON_VALUE_T {
HttpRequestConfig HttpRequest;
bool Enabled;
char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1];
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
Unit PowerUnit;
bool SignInverted;
};
using PowerMeterHttpJsonValue = struct POWERMETER_HTTP_JSON_VALUE_T;
struct POWERMETER_HTTP_JSON_CONFIG_T {
uint32_t PollingInterval;
bool IndividualRequests;
PowerMeterHttpJsonValue Values[POWERMETER_HTTP_JSON_MAX_VALUES];
};
using PowerMeterHttpJsonConfig = struct POWERMETER_HTTP_JSON_CONFIG_T;
struct POWERMETER_HTTP_SML_CONFIG_T {
uint32_t PollingInterval;
HttpRequestConfig HttpRequest;
};
using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T;
enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 };
struct CONFIG_T {
struct {
@ -114,6 +165,7 @@ struct CONFIG_T {
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
bool VerboseLogging;
uint32_t Port;
char ClientId[MQTT_MAX_CLIENTID_STRLEN + 1];
char Username[MQTT_MAX_USERNAME_STRLEN + 1];
char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
char Topic[MQTT_MAX_TOPIC_STRLEN + 1];
@ -186,19 +238,14 @@ struct CONFIG_T {
bool UpdatesOnly;
} Vedirect;
struct {
struct PowerMeterConfig {
bool Enabled;
bool VerboseLogging;
uint32_t Interval;
uint32_t Source;
char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
uint32_t SdmBaudrate;
uint32_t SdmAddress;
uint32_t HttpInterval;
bool HttpIndividualRequests;
PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES];
PowerMeterMqttConfig Mqtt;
PowerMeterSerialSdmConfig SerialSdm;
PowerMeterHttpJsonConfig HttpJson;
PowerMeterHttpSmlConfig HttpSml;
} PowerMeter;
struct {
@ -210,6 +257,7 @@ struct CONFIG_T {
uint32_t Interval;
bool IsInverterBehindPowerMeter;
bool IsInverterSolarPowered;
bool UseOverscalingToCompensateShading;
uint64_t InverterId;
uint8_t InverterChannelId;
int32_t TargetPowerConsumption;
@ -236,7 +284,10 @@ struct CONFIG_T {
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
BatteryVoltageUnit MqttVoltageUnit;
} Battery;
struct {
@ -270,6 +321,18 @@ public:
INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id);
static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target);
static void serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target);
static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target);
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target);
static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target);
static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target);
static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target);
static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
};
extern ConfigurationClass Configuration;

77
include/HttpGetter.h Normal file
View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <memory>
#include <vector>
#include <utility>
#include <string>
#include <HTTPClient.h>
#include <WiFiClient.h>
using up_http_client_t = std::unique_ptr<HTTPClient>;
using sp_wifi_client_t = std::shared_ptr<WiFiClient>;
class HttpRequestResult {
public:
HttpRequestResult(bool success,
up_http_client_t upHttpClient = nullptr,
sp_wifi_client_t spWiFiClient = nullptr)
: _success(success)
, _upHttpClient(std::move(upHttpClient))
, _spWiFiClient(std::move(spWiFiClient)) { }
~HttpRequestResult() {
// the wifi client *must* die *after* the http client, as the http
// client uses the wifi client in its destructor.
if (_upHttpClient) { _upHttpClient->end(); }
_upHttpClient = nullptr;
_spWiFiClient = nullptr;
}
HttpRequestResult(HttpRequestResult const&) = delete;
HttpRequestResult(HttpRequestResult&&) = delete;
HttpRequestResult& operator=(HttpRequestResult const&) = delete;
HttpRequestResult& operator=(HttpRequestResult&&) = delete;
operator bool() const { return _success; }
Stream* getStream() {
if(!_upHttpClient) { return nullptr; }
return _upHttpClient->getStreamPtr();
}
private:
bool _success;
up_http_client_t _upHttpClient;
sp_wifi_client_t _spWiFiClient;
};
class HttpGetter {
public:
explicit HttpGetter(HttpRequestConfig const& cfg)
: _config(cfg) { }
bool init();
void addHeader(char const* key, char const* value);
HttpRequestResult performGetRequest();
char const* getErrorText() const { return _errBuffer; }
private:
String getAuthDigest(String const& authReq, unsigned int counter);
HttpRequestConfig const& _config;
template<typename... Args>
void logError(char const* format, Args... args);
char _errBuffer[256];
bool _useHttps;
String _host;
String _uri;
uint16_t _port;
sp_wifi_client_t _spWiFiClient; // reused for multiple HTTP requests
std::vector<std::pair<std::string, std::string>> _additionalHeaders;
};

View File

@ -1,34 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#include "Configuration.h"
using Auth_t = PowerMeterHttpConfig::Auth;
using Unit_t = PowerMeterHttpConfig::Unit;
class HttpPowerMeterClass {
public:
void init();
bool updateValues();
float getPower(int8_t phase);
char httpPowerMeterError[256];
bool queryPhase(int phase, PowerMeterHttpConfig const& config);
private:
float power[POWERMETER_MAX_PHASES];
HTTPClient httpClient;
String httpResponse;
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config);
bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization);
String extractParam(String& authReq, const String& param, const char delimit);
String getcNonce(const int len);
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
bool tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted);
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
String sha256(const String& data);
};
extern HttpPowerMeterClass HttpPowerMeter;

View File

@ -1,158 +1,158 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include "SPI.h"
#include <mcp_can.h>
#include <mutex>
#include <TaskSchedulerDeclarations.h>
#ifndef HUAWEI_PIN_MISO
#define HUAWEI_PIN_MISO 12
#endif
#ifndef HUAWEI_PIN_MOSI
#define HUAWEI_PIN_MOSI 13
#endif
#ifndef HUAWEI_PIN_SCLK
#define HUAWEI_PIN_SCLK 26
#endif
#ifndef HUAWEI_PIN_IRQ
#define HUAWEI_PIN_IRQ 25
#endif
#ifndef HUAWEI_PIN_CS
#define HUAWEI_PIN_CS 15
#endif
#ifndef HUAWEI_PIN_POWER
#define HUAWEI_PIN_POWER 33
#endif
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
#define MAX_CURRENT_MULTIPLIER 20
// Index values for rec_values array
#define HUAWEI_INPUT_POWER_IDX 0
#define HUAWEI_INPUT_FREQ_IDX 1
#define HUAWEI_INPUT_CURRENT_IDX 2
#define HUAWEI_OUTPUT_POWER_IDX 3
#define HUAWEI_EFFICIENCY_IDX 4
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
#define HUAWEI_INPUT_VOLTAGE_IDX 7
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
#define HUAWEI_OUTPUT_CURRENT_IDX 10
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
// Defines and index values for tx_values array
#define HUAWEI_OFFLINE_VOLTAGE 0x01
#define HUAWEI_ONLINE_VOLTAGE 0x00
#define HUAWEI_OFFLINE_CURRENT 0x04
#define HUAWEI_ONLINE_CURRENT 0x03
// Modes of operation
#define HUAWEI_MODE_OFF 0
#define HUAWEI_MODE_ON 1
#define HUAWEI_MODE_AUTO_EXT 2
#define HUAWEI_MODE_AUTO_INT 3
// Error codes
#define HUAWEI_ERROR_CODE_RX 0x01
#define HUAWEI_ERROR_CODE_TX 0x02
// Wait time/current before shuting down the PSU / charger
// This is set to allow the fan to run for some time
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
// Updateinterval used to request new values from the PSU
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
typedef struct RectifierParameters {
float input_voltage;
float input_frequency;
float input_current;
float input_power;
float input_temp;
float efficiency;
float output_voltage;
float output_current;
float max_output_current;
float output_power;
float output_temp;
float amp_hour;
} RectifierParameters_t;
class HuaweiCanCommClass {
public:
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
void loop();
bool gotNewRxDataFrame(bool clear);
uint8_t getErrorCode(bool clear);
uint32_t getParameterValue(uint8_t parameter);
void setParameterValue(uint16_t in, uint8_t parameterType);
private:
void sendRequest();
SPIClass *SPI;
MCP_CAN *_CAN;
uint8_t _huaweiIrq; // IRQ pin
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
std::mutex _mutex;
uint32_t _recValues[12];
uint16_t _txValues[5];
bool _hasNewTxValue[5];
uint8_t _errorCode;
bool _completeUpdateReceived;
};
class HuaweiCanClass {
public:
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void setValue(float in, uint8_t parameterType);
void setMode(uint8_t mode);
RectifierParameters_t * get();
uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; };
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
uint8_t getMode() const { return _mode; };
private:
void loop();
void processReceivedParameters();
void _setValue(float in, uint8_t parameterType);
Task _loopTask;
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
bool _initialized = false;
uint8_t _huaweiPower; // Power pin
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
RectifierParameters_t _rp;
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
uint8_t _autoPowerEnabledCounter = 0;
bool _autoPowerEnabled = false;
bool _batteryEmergencyCharging = false;
};
extern HuaweiCanClass HuaweiCan;
extern HuaweiCanCommClass HuaweiCanComm;
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include "SPI.h"
#include <mcp_can.h>
#include <mutex>
#include <TaskSchedulerDeclarations.h>
#ifndef HUAWEI_PIN_MISO
#define HUAWEI_PIN_MISO 12
#endif
#ifndef HUAWEI_PIN_MOSI
#define HUAWEI_PIN_MOSI 13
#endif
#ifndef HUAWEI_PIN_SCLK
#define HUAWEI_PIN_SCLK 26
#endif
#ifndef HUAWEI_PIN_IRQ
#define HUAWEI_PIN_IRQ 25
#endif
#ifndef HUAWEI_PIN_CS
#define HUAWEI_PIN_CS 15
#endif
#ifndef HUAWEI_PIN_POWER
#define HUAWEI_PIN_POWER 33
#endif
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
#define MAX_CURRENT_MULTIPLIER 20
// Index values for rec_values array
#define HUAWEI_INPUT_POWER_IDX 0
#define HUAWEI_INPUT_FREQ_IDX 1
#define HUAWEI_INPUT_CURRENT_IDX 2
#define HUAWEI_OUTPUT_POWER_IDX 3
#define HUAWEI_EFFICIENCY_IDX 4
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
#define HUAWEI_INPUT_VOLTAGE_IDX 7
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
#define HUAWEI_OUTPUT_CURRENT_IDX 10
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
// Defines and index values for tx_values array
#define HUAWEI_OFFLINE_VOLTAGE 0x01
#define HUAWEI_ONLINE_VOLTAGE 0x00
#define HUAWEI_OFFLINE_CURRENT 0x04
#define HUAWEI_ONLINE_CURRENT 0x03
// Modes of operation
#define HUAWEI_MODE_OFF 0
#define HUAWEI_MODE_ON 1
#define HUAWEI_MODE_AUTO_EXT 2
#define HUAWEI_MODE_AUTO_INT 3
// Error codes
#define HUAWEI_ERROR_CODE_RX 0x01
#define HUAWEI_ERROR_CODE_TX 0x02
// Wait time/current before shuting down the PSU / charger
// This is set to allow the fan to run for some time
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
// Updateinterval used to request new values from the PSU
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
typedef struct RectifierParameters {
float input_voltage;
float input_frequency;
float input_current;
float input_power;
float input_temp;
float efficiency;
float output_voltage;
float output_current;
float max_output_current;
float output_power;
float output_temp;
float amp_hour;
} RectifierParameters_t;
class HuaweiCanCommClass {
public:
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
void loop();
bool gotNewRxDataFrame(bool clear);
uint8_t getErrorCode(bool clear);
uint32_t getParameterValue(uint8_t parameter);
void setParameterValue(uint16_t in, uint8_t parameterType);
private:
void sendRequest();
SPIClass *SPI;
MCP_CAN *_CAN;
uint8_t _huaweiIrq; // IRQ pin
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
std::mutex _mutex;
uint32_t _recValues[12];
uint16_t _txValues[5];
bool _hasNewTxValue[5];
uint8_t _errorCode;
bool _completeUpdateReceived;
};
class HuaweiCanClass {
public:
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void setValue(float in, uint8_t parameterType);
void setMode(uint8_t mode);
RectifierParameters_t * get();
uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; };
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
uint8_t getMode() const { return _mode; };
private:
void loop();
void processReceivedParameters();
void _setValue(float in, uint8_t parameterType);
Task _loopTask;
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
bool _initialized = false;
uint8_t _huaweiPower; // Power pin
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
RectifierParameters_t _rp;
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
uint8_t _autoPowerEnabledCounter = 0;
bool _autoPowerEnabled = false;
bool _batteryEmergencyCharging = false;
};
extern HuaweiCanClass HuaweiCan;
extern HuaweiCanCommClass HuaweiCanComm;

View File

@ -6,6 +6,7 @@
#include "Battery.h"
#include "JkBmsSerialMessage.h"
#include "JkBmsDummy.h"
//#define JKBMS_DUMMY_SERIAL

196
include/JkBmsDummy.h Normal file
View File

@ -0,0 +1,196 @@
#pragma once
#include <Arduino.h>
#include <vector>
#include "MessageOutput.h"
namespace JkBms {
class DummySerial {
public:
DummySerial() = default;
void begin(uint32_t, uint32_t, int8_t, int8_t) {
MessageOutput.println("JK BMS Dummy Serial: begin()");
}
void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); }
void flush() { }
bool availableForWrite() const { return true; }
size_t write(const uint8_t *buffer, size_t size) {
MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size);
_byte_idx = 0;
_msg_idx = (_msg_idx + 1) % _data.size();
return size;
}
bool available() const {
return _byte_idx < _data[_msg_idx].size();
}
int read() {
if (_byte_idx >= _data[_msg_idx].size()) { return 0; }
return _data[_msg_idx][_byte_idx++];
}
private:
std::vector<std::vector<uint8_t>> const _data =
{
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb,
0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c,
0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07,
0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01,
0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c,
0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f,
0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a,
0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14,
0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02,
0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x53, 0xbb
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0,
0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c,
0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07,
0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba,
0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c,
0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f,
0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b,
0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14,
0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02,
0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x4f, 0xc1
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13,
0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c,
0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07,
0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb,
0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b,
0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f,
0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18,
0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13,
0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02,
0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00,
0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x45, 0xce
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07,
0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c,
0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07,
0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08,
0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c,
0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f,
0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06,
0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13,
0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02,
0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a,
0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00,
0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90,
0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05,
0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03,
0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x41, 0x7b
}
};
size_t _msg_idx = 0;
size_t _byte_idx = 0;
};
} /* namespace JkBms */

View File

@ -1,41 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <AsyncWebSocket.h>
#include <TaskSchedulerDeclarations.h>
#include <Print.h>
#include <freertos/task.h>
#include <mutex>
#include <vector>
#include <unordered_map>
#include <queue>
class MessageOutputClass : public Print {
public:
MessageOutputClass();
void init(Scheduler& scheduler);
size_t write(uint8_t c) override;
size_t write(const uint8_t* buffer, size_t size) override;
void register_ws_output(AsyncWebSocket* output);
private:
void loop();
Task _loopTask;
using message_t = std::vector<uint8_t>;
// we keep a buffer for every task and only write complete lines to the
// serial output and then move them to be pushed through the websocket.
// this way we prevent mangling of messages from different contexts.
std::unordered_map<TaskHandle_t, message_t> _task_messages;
std::queue<message_t> _lines;
AsyncWebSocket* _ws = nullptr;
std::mutex _msgLock;
void serialWrite(message_t const& m);
};
extern MessageOutputClass MessageOutput;
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <AsyncWebSocket.h>
#include <TaskSchedulerDeclarations.h>
#include <Print.h>
#include <freertos/task.h>
#include <mutex>
#include <vector>
#include <unordered_map>
#include <queue>
class MessageOutputClass : public Print {
public:
MessageOutputClass();
void init(Scheduler& scheduler);
size_t write(uint8_t c) override;
size_t write(const uint8_t* buffer, size_t size) override;
void register_ws_output(AsyncWebSocket* output);
private:
void loop();
Task _loopTask;
using message_t = std::vector<uint8_t>;
// we keep a buffer for every task and only write complete lines to the
// serial output and then move them to be pushed through the websocket.
// this way we prevent mangling of messages from different contexts.
std::unordered_map<TaskHandle_t, message_t> _task_messages;
std::queue<message_t> _lines;
AsyncWebSocket* _ws = nullptr;
std::mutex _msgLock;
void serialWrite(message_t const& m);
};
extern MessageOutputClass MessageOutput;

View File

@ -19,9 +19,10 @@ private:
String _voltageTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
std::optional<float> getFloat(std::string const& src, char const* topic);
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total,
char const* jsonPath);
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total,
char const* jsonPath);
};

View File

@ -56,6 +56,9 @@ public:
void publishConfig();
void forceUpdate();
static String getDtuUniqueId();
static String getDtuUrl();
private:
void loop();
void publish(const String& subtopic, const String& payload);
@ -63,7 +66,7 @@ private:
void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = "");
void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload);
void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100);
void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100, float step = 1.0);
void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off);
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
@ -71,9 +74,6 @@ private:
static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = "");
static String getDtuUniqueId();
static String getDtuUrl();
Task _loopTask;
bool _wasConnected = false;

View File

@ -1,44 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <Huawei_can.h>
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandleHuaweiClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class Topic : unsigned {
LimitOnlineVoltage,
LimitOnlineCurrent,
LimitOfflineVoltage,
LimitOfflineCurrent,
Mode
};
void onMqttMessage(Topic t,
const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len,
size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <Huawei_can.h>
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandleHuaweiClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class Topic : unsigned {
LimitOnlineVoltage,
LimitOnlineCurrent,
LimitOfflineVoltage,
LimitOfflineCurrent,
Mode
};
void onMqttMessage(Topic t,
const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len,
size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandleHuaweiClass MqttHandleHuawei;

View File

@ -13,6 +13,9 @@ public:
static String getTopic(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId);
void subscribeTopics();
void unsubscribeTopics();
private:
void loop();
void publishField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId);

View File

@ -1,43 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandlePowerLimiterClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class MqttPowerLimiterCommand : unsigned {
Mode,
BatterySoCStartThreshold,
BatterySoCStopThreshold,
FullSolarPassthroughSoC,
VoltageStartThreshold,
VoltageStopThreshold,
FullSolarPassThroughStartVoltage,
FullSolarPassThroughStopVoltage
};
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandlePowerLimiterClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class MqttPowerLimiterCommand : unsigned {
Mode,
BatterySoCStartThreshold,
BatterySoCStopThreshold,
FullSolarPassthroughSoC,
VoltageStartThreshold,
VoltageStopThreshold,
FullSolarPassThroughStartVoltage,
FullSolarPassThroughStopVoltage,
UpperPowerLimit,
TargetPowerConsumption
};
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;

View File

@ -13,9 +13,10 @@ public:
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max);
void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max, const float step);
void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic);
void createDeviceInfo(JsonObject& object);
void publishBinarySensor(const char* caption, const char* icon, const char* stateTopic, const char* payload_on, const char* payload_off);
void createDeviceInfo(JsonDocument& root);
Task _loopTask;

View File

@ -21,6 +21,7 @@ public:
void unsubscribe(const String& topic);
String getPrefix() const;
String getClientId();
private:
void NetworkEvent(network_event event);
@ -41,4 +42,4 @@ private:
bool _verboseLogging = true;
};
extern MqttSettingsClass MqttSettings;
extern MqttSettingsClass MqttSettings;

View File

@ -44,6 +44,7 @@ public:
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; }
bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; }
enum class Mode : unsigned {
Normal = 0,
@ -74,6 +75,7 @@ private:
Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr;
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
bool _fullSolarPassThroughEnabled = false;

View File

@ -1,78 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <map>
#include <list>
#include <mutex>
#include "SDM.h"
#include "sml.h"
#include "PowerMeterProvider.h"
#include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h>
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
#include <memory>
#include <mutex>
class PowerMeterClass {
public:
enum class Source : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP = 3,
SML = 4,
SMAHM2 = 5
};
void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
uint32_t getLastPowerMeterUpdate();
bool isDataValid();
void updateSettings();
float getPowerTotal() const;
uint32_t getLastUpdate() const;
bool isDataValid() const;
private:
void loop();
void mqtt();
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
Task _loopTask;
bool _verboseLogging = true;
uint32_t _lastPowerMeterCheck;
// Used in Power limiter for safety check
uint32_t _lastPowerMeterUpdate;
float _powerMeter1Power = 0.0;
float _powerMeter2Power = 0.0;
float _powerMeter3Power = 0.0;
float _powerMeter1Voltage = 0.0;
float _powerMeter2Voltage = 0.0;
float _powerMeter3Voltage = 0.0;
float _powerMeterImport = 0.0;
float _powerMeterExport = 0.0;
std::map<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex;
static char constexpr _sdmSerialPortOwner[] = "SDM power meter";
std::unique_ptr<HardwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
void readPowerMeter();
bool smlReadLoop();
const std::list<OBISHandler> smlHandlerList{
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power},
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport},
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport}
};
std::unique_ptr<PowerMeterProvider> _upProvider = nullptr;
};
extern PowerMeterClass PowerMeter;

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <array>
#include <variant>
#include <memory>
#include <condition_variable>
#include <mutex>
#include <stdint.h>
#include "HttpGetter.h"
#include "Configuration.h"
#include "PowerMeterProvider.h"
using Auth_t = HttpRequestConfig::Auth;
using Unit_t = PowerMeterHttpJsonValue::Unit;
class PowerMeterHttpJson : public PowerMeterProvider {
public:
explicit PowerMeterHttpJson(PowerMeterHttpJsonConfig const& cfg)
: _cfg(cfg) { }
~PowerMeterHttpJson();
bool init() final;
void loop() final;
float getPowerTotal() const final;
bool isDataValid() const final;
void doMqttPublish() const final;
using power_values_t = std::array<float, POWERMETER_HTTP_JSON_MAX_VALUES>;
using poll_result_t = std::variant<power_values_t, String>;
poll_result_t poll();
private:
static void pollingLoopHelper(void* context);
std::atomic<bool> _taskDone;
void pollingLoop();
PowerMeterHttpJsonConfig const _cfg;
uint32_t _lastPoll = 0;
mutable std::mutex _valueMutex;
power_values_t _powerValues;
std::array<std::unique_ptr<HttpGetter>, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include <condition_variable>
#include <mutex>
#include <stdint.h>
#include <Arduino.h>
#include "HttpGetter.h"
#include "Configuration.h"
#include "PowerMeterSml.h"
class PowerMeterHttpSml : public PowerMeterSml {
public:
explicit PowerMeterHttpSml(PowerMeterHttpSmlConfig const& cfg)
: PowerMeterSml("PowerMeterHttpSml")
, _cfg(cfg) { }
~PowerMeterHttpSml();
bool init() final;
void loop() final;
bool isDataValid() const final;
// returns an empty string on success,
// returns an error message otherwise.
String poll();
private:
static void pollingLoopHelper(void* context);
std::atomic<bool> _taskDone;
void pollingLoop();
PowerMeterHttpSmlConfig const _cfg;
uint32_t _lastPoll = 0;
std::unique_ptr<HttpGetter> _upHttpGetter;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

37
include/PowerMeterMqtt.h Normal file
View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include "PowerMeterProvider.h"
#include <espMqttClient.h>
#include <vector>
#include <mutex>
#include <array>
class PowerMeterMqtt : public PowerMeterProvider {
public:
explicit PowerMeterMqtt(PowerMeterMqttConfig const& cfg)
: _cfg(cfg) { }
~PowerMeterMqtt();
bool init() final;
void loop() final { }
float getPowerTotal() const final;
void doMqttPublish() const final;
private:
using MsgProperties = espMqttClientTypes::MessageProperties;
void onMessage(MsgProperties const& properties, char const* topic,
uint8_t const* payload, size_t len, size_t index,
size_t total, float* targetVariable, PowerMeterMqttValue const* cfg);
PowerMeterMqttConfig const _cfg;
using power_values_t = std::array<float, POWERMETER_MQTT_MAX_VALUES>;
power_values_t _powerValues;
std::vector<String> _mqttSubscriptions;
mutable std::mutex _mutex;
};

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include "Configuration.h"
class PowerMeterProvider {
public:
virtual ~PowerMeterProvider() { }
enum class Type : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP_JSON = 3,
SERIAL_SML = 4,
SMAHM2 = 5,
HTTP_SML = 6
};
// returns true if the provider is ready for use, false otherwise
virtual bool init() = 0;
virtual void loop() = 0;
virtual float getPowerTotal() const = 0;
virtual bool isDataValid() const;
uint32_t getLastUpdate() const { return _lastUpdate; }
void mqttLoop() const;
protected:
PowerMeterProvider() {
auto const& config = Configuration.get();
_verboseLogging = config.PowerMeter.VerboseLogging;
}
void gotUpdate() { _lastUpdate = millis(); }
void mqttPublish(String const& topic, float const& value) const;
bool _verboseLogging;
private:
virtual void doMqttPublish() const = 0;
// gotUpdate() updates this variable potentially from a different thread
// than users that request to read this variable through getLastUpdate().
std::atomic<uint32_t> _lastUpdate = 0;
mutable uint32_t _lastMqttPublish = 0;
};

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <SoftwareSerial.h>
#include "Configuration.h"
#include "PowerMeterProvider.h"
#include "SDM.h"
class PowerMeterSerialSdm : public PowerMeterProvider {
public:
enum class Phases {
One,
Three
};
PowerMeterSerialSdm(Phases phases, PowerMeterSerialSdmConfig const& cfg)
: _phases(phases)
, _cfg(cfg) { }
~PowerMeterSerialSdm();
bool init() final;
void loop() final;
float getPowerTotal() const final;
bool isDataValid() const final;
void doMqttPublish() const final;
private:
static void pollingLoopHelper(void* context);
bool readValue(std::unique_lock<std::mutex>& lock, uint16_t reg, float& targetVar);
std::atomic<bool> _taskDone;
void pollingLoop();
Phases _phases;
PowerMeterSerialSdmConfig const _cfg;
uint32_t _lastPoll = 0;
float _phase1Power = 0.0;
float _phase2Power = 0.0;
float _phase3Power = 0.0;
float _phase1Voltage = 0.0;
float _phase2Voltage = 0.0;
float _phase3Voltage = 0.0;
float _energyImport = 0.0;
float _energyExport = 0.0;
mutable std::mutex _valueMutex;
std::unique_ptr<SoftwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerMeterSml.h"
#include <SoftwareSerial.h>
class PowerMeterSerialSml : public PowerMeterSml {
public:
PowerMeterSerialSml()
: PowerMeterSml("PowerMeterSerialSml") { }
~PowerMeterSerialSml();
bool init() final;
void loop() final;
private:
// we assume that an SML datagram is complete after no additional
// characters were received for this many milliseconds.
static uint8_t constexpr _datagramGapMillis = 50;
static uint32_t constexpr _baud = 9600;
// size in bytes of the software serial receive buffer. must have the
// capacity to hold a full SML datagram, as we are processing the datagrams
// only after all data of one datagram was received.
static int constexpr _bufCapacity = 1024; // memory usage: 1 byte each
// amount of bits (RX pin state transitions) the software serial can buffer
// without decoding bits to bytes and storing those in the receive buffer.
// this value dictates how ofter we need to call a function of the software
// serial instance that performs bit decoding (we call available()).
static int constexpr _isrCapacity = 256; // memory usage: 8 bytes each (timestamp + pointer)
static void pollingLoopHelper(void* context);
std::atomic<bool> _taskDone;
void pollingLoop();
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
};

69
include/PowerMeterSml.h Normal file
View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <list>
#include <mutex>
#include <optional>
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#include "Configuration.h"
#include "PowerMeterProvider.h"
#include "sml.h"
class PowerMeterSml : public PowerMeterProvider {
public:
float getPowerTotal() const final;
void doMqttPublish() const final;
protected:
explicit PowerMeterSml(char const* user)
: _user(user) { }
void reset();
void processSmlByte(uint8_t byte);
private:
std::string _user;
mutable std::mutex _mutex;
using values_t = struct {
std::optional<float> activePowerTotal = std::nullopt;
std::optional<float> activePowerL1 = std::nullopt;
std::optional<float> activePowerL2 = std::nullopt;
std::optional<float> activePowerL3 = std::nullopt;
std::optional<float> voltageL1 = std::nullopt;
std::optional<float> voltageL2 = std::nullopt;
std::optional<float> voltageL3 = std::nullopt;
std::optional<float> currentL1 = std::nullopt;
std::optional<float> currentL2 = std::nullopt;
std::optional<float> currentL3 = std::nullopt;
std::optional<float> energyImport = std::nullopt;
std::optional<float> energyExport = std::nullopt;
};
values_t _values;
values_t _cache;
using OBISHandler = struct {
uint8_t const OBIS[6];
void (*decoder)(float&);
std::optional<float>* target;
char const* name;
};
const std::list<OBISHandler> smlHandlerList{
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerTotal, "active power total"},
{{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL1, "active power L1"},
{{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL2, "active power L2"},
{{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL3, "active power L3"},
{{0x01, 0x00, 0x20, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL1, "voltage L1"},
{{0x01, 0x00, 0x34, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL2, "voltage L2"},
{{0x01, 0x00, 0x48, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL3, "voltage L3"},
{{0x01, 0x00, 0x1f, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL1, "current L1"},
{{0x01, 0x00, 0x33, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL2, "current L2"},
{{0x01, 0x00, 0x47, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL3, "current L3"},
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyImport, "energy import"},
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyExport, "energy export"}
};
};

View File

@ -5,17 +5,16 @@
#pragma once
#include <cstdint>
#include <TaskSchedulerDeclarations.h>
#include "PowerMeterProvider.h"
class SMA_HMClass {
class PowerMeterUdpSmaHomeManager : public PowerMeterProvider {
public:
void init(Scheduler& scheduler, bool verboseLogging);
void loop();
void event1();
float getPowerTotal() const { return _powerMeterPower; }
float getPowerL1() const { return _powerMeterL1; }
float getPowerL2() const { return _powerMeterL2; }
float getPowerL3() const { return _powerMeterL3; }
~PowerMeterUdpSmaHomeManager();
bool init() final;
void loop() final;
float getPowerTotal() const final { return _powerMeterPower; }
void doMqttPublish() const final;
private:
void Soutput(int kanal, int index, int art, int tarif,
@ -23,14 +22,10 @@ private:
uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
bool _verboseLogging = false;
float _powerMeterPower = 0.0;
float _powerMeterL1 = 0.0;
float _powerMeterL2 = 0.0;
float _powerMeterL3 = 0.0;
uint32_t _previousMillis = 0;
uint32_t _serial = 0;
Task _loopTask;
};
extern SMA_HMClass SMA_HM;

View File

@ -3,27 +3,20 @@
#include "Configuration.h"
#include "Battery.h"
#include <espMqttClient.h>
#include "BatteryCanReceiver.h"
#include <driver/twai.h>
#include <Arduino.h>
#include <memory>
class PylontechCanReceiver : public BatteryProvider {
class PylontechCanReceiver : public BatteryCanReceiver {
public:
bool init(bool verboseLogging) final;
void deinit() final;
void loop() final;
void onMessage(twai_message_t rx_message) final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
uint16_t readUnsignedInt16(uint8_t *data);
int16_t readSignedInt16(uint8_t *data);
float scaleValue(int16_t value, float factor);
bool getBit(uint8_t value, uint8_t bit);
void dummyData();
bool _verboseLogging = true;
std::shared_ptr<PylontechBatteryStats> _stats =
std::make_shared<PylontechBatteryStats>();
};

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include "Battery.h"
#include "BatteryCanReceiver.h"
#include <driver/twai.h>
class PytesCanReceiver : public BatteryCanReceiver {
public:
bool init(bool verboseLogging) final;
void onMessage(twai_message_t rx_message) final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
std::shared_ptr<PytesBatteryStats> _stats =
std::make_shared<PytesBatteryStats>();
};

View File

@ -3,6 +3,7 @@
#include <ArduinoJson.h>
#include <cstdint>
#include <utility>
class Utils {
public:
@ -12,4 +13,12 @@ public:
static void restartDtu();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles();
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>
static std::pair<T, String> getJsonValueByPath(JsonDocument const& root, String const& path);
template <typename T>
static std::optional<T> getNumericValueFromMqttPayload(char const* client,
std::string const& src, char const* topic, char const* jsonPath);
};

View File

@ -1,19 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <AsyncJson.h>
class WebApiHuaweiClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
void getJsonData(JsonVariant& root);
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void onPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <AsyncJson.h>
class WebApiHuaweiClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
void getJsonData(JsonVariant& root);
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void onPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -60,6 +60,7 @@ enum WebApiError {
MqttHassTopicLength,
MqttHassTopicCharacter,
MqttLwtQos,
MqttClientIdLength,
NetworkBase = 8000,
NetworkIpInvalid,

View File

@ -9,6 +9,9 @@ public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
bool otaSupported() const;
void onFirmwareUpdateFinish(AsyncWebServerRequest* request);
void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
void onFirmwareStatus(AsyncWebServerRequest* request);
};

View File

@ -14,8 +14,8 @@ private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const;
void onTestHttpRequest(AsyncWebServerRequest* request);
void onTestHttpJsonRequest(AsyncWebServerRequest* request);
void onTestHttpSmlRequest(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -1,29 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsHuaweiLiveClass {
public:
WebApiWsHuaweiLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsHuaweiLiveClass {
public:
WebApiWsHuaweiLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
};

View File

@ -1,32 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsBatteryLiveClass {
public:
WebApiWsBatteryLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
uint32_t _lastUpdateCheck = 0;
static constexpr uint16_t _responseSize = 1024 + 512;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsBatteryLiveClass {
public:
WebApiWsBatteryLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
uint32_t _lastUpdateCheck = 0;
static constexpr uint16_t _responseSize = 1024 + 512;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
};

View File

@ -9,7 +9,7 @@
#define ACCESS_POINT_NAME "OpenDTU-"
#define ACCESS_POINT_PASSWORD "openDTU42"
#define ACCESS_POINT_TIMEOUT 3;
#define ACCESS_POINT_TIMEOUT 3
#define AUTH_USERNAME "admin"
#define SECURITY_ALLOW_READONLY true
@ -115,11 +115,12 @@
#define VEDIRECT_UPDATESONLY true
#define POWERMETER_ENABLED false
#define POWERMETER_INTERVAL 10
#define POWERMETER_SOURCE 2
#define POWERMETER_SDMBAUDRATE 9600
#define POWERMETER_POLLING_INTERVAL 10
#define POWERMETER_SOURCE 0
#define POWERMETER_SDMADDRESS 1
#define HTTP_REQUEST_TIMEOUT_MS 1000
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
@ -127,6 +128,7 @@
#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

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "CpuTemperature.h"
#include <Arduino.h>
#if defined(CONFIG_IDF_TARGET_ESP32)
// there is no official API available on the original ESP32
extern "C" {
uint8_t temprature_sens_read();
}
#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
#include "driver/temp_sensor.h"
#endif
CpuTemperatureClass CpuTemperature;
float CpuTemperatureClass::read()
{
std::lock_guard<std::mutex> lock(_mutex);
float temperature = NAN;
bool success = false;
#if defined(CONFIG_IDF_TARGET_ESP32)
uint8_t raw = temprature_sens_read();
ESP_LOGV(TAG, "Raw temperature value: %d", raw);
temperature = (raw - 32) / 1.8f;
success = (raw != 128);
#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
temp_sensor_set_config(tsens);
temp_sensor_start();
#if defined(CONFIG_IDF_TARGET_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3))
#error \
"ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271"
#endif
esp_err_t result = temp_sensor_read_celsius(&temperature);
temp_sensor_stop();
success = (result == ESP_OK);
#endif
if (success && std::isfinite(temperature)) {
return temperature;
} else {
ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature);
return NAN;
}
}

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
class CpuTemperatureClass {
public:
float read();
private:
std::mutex _mutex;
};
extern CpuTemperatureClass CpuTemperature;

View File

@ -141,6 +141,9 @@ void HoymilesClass::loop()
if (inv->getZeroYieldDayOnMidnight()) {
inv->Statistics()->zeroDailyData();
}
if (inv->getClearEventlogOnMidnight()) {
inv->EventLog()->clearBuffer();
}
}
lastWeekDay = currentWeekDay;

View File

@ -22,9 +22,9 @@ public:
}
template <typename T>
std::shared_ptr<T> prepareCommand()
std::shared_ptr<T> prepareCommand(InverterAbstract* inv)
{
return std::make_shared<T>();
return std::make_shared<T>(inv);
}
protected:

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -25,8 +25,8 @@ ID Target Addr Source Addr Cmd SCmd ? Limit Type CRC16 CRC8
#define CRC_SIZE 6
ActivePowerControlCommand::ActivePowerControlCommand(const uint64_t target_address, const uint64_t router_address)
: DevControlCommand(target_address, router_address)
ActivePowerControlCommand::ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address)
: DevControlCommand(inv, router_address)
{
_payload[10] = 0x0b;
_payload[11] = 0x00;
@ -62,30 +62,30 @@ void ActivePowerControlCommand::setActivePowerLimit(const float limit, const Pow
udpateCRC(CRC_SIZE);
}
bool ActivePowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) {
inverter.SystemConfigPara()->setLimitPercent(getLimit());
_inv->SystemConfigPara()->setLimitPercent(getLimit());
} else {
const uint16_t max_power = inverter.DevInfo()->getMaxPower();
const uint16_t max_power = _inv->DevInfo()->getMaxPower();
if (max_power > 0) {
inverter.SystemConfigPara()->setLimitPercent(static_cast<float>(getLimit()) / max_power * 100);
_inv->SystemConfigPara()->setLimitPercent(static_cast<float>(getLimit()) / max_power * 100);
} else {
// TODO(tbnobody): Not implemented yet because we only can publish the percentage value
}
}
inverter.SystemConfigPara()->setLastUpdateCommand(millis());
inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK);
_inv->SystemConfigPara()->setLastUpdateCommand(millis());
_inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK);
return true;
}
float ActivePowerControlCommand::getLimit() const
{
const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]);
const float l = (((uint16_t)_payload[12] << 8) | _payload[13]);
return l / 10;
}
@ -94,7 +94,7 @@ PowerLimitControlType ActivePowerControlCommand::getType()
return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]);
}
void ActivePowerControlCommand::gotTimeout(InverterAbstract& inverter)
void ActivePowerControlCommand::gotTimeout()
{
inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK);
}
_inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK);
}

View File

@ -12,14 +12,14 @@ typedef enum { // ToDo: to be verified by field tests
class ActivePowerControlCommand : public DevControlCommand {
public:
explicit ActivePowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0);
explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(InverterAbstract& inverter);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout();
void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent);
float getLimit() const;
PowerLimitControlType getType();
};
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -23,8 +23,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap AlarmId Pa
#include "AlarmDataCommand.h"
#include "inverters/InverterAbstract.h"
AlarmDataCommand::AlarmDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
AlarmDataCommand::AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x11);
@ -36,28 +36,28 @@ String AlarmDataCommand::getCommandName() const
return "AlarmData";
}
bool AlarmDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool AlarmDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.EventLog()->beginAppendFragment();
inverter.EventLog()->clearBuffer();
_inv->EventLog()->beginAppendFragment();
_inv->EventLog()->clearBuffer();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
_inv->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.EventLog()->endAppendFragment();
inverter.EventLog()->setLastAlarmRequestSuccess(CMD_OK);
inverter.EventLog()->setLastUpdate(millis());
_inv->EventLog()->endAppendFragment();
_inv->EventLog()->setLastAlarmRequestSuccess(CMD_OK);
_inv->EventLog()->setLastUpdate(millis());
return true;
}
void AlarmDataCommand::gotTimeout(InverterAbstract& inverter)
void AlarmDataCommand::gotTimeout()
{
inverter.EventLog()->setLastAlarmRequestSuccess(CMD_NOK);
}
_inv->EventLog()->setLastAlarmRequestSuccess(CMD_NOK);
}

View File

@ -5,10 +5,10 @@
class AlarmDataCommand : public MultiDataCommand {
public:
explicit AlarmDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(InverterAbstract& inverter);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout();
};

View File

@ -19,8 +19,8 @@ ID Target Addr Source Addr ? ? ? CH ? CRC8
*/
#include "ChannelChangeCommand.h"
ChannelChangeCommand::ChannelChangeCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t channel)
: CommandAbstract(target_address, router_address)
ChannelChangeCommand::ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t channel)
: CommandAbstract(inv, router_address)
{
_payload[0] = 0x56;
_payload[13] = 0x14;
@ -67,7 +67,7 @@ void ChannelChangeCommand::setCountryMode(const CountryModeId_t mode)
}
}
bool ChannelChangeCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool ChannelChangeCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
return true;
}

View File

@ -6,7 +6,7 @@
class ChannelChangeCommand : public CommandAbstract {
public:
explicit ChannelChangeCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t channel = 0);
explicit ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t channel = 0);
virtual String getCommandName() const;
@ -15,7 +15,7 @@ public:
void setCountryMode(const CountryModeId_t mode);
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual uint8_t getMaxResendCount();
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -29,13 +29,16 @@ Source Address: 80 12 23 04
#include "CommandAbstract.h"
#include "crc.h"
#include <string.h>
#include "../inverters/InverterAbstract.h"
CommandAbstract::CommandAbstract(const uint64_t target_address, const uint64_t router_address)
CommandAbstract::CommandAbstract(InverterAbstract* inv, const uint64_t router_address)
{
memset(_payload, 0, RF_LEN);
_payload_size = 0;
setTargetAddress(target_address);
_inv = inv;
setTargetAddress(_inv->serial());
setRouterAddress(router_address);
setSendCount(0);
setTimeout(0);
@ -122,7 +125,7 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], const uint64_t s
buffer[0] = s.b[3];
}
void CommandAbstract::gotTimeout(InverterAbstract& inverter)
void CommandAbstract::gotTimeout()
{
}

View File

@ -13,7 +13,7 @@ class InverterAbstract;
class CommandAbstract {
public:
explicit CommandAbstract(const uint64_t target_address = 0, const uint64_t router_address = 0);
explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0);
virtual ~CommandAbstract() {};
const uint8_t* getDataPayload();
@ -21,7 +21,6 @@ public:
uint8_t getDataSize() const;
void setTargetAddress(const uint64_t address);
uint64_t getTargetAddress() const;
void setRouterAddress(const uint64_t address);
@ -38,8 +37,8 @@ public:
virtual CommandAbstract* getRequestFrameCommand(const uint8_t frame_no);
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) = 0;
virtual void gotTimeout(InverterAbstract& inverter);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) = 0;
virtual void gotTimeout();
// Sets the amount how often the specific command is resent if all fragments where missing
virtual uint8_t getMaxResendCount() const;
@ -56,6 +55,9 @@ protected:
uint64_t _targetAddress;
uint64_t _routerAddress;
InverterAbstract* _inv;
private:
void setTargetAddress(const uint64_t address);
static void convertSerialToPacketId(uint8_t buffer[], const uint64_t serial);
};
};

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -23,8 +23,8 @@ ID Target Addr Source Addr Cmd Payload CRC16 CRC8
#include "DevControlCommand.h"
#include "crc.h"
DevControlCommand::DevControlCommand(const uint64_t target_address, const uint64_t router_address)
: CommandAbstract(target_address, router_address)
DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t router_address)
: CommandAbstract(inv, router_address)
{
_payload[0] = 0x51;
_payload[9] = 0x81;
@ -39,7 +39,7 @@ void DevControlCommand::udpateCRC(const uint8_t len)
_payload[10 + len + 1] = (uint8_t)(crc);
}
bool DevControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
for (uint8_t i = 0; i < max_fragment_id; i++) {
if (fragment[i].mainCmd != (_payload[0] | 0x80)) {
@ -48,4 +48,4 @@ bool DevControlCommand::handleResponse(InverterAbstract& inverter, const fragmen
}
return true;
}
}

View File

@ -5,10 +5,10 @@
class DevControlCommand : public CommandAbstract {
public:
explicit DevControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0);
explicit DevControlCommand(InverterAbstract* inv, const uint64_t router_address = 0);
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
protected:
void udpateCRC(const uint8_t len);
};
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -21,8 +21,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "DevInfoAllCommand.h"
#include "inverters/InverterAbstract.h"
DevInfoAllCommand::DevInfoAllCommand(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
DevInfoAllCommand::DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x01);
@ -34,22 +34,22 @@ String DevInfoAllCommand::getCommandName() const
return "DevInfoAll";
}
bool DevInfoAllCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool DevInfoAllCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.DevInfo()->beginAppendFragment();
inverter.DevInfo()->clearBufferAll();
_inv->DevInfo()->beginAppendFragment();
_inv->DevInfo()->clearBufferAll();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len);
_inv->DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.DevInfo()->endAppendFragment();
inverter.DevInfo()->setLastUpdateAll(millis());
_inv->DevInfo()->endAppendFragment();
_inv->DevInfo()->setLastUpdateAll(millis());
return true;
}
}

View File

@ -5,9 +5,9 @@
class DevInfoAllCommand : public MultiDataCommand {
public:
explicit DevInfoAllCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -21,8 +21,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "DevInfoSimpleCommand.h"
#include "inverters/InverterAbstract.h"
DevInfoSimpleCommand::DevInfoSimpleCommand(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
DevInfoSimpleCommand::DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x00);
@ -34,22 +34,22 @@ String DevInfoSimpleCommand::getCommandName() const
return "DevInfoSimple";
}
bool DevInfoSimpleCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool DevInfoSimpleCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.DevInfo()->beginAppendFragment();
inverter.DevInfo()->clearBufferSimple();
_inv->DevInfo()->beginAppendFragment();
_inv->DevInfo()->clearBufferSimple();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len);
_inv->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.DevInfo()->endAppendFragment();
inverter.DevInfo()->setLastUpdateSimple(millis());
_inv->DevInfo()->endAppendFragment();
_inv->DevInfo()->setLastUpdateSimple(millis());
return true;
}
}

View File

@ -5,9 +5,9 @@
class DevInfoSimpleCommand : public MultiDataCommand {
public:
explicit DevInfoSimpleCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "Hoymiles.h"
#include "inverters/InverterAbstract.h"
GridOnProFilePara::GridOnProFilePara(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
GridOnProFilePara::GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x02);
@ -35,22 +35,22 @@ String GridOnProFilePara::getCommandName() const
return "GridOnProFilePara";
}
bool GridOnProFilePara::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool GridOnProFilePara::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.GridProfile()->beginAppendFragment();
inverter.GridProfile()->clearBuffer();
_inv->GridProfile()->beginAppendFragment();
_inv->GridProfile()->clearBuffer();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
_inv->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.GridProfile()->endAppendFragment();
inverter.GridProfile()->setLastUpdate(millis());
_inv->GridProfile()->endAppendFragment();
_inv->GridProfile()->setLastUpdate(millis());
return true;
}
}

View File

@ -5,9 +5,9 @@
class GridOnProFilePara : public MultiDataCommand {
public:
explicit GridOnProFilePara(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -28,8 +28,9 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "MultiDataCommand.h"
#include "crc.h"
MultiDataCommand::MultiDataCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t data_type, const time_t time)
: CommandAbstract(target_address, router_address)
MultiDataCommand::MultiDataCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t data_type, const time_t time)
: CommandAbstract(inv, router_address)
, _cmdRequestFrame(inv)
{
_payload[0] = 0x15;
_payload[9] = 0x80;
@ -79,13 +80,12 @@ time_t MultiDataCommand::getTime() const
CommandAbstract* MultiDataCommand::getRequestFrameCommand(const uint8_t frame_no)
{
_cmdRequestFrame.setTargetAddress(getTargetAddress());
_cmdRequestFrame.setFrameNo(frame_no);
return &_cmdRequestFrame;
}
bool MultiDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// All fragments are available --> Check CRC
uint16_t crc = 0xffff, crcRcv = 0;

View File

@ -7,14 +7,14 @@
class MultiDataCommand : public CommandAbstract {
public:
explicit MultiDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0);
explicit MultiDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0);
void setTime(const time_t time);
time_t getTime() const;
CommandAbstract* getRequestFrameCommand(const uint8_t frame_no);
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
protected:
void setDataType(const uint8_t data_type);
@ -23,4 +23,4 @@ protected:
static uint8_t getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id);
RequestFrameCommand _cmdRequestFrame;
};
};

View File

@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "ParaSetCommand.h"
ParaSetCommand::ParaSetCommand(const uint64_t target_address, const uint64_t router_address)
: CommandAbstract(target_address, router_address)
ParaSetCommand::ParaSetCommand(InverterAbstract* inv, const uint64_t router_address)
: CommandAbstract(inv, router_address)
{
_payload[0] = 0x52;
}
}

View File

@ -5,5 +5,5 @@
class ParaSetCommand : public CommandAbstract {
public:
explicit ParaSetCommand(const uint64_t target_address = 0, const uint64_t router_address = 0);
};
explicit ParaSetCommand(InverterAbstract* inv, const uint64_t router_address = 0);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -26,8 +26,8 @@ ID Target Addr Source Addr Cmd SCmd ? CRC16 CRC8
#define CRC_SIZE 2
PowerControlCommand::PowerControlCommand(const uint64_t target_address, const uint64_t router_address)
: DevControlCommand(target_address, router_address)
PowerControlCommand::PowerControlCommand(InverterAbstract* inv, const uint64_t router_address)
: DevControlCommand(inv, router_address)
{
_payload[10] = 0x00; // TurnOn
_payload[11] = 0x00;
@ -44,20 +44,20 @@ String PowerControlCommand::getCommandName() const
return "PowerControl";
}
bool PowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool PowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
inverter.PowerCommand()->setLastUpdateCommand(millis());
inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_OK);
_inv->PowerCommand()->setLastUpdateCommand(millis());
_inv->PowerCommand()->setLastPowerCommandSuccess(CMD_OK);
return true;
}
void PowerControlCommand::gotTimeout(InverterAbstract& inverter)
void PowerControlCommand::gotTimeout()
{
inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_NOK);
_inv->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK);
}
void PowerControlCommand::setPowerOn(const bool state)
@ -76,4 +76,4 @@ void PowerControlCommand::setRestart()
_payload[10] = 0x02; // Restart
udpateCRC(CRC_SIZE); // 2 byte crc
}
}

View File

@ -5,13 +5,13 @@
class PowerControlCommand : public DevControlCommand {
public:
explicit PowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0);
explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(InverterAbstract& inverter);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout();
void setPowerOn(const bool state);
void setRestart();
};
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "Hoymiles.h"
#include "inverters/InverterAbstract.h"
RealTimeRunDataCommand::RealTimeRunDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
RealTimeRunDataCommand::RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x0b);
@ -35,10 +35,10 @@ String RealTimeRunDataCommand::getCommandName() const
return "RealTimeRunData";
}
bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
@ -46,7 +46,7 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fr
// In case of low power in the inverter it occours that some incomplete fragments
// with a valid CRC are received.
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = inverter.Statistics()->getExpectedByteCount();
const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount();
if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize);
@ -56,19 +56,19 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fr
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.Statistics()->beginAppendFragment();
inverter.Statistics()->clearBuffer();
_inv->Statistics()->beginAppendFragment();
_inv->Statistics()->clearBuffer();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
_inv->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.Statistics()->endAppendFragment();
inverter.Statistics()->resetRxFailureCount();
inverter.Statistics()->setLastUpdate(millis());
_inv->Statistics()->endAppendFragment();
_inv->Statistics()->resetRxFailureCount();
_inv->Statistics()->setLastUpdate(millis());
return true;
}
void RealTimeRunDataCommand::gotTimeout(InverterAbstract& inverter)
void RealTimeRunDataCommand::gotTimeout()
{
inverter.Statistics()->incrementRxFailureCount();
}
_inv->Statistics()->incrementRxFailureCount();
}

View File

@ -5,10 +5,10 @@
class RealTimeRunDataCommand : public MultiDataCommand {
public:
explicit RealTimeRunDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(InverterAbstract& inverter);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout();
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -22,8 +22,8 @@ ID Target Addr Source Addr Frm CRC8
*/
#include "RequestFrameCommand.h"
RequestFrameCommand::RequestFrameCommand(const uint64_t target_address, const uint64_t router_address, uint8_t frame_no)
: SingleDataCommand(target_address, router_address)
RequestFrameCommand::RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address, uint8_t frame_no)
: SingleDataCommand(inv, router_address)
{
if (frame_no > 127) {
frame_no = 0;
@ -47,7 +47,7 @@ uint8_t RequestFrameCommand::getFrameNo() const
return _payload[9] & (~0x80);
}
bool RequestFrameCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool RequestFrameCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
return true;
}
}

View File

@ -5,12 +5,12 @@
class RequestFrameCommand : public SingleDataCommand {
public:
explicit RequestFrameCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, uint8_t frame_no = 0);
explicit RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address = 0, uint8_t frame_no = 0);
virtual String getCommandName() const;
void setFrameNo(const uint8_t frame_no);
uint8_t getFrameNo() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -19,8 +19,8 @@ ID Target Addr Source Addr CRC8
*/
#include "SingleDataCommand.h"
SingleDataCommand::SingleDataCommand(const uint64_t target_address, const uint64_t router_address)
: CommandAbstract(target_address, router_address)
SingleDataCommand::SingleDataCommand(InverterAbstract* inv, const uint64_t router_address)
: CommandAbstract(inv, router_address)
{
_payload[0] = 0x15;
setTimeout(100);

View File

@ -5,5 +5,5 @@
class SingleDataCommand : public CommandAbstract {
public:
explicit SingleDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0);
};
explicit SingleDataCommand(InverterAbstract* inv, const uint64_t router_address = 0);
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa
#include "Hoymiles.h"
#include "inverters/InverterAbstract.h"
SystemConfigParaCommand::SystemConfigParaCommand(const uint64_t target_address, const uint64_t router_address, const time_t time)
: MultiDataCommand(target_address, router_address)
SystemConfigParaCommand::SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time)
: MultiDataCommand(inv, router_address)
{
setTime(time);
setDataType(0x05);
@ -35,10 +35,10 @@ String SystemConfigParaCommand::getCommandName() const
return "SystemConfigPara";
}
bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)
{
// Check CRC of whole payload
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) {
return false;
}
@ -46,7 +46,7 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const f
// In case of low power in the inverter it occours that some incomplete fragments
// with a valid CRC are received.
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = inverter.SystemConfigPara()->getExpectedByteCount();
const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount();
if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize);
@ -56,19 +56,19 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const f
// Move all fragments into target buffer
uint8_t offs = 0;
inverter.SystemConfigPara()->beginAppendFragment();
inverter.SystemConfigPara()->clearBuffer();
_inv->SystemConfigPara()->beginAppendFragment();
_inv->SystemConfigPara()->clearBuffer();
for (uint8_t i = 0; i < max_fragment_id; i++) {
inverter.SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
_inv->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
offs += (fragment[i].len);
}
inverter.SystemConfigPara()->endAppendFragment();
inverter.SystemConfigPara()->setLastUpdateRequest(millis());
inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK);
_inv->SystemConfigPara()->endAppendFragment();
_inv->SystemConfigPara()->setLastUpdateRequest(millis());
_inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK);
return true;
}
void SystemConfigParaCommand::gotTimeout(InverterAbstract& inverter)
void SystemConfigParaCommand::gotTimeout()
{
inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK);
}
_inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK);
}

View File

@ -5,10 +5,10 @@
class SystemConfigParaCommand : public MultiDataCommand {
public:
explicit SystemConfigParaCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0);
explicit SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0);
virtual String getCommandName() const;
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(InverterAbstract& inverter);
};
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout();
};

View File

@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x114400000000 && serial <= 0x1144ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1144;
return preSerial == 0x1144 || preSerial == 0x1143;
}
String HMS_2CH::typeName() const

View File

@ -18,10 +18,9 @@ bool HMS_Abstract::sendChangeChannelRequest()
return false;
}
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>();
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>(this);
cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode());
cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setTargetAddress(serial());
_radio->enqueCommand(cmdChannel);
return true;

View File

@ -20,10 +20,9 @@ bool HMT_Abstract::sendChangeChannelRequest()
return false;
}
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>();
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>(this);
cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode());
cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setTargetAddress(serial());
_radio->enqueCommand(cmdChannel);
return true;

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HM_Abstract.h"
#include "HoymilesRadio.h"
@ -30,9 +30,8 @@ bool HM_Abstract::sendStatsRequest()
time_t now;
time(&now);
auto cmd = _radio->prepareCommand<RealTimeRunDataCommand>();
auto cmd = _radio->prepareCommand<RealTimeRunDataCommand>(this);
cmd->setTime(now);
cmd->setTargetAddress(serial());
_radio->enqueCommand(cmd);
return true;
@ -62,9 +61,8 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force)
time_t now;
time(&now);
auto cmd = _radio->prepareCommand<AlarmDataCommand>();
auto cmd = _radio->prepareCommand<AlarmDataCommand>(this);
cmd->setTime(now);
cmd->setTargetAddress(serial());
EventLog()->setLastAlarmRequestSuccess(CMD_PENDING);
_radio->enqueCommand(cmd);
@ -85,14 +83,12 @@ bool HM_Abstract::sendDevInfoRequest()
time_t now;
time(&now);
auto cmdAll = _radio->prepareCommand<DevInfoAllCommand>();
auto cmdAll = _radio->prepareCommand<DevInfoAllCommand>(this);
cmdAll->setTime(now);
cmdAll->setTargetAddress(serial());
_radio->enqueCommand(cmdAll);
auto cmdSimple = _radio->prepareCommand<DevInfoSimpleCommand>();
auto cmdSimple = _radio->prepareCommand<DevInfoSimpleCommand>(this);
cmdSimple->setTime(now);
cmdSimple->setTargetAddress(serial());
_radio->enqueCommand(cmdSimple);
return true;
@ -112,9 +108,8 @@ bool HM_Abstract::sendSystemConfigParaRequest()
time_t now;
time(&now);
auto cmd = _radio->prepareCommand<SystemConfigParaCommand>();
auto cmd = _radio->prepareCommand<SystemConfigParaCommand>(this);
cmd->setTime(now);
cmd->setTargetAddress(serial());
SystemConfigPara()->setLastLimitRequestSuccess(CMD_PENDING);
_radio->enqueCommand(cmd);
@ -138,9 +133,8 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitCon
_activePowerControlLimit = limit;
_activePowerControlType = type;
auto cmd = _radio->prepareCommand<ActivePowerControlCommand>();
auto cmd = _radio->prepareCommand<ActivePowerControlCommand>(this);
cmd->setActivePowerLimit(limit, type);
cmd->setTargetAddress(serial());
SystemConfigPara()->setLastLimitCommandSuccess(CMD_PENDING);
_radio->enqueCommand(cmd);
@ -168,9 +162,8 @@ bool HM_Abstract::sendPowerControlRequest(const bool turnOn)
_powerState = 0;
}
auto cmd = _radio->prepareCommand<PowerControlCommand>();
auto cmd = _radio->prepareCommand<PowerControlCommand>(this);
cmd->setPowerOn(turnOn);
cmd->setTargetAddress(serial());
PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING);
_radio->enqueCommand(cmd);
@ -185,9 +178,8 @@ bool HM_Abstract::sendRestartControlRequest()
_powerState = 2;
auto cmd = _radio->prepareCommand<PowerControlCommand>();
auto cmd = _radio->prepareCommand<PowerControlCommand>(this);
cmd->setRestart();
cmd->setTargetAddress(serial());
PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING);
_radio->enqueCommand(cmd);
@ -227,9 +219,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest()
time_t now;
time(&now);
auto cmd = _radio->prepareCommand<GridOnProFilePara>();
auto cmd = _radio->prepareCommand<GridOnProFilePara>(this);
cmd->setTime(now);
cmd->setTargetAddress(serial());
_radio->enqueCommand(cmd);
return true;

View File

@ -127,6 +127,16 @@ bool InverterAbstract::getZeroYieldDayOnMidnight() const
return _zeroYieldDayOnMidnight;
}
void InverterAbstract::setClearEventlogOnMidnight(const bool enabled)
{
_clearEventlogOnMidnight = enabled;
}
bool InverterAbstract::getClearEventlogOnMidnight() const
{
return _clearEventlogOnMidnight;
}
bool InverterAbstract::sendChangeChannelRequest()
{
return false;
@ -226,7 +236,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
if (cmd.getSendCount() <= cmd.getMaxResendCount()) {
return FRAGMENT_ALL_MISSING_RESEND;
} else {
cmd.gotTimeout(*this);
cmd.gotTimeout();
return FRAGMENT_ALL_MISSING_TIMEOUT;
}
}
@ -237,7 +247,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) {
return _rxFragmentLastPacketId + 1;
} else {
cmd.gotTimeout(*this);
cmd.gotTimeout();
return FRAGMENT_RETRANSMIT_TIMEOUT;
}
}
@ -249,16 +259,16 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) {
return i + 1;
} else {
cmd.gotTimeout(*this);
cmd.gotTimeout();
return FRAGMENT_RETRANSMIT_TIMEOUT;
}
}
}
if (!cmd.handleResponse(*this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) {
cmd.gotTimeout(*this);
if (!cmd.handleResponse(_rxFragmentBuffer, _rxFragmentMaxPacketId)) {
cmd.gotTimeout();
return FRAGMENT_HANDLE_ERROR;
}
return FRAGMENT_OK;
}
}

View File

@ -58,6 +58,9 @@ public:
void setZeroYieldDayOnMidnight(const bool enabled);
bool getZeroYieldDayOnMidnight() const;
void setClearEventlogOnMidnight(const bool enabled);
bool getClearEventlogOnMidnight() const;
void clearRxFragmentBuffer();
void addRxFragment(const uint8_t fragment[], const uint8_t len);
uint8_t verifyAllFragments(CommandAbstract& cmd);
@ -102,6 +105,7 @@ private:
bool _zeroValuesIfUnreachable = false;
bool _zeroYieldDayOnMidnight = false;
bool _clearEventlogOnMidnight = false;
std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser;
@ -109,4 +113,4 @@ private:
std::unique_ptr<PowerCommandParser> _powerCommandParser;
std::unique_ptr<StatisticsParser> _statisticsParser;
std::unique_ptr<SystemConfigParaParser> _systemConfigParaParser;
};
};

View File

@ -7,7 +7,7 @@
| HM_4CH | HM-1000/1200/1500-4T | 1161 |
| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 |
| HMS_1CHv2 | HMS-500-1T v2 | 1125 |
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1144 |
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 |
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 |

View File

@ -1,22 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
/*
This parser is used to parse the response of 'AlarmDataCommand'.
Data structure:
* wcode:
* right 8 bit: Event ID
* bit 13: Start time = PM (12h has to be added to start time)
* bit 12: End time = PM (12h has to be added to start time)
* Start: 12h based start time of the event (PM indicator in wcode)
* End: 12h based start time of the event (PM indicator in wcode)
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 10 11
|<-------------- First log entry -------------->| |<->|
-----------------------------------------------------------------------------------------------------------------------------
95 80 14 82 66 80 14 33 28 01 00 01 80 01 00 01 91 EA 91 EA 00 00 00 00 00 8F 65 -- -- -- -- --
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ ^^ ^^ ^^ ^^^^^ ^^
ID Source Addr Target Addr Idx ? wcode ? Start End ? ? ? ? wcode CRC8
*/
#include "AlarmLogParser.h"
#include "../Hoymiles.h"
#include <cstring>
const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> AlarmLogParser::_alarmMessages = { {
{ AlarmMessageType_t::ALL, 1, "Inverter start", "Wechselrichter gestartet", "L'onduleur a démarré" },
{ AlarmMessageType_t::ALL, 2, "Time calibration", "", "" },
{ AlarmMessageType_t::ALL, 2, "Time calibration", "Zeitabgleich", "" },
{ AlarmMessageType_t::ALL, 3, "EEPROM reading and writing error during operation", "", "" },
{ AlarmMessageType_t::ALL, 4, "Offline", "Offline", "Non connecté" },
{ AlarmMessageType_t::ALL, 11, "Grid voltage surge", "", "" },
{ AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "", "" },
{ AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "", "" },
{ AlarmMessageType_t::ALL, 14, "Grid phase mutation", "", "" },
{ AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "", "" },
{ AlarmMessageType_t::ALL, 11, "Grid voltage surge", "Netz: Überspannungsimpuls", "" },
{ AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "Netz: Spannungseinbruch", "" },
{ AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "Netz: Frequenzänderung", "" },
{ AlarmMessageType_t::ALL, 14, "Grid phase mutation", "Netz: Phasenänderung", "" },
{ AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "Netz: vorübergehende Schwankung", "" },
{ AlarmMessageType_t::ALL, 36, "INV overvoltage or overcurrent", "", "" },

View File

@ -1,7 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 - 2023 Thomas Basler and others
* Copyright (C) 2022 - 2024 Thomas Basler and others
*/
/*
This parser is used to parse the response of 'DevInfoAllCommand' and 'DevInfoSimpleCommand'.
It contains version information of the hardware and firmware. It can also be used to determine
the exact inverter type.
Data structure (DevInfoAllCommand):
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 10 11 12 13
-------------------------------------------------------------------------------------------------------------------------------------------------
95 80 14 82 66 80 14 33 28 81 27 1C 07 E5 04 01 07 2D 00 01 00 00 00 00 DF DD 1E -- -- -- -- --
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^
ID Source Addr Target Addr Idx FW Version FW Year FW Month/Date FW Hour/Minute Bootloader ? ? CRC16 CRC8
Data structure (DevInfoSimpleCommand):
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 10 11 12 13
-------------------------------------------------------------------------------------------------------------------------------------------------
95 80 14 82 66 80 14 33 28 81 27 1C 10 12 71 01 01 00 0A 00 20 01 00 00 E5 F8 95
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^
ID Source Addr Target Addr Idx FW Version HW Part No. HW Version ? ? ? CRC16 CRC8
*/
#include "DevInfoParser.h"
#include "../Hoymiles.h"
#include <cstring>
@ -37,6 +62,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01
{ { 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
{ { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900-2T" }, // 03
{ { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 05

View File

@ -2,6 +2,23 @@
/*
* Copyright (C) 2023 - 2024 Thomas Basler and others
*/
/*
This parser is used to parse the response of 'GridOnProFilePara'.
It contains the whole grid profile of the inverter.
Data structure:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 10 11 12 13
|<---------- Returns till the end of the payload ---------->|
---------------------------------------------------------------------------------------------------------------------------------------------------------------
95 80 14 82 66 80 14 33 28 01 0A 00 20 01 00 0C 08 FC 07 A3 00 0F 09 E2 00 1E E6 -- -- -- -- --
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^
ID Source Addr Target Addr Idx Profile ID Profile Version Section ID Section Version Value Value Value Value CRC16 CRC8
The number of values depends on the respective section and its version. After the last value of a section follows the next section id.
*/
#include "GridProfileParser.h"
#include "../Hoymiles.h"
#include <cstring>
@ -11,10 +28,10 @@
const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> GridProfileParser::_profileTypes = { {
{ 0x02, 0x00, "US - NA_IEEE1547_240V" },
{ 0x03, 0x00, "DE - DE_VDE4105_2018" },
{ 0x03, 0x01, "XX - unknown" },
{ 0x03, 0x01, "DE - DE_VDE4105_2011" },
{ 0x0a, 0x00, "XX - EN 50549-1:2019" },
{ 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" },
{ 0x0d, 0x04, "FR -" },
{ 0x0d, 0x04, "XX - NF_EN_50549-1:2019" },
{ 0x10, 0x00, "ES - ES_RD1699" },
{ 0x12, 0x00, "PL - EU_EN50438" },
{ 0x29, 0x00, "NL - NL_NEN-EN50549-1_2019" },
@ -82,7 +99,7 @@ constexpr frozen::map<uint8_t, GridProfileItemDefinition_t, 0x42> itemDefinition
{ 0x1f, make_value("Start of Frequency Watt Droop (Fstart)", "Hz", 100) },
{ 0x20, make_value("FW Droop Slope (Kpower_Freq)", "Pn%/Hz", 10) },
{ 0x21, make_value("Recovery Ramp Rate (RRR)", "Pn%/s", 100) },
{ 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 100) },
{ 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 10) },
{ 0x23, make_value("Recovery Low Frequency (RVLF)", "Hz", 100) },
{ 0x24, make_value("VW Function Activated", "bool", 1) },
{ 0x25, make_value("Start of Voltage Watt Droop (Vstart)", "V", 10) },

View File

@ -1,7 +1,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 - 2023 Thomas Basler and others
* Copyright (C) 2022 - 2024 Thomas Basler and others
*/
/*
This parser is used to parse the response of 'SystemConfigParaCommand'.
It contains the set inverter limit.
Data structure:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 10 11 12 13
---------------------------------------------------------------------------------------------------------------------------------
95 80 14 82 66 80 14 33 28 81 00 01 03 E8 00 00 03 E8 00 00 00 00 00 00 3C F8 2E -- -- -- -- --
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^
ID Source Addr Target Addr Idx ? Limit percent ? ? ? ? ? CRC16 CRC8
*/
#include "SystemConfigParaParser.h"
#include "../Hoymiles.h"
#include <cstring>

View File

@ -80,7 +80,7 @@ void pushListBuffer(unsigned char byte)
void reduceList()
{
if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0)
if (currentLevel < MAX_TREE_SIZE && nodes[currentLevel] > 0)
nodes[currentLevel]--;
}
@ -171,7 +171,7 @@ void checkMagicByte(unsigned char &byte)
// Datatype Octet String
setState(SML_HDATA, (byte & 0x0F) << 4);
}
else if (byte >= 0xF0 /*&& byte <= 0xFF*/) {
else if (byte >= 0xF0) {
/* Datatype List of ...*/
setState(SML_LISTEXTENDED, (byte & 0x0F) << 4);
}
@ -189,7 +189,13 @@ void checkMagicByte(unsigned char &byte)
}
}
sml_states_t smlState(unsigned char &currentByte)
void smlReset(void)
{
len = 4; // expect start sequence
currentState = SML_START;
}
sml_states_t smlState(unsigned char currentByte)
{
unsigned char size;
if (len > 0)
@ -317,7 +323,7 @@ void smlOBISManufacturer(unsigned char *str, int maxSize)
}
}
void smlPow(double &val, signed char &scaler)
void smlPow(float &val, signed char &scaler)
{
if (scaler < 0) {
while (scaler++) {
@ -372,7 +378,7 @@ void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit)
}
}
void smlOBISWh(double &wh)
void smlOBISWh(float &wh)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT_HOUR);
@ -380,7 +386,7 @@ void smlOBISWh(double &wh)
smlPow(wh, sc);
}
void smlOBISW(double &w)
void smlOBISW(float &w)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT);
@ -388,7 +394,7 @@ void smlOBISW(double &w)
smlPow(w, sc);
}
void smlOBISVolt(double &v)
void smlOBISVolt(float &v)
{
long long int val;
smlOBISByUnit(val, sc, SML_VOLT);
@ -396,10 +402,26 @@ void smlOBISVolt(double &v)
smlPow(v, sc);
}
void smlOBISAmpere(double &a)
void smlOBISAmpere(float &a)
{
long long int val;
smlOBISByUnit(val, sc, SML_AMPERE);
a = val;
smlPow(a, sc);
}
void smlOBISHertz(float &h)
{
long long int val;
smlOBISByUnit(val, sc, SML_HERTZ);
h = val;
smlPow(h, sc);
}
void smlOBISDegree(float &d)
{
long long int val;
smlOBISByUnit(val, sc, SML_DEGREE);
d = val;
smlPow(d, sc);
}

View File

@ -92,15 +92,17 @@ typedef enum {
SML_COUNT = 255
} sml_units_t;
sml_states_t smlState(unsigned char &byte);
void smlReset(void);
sml_states_t smlState(unsigned char byte);
bool smlOBISCheck(const unsigned char *obis);
void smlOBISManufacturer(unsigned char *str, int maxSize);
void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit);
// Be aware that double on Arduino UNO is just 32 bit
void smlOBISWh(double &wh);
void smlOBISW(double &w);
void smlOBISVolt(double &v);
void smlOBISAmpere(double &a);
void smlOBISWh(float &wh);
void smlOBISW(float &w);
void smlOBISVolt(float &v);
void smlOBISAmpere(float &a);
void smlOBISHertz(float &h);
void smlOBISDegree(float &d);
#endif

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
//------------------------------------------------------------------------------
@ -60,7 +60,7 @@ void SDM::begin(void) {
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
sdmSer.begin(_baud, (SoftwareSerialConfig)_config, _rx_pin, _tx_pin);
sdmSer.begin(_baud, (EspSoftwareSerial::Config)_config, _rx_pin, _tx_pin);
#else
sdmSer.begin(_baud);
#endif
@ -77,44 +77,67 @@ void SDM::begin(void) {
}
float SDM::readVal(uint16_t reg, uint8_t node) {
uint16_t temp;
unsigned long resptime;
uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0};
float res = NAN;
startReadVal(reg, node);
uint16_t readErr = SDM_ERR_STILL_WAITING;
while (readErr == SDM_ERR_STILL_WAITING) {
readErr = readValReady(node);
delay(1);
}
if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter
readingerrcode = readErr;
readingerrcount++;
} else {
++readingsuccesscount;
}
if (readErr == SDM_ERR_NO_ERROR) {
return decodeFloatValue();
}
constexpr float res = NAN;
return (res);
}
void SDM::startReadVal(uint16_t reg, uint8_t node, uint8_t functionCode) {
uint8_t data[] = {
node, // Address
functionCode, // Modbus function
highByte(reg), // Start address high byte
lowByte(reg), // Start address low byte
SDM_B_05, // Number of points high byte
SDM_B_06, // Number of points low byte
0, // Checksum low byte
0}; // Checksum high byte
constexpr size_t messageLength = sizeof(data) / sizeof(data[0]);
modbusWrite(data, messageLength);
}
uint16_t SDM::readValReady(uint8_t node, uint8_t functionCode) {
uint16_t readErr = SDM_ERR_NO_ERROR;
sdmarr[2] = highByte(reg);
sdmarr[3] = lowByte(reg);
temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes
sdmarr[6] = lowByte(temp);
sdmarr[7] = highByte(temp);
#if !defined ( USE_HARDWARESERIAL )
sdmSer.listen(); //enable softserial rx interrupt
#endif
flush(); //read serial if any old data is available
dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485)
delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524
sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes
sdmSer.flush(); //clear out tx buffer
dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
resptime = millis();
if (sdmSer.available() < FRAMESIZE && ((millis() - resptime) < msturnaround))
{
return SDM_ERR_STILL_WAITING;
}
while (sdmSer.available() < FRAMESIZE) {
if (millis() - resptime > msturnaround) {
if ((millis() - resptime) > msturnaround) {
readErr = SDM_ERR_TIMEOUT; //err debug (4)
if (sdmSer.available() == 5) {
for(int n=0; n<5; n++) {
sdmarr[n] = sdmSer.read();
}
if (validChecksum(sdmarr, 5)) {
readErr = sdmarr[2];
}
}
break;
}
yield();
delay(1);
}
if (readErr == SDM_ERR_NO_ERROR) { //if no timeout...
@ -125,14 +148,10 @@ float SDM::readVal(uint16_t reg, uint8_t node) {
sdmarr[n] = sdmSer.read();
}
if (sdmarr[0] == node && sdmarr[1] == SDM_B_02 && sdmarr[2] == SDM_REPLY_BYTE_COUNT) {
if ((calculateCRC(sdmarr, FRAMESIZE - 2)) == ((sdmarr[8] << 8) | sdmarr[7])) { //calculate crc from first 7 bytes and compare with received crc (bytes 7 & 8)
((uint8_t*)&res)[3]= sdmarr[3];
((uint8_t*)&res)[2]= sdmarr[4];
((uint8_t*)&res)[1]= sdmarr[5];
((uint8_t*)&res)[0]= sdmarr[6];
} else {
if (sdmarr[0] == node &&
sdmarr[1] == functionCode &&
sdmarr[2] == SDM_REPLY_BYTE_COUNT) {
if (!validChecksum(sdmarr, FRAMESIZE)) {
readErr = SDM_ERR_CRC_ERROR; //err debug (1)
}
@ -159,12 +178,95 @@ float SDM::readVal(uint16_t reg, uint8_t node) {
}
#if !defined ( USE_HARDWARESERIAL )
sdmSer.stopListening(); //disable softserial rx interrupt
// sdmSer.stopListening(); //disable softserial rx interrupt
#endif
return readErr;
}
float SDM::decodeFloatValue() const {
if (validChecksum(sdmarr, FRAMESIZE)) {
float res{};
((uint8_t*)&res)[3]= sdmarr[3];
((uint8_t*)&res)[2]= sdmarr[4];
((uint8_t*)&res)[1]= sdmarr[5];
((uint8_t*)&res)[0]= sdmarr[6];
return res;
}
constexpr float res = NAN;
return res;
}
float SDM::readHoldingRegister(uint16_t reg, uint8_t node) {
startReadVal(reg, node, SDM_READ_HOLDING_REGISTER);
uint16_t readErr = SDM_ERR_STILL_WAITING;
while (readErr == SDM_ERR_STILL_WAITING) {
delay(1);
readErr = readValReady(node, SDM_READ_HOLDING_REGISTER);
}
if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter
readingerrcode = readErr;
readingerrcount++;
} else {
++readingsuccesscount;
}
if (readErr == SDM_ERR_NO_ERROR) {
return decodeFloatValue();
}
constexpr float res = NAN;
return (res);
}
bool SDM::writeHoldingRegister(float value, uint16_t reg, uint8_t node) {
{
uint8_t data[] = {
node, // Address
SDM_WRITE_HOLDING_REGISTER, // Function
highByte(reg), // Starting Address High
lowByte(reg), // Starting Address Low
SDM_B_05, // Number of Registers High
SDM_B_06, // Number of Registers Low
4, // Byte count
((uint8_t*)&value)[3],
((uint8_t*)&value)[2],
((uint8_t*)&value)[1],
((uint8_t*)&value)[0],
0, 0};
constexpr size_t messageLength = sizeof(data) / sizeof(data[0]);
modbusWrite(data, messageLength);
}
uint16_t readErr = SDM_ERR_STILL_WAITING;
while (readErr == SDM_ERR_STILL_WAITING) {
delay(1);
readErr = readValReady(node, SDM_READ_HOLDING_REGISTER);
}
if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter
readingerrcode = readErr;
readingerrcount++;
} else {
++readingsuccesscount;
}
return readErr == SDM_ERR_NO_ERROR;
}
uint32_t SDM::getSerialNumber(uint8_t node) {
uint32_t res{};
readHoldingRegister(SDM_HOLDING_SERIAL_NUMBER, node);
// if (getErrCode() == SDM_ERR_NO_ERROR) {
for (size_t i = 0; i < 4; ++i) {
res = (res << 8) + sdmarr[3 + i];
}
// }
return res;
}
uint16_t SDM::getErrCode(bool _clear) {
uint16_t _tmp = readingerrcode;
if (_clear == true)
@ -224,7 +326,7 @@ uint16_t SDM::getMsTimeout() {
return (mstimeout);
}
uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) {
uint16_t SDM::calculateCRC(const uint8_t *array, uint8_t len) const {
uint16_t _crc, _flag;
_crc = 0xFFFF;
for (uint8_t i = 0; i < len; i++) {
@ -241,10 +343,17 @@ uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) {
void SDM::flush(unsigned long _flushtime) {
unsigned long flushstart = millis();
while (sdmSer.available() || (millis() - flushstart < _flushtime)) {
if (sdmSer.available()) //read serial if any old data is available
sdmSer.flush();
int available = sdmSer.available();
while (available > 0 || ((millis() - flushstart) < _flushtime)) {
while (available > 0) {
--available;
flushstart = millis();
//read serial if any old data is available
sdmSer.read();
}
delay(1);
available = sdmSer.available();
}
}
@ -252,3 +361,58 @@ void SDM::dereSet(bool _state) {
if (_dere_pin != NOT_A_PIN)
digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
}
bool SDM::validChecksum(const uint8_t* data, size_t messageLength) const {
const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes
return data[messageLength - 2] == lowByte(temp) &&
data[messageLength - 1] == highByte(temp);
}
void SDM::modbusWrite(uint8_t* data, size_t messageLength) {
const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes
data[messageLength - 2] = lowByte(temp);
data[messageLength - 1] = highByte(temp);
#if !defined ( USE_HARDWARESERIAL )
sdmSer.listen(); //enable softserial rx interrupt
#endif
flush(); //read serial if any old data is available
if (_dere_pin != NOT_A_PIN) {
dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485)
delay(1); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524
// Need to wait for all bytes in TX buffer are sent.
// N.B. flush() on serial port does often only clear the send buffer, not wait till all is sent.
const unsigned long waitForBytesSent_ms = (messageLength * 11000) / _baud + 1;
resptime = millis() + waitForBytesSent_ms;
}
#if !defined ( USE_HARDWARESERIAL )
// prevent scheduler from messing up the serial message. this task shall only
// be scheduled after the whole serial message was transmitted.
vTaskSuspendAll();
#endif
sdmSer.write(data, messageLength); //send 8 bytes
#if !defined ( USE_HARDWARESERIAL )
xTaskResumeAll();
#endif
if (_dere_pin != NOT_A_PIN) {
const int32_t timeleft = (int32_t) (resptime - millis());
if (timeleft > 0) {
delay(timeleft); //clear out tx buffer
}
dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
flush();
}
resptime = millis();
}

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
//------------------------------------------------------------------------------
@ -66,36 +66,47 @@
#endif
#if !defined ( WAITING_TURNAROUND_DELAY )
#define WAITING_TURNAROUND_DELAY 200 // time in ms to wait for process current request
#define WAITING_TURNAROUND_DELAY 500 // time in ms to wait for process current request
#endif
#if !defined ( RESPONSE_TIMEOUT )
#define RESPONSE_TIMEOUT 500 // time in ms to wait for return response from all devices before next request
#define RESPONSE_TIMEOUT 10 // time in ms to wait for return response from all devices before next request
#endif
#if !defined ( SDM_MIN_DELAY )
#define SDM_MIN_DELAY 20 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#define SDM_MIN_DELAY 1 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#endif
#if !defined ( SDM_MAX_DELAY )
#define SDM_MAX_DELAY 5000 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#define SDM_MAX_DELAY 20 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#endif
//------------------------------------------------------------------------------
#define SDM_ERR_NO_ERROR 0 // no error
#define SDM_ERR_CRC_ERROR 1 // crc error
#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong
#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm
#define SDM_ERR_TIMEOUT 4 // timeout
#define SDM_ERR_ILLEGAL_FUNCTION 1
#define SDM_ERR_ILLEGAL_DATA_ADDRESS 2
#define SDM_ERR_ILLEGAL_DATA_VALUE 3
#define SDM_ERR_SLAVE_DEVICE_FAILURE 5
#define SDM_ERR_CRC_ERROR 11 // crc error
#define SDM_ERR_WRONG_BYTES 12 // bytes b0,b1 or b2 wrong
#define SDM_ERR_NOT_ENOUGHT_BYTES 13 // not enough bytes from sdm
#define SDM_ERR_TIMEOUT 14 // timeout
#define SDM_ERR_EXCEPTION 15
#define SDM_ERR_STILL_WAITING 16
//------------------------------------------------------------------------------
#define SDM_READ_HOLDING_REGISTER 0x03
#define SDM_READ_INPUT_REGISTER 0x04
#define SDM_WRITE_HOLDING_REGISTER 0x10
#define FRAMESIZE 9 // size of out/in array
#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data
#define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1)
#define SDM_B_02 0x04 // BYTE 2 -> function code (default value 0x04 read from 3X input registers)
#define SDM_B_02 SDM_READ_INPUT_REGISTER // BYTE 2 -> function code (default value 0x04 read from 3X input registers)
#define SDM_B_05 0x00 // BYTE 5
#define SDM_B_06 0x02 // BYTE 6
// BYTES 3 & 4 (BELOW)
@ -151,6 +162,8 @@
#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | |
#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | |
#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | |
#define SDM_REACTIVE_POWER_DEMAND 0x006C // VAr | 1 | | | | | | |
#define SDM_MAXIMUM_REACTIVE_POWER_DEMAND 0x006E // VAr | 1 | | | | | | |
#define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 |
#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 |
#define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 |
@ -199,7 +212,10 @@
#define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | |
#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 |
#define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 |
#define SDM_CURRENT_RESETTABLE_IMPORT_REACTIVE_ENERGY 0x0188 // kVArh | | | | | | 1 | 1 |
#define SDM_CURRENT_RESETTABLE_EXPORT_REACTIVE_ENERGY 0x018A // kVArh | | | | | | 1 | 1 |
#define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 |
#define SDM_NET_KVARH 0x018E // kVArh | | | | | | | 1 |
#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 |
#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 |
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -229,6 +245,78 @@
//#define DEVNAME_POWER 0x0004 // W | 1 |
//---------------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------
// REGISTERS LIST FOR DEVICE SETTINGS |
//---------------------------------------------------------------------------------------------------------
// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME |
//---------------------------------------------------------------------------------------------------------
// Read minutes into first demand calculation.
// When the Demand Time reaches the Demand Period
// then the demand values are valid.
#define SDM_HOLDING_DEMAND_TIME 0x0000
// Write demand period: 0~60 minutes.
// Default 60.
// Range: 0~60, 0 means function disabled
#define SDM_HOLDING_DEMAND_PERIOD 0x0002
// Write relay on period in milliseconds:
// 60, 100 or 200 ms.
// default: 100 ms
#define SDM_HOLDING_RELAY_PULSE_WIDTH 0x000C
// Parity / stop bit settings:
// 0 = One stop bit and no parity, default.
// 1 = One stop bit and even parity.
// 2 = One s top bit and odd parity.
// 3 = Two stop bits and no parity.
// Requires a restart to become effective.
#define SDM_HOLDING_NETWORK_PARITY_STOP 0x0012
// Ranges from 1 to 247. Default ID is 1.
#define SDM_HOLDING_METER_ID 0x0014
// Write the network port baud rate for MODBUS Protocol, where:
/*
SDM120 / SDM230:
0 = 2400 baud (default)
1 = 4800 baud
2 = 9600 baud
5 = 1200 baud
SDM320 / SDM530Y:
0 = 2400 baud
1 = 4800 baud
2 = 9600 baud (default)
5 = 1200 band
SDM630 / SDM72 / SDM72V2:
0 = 2400 baud
1 = 4800 baud
2 = 9600 baud (default)
3 = 19200 baud
4 = 38400 baud
*/
#define SDM_HOLDING_BAUD_RATE 0x001C
// Write MODBUS Protocol input parameter for pulse out 1:
// 1: Import active energy
// 2: Import + export (total) active energy
// 4: Export active energy (default).
// 5: Import reactive energy
// 6: Import + export (total) reactive energy
// 8: Export reactive energy
#define SDM_HOLDING_PULSE_1_OUTPUT_MODE 0x0056
#define SDM_HOLDING_SERIAL_NUMBER 0xFC00
#define SDM_HOLDING_SOFTWARE_VERSION 0xFC03
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
class SDM {
@ -252,6 +340,16 @@ class SDM {
void begin(void);
float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node
void startReadVal(uint16_t reg, uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Start sending out the request to read a register from a specific node (allows for async access)
uint16_t readValReady(uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Check to see if a reply is ready reading from a node (allow for async access)
float decodeFloatValue() const;
float readHoldingRegister(uint16_t reg, uint8_t node = SDM_B_01);
bool writeHoldingRegister(float value, uint16_t reg, uint8_t node = SDM_B_01);
uint32_t getSerialNumber(uint8_t node = SDM_B_01);
uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase)
uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase)
uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false)
@ -264,6 +362,11 @@ class SDM {
uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms)
private:
bool validChecksum(const uint8_t* data, size_t messageLength) const;
void modbusWrite(uint8_t* data, size_t messageLength);
#if defined ( USE_HARDWARESERIAL )
HardwareSerial& sdmSer;
#else
@ -292,7 +395,9 @@ class SDM {
uint16_t mstimeout = RESPONSE_TIMEOUT;
uint32_t readingerrcount = 0; // total errors counter
uint32_t readingsuccesscount = 0; // total success counter
uint16_t calculateCRC(uint8_t *array, uint8_t len);
unsigned long resptime = 0;
uint8_t sdmarr[FRAMESIZE] = {};
uint16_t calculateCRC(const uint8_t *array, uint8_t len) const;
void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms
void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM
};

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
@ -14,7 +14,7 @@
* define or undefine USE_HARDWARESERIAL (uncomment only one or none)
*/
//#undef USE_HARDWARESERIAL
#define USE_HARDWARESERIAL
//#define USE_HARDWARESERIAL
//------------------------------------------------------------------------------
@ -32,7 +32,7 @@
#if defined ( USE_HARDWARESERIAL )
#if defined ( ESP32 )
#define SDM_RX_PIN 13
#define SDM_TX_PIN 32
#define SDM_TX_PIN 15
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )

View File

@ -66,6 +66,7 @@ void VeDirectFrameHandler<T>::init(char const* who, int8_t rx, int8_t tx,
Print* msgOut, bool verboseLogging, uint8_t hwSerialPort)
{
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
_vedirectSerial->setRxBufferSize(512); // increased from default (256) to 512 Byte to avoid overflow
_vedirectSerial->end(); // make sure the UART will be re-initialized
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
_vedirectSerial->flush();

View File

@ -1,6 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1E0000,
app1, app, ota_1, 0x1F0000, 0x1E0000,
app0, app, ota_0, 0x10000, 0x3C0000,
spiffs, data, spiffs, 0x3D0000, 0x30000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1E0000 0x3C0000
app1 app ota_1 0x1F0000 0x1E0000
5 spiffs data spiffs 0x3D0000 0x30000

View File

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x3C0000,
spiffs, data, spiffs, 0x3D0000, 0x30000,
app1, app, ota_1, 0x400000, 0x3C0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x3C0000
5 spiffs data spiffs 0x3D0000 0x30000
6 app1 app ota_1 0x400000 0x3C0000

View File

@ -1,26 +0,0 @@
diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
+++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
@@ -97,7 +97,7 @@
static inline bool _init_async_event_queue(){
if(!_async_queue){
- _async_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *));
+ _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE, sizeof(lwip_event_packet_t *));
if(!_async_queue){
return false;
}
diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
+++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
@@ -53,6 +53,10 @@
#define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2
#endif
+#ifndef CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE
+#define CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE 32
+#endif
+
class AsyncClient;
#define ASYNC_MAX_ACK_TIME 5000

View File

@ -3,33 +3,27 @@
# Copyright (C) 2022 Thomas Basler and others
#
import os
import pkg_resources
Import("env")
required_pkgs = {'dulwich'}
installed_pkgs = {pkg.key for pkg in pkg_resources.working_set}
missing_pkgs = required_pkgs - installed_pkgs
if missing_pkgs:
try:
from dulwich import porcelain
except ModuleNotFoundError:
env.Execute('"$PYTHONEXE" -m pip install dulwich')
from dulwich import porcelain
from dulwich import porcelain
def updateFileIfChanged(filename, content):
mustUpdate = True
try:
fp = open(filename, "rb")
if fp.read() == content:
mustUpdate = False
fp.close()
with open(filename, "rb") as fp:
if fp.read() == content:
mustUpdate = False
except:
pass
if mustUpdate:
fp = open(filename, "wb")
fp.write(content)
fp.close()
with open(filename, "wb") as fp:
fp.write(content)
return mustUpdate

View File

@ -9,17 +9,17 @@
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = generic_esp32
default_envs = generic_esp32s3_usb
extra_configs =
platformio_override.ini
[env]
; Make sure to NOT add any spaces in the custom_ci_action property
; (also the position in the file is important)
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
custom_ci_action = generic_esp32_4mb_no_ota,generic_esp32_8mb,generic_esp32s3,generic_esp32s3_usb
framework = arduino
platform = espressif32@6.6.0
platform = espressif32@6.8.1
build_flags =
-DPIOENV=\"$PIOENV\"
@ -27,6 +27,7 @@ build_flags =
-D_TASK_THREAD_SAFE=1
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
-DEMC_TASK_STACK_SIZE=6400
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and
@ -38,23 +39,22 @@ build_unflags =
-std=gnu++11
lib_deps =
mathieucarbou/ESP Async WebServer @ 2.9.5
bblanchon/ArduinoJson @ 7.0.4
https://github.com/bertmelis/espMqttClient.git#v1.6.0
nrf24/RF24 @ 1.4.8
mathieucarbou/ESPAsyncWebServer @ 3.1.2
bblanchon/ArduinoJson @ 7.1.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.9
olikraus/U8g2 @ 2.35.19
buelowp/sunset @ 1.1.7
https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/coryjfowler/MCP_CAN_lib
plerup/EspSoftwareSerial @ ^8.0.1
https://github.com/dok-net/ghostl @ ^1.0.1
plerup/EspSoftwareSerial @ ^8.2.0
extra_scripts =
pre:pio-scripts/auto_firmware_version.py
pre:pio-scripts/patch_apply.py
post:pio-scripts/create_factory_bin.py
board_build.partitions = partitions_custom_4mb.csv
board_build.partitions = partitions_custom_8mb.csv
board_build.filesystem = littlefs
board_build.embed_files =
webapp_dist/index.html.gz
@ -64,7 +64,7 @@ board_build.embed_files =
webapp_dist/js/app.js.gz
webapp_dist/site.webmanifest
custom_patches = async_tcp
custom_patches =
monitor_filters = esp32_exception_decoder, time, log2file, colorize
monitor_speed = 115200
@ -75,7 +75,13 @@ upload_protocol = esptool
; upload_port = COM4
[env:generic_esp32]
[env:generic_esp32_4mb_no_ota]
board = esp32dev
build_flags = ${env.build_flags}
board_build.partitions = partitions_custom_4mb.csv
[env:generic_esp32_8mb]
board = esp32dev
build_flags = ${env.build_flags}
@ -92,12 +98,14 @@ build_flags = ${env.build_flags}
[env:generic_esp32c3]
board = esp32-c3-devkitc-02
board_build.partitions = partitions_custom_4mb.csv
custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags}
[env:generic_esp32c3_usb]
board = esp32-c3-devkitc-02
board_build.partitions = partitions_custom_4mb.csv
custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1
@ -117,17 +125,6 @@ build_flags = ${env.build_flags}
-DARDUINO_USB_CDC_ON_BOOT=1
[env:generic]
board = esp32dev
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=4
-DHOYMILES_PIN_CS=5
[env:olimex_esp32_poe]
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
board = esp32-poe
@ -144,6 +141,7 @@ build_flags = ${env.build_flags}
[env:olimex_esp32_evb]
; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware
board = esp32-evb
board_build.partitions = partitions_custom_4mb.csv
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=15
-DHOYMILES_PIN_MOSI=2
@ -156,16 +154,17 @@ build_flags = ${env.build_flags}
[env:d1_mini_esp32]
board = wemos_d1_mini32
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
board_build.partitions = partitions_custom_4mb.csv
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
-DPYLONTECH_PIN_RX=27
-DPYLONTECH_PIN_TX=14
-DHUAWEI_PIN_MISO=12
@ -178,6 +177,7 @@ build_flags =
[env:wt32_eth01]
; http://www.wireless-tag.com/portfolio/wt32-eth01/
board = wt32-eth01
board_build.partitions = partitions_custom_4mb.csv
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=4
-DHOYMILES_PIN_MOSI=2
@ -204,6 +204,7 @@ build_flags = ${env.build_flags}
; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/
; https://www.az-delivery.de/products/esp32-lolin-lolin32
board = lolin32_lite
board_build.partitions = partitions_custom_4mb.csv
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
@ -214,6 +215,7 @@ build_flags = ${env.build_flags}
[env:lolin_s2_mini]
board = lolin_s2_mini
board_build.partitions = partitions_custom_4mb.csv
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=13
-DHOYMILES_PIN_MOSI=11

View File

@ -5,6 +5,7 @@
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "PytesCanReceiver.h"
BatteryClass Battery;
@ -57,6 +58,9 @@ void BatteryClass::updateSettings()
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
break;
case 4:
_upProvider = std::make_unique<PytesCanReceiver>();
break;
default:
MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider);
return;

175
src/BatteryCanReceiver.cpp Normal file
View File

@ -0,0 +1,175 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "BatteryCanReceiver.h"
#include "MessageOutput.h"
#include "PinMapping.h"
#include <driver/twai.h>
bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)
{
_verboseLogging = verboseLogging;
_providerName = providerName;
MessageOutput.printf("[%s] Initialize interface...\r\n",
_providerName);
const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[%s] Interface rx = %d, tx = %d\r\n",
_providerName, pin.battery_rx, pin.battery_tx);
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.printf("[%s] Invalid pin config\r\n",
_providerName);
return false;
}
auto tx = static_cast<gpio_num_t>(pin.battery_tx);
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL);
// Initialize configuration structures using macro initializers
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
// Install TWAI driver
esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config);
switch (twaiLastResult) {
case ESP_OK:
MessageOutput.printf("[%s] Twai driver installed\r\n",
_providerName);
break;
case ESP_ERR_INVALID_ARG:
MessageOutput.printf("[%s] Twai driver install - invalid arg\r\n",
_providerName);
return false;
break;
case ESP_ERR_NO_MEM:
MessageOutput.printf("[%s] Twai driver install - no memory\r\n",
_providerName);
return false;
break;
case ESP_ERR_INVALID_STATE:
MessageOutput.printf("[%s] Twai driver install - invalid state\r\n",
_providerName);
return false;
break;
}
// Start TWAI driver
twaiLastResult = twai_start();
switch (twaiLastResult) {
case ESP_OK:
MessageOutput.printf("[%s] Twai driver started\r\n",
_providerName);
break;
case ESP_ERR_INVALID_STATE:
MessageOutput.printf("[%s] Twai driver start - invalid state\r\n",
_providerName);
return false;
break;
}
return true;
}
void BatteryCanReceiver::deinit()
{
// Stop TWAI driver
esp_err_t twaiLastResult = twai_stop();
switch (twaiLastResult) {
case ESP_OK:
MessageOutput.printf("[%s] Twai driver stopped\r\n",
_providerName);
break;
case ESP_ERR_INVALID_STATE:
MessageOutput.printf("[%s] Twai driver stop - invalid state\r\n",
_providerName);
break;
}
// Uninstall TWAI driver
twaiLastResult = twai_driver_uninstall();
switch (twaiLastResult) {
case ESP_OK:
MessageOutput.printf("[%s] Twai driver uninstalled\r\n",
_providerName);
break;
case ESP_ERR_INVALID_STATE:
MessageOutput.printf("[%s] Twai driver uninstall - invalid state\r\n",
_providerName);
break;
}
}
void BatteryCanReceiver::loop()
{
// Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer
twai_status_info_t status_info;
esp_err_t twaiLastResult = twai_get_status_info(&status_info);
if (twaiLastResult != ESP_OK) {
switch (twaiLastResult) {
case ESP_ERR_INVALID_ARG:
MessageOutput.printf("[%s] Twai driver get status - invalid arg\r\n",
_providerName);
break;
case ESP_ERR_INVALID_STATE:
MessageOutput.printf("[%s] Twai driver get status - invalid state\r\n",
_providerName);
break;
}
return;
}
if (status_info.msgs_to_rx == 0) {
return;
}
// Wait for message to be received, function is blocking
twai_message_t rx_message;
if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) {
MessageOutput.printf("[%s] Failed to receive message",
_providerName);
return;
}
if (_verboseLogging) {
MessageOutput.printf("[%s] Received CAN message: 0x%04X -",
_providerName, rx_message.identifier);
for (int i = 0; i < rx_message.data_length_code; i++) {
MessageOutput.printf(" %02X", rx_message.data[i]);
}
MessageOutput.printf("\r\n");
}
onMessage(rx_message);
}
uint8_t BatteryCanReceiver::readUnsignedInt8(uint8_t *data)
{
return data[0];
}
uint16_t BatteryCanReceiver::readUnsignedInt16(uint8_t *data)
{
return (data[1] << 8) | data[0];
}
int16_t BatteryCanReceiver::readSignedInt16(uint8_t *data)
{
return this->readUnsignedInt16(data);
}
uint32_t BatteryCanReceiver::readUnsignedInt32(uint8_t *data)
{
return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0];
}
float BatteryCanReceiver::scaleValue(int16_t value, float factor)
{
return value * factor;
}
bool BatteryCanReceiver::getBit(uint8_t value, uint8_t bit)
{
return (value & (1 << bit)) >> bit;
}

View File

@ -26,9 +26,12 @@ static void addLiveViewValue(JsonVariant& root, std::string const& name,
}
static void addLiveViewTextInSection(JsonVariant& root,
std::string const& section, std::string const& name, std::string const& text)
std::string const& section, std::string const& name,
std::string const& text, bool translate = true)
{
root["values"][section][name] = text;
auto jsonValue = root["values"][section][name];
jsonValue["value"] = text;
jsonValue["translate"] = translate;
}
static void addLiveViewTextValue(JsonVariant& root, std::string const& name,
@ -62,6 +65,9 @@ bool BatteryStats::updateAvailable(uint32_t since) const
void BatteryStats::getLiveViewData(JsonVariant& root) const
{
root["manufacturer"] = _manufacturer;
if (!_serial.isEmpty()) {
root["serial"] = _serial;
}
if (!_fwversion.isEmpty()) {
root["fwversion"] = _fwversion;
}
@ -72,6 +78,7 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const
addLiveViewValue(root, "SoC", _soc, "%", _socPrecision);
addLiveViewValue(root, "voltage", _voltage, "V", 2);
addLiveViewValue(root, "current", _current, "A", _currentPrecision);
}
void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
@ -83,7 +90,6 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
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);
addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
@ -113,6 +119,77 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal);
}
void PytesBatteryStats::getLiveViewData(JsonVariant& root) const
{
BatteryStats::getLiveViewData(root);
// values go into the "Status" card of the web application
addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1);
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1);
addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1);
addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1);
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
addLiveViewValue(root, "capacity", _totalCapacity, "Ah", 0);
addLiveViewValue(root, "availableCapacity", _availableCapacity, "Ah", 0);
if (_chargedEnergy != -1) {
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 1);
}
if (_dischargedEnergy != -1) {
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 1);
}
addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
addLiveViewInSection(root, "cells", "cellMinTemperature", _cellMinTemperature, "°C", 0);
addLiveViewInSection(root, "cells", "cellMaxTemperature", _cellMaxTemperature, "°C", 0);
addLiveViewTextInSection(root, "cells", "cellMinVoltageName", _cellMinVoltageName.c_str(), false);
addLiveViewTextInSection(root, "cells", "cellMaxVoltageName", _cellMaxVoltageName.c_str(), false);
addLiveViewTextInSection(root, "cells", "cellMinTemperatureName", _cellMinTemperatureName.c_str(), false);
addLiveViewTextInSection(root, "cells", "cellMaxTemperatureName", _cellMaxTemperatureName.c_str(), false);
addLiveViewInSection(root, "modules", "online", _moduleCountOnline, "", 0);
addLiveViewInSection(root, "modules", "offline", _moduleCountOffline, "", 0);
addLiveViewInSection(root, "modules", "blockingCharge", _moduleCountBlockingCharge, "", 0);
addLiveViewInSection(root, "modules", "blockingDischarge", _moduleCountBlockingDischarge, "", 0);
// alarms and warnings go into the "Issues" card of the web application
addLiveViewWarning(root, "highCurrentDischarge", _warningHighDischargeCurrent);
addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge);
addLiveViewWarning(root, "highCurrentCharge", _warningHighChargeCurrent);
addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge);
addLiveViewWarning(root, "lowVoltage", _warningLowVoltage);
addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage);
addLiveViewWarning(root, "highVoltage", _warningHighVoltage);
addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage);
addLiveViewWarning(root, "lowTemperature", _warningLowTemperature);
addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature);
addLiveViewWarning(root, "highTemperature", _warningHighTemperature);
addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature);
addLiveViewWarning(root, "lowTemperatureCharge", _warningLowTemperatureCharge);
addLiveViewAlarm(root, "underTemperatureCharge", _alarmUnderTemperatureCharge);
addLiveViewWarning(root, "highTemperatureCharge", _warningHighTemperatureCharge);
addLiveViewAlarm(root, "overTemperatureCharge", _alarmOverTemperatureCharge);
addLiveViewWarning(root, "bmsInternal", _warningInternalFailure);
addLiveViewAlarm(root, "bmsInternal", _alarmInternalFailure);
addLiveViewWarning(root, "cellDiffVoltage", _warningCellImbalance);
addLiveViewAlarm(root, "cellDiffVoltage", _alarmCellImbalance);
}
void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
{
BatteryStats::getLiveViewData(root);
@ -120,11 +197,6 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
using Label = JkBms::DataPointLabel;
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
if (oCurrent.has_value()) {
addLiveViewValue(root, "current",
static_cast<float>(*oCurrent) / 1000, "A", 2);
}
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
@ -226,8 +298,15 @@ void BatteryStats::mqttPublish() const
{
MqttSettings.publish("battery/manufacturer", _manufacturer);
MqttSettings.publish("battery/dataAge", String(getAgeSeconds()));
MqttSettings.publish("battery/stateOfCharge", String(_soc));
MqttSettings.publish("battery/voltage", String(_voltage));
if (isSoCValid()) {
MqttSettings.publish("battery/stateOfCharge", String(_soc));
}
if (isVoltageValid()) {
MqttSettings.publish("battery/voltage", String(_voltage));
}
if (isCurrentValid()) {
MqttSettings.publish("battery/current", String(_current));
}
}
void PylontechBatteryStats::mqttPublish() const
@ -238,7 +317,6 @@ void PylontechBatteryStats::mqttPublish() const
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));
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge));
@ -259,6 +337,67 @@ void PylontechBatteryStats::mqttPublish() const
MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately));
}
void PytesBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltageLimit));
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimit));
MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit));
MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit));
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
MqttSettings.publish("battery/temperature", String(_temperature));
if (_chargedEnergy != -1) {
MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy));
}
if (_dischargedEnergy != -1) {
MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy));
}
MqttSettings.publish("battery/capacity", String(_totalCapacity));
MqttSettings.publish("battery/availableCapacity", String(_availableCapacity));
MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
MqttSettings.publish("battery/CellMinTemperature", String(_cellMinTemperature));
MqttSettings.publish("battery/CellMaxTemperature", String(_cellMaxTemperature));
MqttSettings.publish("battery/CellMinVoltageName", String(_cellMinVoltageName));
MqttSettings.publish("battery/CellMaxVoltageName", String(_cellMaxVoltageName));
MqttSettings.publish("battery/CellMinTemperatureName", String(_cellMinTemperatureName));
MqttSettings.publish("battery/CellMaxTemperatureName", String(_cellMaxTemperatureName));
MqttSettings.publish("battery/modulesOnline", String(_moduleCountOnline));
MqttSettings.publish("battery/modulesOffline", String(_moduleCountOffline));
MqttSettings.publish("battery/modulesBlockingCharge", String(_moduleCountBlockingCharge));
MqttSettings.publish("battery/modulesBlockingDischarge", String(_moduleCountBlockingDischarge));
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge));
MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage));
MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage));
MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature));
MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature));
MqttSettings.publish("battery/alarm/underTemperatureCharge", String(_alarmUnderTemperatureCharge));
MqttSettings.publish("battery/alarm/overTemperatureCharge", String(_alarmOverTemperatureCharge));
MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmInternalFailure));
MqttSettings.publish("battery/alarm/cellImbalance", String(_alarmCellImbalance));
MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighDischargeCurrent));
MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighChargeCurrent));
MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage));
MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage));
MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature));
MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature));
MqttSettings.publish("battery/warning/lowTemperatureCharge", String(_warningLowTemperatureCharge));
MqttSettings.publish("battery/warning/highTemperatureCharge", String(_warningHighTemperatureCharge));
MqttSettings.publish("battery/warning/bmsInternal", String(_warningInternalFailure));
MqttSettings.publish("battery/warning/cellImbalance", String(_warningCellImbalance));
}
void JkBmsBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
@ -365,6 +504,13 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
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>();
@ -405,9 +551,9 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
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());
BatteryStats::setCurrent(static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000, 2/*precision*/, millis());
_fwversion = shuntData.getFwVersionFormatted();
_current = static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000;
_chargeCycles = shuntData.H4;
_timeToGo = shuntData.TTG / 60;
_chargedEnergy = static_cast<float>(shuntData.H18) / 100;
@ -434,7 +580,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
BatteryStats::getLiveViewData(root);
// values go into the "Status" card of the web application
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2);
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2);
@ -457,7 +602,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
void VictronSmartShuntStats::mqttPublish() const {
BatteryStats::mqttPublish();
MqttSettings.publish("battery/current", String(_current));
MqttSettings.publish("battery/chargeCycles", String(_chargeCycles));
MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy));
MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy));

View File

@ -4,9 +4,9 @@
*/
#include "Configuration.h"
#include "MessageOutput.h"
#include "NetworkSettings.h"
#include "Utils.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <nvs_flash.h>
@ -17,6 +17,63 @@ void ConfigurationClass::init()
memset(&config, 0x0, sizeof(config));
}
void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target)
{
JsonObject target_http_config = target["http_request"].to<JsonObject>();
target_http_config["url"] = source.Url;
target_http_config["auth_type"] = source.AuthType;
target_http_config["username"] = source.Username;
target_http_config["password"] = source.Password;
target_http_config["header_key"] = source.HeaderKey;
target_http_config["header_value"] = source.HeaderValue;
target_http_config["timeout"] = source.Timeout;
}
void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target)
{
JsonArray values = target["values"].to<JsonArray>();
for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) {
JsonObject t = values.add<JsonObject>();
PowerMeterMqttValue const& s = source.Values[i];
t["topic"] = s.Topic;
t["json_path"] = s.JsonPath;
t["unit"] = s.PowerUnit;
t["sign_inverted"] = s.SignInverted;
}
}
void ConfigurationClass::serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target)
{
target["address"] = source.Address;
target["polling_interval"] = source.PollingInterval;
}
void ConfigurationClass::serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target)
{
target["polling_interval"] = source.PollingInterval;
target["individual_requests"] = source.IndividualRequests;
JsonArray values = target["values"].to<JsonArray>();
for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
JsonObject t = values.add<JsonObject>();
PowerMeterHttpJsonValue const& s = source.Values[i];
serializeHttpRequestConfig(s.HttpRequest, t);
t["enabled"] = s.Enabled;
t["json_path"] = s.JsonPath;
t["unit"] = s.PowerUnit;
t["sign_inverted"] = s.SignInverted;
}
}
void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target)
{
target["polling_interval"] = source.PollingInterval;
serializeHttpRequestConfig(source.HttpRequest, target);
}
bool ConfigurationClass::write()
{
File f = LittleFS.open(CONFIG_FILENAME, "w");
@ -59,6 +116,7 @@ bool ConfigurationClass::write()
mqtt["verbose_logging"] = config.Mqtt.VerboseLogging;
mqtt["hostname"] = config.Mqtt.Hostname;
mqtt["port"] = config.Mqtt.Port;
mqtt["clientid"] = config.Mqtt.ClientId;
mqtt["username"] = config.Mqtt.Username;
mqtt["password"] = config.Mqtt.Password;
mqtt["topic"] = config.Mqtt.Topic;
@ -130,6 +188,7 @@ bool ConfigurationClass::write()
inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold;
inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable;
inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight;
inv["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight;
inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection;
JsonArray channel = inv["channel"].to<JsonArray>();
@ -149,31 +208,19 @@ bool ConfigurationClass::write()
JsonObject powermeter = doc["powermeter"].to<JsonObject>();
powermeter["enabled"] = config.PowerMeter.Enabled;
powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging;
powermeter["interval"] = config.PowerMeter.Interval;
powermeter["source"] = config.PowerMeter.Source;
powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1;
powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2;
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3;
powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate;
powermeter["sdmaddress"] = config.PowerMeter.SdmAddress;
powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
JsonArray powermeter_http_phases = powermeter["http_phases"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases.add<JsonObject>();
JsonObject powermeter_mqtt = powermeter["mqtt"].to<JsonObject>();
serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, powermeter_mqtt);
powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url;
powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType;
powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username;
powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password;
powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey;
powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue;
powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath;
powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
}
JsonObject powermeter_serial_sdm = powermeter["serial_sdm"].to<JsonObject>();
serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, powermeter_serial_sdm);
JsonObject powermeter_http_json = powermeter["http_json"].to<JsonObject>();
serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, powermeter_http_json);
JsonObject powermeter_http_sml = powermeter["http_sml"].to<JsonObject>();
serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
powerlimiter["enabled"] = config.PowerLimiter.Enabled;
@ -184,6 +231,7 @@ bool ConfigurationClass::write()
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;
@ -209,7 +257,10 @@ bool ConfigurationClass::write()
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttSocTopic;
battery["mqtt_json_path"] = config.Battery.MqttSocJsonPath;
battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
battery["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath;
battery["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit;
JsonObject huawei = doc["huawei"].to<JsonObject>();
huawei["enabled"] = config.Huawei.Enabled;
@ -239,6 +290,69 @@ bool ConfigurationClass::write()
return true;
}
void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target)
{
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.
if (source_http_config.isNull()) { source_http_config = source; }
strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url));
target.AuthType = source_http_config["auth_type"] | HttpRequestConfig::Auth::None;
strlcpy(target.Username, source_http_config["username"] | "", sizeof(target.Username));
strlcpy(target.Password, source_http_config["password"] | "", sizeof(target.Password));
strlcpy(target.HeaderKey, source_http_config["header_key"] | "", sizeof(target.HeaderKey));
strlcpy(target.HeaderValue, source_http_config["header_value"] | "", sizeof(target.HeaderValue));
target.Timeout = source_http_config["timeout"] | HTTP_REQUEST_TIMEOUT_MS;
}
void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target)
{
for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) {
PowerMeterMqttValue& t = target.Values[i];
JsonObject s = source["values"][i];
strlcpy(t.Topic, s["topic"] | "", sizeof(t.Topic));
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
t.PowerUnit = s["unit"] | PowerMeterMqttValue::Unit::Watts;
t.SignInverted = s["sign_inverted"] | false;
}
}
void ConfigurationClass::deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target)
{
target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL;
target.Address = source["address"] | POWERMETER_SDMADDRESS;
}
void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target)
{
target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL;
target.IndividualRequests = source["individual_requests"] | false;
JsonArray values = source["values"].as<JsonArray>();
for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
PowerMeterHttpJsonValue& t = target.Values[i];
JsonObject s = values[i];
deserializeHttpRequestConfig(s, t.HttpRequest);
t.Enabled = s["enabled"] | false;
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts;
t.SignInverted = s["sign_inverted"] | false;
}
target.Values[0].Enabled = true;
}
void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target)
{
target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL;
deserializeHttpRequestConfig(source, target.HttpRequest);
}
bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
@ -318,6 +432,7 @@ bool ConfigurationClass::read()
config.Mqtt.VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING;
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
strlcpy(config.Mqtt.ClientId, mqtt["clientid"] | NetworkSettings.getApName().c_str(), sizeof(config.Mqtt.ClientId));
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password));
strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic));
@ -390,6 +505,7 @@ bool ConfigurationClass::read()
config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD;
config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false;
config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false;
config.Inverter[i].ClearEventlogOnMidnight = inv["clear_eventlog"] | false;
config.Inverter[i].YieldDayCorrection = inv["yieldday_correction"] | false;
JsonArray channel = inv["channel"];
@ -408,30 +524,51 @@ bool ConfigurationClass::read()
JsonObject powermeter = doc["powermeter"];
config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED;
config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL;
config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE;
strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1));
strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2));
strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3));
config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false;
JsonArray powermeter_http_phases = powermeter["http_phases"];
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt);
config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url));
config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None;
strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username));
strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue));
config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath));
config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts;
config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false;
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 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));
strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic));
strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic));
}
deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm);
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025.
if (!powermeter["sdmaddress"].isNull()) {
config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"];
}
JsonObject powermeter_http_json = powermeter["http_json"];
deserializePowerMeterHttpJsonConfig(powermeter_http_json, config.PowerMeter.HttpJson);
JsonObject powermeter_sml = powermeter["http_sml"];
deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml);
// process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025.
if (!powermeter["http_phases"].isNull()) {
auto& target = config.PowerMeter.HttpJson;
for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
PowerMeterHttpJsonValue& t = target.Values[i];
JsonObject s = powermeter["http_phases"][i];
deserializeHttpRequestConfig(s, t.HttpRequest);
t.Enabled = s["enabled"] | false;
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts;
t.SignInverted = s["sign_inverted"] | false;
}
target.IndividualRequests = powermeter["http_individual_requests"] | false;
}
JsonObject powerlimiter = doc["powerlimiter"];
@ -444,6 +581,7 @@ bool ConfigurationClass::read()
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;
@ -469,7 +607,10 @@ bool ConfigurationClass::read()
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic));
strlcpy(config.Battery.MqttSocJsonPath, battery["mqtt_json_path"] | "", sizeof(config.Battery.MqttSocJsonPath));
strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
strlcpy(config.Battery.MqttVoltageJsonPath, battery["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath));
config.Battery.MqttVoltageUnit = battery["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;

View File

@ -294,7 +294,7 @@ void DisplayGraphicClass::loop()
_display->drawBox(0, y, _display->getDisplayWidth(), lineHeight);
_display->setDrawColor(1);
auto acPower = PowerMeter.getPowerTotal(false);
auto acPower = PowerMeter.getPowerTotal();
if (acPower > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
} else {

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