Prepare Release 2024-08-18
merge development into master
This commit is contained in:
commit
5a6fe9d174
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -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:
|
||||
|
||||
@ -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": [
|
||||
|
||||
28
include/BatteryCanReceiver.h
Normal file
28
include/BatteryCanReceiver.h
Normal 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";
|
||||
};
|
||||
@ -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 { }
|
||||
};
|
||||
|
||||
@ -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
77
include/HttpGetter.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
#include "Battery.h"
|
||||
#include "JkBmsSerialMessage.h"
|
||||
#include "JkBmsDummy.h"
|
||||
|
||||
//#define JKBMS_DUMMY_SERIAL
|
||||
|
||||
|
||||
196
include/JkBmsDummy.h
Normal file
196
include/JkBmsDummy.h
Normal 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 */
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ public:
|
||||
void unsubscribe(const String& topic);
|
||||
|
||||
String getPrefix() const;
|
||||
String getClientId();
|
||||
|
||||
private:
|
||||
void NetworkEvent(network_event event);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
53
include/PowerMeterHttpJson.h
Normal file
53
include/PowerMeterHttpJson.h
Normal 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;
|
||||
};
|
||||
45
include/PowerMeterHttpSml.h
Normal file
45
include/PowerMeterHttpSml.h
Normal 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
37
include/PowerMeterMqtt.h
Normal 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;
|
||||
};
|
||||
51
include/PowerMeterProvider.h
Normal file
51
include/PowerMeterProvider.h
Normal 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;
|
||||
};
|
||||
60
include/PowerMeterSerialSdm.h
Normal file
60
include/PowerMeterSerialSdm.h
Normal 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;
|
||||
};
|
||||
44
include/PowerMeterSerialSml.h
Normal file
44
include/PowerMeterSerialSml.h
Normal 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
69
include/PowerMeterSml.h
Normal 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"}
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
@ -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>();
|
||||
};
|
||||
|
||||
19
include/PytesCanReceiver.h
Normal file
19
include/PytesCanReceiver.h
Normal 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>();
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -60,6 +60,7 @@ enum WebApiError {
|
||||
MqttHassTopicLength,
|
||||
MqttHassTopicCharacter,
|
||||
MqttLwtQos,
|
||||
MqttClientIdLength,
|
||||
|
||||
NetworkBase = 8000,
|
||||
NetworkIpInvalid,
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
51
lib/CpuTemperature/src/CpuTemperature.cpp
Normal file
51
lib/CpuTemperature/src/CpuTemperature.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
14
lib/CpuTemperature/src/CpuTemperature.h
Normal file
14
lib/CpuTemperature/src/CpuTemperature.h
Normal 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;
|
||||
@ -141,6 +141,9 @@ void HoymilesClass::loop()
|
||||
if (inv->getZeroYieldDayOnMidnight()) {
|
||||
inv->Statistics()->zeroDailyData();
|
||||
}
|
||||
if (inv->getClearEventlogOnMidnight()) {
|
||||
inv->EventLog()->clearBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
lastWeekDay = currentWeekDay;
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -1,6 +1,26 @@
|
||||
// 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"
|
||||
@ -8,15 +28,15 @@
|
||||
|
||||
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", "", "" },
|
||||
|
||||
|
||||
@ -1,6 +1,31 @@
|
||||
// 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"
|
||||
@ -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
|
||||
|
||||
@ -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) },
|
||||
|
||||
@ -1,6 +1,20 @@
|
||||
// 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"
|
||||
|
||||
@ -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 ¤tByte)
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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 )
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
6
partitions_custom_8mb.csv
Normal file
6
partitions_custom_8mb.csv
Normal 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,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
|
||||
@ -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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
175
src/BatteryCanReceiver.cpp
Normal 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;
|
||||
}
|
||||
@ -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,9 +298,16 @@ 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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
236
src/HttpGetter.cpp
Normal 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 });
|
||||
}
|
||||
@ -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;
|
||||
@ -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());
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user