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:
@ -66,3 +73,16 @@ body:
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
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:
@ -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:

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": [

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

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

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

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

@ -23,7 +23,9 @@ private:
VoltageStartThreshold,
VoltageStopThreshold,
FullSolarPassThroughStartVoltage,
FullSolarPassThroughStopVoltage
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);

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);

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

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

@ -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,12 +12,12 @@ 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;

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)) {

View File

@ -5,9 +5,9 @@
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);

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)

View File

@ -5,12 +5,12 @@
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,14 +259,14 @@ 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;
}

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;

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")
with open(filename, "rb") as fp:
if fp.read() == content:
mustUpdate = False
fp.close()
except:
pass
if mustUpdate:
fp = open(filename, "wb")
with open(filename, "wb") as fp:
fp.write(content)
fp.close()
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,6 +154,7 @@ build_flags = ${env.build_flags}
[env:d1_mini_esp32]
board = wemos_d1_mini32
board_build.partitions = partitions_custom_4mb.csv
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=19
@ -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()));
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 {

236
src/HttpGetter.cpp Normal file
View File

@ -0,0 +1,236 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "HttpGetter.h"
#include <WiFiClientSecure.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <ESPmDNS.h>
template<typename... Args>
void HttpGetter::logError(char const* format, Args... args) {
snprintf(_errBuffer, sizeof(_errBuffer), format, args...);
}
bool HttpGetter::init()
{
String url(_config.Url);
int index = url.indexOf(':');
if (index < 0) {
logError("failed to parse URL protocol: no colon in URL");
return false;
}
String protocol = url.substring(0, index);
if (protocol != "http" && protocol != "https") {
logError("failed to parse URL protocol: '%s' is neither 'http' nor 'https'", protocol.c_str());
return false;
}
_useHttps = (protocol == "https");
// initialize port to default values for http or https.
// port will be overwritten below in case port is explicitly defined
_port = _useHttps ? 443 : 80;
String slashes = url.substring(index + 1, index + 3);
if (slashes != "//") {
logError("expected two forward slashes after first colon in URL");
return false;
}
_uri = url.substring(index + 3); // without protocol identifier
index = _uri.indexOf('/');
if (index == -1) {
index = _uri.length();
_uri += '/';
}
_host = _uri.substring(0, index);
_uri.remove(0, index); // remove host part
index = _host.indexOf('@');
if (index >= 0) {
// basic authentication is only supported through setting username
// and password using the respective inputs, not embedded into the URL.
// to avoid regressions, we remove username and password from the host
// part of the URL.
_host.remove(0, index + 1); // remove auth part including @
}
// get port
index = _host.indexOf(':');
if (index >= 0) {
_host = _host.substring(0, index); // up until colon
_port = _host.substring(index + 1).toInt(); // after colon
}
if (_useHttps) {
auto secureWifiClient = std::make_shared<WiFiClientSecure>();
secureWifiClient->setInsecure();
_spWiFiClient = std::move(secureWifiClient);
} else {
_spWiFiClient = std::make_shared<WiFiClient>();
}
return true;
}
HttpRequestResult HttpGetter::performGetRequest()
{
// hostByName in WiFiGeneric fails to resolve local names. issue described at
// https://github.com/espressif/arduino-esp32/issues/3822 and in analyzed in
// depth at https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
// in conclusion: we cannot rely on _upHttpClient->begin(*wifiClient, url) to resolve
// IP adresses. have to do it manually.
IPAddress ipaddr((uint32_t)0);
if (!ipaddr.fromString(_host)) {
// host is not an IP address, so try to resolve the name to an address.
// first try locally via mDNS, then via DNS. WiFiGeneric::hostByName()
// will spam the console if done the other way around.
ipaddr = INADDR_NONE;
if (Configuration.get().Mdns.Enabled) {
ipaddr = MDNS.queryHost(_host); // INADDR_NONE if failed
}
if (ipaddr == INADDR_NONE && !WiFiGenericClass::hostByName(_host.c_str(), ipaddr)) {
logError("failed to resolve host '%s' via DNS", _host.c_str());
return { false };
}
}
auto upTmpHttpClient = std::make_unique<HTTPClient>();
// use HTTP1.0 to avoid problems with chunked transfer encoding when the
// stream is later used to read the server's response.
upTmpHttpClient->useHTTP10(true);
if (!upTmpHttpClient->begin(*_spWiFiClient, ipaddr.toString(), _port, _uri, _useHttps)) {
logError("HTTP client begin() failed for %s://%s",
(_useHttps ? "https" : "http"), _host.c_str());
return { false };
}
upTmpHttpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
upTmpHttpClient->setUserAgent("OpenDTU-OnBattery");
upTmpHttpClient->setConnectTimeout(_config.Timeout);
upTmpHttpClient->setTimeout(_config.Timeout);
for (auto const& h : _additionalHeaders) {
upTmpHttpClient->addHeader(h.first.c_str(), h.second.c_str());
}
if (strlen(_config.HeaderKey) > 0) {
upTmpHttpClient->addHeader(_config.HeaderKey, _config.HeaderValue);
}
using Auth_t = HttpRequestConfig::Auth;
switch (_config.AuthType) {
case Auth_t::None:
break;
case Auth_t::Basic: {
String credentials = String(_config.Username) + ":" + _config.Password;
String authorization = "Basic " + base64::encode(credentials);
upTmpHttpClient->addHeader("Authorization", authorization);
break;
}
case Auth_t::Digest: {
const char *headers[1] = {"WWW-Authenticate"};
upTmpHttpClient->collectHeaders(headers, 1);
break;
}
}
int httpCode = upTmpHttpClient->GET();
if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) {
if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) {
logError("Cannot perform digest authentication as server did "
"not send a WWW-Authenticate header");
return { false };
}
String authReq = upTmpHttpClient->header("WWW-Authenticate");
String authorization = getAuthDigest(authReq, 1);
upTmpHttpClient->addHeader("Authorization", authorization);
httpCode = upTmpHttpClient->GET();
}
if (httpCode <= 0) {
logError("HTTP Error: %s", upTmpHttpClient->errorToString(httpCode).c_str());
return { false };
}
if (httpCode != HTTP_CODE_OK) {
logError("Bad HTTP code: %d", httpCode);
return { false };
}
return { true, std::move(upTmpHttpClient), _spWiFiClient };
}
static String sha256(const String& data) {
uint8_t hash[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // select SHA256
mbedtls_sha256_update(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
char res[sizeof(hash) * 2 + 1];
for (int i = 0; i < sizeof(hash); i++) {
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
}
return res;
}
static String extractParam(String const& authReq, String const& param, char delimiter) {
auto begin = authReq.indexOf(param);
if (begin == -1) { return ""; }
auto end = authReq.indexOf(delimiter, begin + param.length());
return authReq.substring(begin + param.length(), end);
}
static String getcNonce(int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
String s = "";
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
return s;
}
String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) {
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
// sha256 of the user:realm:password
String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password);
// sha256 of method:uri
String ha2 = sha256("GET:" + _uri);
// sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) +
":" + cNonce + ":" + "auth" + ":" + ha2);
// Final authorization String
return String("Digest username=\"") + _config.Username +
"\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" +
_uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc +
", qop=auth, response=\"" + response + "\", algorithm=SHA-256";
}
void HttpGetter::addHeader(char const* key, char const* value)
{
_additionalHeaders.push_back({ key, value });
}

View File

@ -1,395 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <memory>
#include <ESPmDNS.h>
void HttpPowerMeterClass::init()
{
}
float HttpPowerMeterClass::getPower(int8_t phase)
{
if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; }
return power[phase - 1];
}
bool HttpPowerMeterClass::updateValues()
{
auto const& config = Configuration.get();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
auto const& phaseConfig = config.PowerMeter.Http_Phase[i];
if (!phaseConfig.Enabled) {
power[i] = 0.0;
continue;
}
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (!queryPhase(i, phaseConfig)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false;
}
continue;
}
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false;
}
}
return true;
}
bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config)
{
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
//and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
//in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses.
//have to do it manually here. Feels Hacky...
String protocol;
String host;
String uri;
String base64Authorization;
uint16_t port;
extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization);
IPAddress ipaddr((uint32_t)0);
//first check if "host" is already an IP adress
if (!ipaddr.fromString(host))
{
//"host"" is not an IP address so try to resolve the IP adress
//first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around.
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str());
//ensure we try resolving via DNS even if mDNS is disabled
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
else
{
ipaddr = MDNS.queryHost(host);
if (ipaddr == INADDR_NONE){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str());
//when we cannot find local server via mDNS, try resolving via DNS
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
}
}
// secureWifiClient MUST be created before HTTPClient
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
std::unique_ptr<WiFiClient> wifiClient;
bool https = protocol == "https";
if (https) {
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
secureWifiClient->setInsecure();
wifiClient = std::move(secureWifiClient);
} else {
wifiClient = std::make_unique<WiFiClient>();
}
return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config);
}
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config)
{
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str());
return false;
}
prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue);
if (config.AuthType == Auth_t::Digest) {
const char *headers[1] = {"WWW-Authenticate"};
httpClient.collectHeaders(headers, 1);
} else if (config.AuthType == Auth_t::Basic) {
String authString = config.Username;
authString += ":";
authString += config.Password;
String auth = "Basic ";
auth.concat(base64::encode(authString));
httpClient.addHeader("Authorization", auth);
}
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) {
// Handle authentication challenge
if (httpClient.hasHeader("WWW-Authenticate")) {
String authReq = httpClient.header("WWW-Authenticate");
String authorization = getDigestAuth(authReq, String(config.Username), String(config.Password), "GET", String(uri), 1);
httpClient.end();
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str());
return false;
}
prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue);
httpClient.addHeader("Authorization", authorization);
httpCode = httpClient.GET();
}
}
if (httpCode <= 0) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
return false;
}
if (httpCode != HTTP_CODE_OK) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
return false;
}
httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly
httpClient.end();
// TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it
// will be called twice for each phase when doing separate requests.
return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted);
}
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
int _begin = authReq.indexOf(param);
if (_begin == -1) { return ""; }
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}
String HttpPowerMeterClass::getcNonce(const int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
String s = "";
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
return s;
}
String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
//sha256 of the user:realm:password
String ha1 = sha256(username + ":" + realm + ":" + password);
//sha256 of method:uri
String ha2 = sha256(method + ":" + uri);
//sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2);
//Final authorization String;
String authorization = "Digest username=\"";
authorization += username;
authorization += "\", realm=\"";
authorization += realm;
authorization += "\", nonce=\"";
authorization += nonce;
authorization += "\", uri=\"";
authorization += uri;
authorization += "\", cnonce=\"";
authorization += cNonce;
authorization += "\", nc=";
authorization += String(nc);
authorization += ", qop=auth, response=\"";
authorization += response;
authorization += "\", algorithm=SHA-256";
return authorization;
}
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted)
{
JsonDocument root;
const DeserializationError error = deserializeJson(root, httpResponse);
if (error) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Unable to parse server response as JSON"));
return false;
}
constexpr char delimiter = '/';
int start = 0;
int end = jsonPath.indexOf(delimiter);
auto value = root.as<JsonVariantConst>();
auto getNext = [this, &value, &jsonPath, &start](String const& key) -> bool {
// handle double forward slashes and paths starting or ending with a slash
if (key.isEmpty()) { return true; }
if (key[0] == '[' && key[key.length() - 1] == ']') {
if (!value.is<JsonArrayConst>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Cannot access non-array JSON node "
"using array index '%s' (JSON path '%s', position %i)"),
key.c_str(), jsonPath.c_str(), start);
return false;
}
auto idx = key.substring(1, key.length() - 1).toInt();
value = value[idx];
if (value.isNull()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Unable to access JSON array "
"index %li (JSON path '%s', position %i)"),
idx, jsonPath.c_str(), start);
return false;
}
return true;
}
value = value[key];
if (value.isNull()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Unable to access JSON key "
"'%s' (JSON path '%s', position %i)"),
key.c_str(), jsonPath.c_str(), start);
return false;
}
return true;
};
// NOTE: "Because ArduinoJson implements the Null Object Pattern, it is
// always safe to read the object: if the key doesn't exist, it returns an
// empty value."
while (end != -1) {
if (!getNext(jsonPath.substring(start, end))) { return false; }
start = end + 1;
end = jsonPath.indexOf(delimiter, start);
}
if (!getNext(jsonPath.substring(start))) { return false; }
if (!value.is<float>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] not a float: '%s'"),
value.as<String>().c_str());
return false;
}
// this value is supposed to be in Watts and positive if energy is consumed.
power[phase] = value.as<float>();
switch (unit) {
case Unit_t::MilliWatts:
power[phase] /= 1000;
break;
case Unit_t::KiloWatts:
power[phase] *= 1000;
break;
default:
break;
}
if (signInverted) { power[phase] *= -1; }
return true;
}
//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250
bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization)
{
// check for : (http: or https:
int index = url.indexOf(':');
if(index < 0) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("failed to parse protocol"));
return false;
}
_protocol = url.substring(0, index);
//initialize port to default values for http or https.
//port will be overwritten below in case port is explicitly defined
_port = (_protocol == "https" ? 443 : 80);
url.remove(0, (index + 3)); // remove http:// or https://
index = url.indexOf('/');
if (index == -1) {
index = url.length();
url += '/';
}
String host = url.substring(0, index);
url.remove(0, index); // remove host part
// get Authorization
index = host.indexOf('@');
if(index >= 0) {
// auth info
String auth = host.substring(0, index);
host.remove(0, index + 1); // remove auth part including @
_base64Authorization = base64::encode(auth);
}
// get port
index = host.indexOf(':');
String the_host;
if(index >= 0) {
the_host = host.substring(0, index); // hostname
host.remove(0, (index + 1)); // remove hostname + :
_port = host.toInt(); // get port
} else {
the_host = host;
}
_host = the_host;
_uri = url;
return true;
}
String HttpPowerMeterClass::sha256(const String& data) {
uint8_t hash[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // select SHA256
mbedtls_sha256_update(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
char res[sizeof(hash) * 2 + 1];
for (int i = 0; i < sizeof(hash); i++) {
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
}
return res;
}
void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) {
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery");
httpClient.setConnectTimeout(timeout);
httpClient.setTimeout(timeout);
httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json");
if (strlen(httpHeader) > 0) {
httpClient.addHeader(httpHeader, httpValue);
}
}
HttpPowerMeterClass HttpPowerMeter;

View File

@ -371,12 +371,12 @@ void HuaweiCanClass::loop()
}
}
if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis &&
if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis &&
_autoPowerEnabledCounter > 0) {
// We have received a new PowerMeter value. Also we're _autoPowerEnabled
// So we're good to calculate a new limit
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate();
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate();
// Calculate new power limit
float newPowerLimit = -1 * round(PowerMeter.getPowerTotal());

View File

@ -85,6 +85,7 @@ void InverterSettingsClass::init(Scheduler& scheduler)
inv->setReachableThreshold(config.Inverter[i].ReachableThreshold);
inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable);
inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight);
inv->setClearEventlogOnMidnight(config.Inverter[i].ClearEventlogOnMidnight);
inv->Statistics()->setYieldDayCorrection(config.Inverter[i].YieldDayCorrection);
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower);

View File

@ -10,195 +10,6 @@
namespace JkBms {
#ifdef JKBMS_DUMMY_SERIAL
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;
};
#endif
bool Controller::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;

View File

@ -27,7 +27,6 @@ void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
{
// on ESP32-S3, Serial.flush() blocks until a serial console is attached.
// operator bool() of HWCDC returns false if the device is not attached to
// a USB host. in general it makes sense to skip writing entirely if the
// default serial port is not ready.
@ -37,7 +36,6 @@ void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
while (written < m.size()) {
written += Serial.write(m.data() + written, m.size() - written);
}
Serial.flush();
}
size_t MessageOutputClass::write(uint8_t c)

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