Merge development into master to prepare release 2024.03.23
Prepare release 2024.03.23
This commit is contained in:
commit
0169b29cfd
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -5,7 +5,6 @@
|
|||||||
"DavidAnson.vscode-markdownlint",
|
"DavidAnson.vscode-markdownlint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
"Vue.vscode-typescript-vue-plugin",
|
|
||||||
"platformio.platformio-ide"
|
"platformio.platformio-ide"
|
||||||
],
|
],
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
|
|||||||
@ -29,10 +29,6 @@ Summer 2022 I bought my Victron MPPT battery charger, and didn't like the idea t
|
|||||||
|
|
||||||
This project is still under development and adds following features:
|
This project is still under development and adds following features:
|
||||||
|
|
||||||
> **Warning**
|
|
||||||
>
|
|
||||||
> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view.
|
|
||||||
|
|
||||||
* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded directly from [Victron's website](https://www.victronenergy.com/support-and-downloads/technical-information).
|
* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded directly from [Victron's website](https://www.victronenergy.com/support-and-downloads/technical-information).
|
||||||
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
|
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
|
||||||
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
|
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
|
||||||
|
|||||||
@ -49,9 +49,7 @@ The SN65HVD230 CAN bus transceiver is used to interface with the Pylontech batte
|
|||||||
|
|
||||||
### MCP2515 CAN bus module
|
### MCP2515 CAN bus module
|
||||||
|
|
||||||
The MCP2515 CAN bus module consists of a CAN bus controller and a CAN bus transceiver and is used to interface with the Huawei AC charger. This CAN bus operates at 125kbit/s. The module is connected via SPI and currently requires a separate SPI bus. If you want to use the Huawei AC charger make sure to get an ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future. Please note: Using the Huawei AC charger in combination with the CMT2300A radio board is not supported at the moment.
|
See [Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details.
|
||||||
|
|
||||||
MCP2515 CAN bus modules that are widely available are designed for 5V supply voltage. To make them work with 3.3V / the ESP32 a modification is required. [This modification is described here.](https://forums.raspberrypi.com/viewtopic.php?t=141052)
|
|
||||||
|
|
||||||
### Relay module
|
### Relay module
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
@ -9,28 +8,28 @@
|
|||||||
#include "BatteryStats.h"
|
#include "BatteryStats.h"
|
||||||
|
|
||||||
class BatteryProvider {
|
class BatteryProvider {
|
||||||
public:
|
public:
|
||||||
// returns true if the provider is ready for use, false otherwise
|
// returns true if the provider is ready for use, false otherwise
|
||||||
virtual bool init(bool verboseLogging) = 0;
|
virtual bool init(bool verboseLogging) = 0;
|
||||||
|
virtual void deinit() = 0;
|
||||||
virtual void deinit() = 0;
|
virtual void loop() = 0;
|
||||||
virtual void loop() = 0;
|
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
|
||||||
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
|
virtual bool usesHwPort2() const { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class BatteryClass {
|
class BatteryClass {
|
||||||
public:
|
public:
|
||||||
void init(Scheduler&);
|
void init(Scheduler&);
|
||||||
void updateSettings();
|
void updateSettings();
|
||||||
|
|
||||||
std::shared_ptr<BatteryStats const> getStats() const;
|
std::shared_ptr<BatteryStats const> getStats() const;
|
||||||
private:
|
|
||||||
void loop();
|
|
||||||
|
|
||||||
Task _loopTask;
|
private:
|
||||||
|
void loop();
|
||||||
|
|
||||||
mutable std::mutex _mutex;
|
Task _loopTask;
|
||||||
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
|
mutable std::mutex _mutex;
|
||||||
|
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern BatteryClass Battery;
|
extern BatteryClass Battery;
|
||||||
|
|||||||
@ -35,18 +35,22 @@ class BatteryStats {
|
|||||||
bool isSoCValid() const { return _lastUpdateSoC > 0; }
|
bool isSoCValid() const { return _lastUpdateSoC > 0; }
|
||||||
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }
|
bool isVoltageValid() const { return _lastUpdateVoltage > 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 needsCharging() const { return false; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void mqttPublish() const;
|
virtual void mqttPublish() const;
|
||||||
|
|
||||||
void setSoC(float soc, uint8_t precision, uint32_t timestamp) {
|
void setSoC(float soc, uint8_t precision, uint32_t timestamp) {
|
||||||
_soc = soc;
|
_soc = soc;
|
||||||
_socPrecision = precision;
|
_socPrecision = precision;
|
||||||
_lastUpdateSoC = timestamp;
|
_lastUpdateSoC = _lastUpdate = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVoltage(float voltage, uint32_t timestamp) {
|
void setVoltage(float voltage, uint32_t timestamp) {
|
||||||
_voltage = voltage;
|
_voltage = voltage;
|
||||||
_lastUpdateVoltage = timestamp;
|
_lastUpdateVoltage = _lastUpdate = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _manufacturer = "unknown";
|
String _manufacturer = "unknown";
|
||||||
@ -67,6 +71,7 @@ class PylontechBatteryStats : public BatteryStats {
|
|||||||
public:
|
public:
|
||||||
void getLiveViewData(JsonVariant& root) const final;
|
void getLiveViewData(JsonVariant& root) const final;
|
||||||
void mqttPublish() const final;
|
void mqttPublish() const final;
|
||||||
|
bool needsCharging() const final { return _chargeImmediately; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
|
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
|
||||||
@ -147,6 +152,9 @@ class VictronSmartShuntStats : public BatteryStats {
|
|||||||
float _chargedEnergy;
|
float _chargedEnergy;
|
||||||
float _dischargedEnergy;
|
float _dischargedEnergy;
|
||||||
String _modelName;
|
String _modelName;
|
||||||
|
int32_t _instantaneousPower;
|
||||||
|
float _consumedAmpHours;
|
||||||
|
int32_t _lastFullCharge;
|
||||||
|
|
||||||
bool _alarmLowVoltage;
|
bool _alarmLowVoltage;
|
||||||
bool _alarmHighVoltage;
|
bool _alarmHighVoltage;
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
#define MQTT_MAX_CERT_STRLEN 2560
|
#define MQTT_MAX_CERT_STRLEN 2560
|
||||||
|
|
||||||
#define INV_MAX_NAME_STRLEN 31
|
#define INV_MAX_NAME_STRLEN 31
|
||||||
#define INV_MAX_COUNT 5
|
#define INV_MAX_COUNT 10
|
||||||
#define INV_MAX_CHAN_COUNT 6
|
#define INV_MAX_CHAN_COUNT 6
|
||||||
|
|
||||||
#define CHAN_MAX_NAME_STRLEN 31
|
#define CHAN_MAX_NAME_STRLEN 31
|
||||||
@ -204,10 +204,11 @@ struct CONFIG_T {
|
|||||||
bool VerboseLogging;
|
bool VerboseLogging;
|
||||||
bool SolarPassThroughEnabled;
|
bool SolarPassThroughEnabled;
|
||||||
uint8_t SolarPassThroughLosses;
|
uint8_t SolarPassThroughLosses;
|
||||||
uint8_t BatteryDrainStategy;
|
bool BatteryAlwaysUseAtNight;
|
||||||
uint32_t Interval;
|
uint32_t Interval;
|
||||||
bool IsInverterBehindPowerMeter;
|
bool IsInverterBehindPowerMeter;
|
||||||
uint8_t InverterId;
|
bool IsInverterSolarPowered;
|
||||||
|
uint64_t InverterId;
|
||||||
uint8_t InverterChannelId;
|
uint8_t InverterChannelId;
|
||||||
int32_t TargetPowerConsumption;
|
int32_t TargetPowerConsumption;
|
||||||
int32_t TargetPowerConsumptionHysteresis;
|
int32_t TargetPowerConsumptionHysteresis;
|
||||||
@ -260,6 +261,7 @@ public:
|
|||||||
|
|
||||||
INVERTER_CONFIG_T* getFreeInverterSlot();
|
INVERTER_CONFIG_T* getFreeInverterSlot();
|
||||||
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
|
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
|
||||||
|
void deleteInverterById(const uint8_t id);
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ConfigurationClass Configuration;
|
extern ConfigurationClass Configuration;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ private:
|
|||||||
String extractParam(String& authReq, const String& param, const char delimit);
|
String extractParam(String& authReq, const String& param, const char delimit);
|
||||||
String getcNonce(const int len);
|
String getcNonce(const int len);
|
||||||
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
|
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
|
||||||
bool tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath);
|
bool tryGetFloatValueForPhase(int phase, const char* jsonPath);
|
||||||
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
|
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
|
||||||
String sha256(const String& data);
|
String sha256(const String& data);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class Controller : public BatteryProvider {
|
|||||||
void deinit() final;
|
void deinit() final;
|
||||||
void loop() final;
|
void loop() final;
|
||||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||||
|
bool usesHwPort2() const final { return true; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Status : unsigned {
|
enum class Status : unsigned {
|
||||||
|
|||||||
@ -5,23 +5,23 @@
|
|||||||
#include <espMqttClient.h>
|
#include <espMqttClient.h>
|
||||||
|
|
||||||
class MqttBattery : public BatteryProvider {
|
class MqttBattery : public BatteryProvider {
|
||||||
public:
|
public:
|
||||||
MqttBattery() = default;
|
MqttBattery() = default;
|
||||||
|
|
||||||
bool init(bool verboseLogging) final;
|
bool init(bool verboseLogging) final;
|
||||||
void deinit() final;
|
void deinit() final;
|
||||||
void loop() final { return; } // this class is event-driven
|
void loop() final { return; } // this class is event-driven
|
||||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool _verboseLogging = false;
|
bool _verboseLogging = false;
|
||||||
String _socTopic;
|
String _socTopic;
|
||||||
String _voltageTopic;
|
String _voltageTopic;
|
||||||
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
||||||
|
|
||||||
std::optional<float> getFloat(std::string const& src, char const* topic);
|
std::optional<float> getFloat(std::string const& src, char const* topic);
|
||||||
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
|
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);
|
||||||
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,19 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void loop();
|
void loop();
|
||||||
void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
|
||||||
|
enum class MqttPowerLimiterCommand : unsigned {
|
||||||
|
Mode,
|
||||||
|
BatterySoCStartThreshold,
|
||||||
|
BatterySoCStopThreshold,
|
||||||
|
FullSolarPassthroughSoC,
|
||||||
|
VoltageStartThreshold,
|
||||||
|
VoltageStopThreshold,
|
||||||
|
FullSolarPassThroughStartVoltage,
|
||||||
|
FullSolarPassThroughStopVoltage
|
||||||
|
};
|
||||||
|
|
||||||
|
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||||
|
|
||||||
Task _loopTask;
|
Task _loopTask;
|
||||||
|
|
||||||
|
|||||||
26
include/MqttHandlePowerLimiterHass.h
Normal file
26
include/MqttHandlePowerLimiterHass.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class MqttHandlePowerLimiterHassClass {
|
||||||
|
public:
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
void publishConfig();
|
||||||
|
void forceUpdate();
|
||||||
|
|
||||||
|
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 publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic);
|
||||||
|
void createDeviceInfo(JsonObject& object);
|
||||||
|
|
||||||
|
Task _loopTask;
|
||||||
|
|
||||||
|
bool _wasConnected = false;
|
||||||
|
bool _updateForced = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
|
||||||
@ -4,6 +4,7 @@
|
|||||||
#include "VeDirectMpptController.h"
|
#include "VeDirectMpptController.h"
|
||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <map>
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
#ifndef VICTRON_PIN_RX
|
#ifndef VICTRON_PIN_RX
|
||||||
@ -20,7 +21,7 @@ public:
|
|||||||
void forceUpdate();
|
void forceUpdate();
|
||||||
private:
|
private:
|
||||||
void loop();
|
void loop();
|
||||||
VeDirectMpptController::veMpptStruct _kvFrame{};
|
std::map<std::string, VeDirectMpptController::veMpptStruct> _kvFrames;
|
||||||
|
|
||||||
Task _loopTask;
|
Task _loopTask;
|
||||||
|
|
||||||
@ -31,6 +32,9 @@ private:
|
|||||||
uint32_t _nextPublishFull = 1;
|
uint32_t _nextPublishFull = 1;
|
||||||
|
|
||||||
bool _PublishFull;
|
bool _PublishFull;
|
||||||
|
|
||||||
|
void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
|
||||||
|
VeDirectMpptController::veMpptStruct &frame) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern MqttHandleVedirectClass MqttHandleVedirect;
|
extern MqttHandleVedirectClass MqttHandleVedirect;
|
||||||
@ -14,9 +14,15 @@ public:
|
|||||||
private:
|
private:
|
||||||
void loop();
|
void loop();
|
||||||
void publish(const String& subtopic, const String& payload);
|
void publish(const String& subtopic, const String& payload);
|
||||||
void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off);
|
void publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL);
|
const char *payload_on, const char *payload_off,
|
||||||
void createDeviceInfo(JsonObject& object);
|
const VeDirectMpptController::spData_t &spMpptData);
|
||||||
|
void publishSensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
|
const char *deviceClass, const char *stateClass,
|
||||||
|
const char *unitOfMeasurement,
|
||||||
|
const VeDirectMpptController::spData_t &spMpptData);
|
||||||
|
void createDeviceInfo(JsonObject &object,
|
||||||
|
const VeDirectMpptController::spData_t &spMpptData);
|
||||||
|
|
||||||
Task _loopTask;
|
Task _loopTask;
|
||||||
|
|
||||||
|
|||||||
@ -38,8 +38,13 @@ struct PinMapping_t {
|
|||||||
uint8_t display_clk;
|
uint8_t display_clk;
|
||||||
uint8_t display_cs;
|
uint8_t display_cs;
|
||||||
uint8_t display_reset;
|
uint8_t display_reset;
|
||||||
|
int8_t led[PINMAPPING_LED_COUNT];
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
int8_t victron_tx;
|
int8_t victron_tx;
|
||||||
int8_t victron_rx;
|
int8_t victron_rx;
|
||||||
|
int8_t victron_tx2;
|
||||||
|
int8_t victron_rx2;
|
||||||
int8_t battery_rx;
|
int8_t battery_rx;
|
||||||
int8_t battery_rxen;
|
int8_t battery_rxen;
|
||||||
int8_t battery_tx;
|
int8_t battery_tx;
|
||||||
@ -50,7 +55,9 @@ struct PinMapping_t {
|
|||||||
int8_t huawei_irq;
|
int8_t huawei_irq;
|
||||||
int8_t huawei_cs;
|
int8_t huawei_cs;
|
||||||
int8_t huawei_power;
|
int8_t huawei_power;
|
||||||
int8_t led[PINMAPPING_LED_COUNT];
|
int8_t powermeter_rx;
|
||||||
|
int8_t powermeter_tx;
|
||||||
|
int8_t powermeter_dere;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PinMappingClass {
|
class PinMappingClass {
|
||||||
|
|||||||
@ -40,8 +40,11 @@ public:
|
|||||||
InverterPowerCmdPending,
|
InverterPowerCmdPending,
|
||||||
InverterDevInfoPending,
|
InverterDevInfoPending,
|
||||||
InverterStatsPending,
|
InverterStatsPending,
|
||||||
|
CalculatedLimitBelowMinLimit,
|
||||||
UnconditionalSolarPassthrough,
|
UnconditionalSolarPassthrough,
|
||||||
NoVeDirect,
|
NoVeDirect,
|
||||||
|
NoEnergy,
|
||||||
|
HuaweiPsu,
|
||||||
Settling,
|
Settling,
|
||||||
Stable,
|
Stable,
|
||||||
};
|
};
|
||||||
@ -91,10 +94,10 @@ private:
|
|||||||
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
|
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
|
||||||
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
|
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
|
||||||
bool canUseDirectSolarPower();
|
bool canUseDirectSolarPower();
|
||||||
int32_t calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool solarPowerEnabled, bool batteryDischargeEnabled);
|
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, bool batteryPower);
|
||||||
bool updateInverter();
|
bool updateInverter();
|
||||||
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
|
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
|
||||||
int32_t getSolarChargePower();
|
int32_t getSolarPower();
|
||||||
float getLoadCorrectedVoltage();
|
float getLoadCorrectedVoltage();
|
||||||
bool testThreshold(float socThreshold, float voltThreshold,
|
bool testThreshold(float socThreshold, float voltThreshold,
|
||||||
std::function<bool(float, float)> compare);
|
std::function<bool(float, float)> compare);
|
||||||
|
|||||||
@ -6,21 +6,11 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <list>
|
#include <list>
|
||||||
|
#include <mutex>
|
||||||
#include "SDM.h"
|
#include "SDM.h"
|
||||||
#include "sml.h"
|
#include "sml.h"
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <SoftwareSerial.h>
|
||||||
#ifndef SDM_RX_PIN
|
|
||||||
#define SDM_RX_PIN 13
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef SDM_TX_PIN
|
|
||||||
#define SDM_TX_PIN 32
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef SML_RX_PIN
|
|
||||||
#define SML_RX_PIN 35
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const unsigned char OBIS[6];
|
const unsigned char OBIS[6];
|
||||||
@ -30,12 +20,13 @@ typedef struct {
|
|||||||
|
|
||||||
class PowerMeterClass {
|
class PowerMeterClass {
|
||||||
public:
|
public:
|
||||||
enum SOURCE {
|
enum class Source : unsigned {
|
||||||
SOURCE_MQTT = 0,
|
MQTT = 0,
|
||||||
SOURCE_SDM1PH = 1,
|
SDM1PH = 1,
|
||||||
SOURCE_SDM3PH = 2,
|
SDM3PH = 2,
|
||||||
SOURCE_HTTP = 3,
|
HTTP = 3,
|
||||||
SOURCE_SML = 4
|
SML = 4,
|
||||||
|
SMAHM2 = 5
|
||||||
};
|
};
|
||||||
void init(Scheduler& scheduler);
|
void init(Scheduler& scheduler);
|
||||||
float getPowerTotal(bool forceUpdate = true);
|
float getPowerTotal(bool forceUpdate = true);
|
||||||
@ -48,7 +39,7 @@ private:
|
|||||||
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties,
|
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties,
|
||||||
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||||
|
|
||||||
Task _loopTask;
|
Task _loopTask;
|
||||||
|
|
||||||
bool _verboseLogging = true;
|
bool _verboseLogging = true;
|
||||||
uint32_t _lastPowerMeterCheck;
|
uint32_t _lastPowerMeterCheck;
|
||||||
@ -66,6 +57,11 @@ private:
|
|||||||
|
|
||||||
std::map<String, float*> _mqttSubscriptions;
|
std::map<String, float*> _mqttSubscriptions;
|
||||||
|
|
||||||
|
mutable std::mutex _mutex;
|
||||||
|
|
||||||
|
std::unique_ptr<SDM> _upSdm = nullptr;
|
||||||
|
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
|
||||||
|
|
||||||
void readPowerMeter();
|
void readPowerMeter();
|
||||||
|
|
||||||
bool smlReadLoop();
|
bool smlReadLoop();
|
||||||
|
|||||||
36
include/SMA_HM.h
Normal file
36
include/SMA_HM.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Holger-Steffen Stapf
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class SMA_HMClass {
|
||||||
|
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; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Soutput(int kanal, int index, int art, int tarif,
|
||||||
|
char const* name, float value, uint32_t timestamp);
|
||||||
|
|
||||||
|
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;
|
||||||
27
include/SerialPortManager.h
Normal file
27
include/SerialPortManager.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
class SerialPortManagerClass {
|
||||||
|
public:
|
||||||
|
bool allocateMpptPort(int port);
|
||||||
|
bool allocateBatteryPort(int port);
|
||||||
|
void invalidateBatteryPort();
|
||||||
|
void invalidateMpptPorts();
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum Owner {
|
||||||
|
BATTERY,
|
||||||
|
MPPT
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<uint8_t, Owner> allocatedPorts;
|
||||||
|
|
||||||
|
bool allocatePort(uint8_t port, Owner owner);
|
||||||
|
void invalidate(Owner owner);
|
||||||
|
|
||||||
|
static const char* print(Owner owner);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern SerialPortManagerClass SerialPortManager;
|
||||||
@ -11,5 +11,6 @@ public:
|
|||||||
static int getTimezoneOffset();
|
static int getTimezoneOffset();
|
||||||
static void restartDtu();
|
static void restartDtu();
|
||||||
static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
|
static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
|
||||||
|
static bool checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
|
||||||
static void removeAllFiles();
|
static void removeAllFiles();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "VeDirectMpptController.h"
|
#include "VeDirectMpptController.h"
|
||||||
|
#include "Configuration.h"
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
class VictronMpptClass {
|
class VictronMpptClass {
|
||||||
@ -16,12 +17,15 @@ public:
|
|||||||
void updateSettings();
|
void updateSettings();
|
||||||
|
|
||||||
bool isDataValid() const;
|
bool isDataValid() const;
|
||||||
|
bool isDataValid(size_t idx) const;
|
||||||
|
|
||||||
// returns the data age of all controllers,
|
// returns the data age of all controllers,
|
||||||
// i.e, the youngest data's age is returned.
|
// i.e, the youngest data's age is returned.
|
||||||
uint32_t getDataAgeMillis() const;
|
uint32_t getDataAgeMillis() const;
|
||||||
|
uint32_t getDataAgeMillis(size_t idx) const;
|
||||||
|
|
||||||
VeDirectMpptController::spData_t getData(size_t idx = 0) const;
|
size_t controllerAmount() const { return _controllers.size(); }
|
||||||
|
std::optional<VeDirectMpptController::spData_t> getData(size_t idx = 0) const;
|
||||||
|
|
||||||
// total output of all MPPT charge controllers in Watts
|
// total output of all MPPT charge controllers in Watts
|
||||||
int32_t getPowerOutputWatts() const;
|
int32_t getPowerOutputWatts() const;
|
||||||
@ -50,6 +54,8 @@ private:
|
|||||||
mutable std::mutex _mutex;
|
mutable std::mutex _mutex;
|
||||||
using controller_t = std::unique_ptr<VeDirectMpptController>;
|
using controller_t = std::unique_ptr<VeDirectMpptController>;
|
||||||
std::vector<controller_t> _controllers;
|
std::vector<controller_t> _controllers;
|
||||||
|
|
||||||
|
bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort);
|
||||||
};
|
};
|
||||||
|
|
||||||
extern VictronMpptClass VictronMppt;
|
extern VictronMpptClass VictronMppt;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ public:
|
|||||||
void deinit() final { }
|
void deinit() final { }
|
||||||
void loop() final;
|
void loop() final;
|
||||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||||
|
bool usesHwPort2() const final { return true; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t _lastUpdate = 0;
|
uint32_t _lastUpdate = 0;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void onStatus(AsyncWebServerRequest* request);
|
void onStatus(AsyncWebServerRequest* request);
|
||||||
|
void onMetaData(AsyncWebServerRequest* request);
|
||||||
void onAdminGet(AsyncWebServerRequest* request);
|
void onAdminGet(AsyncWebServerRequest* request);
|
||||||
void onAdminPost(AsyncWebServerRequest* request);
|
void onAdminPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ArduinoJson.h"
|
#include "ArduinoJson.h"
|
||||||
|
#include "Configuration.h"
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
#include <VeDirectMpptController.h>
|
#include <VeDirectMpptController.h>
|
||||||
@ -13,16 +14,18 @@ public:
|
|||||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void generateJsonResponse(JsonVariant& root);
|
void generateJsonResponse(JsonVariant& root, bool fullUpdate);
|
||||||
|
static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData);
|
||||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||||
|
bool hasUpdate(size_t idx);
|
||||||
|
|
||||||
AsyncWebServer* _server;
|
AsyncWebServer* _server;
|
||||||
AsyncWebSocket _ws;
|
AsyncWebSocket _ws;
|
||||||
|
|
||||||
uint32_t _lastWsPublish = 0;
|
uint32_t _lastFullPublish = 0;
|
||||||
uint32_t _dataAgeMillis = 0;
|
uint32_t _lastPublish = 0;
|
||||||
static constexpr uint16_t _responseSize = 1024 + 128;
|
uint16_t responseSize() const;
|
||||||
|
|
||||||
std::mutex _mutex;
|
std::mutex _mutex;
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,11 @@
|
|||||||
|
|
||||||
#define REACHABLE_THRESHOLD 2U
|
#define REACHABLE_THRESHOLD 2U
|
||||||
|
|
||||||
|
#define LED_BRIGHTNESS 100U
|
||||||
|
|
||||||
#define MAX_INVERTER_LIMIT 2250
|
#define MAX_INVERTER_LIMIT 2250
|
||||||
|
|
||||||
|
// values specific to downstream project OpenDTU-OnBattery start here:
|
||||||
#define VEDIRECT_ENABLED false
|
#define VEDIRECT_ENABLED false
|
||||||
#define VEDIRECT_VERBOSE_LOGGING false
|
#define VEDIRECT_VERBOSE_LOGGING false
|
||||||
#define VEDIRECT_UPDATESONLY true
|
#define VEDIRECT_UPDATESONLY true
|
||||||
@ -115,14 +119,14 @@
|
|||||||
#define POWERMETER_SDMBAUDRATE 9600
|
#define POWERMETER_SDMBAUDRATE 9600
|
||||||
#define POWERMETER_SDMADDRESS 1
|
#define POWERMETER_SDMADDRESS 1
|
||||||
|
|
||||||
|
|
||||||
#define POWERLIMITER_ENABLED false
|
#define POWERLIMITER_ENABLED false
|
||||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
|
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
|
||||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
|
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
|
||||||
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
|
#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false
|
||||||
#define POWERLIMITER_INTERVAL 10
|
#define POWERLIMITER_INTERVAL 10
|
||||||
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
||||||
#define POWERLIMITER_INVERTER_ID 0
|
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
|
||||||
|
#define POWERLIMITER_INVERTER_ID 0ULL
|
||||||
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
|
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
|
||||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
|
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
|
||||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
|
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
|
||||||
@ -152,57 +156,3 @@
|
|||||||
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
|
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
|
||||||
|
|
||||||
#define VERBOSE_LOGGING true
|
#define VERBOSE_LOGGING true
|
||||||
|
|
||||||
#define LED_BRIGHTNESS 100U
|
|
||||||
|
|
||||||
#define MAX_INVERTER_LIMIT 2250
|
|
||||||
#define VEDIRECT_ENABLED false
|
|
||||||
#define VEDIRECT_VERBOSE_LOGGING false
|
|
||||||
#define VEDIRECT_UPDATESONLY true
|
|
||||||
|
|
||||||
#define POWERMETER_ENABLED false
|
|
||||||
#define POWERMETER_INTERVAL 10
|
|
||||||
#define POWERMETER_SOURCE 2
|
|
||||||
#define POWERMETER_SDMBAUDRATE 9600
|
|
||||||
#define POWERMETER_SDMADDRESS 1
|
|
||||||
|
|
||||||
|
|
||||||
#define POWERLIMITER_ENABLED false
|
|
||||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
|
|
||||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
|
|
||||||
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
|
|
||||||
#define POWERLIMITER_INTERVAL 10
|
|
||||||
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
|
||||||
#define POWERLIMITER_INVERTER_ID 0
|
|
||||||
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
|
|
||||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
|
|
||||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
|
|
||||||
#define POWERLIMITER_LOWER_POWER_LIMIT 10
|
|
||||||
#define POWERLIMITER_UPPER_POWER_LIMIT 800
|
|
||||||
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
|
|
||||||
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
|
|
||||||
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
|
|
||||||
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
|
|
||||||
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
|
|
||||||
#define POWERLIMITER_RESTART_HOUR -1
|
|
||||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
|
|
||||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
|
|
||||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
|
|
||||||
|
|
||||||
#define BATTERY_ENABLED false
|
|
||||||
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
|
|
||||||
#define BATTERY_JKBMS_INTERFACE 0
|
|
||||||
#define BATTERY_JKBMS_POLLING_INTERVAL 5
|
|
||||||
|
|
||||||
#define HUAWEI_ENABLED false
|
|
||||||
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
|
|
||||||
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
|
|
||||||
#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0
|
|
||||||
#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150
|
|
||||||
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
|
|
||||||
|
|
||||||
#define VERBOSE_LOGGING true
|
|
||||||
|
|
||||||
#define LED_BRIGHTNESS 100U
|
|
||||||
|
|
||||||
#define MAX_INVERTER_LIMIT 2250
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022-2023 Thomas Basler and others
|
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||||
*/
|
*/
|
||||||
#include "Hoymiles.h"
|
#include "Hoymiles.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
#include "inverters/HERF_2CH.h"
|
||||||
|
#include "inverters/HERF_4CH.h"
|
||||||
#include "inverters/HMS_1CH.h"
|
#include "inverters/HMS_1CH.h"
|
||||||
#include "inverters/HMS_1CHv2.h"
|
#include "inverters/HMS_1CHv2.h"
|
||||||
#include "inverters/HMS_2CH.h"
|
#include "inverters/HMS_2CH.h"
|
||||||
@ -168,6 +170,10 @@ std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, c
|
|||||||
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
|
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
|
||||||
} else if (HM_1CH::isValidSerial(serial)) {
|
} else if (HM_1CH::isValidSerial(serial)) {
|
||||||
i = std::make_shared<HM_1CH>(_radioNrf.get(), serial);
|
i = std::make_shared<HM_1CH>(_radioNrf.get(), serial);
|
||||||
|
} else if (HERF_2CH::isValidSerial(serial)) {
|
||||||
|
i = std::make_shared<HERF_2CH>(_radioNrf.get(), serial);
|
||||||
|
} else if (HERF_4CH::isValidSerial(serial)) {
|
||||||
|
i = std::make_shared<HERF_4CH>(_radioNrf.get(), serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i) {
|
if (i) {
|
||||||
|
|||||||
62
lib/Hoymiles/src/inverters/HERF_2CH.cpp
Normal file
62
lib/Hoymiles/src/inverters/HERF_2CH.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "HERF_2CH.h"
|
||||||
|
|
||||||
|
static const byteAssign_t byteAssignment[] = {
|
||||||
|
{ TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 },
|
||||||
|
{ TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 },
|
||||||
|
{ TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 },
|
||||||
|
{ TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 },
|
||||||
|
{ TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 },
|
||||||
|
{ TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 },
|
||||||
|
|
||||||
|
{ TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 },
|
||||||
|
{ TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 },
|
||||||
|
{ TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 },
|
||||||
|
{ TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 },
|
||||||
|
{ TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 },
|
||||||
|
{ TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 },
|
||||||
|
|
||||||
|
{ TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 },
|
||||||
|
{ TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 },
|
||||||
|
{ TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 },
|
||||||
|
{ TYPE_AC, CH0, FLD_Q, UNIT_VAR, 32, 2, 10, false, 1 },
|
||||||
|
{ TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 },
|
||||||
|
{ TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 },
|
||||||
|
|
||||||
|
{ TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 },
|
||||||
|
{ TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 },
|
||||||
|
|
||||||
|
{ TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 },
|
||||||
|
{ TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 },
|
||||||
|
{ TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 },
|
||||||
|
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
|
||||||
|
};
|
||||||
|
|
||||||
|
HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial)
|
||||||
|
: HM_Abstract(radio, serial) {};
|
||||||
|
|
||||||
|
bool HERF_2CH::isValidSerial(const uint64_t serial)
|
||||||
|
{
|
||||||
|
// serial >= 0x282100000000 && serial <= 0x2821ffffffff
|
||||||
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
|
return preSerial == 0x2821;
|
||||||
|
}
|
||||||
|
|
||||||
|
String HERF_2CH::typeName() const
|
||||||
|
{
|
||||||
|
return "HERF-800-2T";
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteAssign_t* HERF_2CH::getByteAssignment() const
|
||||||
|
{
|
||||||
|
return byteAssignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t HERF_2CH::getByteAssignmentSize() const
|
||||||
|
{
|
||||||
|
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
|
||||||
|
}
|
||||||
13
lib/Hoymiles/src/inverters/HERF_2CH.h
Normal file
13
lib/Hoymiles/src/inverters/HERF_2CH.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "HM_Abstract.h"
|
||||||
|
|
||||||
|
class HERF_2CH : public HM_Abstract {
|
||||||
|
public:
|
||||||
|
explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial);
|
||||||
|
static bool isValidSerial(const uint64_t serial);
|
||||||
|
String typeName() const;
|
||||||
|
const byteAssign_t* getByteAssignment() const;
|
||||||
|
uint8_t getByteAssignmentSize() const;
|
||||||
|
};
|
||||||
20
lib/Hoymiles/src/inverters/HERF_4CH.cpp
Normal file
20
lib/Hoymiles/src/inverters/HERF_4CH.cpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "HERF_4CH.h"
|
||||||
|
|
||||||
|
HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial)
|
||||||
|
: HM_4CH(radio, serial) {};
|
||||||
|
|
||||||
|
bool HERF_4CH::isValidSerial(const uint64_t serial)
|
||||||
|
{
|
||||||
|
// serial >= 0x280100000000 && serial <= 0x2801ffffffff
|
||||||
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
|
return preSerial == 0x2801;
|
||||||
|
}
|
||||||
|
|
||||||
|
String HERF_4CH::typeName() const
|
||||||
|
{
|
||||||
|
return "HERF-1600/1800-4T";
|
||||||
|
}
|
||||||
11
lib/Hoymiles/src/inverters/HERF_4CH.h
Normal file
11
lib/Hoymiles/src/inverters/HERF_4CH.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "HM_4CH.h"
|
||||||
|
|
||||||
|
class HERF_4CH : public HM_4CH {
|
||||||
|
public:
|
||||||
|
explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial);
|
||||||
|
static bool isValidSerial(const uint64_t serial);
|
||||||
|
String typeName() const;
|
||||||
|
};
|
||||||
@ -33,7 +33,7 @@ HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMS_1CH::isValidSerial(const uint64_t serial)
|
bool HMS_1CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x112400000000 && serial <= 0x112499999999
|
// serial >= 0x112400000000 && serial <= 0x1124ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1124;
|
return preSerial == 0x1124;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMS_1CHv2::isValidSerial(const uint64_t serial)
|
bool HMS_1CHv2::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x112500000000 && serial <= 0x112599999999
|
// serial >= 0x112500000000 && serial <= 0x1125ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1125;
|
return preSerial == 0x1125;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMS_2CH::isValidSerial(const uint64_t serial)
|
bool HMS_2CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x114400000000 && serial <= 0x114499999999
|
// serial >= 0x114400000000 && serial <= 0x1144ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1144;
|
return preSerial == 0x1144;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMS_4CH::isValidSerial(const uint64_t serial)
|
bool HMS_4CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x116400000000 && serial <= 0x116499999999
|
// serial >= 0x116400000000 && serial <= 0x1164ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1164;
|
return preSerial == 0x1164;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMT_4CH::isValidSerial(const uint64_t serial)
|
bool HMT_4CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x136100000000 && serial <= 0x136199999999
|
// serial >= 0x136100000000 && serial <= 0x1361ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1361;
|
return preSerial == 0x1361;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,7 +77,7 @@ HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HMT_6CH::isValidSerial(const uint64_t serial)
|
bool HMT_6CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x138200000000 && serial <= 0x138299999999
|
// serial >= 0x138200000000 && serial <= 0x1382ffffffff
|
||||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||||
return preSerial == 0x1382;
|
return preSerial == 0x1382;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HM_1CH::isValidSerial(const uint64_t serial)
|
bool HM_1CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x112100000000 && serial <= 0x112199999999
|
// serial >= 0x112100000000 && serial <= 0x1121ffffffff
|
||||||
|
|
||||||
uint8_t preId[2];
|
uint8_t preId[2];
|
||||||
preId[0] = (uint8_t)(serial >> 40);
|
preId[0] = (uint8_t)(serial >> 40);
|
||||||
|
|||||||
@ -41,7 +41,7 @@ HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HM_2CH::isValidSerial(const uint64_t serial)
|
bool HM_2CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x114100000000 && serial <= 0x114199999999
|
// serial >= 0x114100000000 && serial <= 0x1141ffffffff
|
||||||
|
|
||||||
uint8_t preId[2];
|
uint8_t preId[2];
|
||||||
preId[0] = (uint8_t)(serial >> 40);
|
preId[0] = (uint8_t)(serial >> 40);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial)
|
|||||||
|
|
||||||
bool HM_4CH::isValidSerial(const uint64_t serial)
|
bool HM_4CH::isValidSerial(const uint64_t serial)
|
||||||
{
|
{
|
||||||
// serial >= 0x116100000000 && serial <= 0x116199999999
|
// serial >= 0x116100000000 && serial <= 0x1161ffffffff
|
||||||
|
|
||||||
uint8_t preId[2];
|
uint8_t preId[2];
|
||||||
preId[0] = (uint8_t)(serial >> 40);
|
preId[0] = (uint8_t)(serial >> 40);
|
||||||
|
|||||||
@ -11,3 +11,5 @@
|
|||||||
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
|
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
|
||||||
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
|
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
|
||||||
| HMT_6CH | HMT-1800/2250-6T | 1382 |
|
| HMT_6CH | HMT-1800/2250-6T | 1382 |
|
||||||
|
| HERF_2CH | HERF 800 | 2821 |
|
||||||
|
| HERF_4CH | HERF 1800 | 2801 |
|
||||||
|
|||||||
@ -55,11 +55,12 @@ const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> AlarmLogParser::_alarmMe
|
|||||||
{ AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" },
|
{ AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" },
|
||||||
{ AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" },
|
{ AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" },
|
||||||
{ AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" },
|
{ AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" },
|
||||||
{ AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Eletrizitätsnetzausfall", "Réseau: Panne du réseau électrique" },
|
{ AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Elektrizitätsnetzausfall", "Réseau: Panne du réseau électrique" },
|
||||||
{ AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" },
|
{ AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" },
|
||||||
{ AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection d’îlots" },
|
{ AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection d’îlots" },
|
||||||
|
|
||||||
{ AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" },
|
{ AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" },
|
||||||
|
{ AlarmMessageType_t::ALL, 152, "Grid: Phase angle difference between two phases exceeded 5° >10 times", "", "" },
|
||||||
{ AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" },
|
{ AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" },
|
||||||
{ AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" },
|
{ AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" },
|
||||||
{ AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" },
|
{ AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" },
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
#define ALARM_LOG_ENTRY_SIZE 12
|
#define ALARM_LOG_ENTRY_SIZE 12
|
||||||
#define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4)
|
#define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4)
|
||||||
|
|
||||||
#define ALARM_MSG_COUNT 130
|
#define ALARM_MSG_COUNT 131
|
||||||
|
|
||||||
struct AlarmLogEntry_t {
|
struct AlarmLogEntry_t {
|
||||||
uint16_t MessageId;
|
uint16_t MessageId;
|
||||||
|
|||||||
@ -52,7 +52,11 @@ const devInfo_t devInfo[] = {
|
|||||||
{ { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0
|
{ { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0
|
||||||
|
|
||||||
{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
|
{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
|
||||||
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01
|
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01
|
||||||
|
|
||||||
|
{ { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00
|
||||||
|
{ { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00
|
||||||
|
{ { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00
|
||||||
};
|
};
|
||||||
|
|
||||||
DevInfoParser::DevInfoParser()
|
DevInfoParser::DevInfoParser()
|
||||||
@ -200,7 +204,7 @@ bool DevInfoParser::containsValidData() const
|
|||||||
struct tm info;
|
struct tm info;
|
||||||
localtime_r(&t, &info);
|
localtime_r(&t, &info);
|
||||||
|
|
||||||
return info.tm_year > (2016 - 1900);
|
return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t DevInfoParser::getDevIdx() const
|
uint8_t DevInfoParser::getDevIdx() const
|
||||||
|
|||||||
@ -281,13 +281,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
|
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
|
||||||
if (_lastUpdate == 0) {
|
return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (strlen(frame.SER) == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t VeDirectFrameHandler::getLastUpdate() const
|
uint32_t VeDirectFrameHandler::getLastUpdate() const
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "VeDirectMpptController.h"
|
#include "VeDirectMpptController.h"
|
||||||
|
|
||||||
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
|
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
|
||||||
{
|
{
|
||||||
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1);
|
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort);
|
||||||
_spData = std::make_shared<veMpptStruct>();
|
_spData = std::make_shared<veMpptStruct>();
|
||||||
if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); }
|
if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class VeDirectMpptController : public VeDirectFrameHandler {
|
|||||||
public:
|
public:
|
||||||
VeDirectMpptController() = default;
|
VeDirectMpptController() = default;
|
||||||
|
|
||||||
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
|
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
|
||||||
bool isDataValid() const; // return true if data valid and not outdated
|
bool isDataValid() const; // return true if data valid and not outdated
|
||||||
|
|
||||||
struct veMpptStruct : veStruct {
|
struct veMpptStruct : veStruct {
|
||||||
@ -49,7 +49,7 @@ public:
|
|||||||
double VPV; // panel voltage in V
|
double VPV; // panel voltage in V
|
||||||
double IPV; // panel current in A (calculated)
|
double IPV; // panel current in A (calculated)
|
||||||
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
|
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
|
||||||
uint8_t CS; // current state of operation e. g. OFF or Bulk
|
uint8_t CS; // current state of operation e.g. OFF or Bulk
|
||||||
uint8_t ERR; // error code
|
uint8_t ERR; // error code
|
||||||
uint32_t OR; // off reason
|
uint32_t OR; // off reason
|
||||||
uint32_t HSDS; // day sequence number 1...365
|
uint32_t HSDS; // day sequence number 1...365
|
||||||
|
|||||||
@ -36,11 +36,11 @@ build_unflags =
|
|||||||
-std=gnu++11
|
-std=gnu++11
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
mathieucarbou/ESP Async WebServer @ 2.7.0
|
mathieucarbou/ESP Async WebServer @ 2.8.1
|
||||||
bblanchon/ArduinoJson @ ^6.21.5
|
bblanchon/ArduinoJson @ ^6.21.5
|
||||||
https://github.com/bertmelis/espMqttClient.git#v1.6.0
|
https://github.com/bertmelis/espMqttClient.git#v1.6.0
|
||||||
nrf24/RF24 @ ^1.4.8
|
nrf24/RF24 @ ^1.4.8
|
||||||
olikraus/U8g2 @ ^2.35.9
|
olikraus/U8g2 @ ^2.35.15
|
||||||
buelowp/sunset @ ^1.1.7
|
buelowp/sunset @ ^1.1.7
|
||||||
https://github.com/arkhipenko/TaskScheduler#testing
|
https://github.com/arkhipenko/TaskScheduler#testing
|
||||||
https://github.com/coryjfowler/MCP_CAN_lib
|
https://github.com/coryjfowler/MCP_CAN_lib
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "JkBmsController.h"
|
#include "JkBmsController.h"
|
||||||
#include "VictronSmartShunt.h"
|
#include "VictronSmartShunt.h"
|
||||||
#include "MqttBattery.h"
|
#include "MqttBattery.h"
|
||||||
|
#include "SerialPortManager.h"
|
||||||
|
|
||||||
BatteryClass Battery;
|
BatteryClass Battery;
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ void BatteryClass::updateSettings()
|
|||||||
_upProvider->deinit();
|
_upProvider->deinit();
|
||||||
_upProvider = nullptr;
|
_upProvider = nullptr;
|
||||||
}
|
}
|
||||||
|
SerialPortManager.invalidateBatteryPort();
|
||||||
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
if (!config.Battery.Enabled) { return; }
|
if (!config.Battery.Enabled) { return; }
|
||||||
@ -47,23 +49,32 @@ void BatteryClass::updateSettings()
|
|||||||
switch (config.Battery.Provider) {
|
switch (config.Battery.Provider) {
|
||||||
case 0:
|
case 0:
|
||||||
_upProvider = std::make_unique<PylontechCanReceiver>();
|
_upProvider = std::make_unique<PylontechCanReceiver>();
|
||||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
_upProvider = std::make_unique<JkBms::Controller>();
|
_upProvider = std::make_unique<JkBms::Controller>();
|
||||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
_upProvider = std::make_unique<MqttBattery>();
|
_upProvider = std::make_unique<MqttBattery>();
|
||||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
_upProvider = std::make_unique<VictronSmartShunt>();
|
_upProvider = std::make_unique<VictronSmartShunt>();
|
||||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
|
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
|
||||||
break;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_upProvider->usesHwPort2()) {
|
||||||
|
if (!SerialPortManager.allocateBatteryPort(2)) {
|
||||||
|
MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2);
|
||||||
|
_upProvider = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_upProvider->init(verboseLogging)) {
|
||||||
|
SerialPortManager.invalidateBatteryPort();
|
||||||
|
_upProvider = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,8 @@ static void addLiveViewAlarm(JsonVariant& root, std::string const& name,
|
|||||||
|
|
||||||
bool BatteryStats::updateAvailable(uint32_t since) const
|
bool BatteryStats::updateAvailable(uint32_t since) const
|
||||||
{
|
{
|
||||||
|
if (_lastUpdate == 0) { return false; } // no data at all processed yet
|
||||||
|
|
||||||
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
|
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
|
||||||
return (_lastUpdate - since) < halfOfAllMillis;
|
return (_lastUpdate - since) < halfOfAllMillis;
|
||||||
}
|
}
|
||||||
@ -379,12 +381,14 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c
|
|||||||
_modelName = shuntData.getPidAsString().data();
|
_modelName = shuntData.getPidAsString().data();
|
||||||
_chargeCycles = shuntData.H4;
|
_chargeCycles = shuntData.H4;
|
||||||
_timeToGo = shuntData.TTG / 60;
|
_timeToGo = shuntData.TTG / 60;
|
||||||
_chargedEnergy = shuntData.H18 / 100;
|
_chargedEnergy = static_cast<float>(shuntData.H18) / 100;
|
||||||
_dischargedEnergy = shuntData.H17 / 100;
|
_dischargedEnergy = static_cast<float>(shuntData.H17) / 100;
|
||||||
_manufacturer = "Victron " + _modelName;
|
_manufacturer = "Victron " + _modelName;
|
||||||
_temperature = shuntData.T;
|
_temperature = shuntData.T;
|
||||||
_tempPresent = shuntData.tempPresent;
|
_tempPresent = shuntData.tempPresent;
|
||||||
|
_instantaneousPower = shuntData.P;
|
||||||
|
_consumedAmpHours = static_cast<float>(shuntData.CE) / 1000;
|
||||||
|
_lastFullCharge = shuntData.H9 / 60;
|
||||||
// shuntData.AR is a bitfield, so we need to check each bit individually
|
// shuntData.AR is a bitfield, so we need to check each bit individually
|
||||||
_alarmLowVoltage = shuntData.AR & 1;
|
_alarmLowVoltage = shuntData.AR & 1;
|
||||||
_alarmHighVoltage = shuntData.AR & 2;
|
_alarmHighVoltage = shuntData.AR & 2;
|
||||||
@ -401,8 +405,11 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
|
|||||||
// values go into the "Status" card of the web application
|
// values go into the "Status" card of the web application
|
||||||
addLiveViewValue(root, "current", _current, "A", 1);
|
addLiveViewValue(root, "current", _current, "A", 1);
|
||||||
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
|
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
|
||||||
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1);
|
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2);
|
||||||
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1);
|
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2);
|
||||||
|
addLiveViewValue(root, "instantaneousPower", _instantaneousPower, "W", 0);
|
||||||
|
addLiveViewValue(root, "consumedAmpHours", _consumedAmpHours, "Ah", 3);
|
||||||
|
addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0);
|
||||||
if (_tempPresent) {
|
if (_tempPresent) {
|
||||||
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
|
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
|
||||||
}
|
}
|
||||||
@ -421,4 +428,7 @@ void VictronSmartShuntStats::mqttPublish() const {
|
|||||||
MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles));
|
MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles));
|
||||||
MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy));
|
MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy));
|
||||||
MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy));
|
MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy));
|
||||||
|
MqttSettings.publish(F("battery/instantaneousPower"), String(_instantaneousPower));
|
||||||
|
MqttSettings.publish(F("battery/consumedAmpHours"), String(_consumedAmpHours));
|
||||||
|
MqttSettings.publish(F("battery/lastFullCharge"), String(_lastFullCharge));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,9 +182,10 @@ bool ConfigurationClass::write()
|
|||||||
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
||||||
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
||||||
powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
||||||
powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
|
powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
|
||||||
powerlimiter["interval"] = config.PowerLimiter.Interval;
|
powerlimiter["interval"] = config.PowerLimiter.Interval;
|
||||||
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
||||||
|
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
|
||||||
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
|
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
|
||||||
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
|
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
|
||||||
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
|
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
|
||||||
@ -428,9 +429,11 @@ bool ConfigurationClass::read()
|
|||||||
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
|
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
|
||||||
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
|
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
|
||||||
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
|
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
|
||||||
config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY;
|
config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
|
||||||
|
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting
|
||||||
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
|
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
|
||||||
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
|
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.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
|
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
|
||||||
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
|
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
|
||||||
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
|
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
|
||||||
@ -560,4 +563,26 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConfigurationClass::deleteInverterById(const uint8_t id)
|
||||||
|
{
|
||||||
|
config.Inverter[id].Serial = 0ULL;
|
||||||
|
strlcpy(config.Inverter[id].Name, "", sizeof(config.Inverter[id].Name));
|
||||||
|
config.Inverter[id].Order = 0;
|
||||||
|
|
||||||
|
config.Inverter[id].Poll_Enable = true;
|
||||||
|
config.Inverter[id].Poll_Enable_Night = true;
|
||||||
|
config.Inverter[id].Command_Enable = true;
|
||||||
|
config.Inverter[id].Command_Enable_Night = true;
|
||||||
|
config.Inverter[id].ReachableThreshold = REACHABLE_THRESHOLD;
|
||||||
|
config.Inverter[id].ZeroRuntimeDataIfUnrechable = false;
|
||||||
|
config.Inverter[id].ZeroYieldDayOnMidnight = false;
|
||||||
|
config.Inverter[id].YieldDayCorrection = false;
|
||||||
|
|
||||||
|
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||||
|
config.Inverter[id].channel[c].MaxChannelPower = 0;
|
||||||
|
config.Inverter[id].channel[c].YieldTotalOffset = 0.0f;
|
||||||
|
strlcpy(config.Inverter[id].channel[c].Name, "", sizeof(config.Inverter[id].channel[c].Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationClass Configuration;
|
ConfigurationClass Configuration;
|
||||||
|
|||||||
@ -31,13 +31,19 @@ const uint8_t languages[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
|
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
|
||||||
|
|
||||||
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
|
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
|
||||||
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
|
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
|
||||||
|
|
||||||
static const char* const i18n_meter_power_w[] = { "grid: %.0f W", "Netz: %.0f W", "reseau: %.0f W" };
|
static const char* const i18n_meter_power_w[] = { "grid: %.0f W", "Netz: %.0f W", "reseau: %.0f W" };
|
||||||
static const char* const i18n_meter_power_kw[] = { "grid: %.1f kW", "Netz: %.1f kW", "reseau: %.1f kW" };
|
static const char* const i18n_meter_power_kw[] = { "grid: %.1f kW", "Netz: %.1f kW", "reseau: %.1f kW" };
|
||||||
|
|
||||||
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
|
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
|
||||||
|
static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" };
|
||||||
|
|
||||||
static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" };
|
static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" };
|
||||||
static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" };
|
static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" };
|
||||||
|
|
||||||
static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" };
|
static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" };
|
||||||
|
|
||||||
DisplayGraphicClass::DisplayGraphicClass()
|
DisplayGraphicClass::DisplayGraphicClass()
|
||||||
@ -133,6 +139,10 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line)
|
|||||||
offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens
|
offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens
|
||||||
dispX += offset;
|
dispX += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dispX > _display->getDisplayWidth()) {
|
||||||
|
dispX = 0;
|
||||||
|
}
|
||||||
_display->drawStr(dispX, _lineOffsets[line], text);
|
_display->drawStr(dispX, _lineOffsets[line], text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,15 +251,20 @@ void DisplayGraphicClass::loop()
|
|||||||
//<=======================
|
//<=======================
|
||||||
|
|
||||||
if (showText) {
|
if (showText) {
|
||||||
//=====> Today & Total Production =======
|
// Daily production
|
||||||
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled());
|
float wattsToday = Datastore.getTotalAcYieldDayEnabled();
|
||||||
|
if (wattsToday >= 10000) {
|
||||||
|
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000);
|
||||||
|
} else {
|
||||||
|
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday);
|
||||||
|
}
|
||||||
printText(_fmtText, 1);
|
printText(_fmtText, 1);
|
||||||
|
|
||||||
const float watts = Datastore.getTotalAcYieldTotalEnabled();
|
// Total production
|
||||||
auto const format = (watts >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
|
const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
|
||||||
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], watts);
|
auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
|
||||||
|
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal);
|
||||||
printText(_fmtText, 2);
|
printText(_fmtText, 2);
|
||||||
//<=======================
|
|
||||||
|
|
||||||
//=====> IP or Date-Time ========
|
//=====> IP or Date-Time ========
|
||||||
// Change every 3 seconds
|
// Change every 3 seconds
|
||||||
|
|||||||
@ -38,6 +38,13 @@ bool HttpPowerMeterClass::updateValues()
|
|||||||
MessageOutput.printf("%s\r\n", httpPowerMeterError);
|
MessageOutput.printf("%s\r\n", httpPowerMeterError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath)) {
|
||||||
|
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;
|
return true;
|
||||||
@ -139,9 +146,21 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S
|
|||||||
httpCode = httpClient.GET();
|
httpCode = httpClient.GET();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool result = tryGetFloatValueForPhase(phase, httpCode, jsonPath);
|
|
||||||
|
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();
|
httpClient.end();
|
||||||
return result;
|
|
||||||
|
return tryGetFloatValueForPhase(phase, jsonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
|
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
|
||||||
@ -199,27 +218,18 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam
|
|||||||
return authorization;
|
return authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath)
|
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath)
|
||||||
{
|
{
|
||||||
bool success = false;
|
FirebaseJson json;
|
||||||
if (httpCode == HTTP_CODE_OK) {
|
json.setJsonData(httpResponse);
|
||||||
httpResponse = httpClient.getString(); //very unfortunate that we cannot parse WifiClient stream directly
|
FirebaseJsonData value;
|
||||||
FirebaseJson json;
|
if (!json.get(value, jsonPath)) {
|
||||||
json.setJsonData(httpResponse);
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath);
|
||||||
FirebaseJsonData value;
|
return false;
|
||||||
if (!json.get(value, jsonPath)) {
|
|
||||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath);
|
|
||||||
}else {
|
|
||||||
power[phase] = value.to<float>();
|
|
||||||
//MessageOutput.printf("Power for Phase %i: %5.2fW\r\n", phase, power[phase]);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
} else if (httpCode <= 0) {
|
|
||||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
|
|
||||||
} else if (httpCode != HTTP_CODE_OK) {
|
|
||||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
|
|
||||||
}
|
}
|
||||||
return success;
|
|
||||||
|
power[phase] = value.to<float>();
|
||||||
|
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
|
//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
|
||||||
|
|||||||
@ -325,7 +325,13 @@ void HuaweiCanClass::loop()
|
|||||||
|
|
||||||
// Check if inverter used by the power limiter is active
|
// Check if inverter used by the power limiter is active
|
||||||
std::shared_ptr<InverterAbstract> inverter =
|
std::shared_ptr<InverterAbstract> inverter =
|
||||||
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);
|
||||||
|
|
||||||
|
if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
|
||||||
|
// we previously had an index saved as InverterId. fall back to the
|
||||||
|
// respective positional lookup if InverterId is not a known serial.
|
||||||
|
inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
||||||
|
}
|
||||||
|
|
||||||
if (inverter != nullptr) {
|
if (inverter != nullptr) {
|
||||||
if(inverter->isProducing()) {
|
if(inverter->isProducing()) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass;
|
|||||||
void MqttHandleVedirectHassClass::init(Scheduler& scheduler)
|
void MqttHandleVedirectHassClass::init(Scheduler& scheduler)
|
||||||
{
|
{
|
||||||
scheduler.addTask(_loopTask);
|
scheduler.addTask(_loopTask);
|
||||||
_loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this));
|
_loopTask.setCallback([this] { loop(); });
|
||||||
_loopTask.setIterations(TASK_FOREVER);
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
_loopTask.enable();
|
_loopTask.enable();
|
||||||
}
|
}
|
||||||
@ -55,43 +55,56 @@ void MqttHandleVedirectHassClass::publishConfig()
|
|||||||
if (!MqttSettings.getConnected()) {
|
if (!MqttSettings.getConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ensure data is revieved from victron
|
|
||||||
if (!VictronMppt.isDataValid()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// device info
|
// device info
|
||||||
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF");
|
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
publishSensor("MPPT serial number", "mdi:counter", "SER");
|
// ensure data is received from victron
|
||||||
publishSensor("MPPT firmware number", "mdi:counter", "FW");
|
if (!VictronMppt.isDataValid(idx)) {
|
||||||
publishSensor("MPPT state of operation", "mdi:wrench", "CS");
|
continue;
|
||||||
publishSensor("MPPT error code", "mdi:bell", "ERR");
|
}
|
||||||
publishSensor("MPPT off reason", "mdi:wrench", "OR");
|
|
||||||
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT");
|
|
||||||
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d");
|
|
||||||
|
|
||||||
// battery info
|
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
|
||||||
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V");
|
if (!spOptMpptData.has_value()) {
|
||||||
publishSensor("Battery current", NULL, "I", "current", "measurement", "A");
|
continue;
|
||||||
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W");
|
}
|
||||||
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%");
|
|
||||||
|
|
||||||
// panel info
|
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
|
||||||
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V");
|
|
||||||
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A");
|
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData);
|
||||||
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W");
|
publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData);
|
||||||
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh");
|
publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData);
|
||||||
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh");
|
publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData);
|
||||||
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W");
|
publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData);
|
||||||
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh");
|
publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData);
|
||||||
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W");
|
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData);
|
||||||
|
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData);
|
||||||
|
|
||||||
|
// battery info
|
||||||
|
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData);
|
||||||
|
publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData);
|
||||||
|
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData);
|
||||||
|
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData);
|
||||||
|
|
||||||
|
// panel info
|
||||||
|
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData);
|
||||||
|
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData);
|
||||||
|
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData);
|
||||||
|
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData);
|
||||||
|
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData);
|
||||||
|
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData);
|
||||||
|
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData);
|
||||||
|
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData);
|
||||||
|
}
|
||||||
|
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
|
void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
|
const char *deviceClass, const char *stateClass,
|
||||||
|
const char *unitOfMeasurement,
|
||||||
|
const VeDirectMpptController::spData_t &spMpptData)
|
||||||
{
|
{
|
||||||
String serial = VictronMppt.getData()->SER;
|
String serial = spMpptData->SER;
|
||||||
|
|
||||||
String sensorId = caption;
|
String sensorId = caption;
|
||||||
sensorId.replace(" ", "_");
|
sensorId.replace(" ", "_");
|
||||||
@ -126,7 +139,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonObject deviceObj = root.createNestedObject("dev");
|
JsonObject deviceObj = root.createNestedObject("dev");
|
||||||
createDeviceInfo(deviceObj);
|
createDeviceInfo(deviceObj, spMpptData);
|
||||||
|
|
||||||
if (Configuration.get().Mqtt.Hass.Expire) {
|
if (Configuration.get().Mqtt.Hass.Expire) {
|
||||||
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
|
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
|
||||||
@ -138,14 +151,18 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
|
|||||||
root["stat_cla"] = stateClass;
|
root["stat_cla"] = stateClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
publish(configTopic, buffer);
|
publish(configTopic, buffer);
|
||||||
|
|
||||||
}
|
}
|
||||||
void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
|
void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
|
const char *payload_on, const char *payload_off,
|
||||||
|
const VeDirectMpptController::spData_t &spMpptData)
|
||||||
{
|
{
|
||||||
String serial = VictronMppt.getData()->SER;
|
String serial = spMpptData->SER;
|
||||||
|
|
||||||
String sensorId = caption;
|
String sensorId = caption;
|
||||||
sensorId.replace(" ", "_");
|
sensorId.replace(" ", "_");
|
||||||
@ -178,16 +195,18 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonObject deviceObj = root.createNestedObject("dev");
|
JsonObject deviceObj = root.createNestedObject("dev");
|
||||||
createDeviceInfo(deviceObj);
|
createDeviceInfo(deviceObj, spMpptData);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
publish(configTopic, buffer);
|
publish(configTopic, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
|
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object,
|
||||||
|
const VeDirectMpptController::spData_t &spMpptData)
|
||||||
{
|
{
|
||||||
auto spMpptData = VictronMppt.getData();
|
|
||||||
String serial = spMpptData->SER;
|
String serial = spMpptData->SER;
|
||||||
object["name"] = "Victron(" + serial + ")";
|
object["name"] = "Victron(" + serial + ")";
|
||||||
object["ids"] = serial;
|
object["ids"] = serial;
|
||||||
|
|||||||
@ -111,6 +111,14 @@ void MqttHandleBatteryHassClass::loop()
|
|||||||
case 2: // SoC from MQTT
|
case 2: // SoC from MQTT
|
||||||
break;
|
break;
|
||||||
case 3: // Victron SmartShunt
|
case 3: // Victron SmartShunt
|
||||||
|
publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V");
|
||||||
|
publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A");
|
||||||
|
publishSensor("Instantaneous Power", NULL, "instantaneousPower", "power", "measurement", "W");
|
||||||
|
publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh");
|
||||||
|
publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh");
|
||||||
|
publishSensor("Charge Cycles", "mdi:counter", "chargeCycles");
|
||||||
|
publishSensor("Consumed Amp Hours", NULL, "consumedAmpHours", NULL, "measurement", "Ah");
|
||||||
|
publishSensor("Last Full Charge", "mdi:timelapse", "lastFullCharge", NULL, NULL, "min");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +164,7 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char*
|
|||||||
createDeviceInfo(deviceObj);
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
if (Configuration.get().Mqtt.Hass.Expire) {
|
if (Configuration.get().Mqtt.Hass.Expire) {
|
||||||
root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() * 3;
|
root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() / 1000 * 3;
|
||||||
}
|
}
|
||||||
if (deviceClass != NULL) {
|
if (deviceClass != NULL) {
|
||||||
root["dev_cla"] = deviceClass;
|
root["dev_cla"] = deviceClass;
|
||||||
@ -165,6 +173,8 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char*
|
|||||||
root["stat_cla"] = stateClass;
|
root["stat_cla"] = stateClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
publish(configTopic, buffer);
|
publish(configTopic, buffer);
|
||||||
@ -208,6 +218,8 @@ void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const
|
|||||||
JsonObject deviceObj = root.createNestedObject("dev");
|
JsonObject deviceObj = root.createNestedObject("dev");
|
||||||
createDeviceInfo(deviceObj);
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
publish(configTopic, buffer);
|
publish(configTopic, buffer);
|
||||||
|
|||||||
@ -25,8 +25,25 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
|
|||||||
using std::placeholders::_5;
|
using std::placeholders::_5;
|
||||||
using std::placeholders::_6;
|
using std::placeholders::_6;
|
||||||
|
|
||||||
String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode";
|
String const& prefix = MqttSettings.getPrefix();
|
||||||
MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6));
|
|
||||||
|
auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) {
|
||||||
|
String fullTopic(prefix + "powerlimiter/cmd/" + subTopic);
|
||||||
|
MqttSettings.subscribe(fullTopic.c_str(), 0,
|
||||||
|
std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command,
|
||||||
|
std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4,
|
||||||
|
std::placeholders::_5, std::placeholders::_6));
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold);
|
||||||
|
subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold);
|
||||||
|
subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC);
|
||||||
|
subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold);
|
||||||
|
subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold);
|
||||||
|
subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage);
|
||||||
|
subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage);
|
||||||
|
subscribe("mode", MqttPowerLimiterCommand::Mode);
|
||||||
|
|
||||||
_lastPublish = millis();
|
_lastPublish = millis();
|
||||||
}
|
}
|
||||||
@ -50,51 +67,113 @@ void MqttHandlePowerLimiterClass::loop()
|
|||||||
|
|
||||||
if (!MqttSettings.getConnected() ) { return; }
|
if (!MqttSettings.getConnected() ) { return; }
|
||||||
|
|
||||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) {
|
||||||
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
|
||||||
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
|
||||||
|
|
||||||
yield();
|
|
||||||
_lastPublish = millis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties,
|
|
||||||
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
|
||||||
{
|
|
||||||
std::string strValue(reinterpret_cast<const char*>(payload), len);
|
|
||||||
int intValue = -1;
|
|
||||||
try {
|
|
||||||
intValue = std::stoi(strValue);
|
|
||||||
}
|
|
||||||
catch (std::invalid_argument const& e) {
|
|
||||||
MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n",
|
|
||||||
topic, strValue.c_str());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
_lastPublish = millis();
|
||||||
|
|
||||||
using Mode = PowerLimiterClass::Mode;
|
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||||
switch (static_cast<Mode>(intValue)) {
|
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
||||||
case Mode::UnconditionalFullSolarPassthrough:
|
|
||||||
MessageOutput.println("Power limiter unconditional full solar PT");
|
// no thresholds are relevant for setups without a battery
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
|
||||||
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
|
||||||
break;
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
|
||||||
case Mode::Disabled:
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
|
||||||
MessageOutput.println("Power limiter disabled (override)");
|
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
if (config.Vedirect.Enabled) {
|
||||||
&PowerLimiter, Mode::Disabled));
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage));
|
||||||
break;
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage));
|
||||||
case Mode::Normal:
|
}
|
||||||
MessageOutput.println("Power limiter normal operation");
|
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; }
|
||||||
&PowerLimiter, Mode::Normal));
|
|
||||||
break;
|
MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold));
|
||||||
default:
|
MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold));
|
||||||
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
|
|
||||||
break;
|
if (config.Vedirect.Enabled) {
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||||
|
{
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
std::string strValue(reinterpret_cast<const char*>(payload), len);
|
||||||
|
float payload_val = -1;
|
||||||
|
try {
|
||||||
|
payload_val = std::stof(strValue);
|
||||||
|
}
|
||||||
|
catch (std::invalid_argument const& e) {
|
||||||
|
MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||||
|
topic, strValue.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int intValue = static_cast<int>(payload_val);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case MqttPowerLimiterCommand::Mode:
|
||||||
|
{
|
||||||
|
using Mode = PowerLimiterClass::Mode;
|
||||||
|
Mode mode = static_cast<Mode>(intValue);
|
||||||
|
if (mode == Mode::UnconditionalFullSolarPassthrough) {
|
||||||
|
MessageOutput.println("Power limiter unconditional full solar PT");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
||||||
|
} else if (mode == Mode::Disabled) {
|
||||||
|
MessageOutput.println("Power limiter disabled (override)");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::Disabled));
|
||||||
|
} else if (mode == Mode::Normal) {
|
||||||
|
MessageOutput.println("Power limiter normal operation");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::Normal));
|
||||||
|
} else {
|
||||||
|
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case MqttPowerLimiterCommand::BatterySoCStartThreshold:
|
||||||
|
if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.BatterySocStartThreshold = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::BatterySoCStopThreshold:
|
||||||
|
if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.BatterySocStopThreshold = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassthroughSoC:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughSoc = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::VoltageStartThreshold:
|
||||||
|
if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.VoltageStartThreshold = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::VoltageStopThreshold:
|
||||||
|
if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.VoltageStopThreshold = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not reached if the value did not change
|
||||||
|
Configuration.write();
|
||||||
|
}
|
||||||
|
|||||||
206
src/MqttHandlePowerLimiterHass.cpp
Normal file
206
src/MqttHandlePowerLimiterHass.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
#include "NetworkSettings.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
|
MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::init(Scheduler& scheduler)
|
||||||
|
{
|
||||||
|
scheduler.addTask(_loopTask);
|
||||||
|
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterHassClass::loop, this));
|
||||||
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
|
_loopTask.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::loop()
|
||||||
|
{
|
||||||
|
if (!Configuration.get().PowerLimiter.Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_updateForced) {
|
||||||
|
publishConfig();
|
||||||
|
_updateForced = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MqttSettings.getConnected() && !_wasConnected) {
|
||||||
|
// Connection established
|
||||||
|
_wasConnected = true;
|
||||||
|
publishConfig();
|
||||||
|
} else if (!MqttSettings.getConnected() && _wasConnected) {
|
||||||
|
// Connection lost
|
||||||
|
_wasConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::forceUpdate()
|
||||||
|
{
|
||||||
|
_updateForced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publishConfig()
|
||||||
|
{
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
if (!config.Mqtt.Hass.Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MqttSettings.getConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.PowerLimiter.Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode");
|
||||||
|
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// as this project revolves around Hoymiles inverters, 16 - 60 V is a reasonable voltage range
|
||||||
|
publishNumber("DPL battery voltage start threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60);
|
||||||
|
publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60);
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
publishNumber("DPL full solar passthrough start voltage",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/voltage/full_solar_passthrough_start",
|
||||||
|
"threshold/voltage/full_solar_passthrough_start", "V", 16, 60);
|
||||||
|
publishNumber("DPL full solar passthrough stop voltage",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/voltage/full_solar_passthrough_stop",
|
||||||
|
"threshold/voltage/full_solar_passthrough_stop", "V", 16, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Battery.Enabled && !config.PowerLimiter.IgnoreSoc) {
|
||||||
|
publishNumber("DPL battery SoC start threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100);
|
||||||
|
publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100);
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
publishNumber("DPL full solar passthrough SoC",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/soc/full_solar_passthrough",
|
||||||
|
"threshold/soc/full_solar_passthrough", "%", 0, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publishSelect(
|
||||||
|
const char* caption, const char* icon, const char* category,
|
||||||
|
const char* commandTopic, const char* stateTopic)
|
||||||
|
{
|
||||||
|
|
||||||
|
String selectId = caption;
|
||||||
|
selectId.replace(" ", "_");
|
||||||
|
selectId.toLowerCase();
|
||||||
|
|
||||||
|
const String configTopic = "select/powerlimiter/" + selectId + "/config";
|
||||||
|
|
||||||
|
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
|
||||||
|
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
|
||||||
|
|
||||||
|
DynamicJsonDocument root(1024);
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root["name"] = caption;
|
||||||
|
root["uniq_id"] = selectId;
|
||||||
|
if (strcmp(icon, "")) {
|
||||||
|
root["ic"] = icon;
|
||||||
|
}
|
||||||
|
root["ent_cat"] = category;
|
||||||
|
root["cmd_t"] = cmdTopic;
|
||||||
|
root["stat_t"] = statTopic;
|
||||||
|
JsonArray options = root.createNestedArray("options");
|
||||||
|
options.add("0");
|
||||||
|
options.add("1");
|
||||||
|
options.add("2");
|
||||||
|
|
||||||
|
JsonObject deviceObj = root.createNestedObject("dev");
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
|
String buffer;
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::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)
|
||||||
|
{
|
||||||
|
|
||||||
|
String numberId = caption;
|
||||||
|
numberId.replace(" ", "_");
|
||||||
|
numberId.toLowerCase();
|
||||||
|
|
||||||
|
const String configTopic = "number/powerlimiter/" + numberId + "/config";
|
||||||
|
|
||||||
|
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
|
||||||
|
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
|
||||||
|
|
||||||
|
DynamicJsonDocument root(1024);
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root["name"] = caption;
|
||||||
|
root["uniq_id"] = numberId;
|
||||||
|
if (strcmp(icon, "")) {
|
||||||
|
root["ic"] = icon;
|
||||||
|
}
|
||||||
|
root["ent_cat"] = category;
|
||||||
|
root["cmd_t"] = cmdTopic;
|
||||||
|
root["stat_t"] = statTopic;
|
||||||
|
root["unit_of_meas"] = unitOfMeasure;
|
||||||
|
root["min"] = min;
|
||||||
|
root["max"] = max;
|
||||||
|
root["mode"] = "box";
|
||||||
|
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
if (config.Mqtt.Hass.Expire) {
|
||||||
|
root["exp_aft"] = config.Mqtt.PublishInterval * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject deviceObj = root.createNestedObject("dev");
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
|
String buffer;
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object)
|
||||||
|
{
|
||||||
|
object["name"] = "Dynamic Power Limiter";
|
||||||
|
object["ids"] = "0002";
|
||||||
|
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||||
|
object["mf"] = "OpenDTU";
|
||||||
|
object["mdl"] = "Dynamic Power Limiter";
|
||||||
|
object["sw"] = AUTO_GIT_HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publish(const String& subtopic, const String& payload)
|
||||||
|
{
|
||||||
|
String topic = Configuration.get().Mqtt.Hass.Topic;
|
||||||
|
topic += subtopic;
|
||||||
|
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ MqttHandleVedirectClass MqttHandleVedirect;
|
|||||||
void MqttHandleVedirectClass::init(Scheduler& scheduler)
|
void MqttHandleVedirectClass::init(Scheduler& scheduler)
|
||||||
{
|
{
|
||||||
scheduler.addTask(_loopTask);
|
scheduler.addTask(_loopTask);
|
||||||
_loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this));
|
_loopTask.setCallback([this] { loop(); });
|
||||||
_loopTask.setIterations(TASK_FOREVER);
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
_loopTask.enable();
|
_loopTask.enable();
|
||||||
|
|
||||||
@ -41,10 +41,6 @@ void MqttHandleVedirectClass::loop()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VictronMppt.isDataValid()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) {
|
if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) {
|
||||||
// determine if this cycle should publish full values or updates only
|
// determine if this cycle should publish full values or updates only
|
||||||
if (_nextPublishFull <= _nextPublishUpdatesOnly) {
|
if (_nextPublishFull <= _nextPublishUpdatesOnly) {
|
||||||
@ -62,82 +58,23 @@ void MqttHandleVedirectClass::loop()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto spMpptData = VictronMppt.getData();
|
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
String value;
|
if (!VictronMppt.isDataValid(idx)) {
|
||||||
String topic = "victron/";
|
continue;
|
||||||
topic.concat(spMpptData->SER);
|
}
|
||||||
topic.concat("/");
|
|
||||||
|
|
||||||
if (_PublishFull || spMpptData->PID != _kvFrame.PID)
|
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
|
||||||
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
|
if (!spOptMpptData.has_value()) {
|
||||||
if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0)
|
continue;
|
||||||
MqttSettings.publish(topic + "SER", spMpptData->SER );
|
}
|
||||||
if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0)
|
|
||||||
MqttSettings.publish(topic + "FW", spMpptData->FW);
|
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
|
||||||
if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD)
|
|
||||||
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF");
|
VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER];
|
||||||
if (_PublishFull || spMpptData->CS != _kvFrame.CS)
|
publish_mppt_data(spMpptData, _kvFrame);
|
||||||
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
|
if (!_PublishFull) {
|
||||||
if (_PublishFull || spMpptData->ERR != _kvFrame.ERR)
|
_kvFrames[spMpptData->SER] = *spMpptData;
|
||||||
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
|
}
|
||||||
if (_PublishFull || spMpptData->OR != _kvFrame.OR)
|
|
||||||
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
|
|
||||||
if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT)
|
|
||||||
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
|
|
||||||
if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) {
|
|
||||||
value = spMpptData->HSDS;
|
|
||||||
MqttSettings.publish(topic + "HSDS", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->V != _kvFrame.V) {
|
|
||||||
value = spMpptData->V;
|
|
||||||
MqttSettings.publish(topic + "V", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->I != _kvFrame.I) {
|
|
||||||
value = spMpptData->I;
|
|
||||||
MqttSettings.publish(topic + "I", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->P != _kvFrame.P) {
|
|
||||||
value = spMpptData->P;
|
|
||||||
MqttSettings.publish(topic + "P", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) {
|
|
||||||
value = spMpptData->VPV;
|
|
||||||
MqttSettings.publish(topic + "VPV", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) {
|
|
||||||
value = spMpptData->IPV;
|
|
||||||
MqttSettings.publish(topic + "IPV", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) {
|
|
||||||
value = spMpptData->PPV;
|
|
||||||
MqttSettings.publish(topic + "PPV", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->E != _kvFrame.E) {
|
|
||||||
value = spMpptData->E;
|
|
||||||
MqttSettings.publish(topic + "E", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->H19 != _kvFrame.H19) {
|
|
||||||
value = spMpptData->H19;
|
|
||||||
MqttSettings.publish(topic + "H19", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->H20 != _kvFrame.H20) {
|
|
||||||
value = spMpptData->H20;
|
|
||||||
MqttSettings.publish(topic + "H20", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->H21 != _kvFrame.H21) {
|
|
||||||
value = spMpptData->H21;
|
|
||||||
MqttSettings.publish(topic + "H21", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->H22 != _kvFrame.H22) {
|
|
||||||
value = spMpptData->H22;
|
|
||||||
MqttSettings.publish(topic + "H22", value);
|
|
||||||
}
|
|
||||||
if (_PublishFull || spMpptData->H23 != _kvFrame.H23) {
|
|
||||||
value = spMpptData->H23;
|
|
||||||
MqttSettings.publish(topic + "H23", value);
|
|
||||||
}
|
|
||||||
if (!_PublishFull) {
|
|
||||||
_kvFrame = *spMpptData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now calculate next points of time to publish
|
// now calculate next points of time to publish
|
||||||
@ -166,3 +103,80 @@ void MqttHandleVedirectClass::loop()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
|
||||||
|
VeDirectMpptController::veMpptStruct &frame) const {
|
||||||
|
String value;
|
||||||
|
String topic = "victron/";
|
||||||
|
topic.concat(spMpptData->SER);
|
||||||
|
topic.concat("/");
|
||||||
|
|
||||||
|
if (_PublishFull || spMpptData->PID != frame.PID)
|
||||||
|
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
|
||||||
|
if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0)
|
||||||
|
MqttSettings.publish(topic + "SER", spMpptData->SER );
|
||||||
|
if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0)
|
||||||
|
MqttSettings.publish(topic + "FW", spMpptData->FW);
|
||||||
|
if (_PublishFull || spMpptData->LOAD != frame.LOAD)
|
||||||
|
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF");
|
||||||
|
if (_PublishFull || spMpptData->CS != frame.CS)
|
||||||
|
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
|
||||||
|
if (_PublishFull || spMpptData->ERR != frame.ERR)
|
||||||
|
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
|
||||||
|
if (_PublishFull || spMpptData->OR != frame.OR)
|
||||||
|
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
|
||||||
|
if (_PublishFull || spMpptData->MPPT != frame.MPPT)
|
||||||
|
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
|
||||||
|
if (_PublishFull || spMpptData->HSDS != frame.HSDS) {
|
||||||
|
value = spMpptData->HSDS;
|
||||||
|
MqttSettings.publish(topic + "HSDS", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->V != frame.V) {
|
||||||
|
value = spMpptData->V;
|
||||||
|
MqttSettings.publish(topic + "V", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->I != frame.I) {
|
||||||
|
value = spMpptData->I;
|
||||||
|
MqttSettings.publish(topic + "I", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->P != frame.P) {
|
||||||
|
value = spMpptData->P;
|
||||||
|
MqttSettings.publish(topic + "P", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->VPV != frame.VPV) {
|
||||||
|
value = spMpptData->VPV;
|
||||||
|
MqttSettings.publish(topic + "VPV", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->IPV != frame.IPV) {
|
||||||
|
value = spMpptData->IPV;
|
||||||
|
MqttSettings.publish(topic + "IPV", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->PPV != frame.PPV) {
|
||||||
|
value = spMpptData->PPV;
|
||||||
|
MqttSettings.publish(topic + "PPV", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->E != frame.E) {
|
||||||
|
value = spMpptData->E;
|
||||||
|
MqttSettings.publish(topic + "E", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->H19 != frame.H19) {
|
||||||
|
value = spMpptData->H19;
|
||||||
|
MqttSettings.publish(topic + "H19", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->H20 != frame.H20) {
|
||||||
|
value = spMpptData->H20;
|
||||||
|
MqttSettings.publish(topic + "H20", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->H21 != frame.H21) {
|
||||||
|
value = spMpptData->H21;
|
||||||
|
MqttSettings.publish(topic + "H21", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->H22 != frame.H22) {
|
||||||
|
value = spMpptData->H22;
|
||||||
|
MqttSettings.publish(topic + "H22", value);
|
||||||
|
}
|
||||||
|
if (_PublishFull || spMpptData->H23 != frame.H23) {
|
||||||
|
value = spMpptData->H23;
|
||||||
|
MqttSettings.publish(topic + "H23", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -144,6 +144,18 @@
|
|||||||
#define HUAWEI_PIN_POWER -1
|
#define HUAWEI_PIN_POWER -1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_RX
|
||||||
|
#define POWERMETER_PIN_RX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_TX
|
||||||
|
#define POWERMETER_PIN_TX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_DERE
|
||||||
|
#define POWERMETER_PIN_DERE -1
|
||||||
|
#endif
|
||||||
|
|
||||||
PinMappingClass PinMapping;
|
PinMappingClass PinMapping;
|
||||||
|
|
||||||
PinMappingClass::PinMappingClass()
|
PinMappingClass::PinMappingClass()
|
||||||
@ -182,8 +194,15 @@ PinMappingClass::PinMappingClass()
|
|||||||
_pinMapping.display_cs = DISPLAY_CS;
|
_pinMapping.display_cs = DISPLAY_CS;
|
||||||
_pinMapping.display_reset = DISPLAY_RESET;
|
_pinMapping.display_reset = DISPLAY_RESET;
|
||||||
|
|
||||||
_pinMapping.victron_tx = VICTRON_PIN_TX;
|
_pinMapping.led[0] = LED0;
|
||||||
|
_pinMapping.led[1] = LED1;
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
_pinMapping.victron_rx = VICTRON_PIN_RX;
|
_pinMapping.victron_rx = VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx = VICTRON_PIN_TX;
|
||||||
|
|
||||||
|
_pinMapping.victron_rx2 = VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx2 = VICTRON_PIN_TX;
|
||||||
|
|
||||||
_pinMapping.battery_rx = BATTERY_PIN_RX;
|
_pinMapping.battery_rx = BATTERY_PIN_RX;
|
||||||
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
|
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
|
||||||
@ -196,8 +215,10 @@ PinMappingClass::PinMappingClass()
|
|||||||
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
|
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
|
||||||
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
|
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
|
||||||
_pinMapping.huawei_power = HUAWEI_PIN_POWER;
|
_pinMapping.huawei_power = HUAWEI_PIN_POWER;
|
||||||
_pinMapping.led[0] = LED0;
|
|
||||||
_pinMapping.led[1] = LED1;
|
_pinMapping.powermeter_rx = POWERMETER_PIN_RX;
|
||||||
|
_pinMapping.powermeter_tx = POWERMETER_PIN_TX;
|
||||||
|
_pinMapping.powermeter_dere = POWERMETER_PIN_DERE;
|
||||||
}
|
}
|
||||||
|
|
||||||
PinMapping_t& PinMappingClass::get()
|
PinMapping_t& PinMappingClass::get()
|
||||||
@ -257,8 +278,14 @@ bool PinMappingClass::init(const String& deviceMapping)
|
|||||||
_pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS;
|
_pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS;
|
||||||
_pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET;
|
_pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET;
|
||||||
|
|
||||||
|
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
||||||
|
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
|
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
|
||||||
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
|
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
|
||||||
|
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX;
|
||||||
|
|
||||||
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
|
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
|
||||||
_pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN;
|
_pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN;
|
||||||
@ -272,8 +299,9 @@ bool PinMappingClass::init(const String& deviceMapping)
|
|||||||
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
|
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
|
||||||
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
|
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
|
||||||
|
|
||||||
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
_pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX;
|
||||||
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
_pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX;
|
||||||
|
_pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status
|
|||||||
{
|
{
|
||||||
static const frozen::string missing = "programmer error: missing status text";
|
static const frozen::string missing = "programmer error: missing status text";
|
||||||
|
|
||||||
static const frozen::map<Status, frozen::string, 18> texts = {
|
static const frozen::map<Status, frozen::string, 21> texts = {
|
||||||
{ Status::Initializing, "initializing (should not see me)" },
|
{ Status::Initializing, "initializing (should not see me)" },
|
||||||
{ Status::DisabledByConfig, "disabled by configuration" },
|
{ Status::DisabledByConfig, "disabled by configuration" },
|
||||||
{ Status::DisabledByMqtt, "disabled by MQTT" },
|
{ Status::DisabledByMqtt, "disabled by MQTT" },
|
||||||
@ -47,8 +47,11 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status
|
|||||||
{ Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" },
|
{ Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" },
|
||||||
{ Status::InverterDevInfoPending, "waiting for inverter device information to be available" },
|
{ Status::InverterDevInfoPending, "waiting for inverter device information to be available" },
|
||||||
{ Status::InverterStatsPending, "waiting for sufficiently recent inverter data" },
|
{ Status::InverterStatsPending, "waiting for sufficiently recent inverter data" },
|
||||||
|
{ Status::CalculatedLimitBelowMinLimit, "calculated limit is less than lower power limit" },
|
||||||
{ Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" },
|
{ Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" },
|
||||||
{ Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" },
|
{ Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" },
|
||||||
|
{ Status::NoEnergy, "no energy source available to power the inverter from" },
|
||||||
|
{ Status::HuaweiPsu, "DPL stands by while Huawei PSU is enabled/charging" },
|
||||||
{ Status::Stable, "the system is stable, the last power limit is still valid" },
|
{ Status::Stable, "the system is stable, the last power limit is still valid" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,8 +72,8 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status)
|
|||||||
// should just be silent while it is disabled.
|
// should just be silent while it is disabled.
|
||||||
if (status == Status::DisabledByConfig && _lastStatus == status) { return; }
|
if (status == Status::DisabledByConfig && _lastStatus == status) { return; }
|
||||||
|
|
||||||
MessageOutput.printf("[%11.3f] DPL: %s\r\n",
|
MessageOutput.printf("[DPL::announceStatus] %s\r\n",
|
||||||
static_cast<double>(millis())/1000, getStatusText(status).data());
|
getStatusText(status).data());
|
||||||
|
|
||||||
_lastStatus = status;
|
_lastStatus = status;
|
||||||
_lastStatusPrinted = millis();
|
_lastStatusPrinted = millis();
|
||||||
@ -89,7 +92,15 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status)
|
|||||||
_shutdownPending = true;
|
_shutdownPending = true;
|
||||||
|
|
||||||
_oTargetPowerState = false;
|
_oTargetPowerState = false;
|
||||||
_oTargetPowerLimitWatts = Configuration.get().PowerLimiter.LowerPowerLimit;
|
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
if ( (Status::PowerMeterTimeout == status ||
|
||||||
|
Status::CalculatedLimitBelowMinLimit == status)
|
||||||
|
&& config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
|
_oTargetPowerState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_oTargetPowerLimitWatts = config.PowerLimiter.LowerPowerLimit;
|
||||||
return updateInverter();
|
return updateInverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +137,13 @@ void PowerLimiterClass::loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<InverterAbstract> currentInverter =
|
std::shared_ptr<InverterAbstract> currentInverter =
|
||||||
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);
|
||||||
|
|
||||||
|
if (currentInverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
|
||||||
|
// we previously had an index saved as InverterId. fall back to the
|
||||||
|
// respective positional lookup if InverterId is not a known serial.
|
||||||
|
currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
||||||
|
}
|
||||||
|
|
||||||
// in case of (newly) broken configuration, shut down
|
// in case of (newly) broken configuration, shut down
|
||||||
// the last inverter we worked with (if any)
|
// the last inverter we worked with (if any)
|
||||||
@ -224,38 +241,32 @@ void PowerLimiterClass::loop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery charging cycle conditions
|
auto getBatteryPower = [this,&config]() -> bool {
|
||||||
// First we always disable discharge if the battery is empty
|
if (config.PowerLimiter.IsInverterSolarPowered) { return false; }
|
||||||
if (isStopThresholdReached()) {
|
|
||||||
// Disable battery discharge when empty
|
|
||||||
_batteryDischargeEnabled = false;
|
|
||||||
} else {
|
|
||||||
// UI: Solar Passthrough Enabled -> false
|
|
||||||
// Battery discharge can be enabled when start threshold is reached
|
|
||||||
if (!config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached()) {
|
|
||||||
_batteryDischargeEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT
|
if (isStopThresholdReached()) { return false; }
|
||||||
if (config.PowerLimiter.SolarPassThroughEnabled && config.PowerLimiter.BatteryDrainStategy == EMPTY_AT_NIGHT) {
|
|
||||||
if(isStartThresholdReached()) {
|
if (isStartThresholdReached()) { return true; }
|
||||||
// In this case we should only discharge the battery as long it is above startThreshold
|
|
||||||
_batteryDischargeEnabled = true;
|
// with solar passthrough, and the respective switch enabled, we
|
||||||
|
// may start discharging the battery when it is nighttime. we also
|
||||||
|
// stop the discharge cycle if it becomes daytime again.
|
||||||
|
// TODO(schlimmchen): should be supported by sunrise and sunset, such
|
||||||
|
// that a thunderstorm or other events that drastically lower the solar
|
||||||
|
// power do not cause the start of a discharge cycle during the day.
|
||||||
|
if (config.PowerLimiter.SolarPassThroughEnabled &&
|
||||||
|
config.PowerLimiter.BatteryAlwaysUseAtNight) {
|
||||||
|
return getSolarPower() == 0;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// In this case we should only discharge the battery when there is no sunshine
|
|
||||||
_batteryDischargeEnabled = !canUseDirectSolarPower();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL
|
// we are between start and stop threshold and keep the state that was
|
||||||
// Battery discharge can be enabled when start threshold is reached
|
// last triggered, either charging or discharging.
|
||||||
if (config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter.BatteryDrainStategy == EMPTY_WHEN_FULL) {
|
return _batteryDischargeEnabled;
|
||||||
_batteryDischargeEnabled = true;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_verboseLogging) {
|
_batteryDischargeEnabled = getBatteryPower();
|
||||||
|
|
||||||
|
if (_verboseLogging && !config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n",
|
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n",
|
||||||
(config.Battery.Enabled?"enabled":"disabled"),
|
(config.Battery.Enabled?"enabled":"disabled"),
|
||||||
Battery.getStats()->getSoC(),
|
Battery.getStats()->getSoC(),
|
||||||
@ -270,24 +281,15 @@ void PowerLimiterClass::loop()
|
|||||||
config.PowerLimiter.VoltageStartThreshold,
|
config.PowerLimiter.VoltageStartThreshold,
|
||||||
config.PowerLimiter.VoltageStopThreshold);
|
config.PowerLimiter.VoltageStopThreshold);
|
||||||
|
|
||||||
MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, inverter %s producing\r\n",
|
MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, SolarPT %sabled, use at night: %s\r\n",
|
||||||
(isStartThresholdReached()?"yes":"no"),
|
(isStartThresholdReached()?"yes":"no"),
|
||||||
(isStopThresholdReached()?"yes":"no"),
|
(isStopThresholdReached()?"yes":"no"),
|
||||||
(_inverter->isProducing()?"is":"is NOT"));
|
(config.PowerLimiter.SolarPassThroughEnabled?"en":"dis"),
|
||||||
|
(config.PowerLimiter.BatteryAlwaysUseAtNight?"yes":"no"));
|
||||||
MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n",
|
};
|
||||||
(config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"),
|
|
||||||
config.PowerLimiter.BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no"));
|
|
||||||
|
|
||||||
MessageOutput.printf("[DPL::loop] battery discharging %s, PowerMeter: %d W, target consumption: %d W\r\n",
|
|
||||||
(_batteryDischargeEnabled?"allowed":"prevented"),
|
|
||||||
static_cast<int32_t>(round(PowerMeter.getPowerTotal())),
|
|
||||||
config.PowerLimiter.TargetPowerConsumption);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!)
|
// Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!)
|
||||||
int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled);
|
bool limitUpdated = calcPowerLimit(_inverter, getSolarPower(), _batteryDischargeEnabled);
|
||||||
bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit);
|
|
||||||
|
|
||||||
_lastCalculation = millis();
|
_lastCalculation = millis();
|
||||||
|
|
||||||
@ -310,7 +312,7 @@ void PowerLimiterClass::loop()
|
|||||||
float PowerLimiterClass::getBatteryVoltage(bool log) {
|
float PowerLimiterClass::getBatteryVoltage(bool log) {
|
||||||
if (!_inverter) {
|
if (!_inverter) {
|
||||||
// there should be no need to call this method if no target inverter is known
|
// there should be no need to call this method if no target inverter is known
|
||||||
MessageOutput.println("DPL getBatteryVoltage: no inverter (programmer error)");
|
MessageOutput.println("[DPL::getBatteryVoltage] no inverter (programmer error)");
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,47 +403,59 @@ uint8_t PowerLimiterClass::getPowerLimiterState() {
|
|||||||
return PL_UI_STATE_INACTIVE;
|
return PL_UI_STATE_INACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PowerLimiterClass::canUseDirectSolarPower()
|
|
||||||
{
|
|
||||||
CONFIG_T& config = Configuration.get();
|
|
||||||
|
|
||||||
if (!config.PowerLimiter.SolarPassThroughEnabled
|
|
||||||
|| isBelowStopThreshold()
|
|
||||||
|| !VictronMppt.isDataValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return VictronMppt.getPowerOutputWatts() >= 20; // enough power?
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Logic table
|
// Logic table
|
||||||
// | Case # | batteryDischargeEnabled | solarPowerEnabled | useFullSolarPassthrough | Result |
|
// | Case # | batteryPower | solarPower > 0 | useFullSolarPassthrough | Result |
|
||||||
// | 1 | false | false | doesn't matter | PL = 0 |
|
// | 1 | false | false | doesn't matter | PL = 0 |
|
||||||
// | 2 | false | true | doesn't matter | PL = Victron Power |
|
// | 2 | false | true | doesn't matter | PL = Victron Power |
|
||||||
// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) |
|
// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) |
|
||||||
// | 4 | true | false | true | PL = PowerMeter value |
|
// | 4 | true | false | true | PL = PowerMeter value |
|
||||||
// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) |
|
// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) |
|
||||||
|
|
||||||
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool solarPowerEnabled, bool batteryDischargeEnabled)
|
bool PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPowerDC, bool batteryPower)
|
||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("[DPL::calcPowerLimit] battery use %s, solar power (DC): %d W\r\n",
|
||||||
int32_t acPower = 0;
|
(batteryPower?"allowed":"prevented"), solarPowerDC);
|
||||||
int32_t newPowerLimit = round(PowerMeter.getPowerTotal());
|
|
||||||
|
|
||||||
if (!solarPowerEnabled && !batteryDischargeEnabled) {
|
|
||||||
// Case 1 - No energy sources available
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (solarPowerDC <= 0 && !batteryPower) {
|
||||||
|
return shutdown(Status::NoEnergy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check if the PSU is on and disable the Power Limiter in this case.
|
||||||
|
// The PSU should reduce power or shut down first before the Power Limiter
|
||||||
|
// kicks in. The only case where this is not desired is if the battery is
|
||||||
|
// over the Full Solar Passthrough Threshold. In this case the Power
|
||||||
|
// Limiter should run and the PSU will shut down as a consequence.
|
||||||
|
if (!useFullSolarPassthrough() && HuaweiCan.getAutoPowerStatus()) {
|
||||||
|
return shutdown(Status::HuaweiPsu);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto powerMeter = static_cast<int32_t>(PowerMeter.getPowerTotal());
|
||||||
|
|
||||||
|
auto inverterOutput = static_cast<int32_t>(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC));
|
||||||
|
|
||||||
|
auto solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC);
|
||||||
|
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("[DPL::calcPowerLimit] power meter: %d W, "
|
||||||
|
"target consumption: %d W, inverter output: %d W, solar power (AC): %d\r\n",
|
||||||
|
powerMeter,
|
||||||
|
config.PowerLimiter.TargetPowerConsumption,
|
||||||
|
inverterOutput,
|
||||||
|
solarPowerAC);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto newPowerLimit = powerMeter;
|
||||||
|
|
||||||
if (config.PowerLimiter.IsInverterBehindPowerMeter) {
|
if (config.PowerLimiter.IsInverterBehindPowerMeter) {
|
||||||
// If the inverter the behind the power meter (part of measurement),
|
// If the inverter the behind the power meter (part of measurement),
|
||||||
// the produced power of this inverter has also to be taken into account.
|
// the produced power of this inverter has also to be taken into account.
|
||||||
// We don't use FLD_PAC from the statistics, because that
|
// We don't use FLD_PAC from the statistics, because that
|
||||||
// data might be too old and unreliable.
|
// data might be too old and unreliable.
|
||||||
acPower = static_cast<int>(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC));
|
newPowerLimit += inverterOutput;
|
||||||
newPowerLimit += acPower;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're not trying to hit 0 exactly but take an offset into account
|
// We're not trying to hit 0 exactly but take an offset into account
|
||||||
@ -449,38 +463,37 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
|
|||||||
// Case 3
|
// Case 3
|
||||||
newPowerLimit -= config.PowerLimiter.TargetPowerConsumption;
|
newPowerLimit -= config.PowerLimiter.TargetPowerConsumption;
|
||||||
|
|
||||||
// At this point we've calculated the required energy to compensate for household consumption.
|
if (!batteryPower) {
|
||||||
// If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power
|
newPowerLimit = std::min(newPowerLimit, solarPowerAC);
|
||||||
// The next step is to determine if the Solar power as provided by the Victron charger
|
|
||||||
// actually constrains or dictates another inverter power value
|
|
||||||
int32_t adjustedVictronChargePower = inverterPowerDcToAc(inverter, getSolarChargePower());
|
|
||||||
|
|
||||||
// Battery can be discharged and we should output max (Victron solar power || power meter value)
|
// do not drain the battery. use as much power as needed to match the
|
||||||
if(batteryDischargeEnabled && useFullSolarPassthrough()) {
|
// household consumption, but not more than the available solar power.
|
||||||
// Case 5
|
|
||||||
newPowerLimit = newPowerLimit > adjustedVictronChargePower ? newPowerLimit : adjustedVictronChargePower;
|
|
||||||
} else {
|
|
||||||
// We check if the PSU is on and disable the Power Limiter in this case.
|
|
||||||
// The PSU should reduce power or shut down first before the Power Limiter kicks in
|
|
||||||
// The only case where this is not desired is if the battery is over the Full Solar Passthrough Threshold
|
|
||||||
// In this case the Power Limiter should start. The PSU will shutdown when the Power Limiter is active
|
|
||||||
if (HuaweiCan.getAutoPowerStatus()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should use Victron solar power only (corrected by efficiency factor)
|
|
||||||
if (solarPowerEnabled && !batteryDischargeEnabled) {
|
|
||||||
// Case 2 - Limit power to solar power only
|
|
||||||
if (_verboseLogging) {
|
if (_verboseLogging) {
|
||||||
MessageOutput.printf("[DPL::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d W, newPowerLimit: %d W\r\n",
|
MessageOutput.printf("[DPL::calcPowerLimit] limited to solar power: %d W\r\n",
|
||||||
adjustedVictronChargePower, newPowerLimit);
|
newPowerLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower);
|
return setNewPowerLimit(inverter, newPowerLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPowerLimit;
|
// convert all solar power if full solar-passthrough is active
|
||||||
|
if (useFullSolarPassthrough()) {
|
||||||
|
newPowerLimit = std::max(newPowerLimit, solarPowerAC);
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("[DPL::calcPowerLimit] full solar-passthrough active: %d W\r\n",
|
||||||
|
newPowerLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setNewPowerLimit(inverter, newPowerLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("[DPL::calcPowerLimit] match power meter with limit of %d W\r\n",
|
||||||
|
newPowerLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setNewPowerLimit(inverter, newPowerLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -571,7 +584,7 @@ bool PowerLimiterClass::updateInverter()
|
|||||||
uint32_t lastLimitCommandMillis = _inverter->SystemConfigPara()->getLastUpdateCommand();
|
uint32_t lastLimitCommandMillis = _inverter->SystemConfigPara()->getLastUpdateCommand();
|
||||||
if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis &&
|
if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis &&
|
||||||
CMD_OK == lastLimitCommandState) {
|
CMD_OK == lastLimitCommandState) {
|
||||||
MessageOutput.printf("[DPL:updateInverter] actual limit is %.1f %% "
|
MessageOutput.printf("[DPL::updateInverter] actual limit is %.1f %% "
|
||||||
"(%.0f W respectively), effective %d ms after update started, "
|
"(%.0f W respectively), effective %d ms after update started, "
|
||||||
"requested were %.1f %%\r\n",
|
"requested were %.1f %%\r\n",
|
||||||
currentRelativeLimit,
|
currentRelativeLimit,
|
||||||
@ -580,7 +593,7 @@ bool PowerLimiterClass::updateInverter()
|
|||||||
newRelativeLimit);
|
newRelativeLimit);
|
||||||
|
|
||||||
if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) {
|
if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) {
|
||||||
MessageOutput.printf("[DPL:updateInverter] NOTE: expected limit of %.1f %% "
|
MessageOutput.printf("[DPL::updateInverter] NOTE: expected limit of %.1f %% "
|
||||||
"and actual limit of %.1f %% mismatch by more than 2 %%, "
|
"and actual limit of %.1f %% mismatch by more than 2 %%, "
|
||||||
"is the DPL in exclusive control over the inverter?\r\n",
|
"is the DPL in exclusive control over the inverter?\r\n",
|
||||||
newRelativeLimit, currentRelativeLimit);
|
newRelativeLimit, currentRelativeLimit);
|
||||||
@ -657,9 +670,10 @@ static int32_t scalePowerLimit(std::shared_ptr<InverterAbstract> inverter, int32
|
|||||||
|
|
||||||
if (dcProdChnls == 0 || dcProdChnls == dcTotalChnls) { return newLimit; }
|
if (dcProdChnls == 0 || dcProdChnls == dcTotalChnls) { return newLimit; }
|
||||||
|
|
||||||
MessageOutput.printf("[DPL::scalePowerLimit] %d channels total, %d producing "
|
auto scaled = static_cast<int32_t>(newLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
|
||||||
"channels, scaling power limit\r\n", dcTotalChnls, dcProdChnls);
|
MessageOutput.printf("[DPL::scalePowerLimit] %d/%d channels are producing, "
|
||||||
return round(newLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
|
"scaling from %d to %d W\r\n", dcProdChnls, dcTotalChnls, newLimit, scaled);
|
||||||
|
return scaled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -670,17 +684,23 @@ static int32_t scalePowerLimit(std::shared_ptr<InverterAbstract> inverter, int32
|
|||||||
*/
|
*/
|
||||||
bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
|
bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
|
||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
auto const& config = Configuration.get();
|
||||||
|
auto lowerLimit = config.PowerLimiter.LowerPowerLimit;
|
||||||
|
auto upperLimit = config.PowerLimiter.UpperPowerLimit;
|
||||||
|
auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
||||||
|
|
||||||
// Stop the inverter if limit is below threshold.
|
if (_verboseLogging) {
|
||||||
if (newPowerLimit < config.PowerLimiter.LowerPowerLimit) {
|
MessageOutput.printf("[DPL::setNewPowerLimit] input limit: %d W, "
|
||||||
// the status must not change outside of loop(). this condition is
|
"lower limit: %d W, upper limit: %d W, hysteresis: %d W\r\n",
|
||||||
// communicated through log messages already.
|
newPowerLimit, lowerLimit, upperLimit, hysteresis);
|
||||||
return shutdown();
|
}
|
||||||
|
|
||||||
|
if (newPowerLimit < lowerLimit) {
|
||||||
|
return shutdown(Status::CalculatedLimitBelowMinLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforce configured upper power limit
|
// enforce configured upper power limit
|
||||||
int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter.UpperPowerLimit);
|
int32_t effPowerLimit = std::min(newPowerLimit, upperLimit);
|
||||||
|
|
||||||
// early in the loop we make it a pre-requisite that this
|
// early in the loop we make it a pre-requisite that this
|
||||||
// value is non-zero, so we can assume it to be valid.
|
// value is non-zero, so we can assume it to be valid.
|
||||||
@ -694,12 +714,12 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver
|
|||||||
effPowerLimit = std::min<int32_t>(effPowerLimit, maxPower);
|
effPowerLimit = std::min<int32_t>(effPowerLimit, maxPower);
|
||||||
|
|
||||||
auto diff = std::abs(currentLimitAbs - effPowerLimit);
|
auto diff = std::abs(currentLimitAbs - effPowerLimit);
|
||||||
auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
|
||||||
|
|
||||||
if (_verboseLogging) {
|
if (_verboseLogging) {
|
||||||
MessageOutput.printf("[DPL::setNewPowerLimit] calculated: %d W, "
|
MessageOutput.printf("[DPL::setNewPowerLimit] inverter max: %d W, "
|
||||||
"requesting: %d W, reported: %d W, diff: %d W, hysteresis: %d W\r\n",
|
"inverter %s producing, requesting: %d W, reported: %d W, "
|
||||||
newPowerLimit, effPowerLimit, currentLimitAbs, diff, hysteresis);
|
"diff: %d W\r\n", maxPower, (inverter->isProducing()?"is":"is NOT"),
|
||||||
|
effPowerLimit, currentLimitAbs, diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff > hysteresis) {
|
if (diff > hysteresis) {
|
||||||
@ -710,20 +730,33 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver
|
|||||||
return updateInverter();
|
return updateInverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t PowerLimiterClass::getSolarChargePower()
|
int32_t PowerLimiterClass::getSolarPower()
|
||||||
{
|
{
|
||||||
if (!canUseDirectSolarPower()) {
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
|
// the returned value is arbitrary, as long as it's
|
||||||
|
// greater than the inverters max DC power consumption.
|
||||||
|
return 10 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.PowerLimiter.SolarPassThroughEnabled
|
||||||
|
|| isBelowStopThreshold()
|
||||||
|
|| !VictronMppt.isDataValid()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return VictronMppt.getPowerOutputWatts();
|
auto solarPower = VictronMppt.getPowerOutputWatts();
|
||||||
|
if (solarPower < 20) { return 0; } // too little to work with
|
||||||
|
|
||||||
|
return solarPower;
|
||||||
}
|
}
|
||||||
|
|
||||||
float PowerLimiterClass::getLoadCorrectedVoltage()
|
float PowerLimiterClass::getLoadCorrectedVoltage()
|
||||||
{
|
{
|
||||||
if (!_inverter) {
|
if (!_inverter) {
|
||||||
// there should be no need to call this method if no target inverter is known
|
// there should be no need to call this method if no target inverter is known
|
||||||
MessageOutput.println("DPL getLoadCorrectedVoltage: no inverter (programmer error)");
|
MessageOutput.println("[DPL::getLoadCorrectedVoltage] no inverter (programmer error)");
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -805,6 +838,12 @@ void PowerLimiterClass::calcNextInverterRestart()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
|
_nextInverterRestart = 1;
|
||||||
|
MessageOutput.println("[DPL::calcNextInverterRestart] not restarting solar-powered inverters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// read time from timeserver, if time is not synced then return
|
// read time from timeserver, if time is not synced then return
|
||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
if (getLocalTime(&timeinfo, 5)) {
|
if (getLocalTime(&timeinfo, 5)) {
|
||||||
@ -835,12 +874,13 @@ void PowerLimiterClass::calcNextInverterRestart()
|
|||||||
|
|
||||||
bool PowerLimiterClass::useFullSolarPassthrough()
|
bool PowerLimiterClass::useFullSolarPassthrough()
|
||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
// solar passthrough only applies to setups with battery-powered inverters
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) { return false; }
|
||||||
|
|
||||||
// We only do full solar PT if general solar PT is enabled
|
// We only do full solar PT if general solar PT is enabled
|
||||||
if(!config.PowerLimiter.SolarPassThroughEnabled) {
|
if(!config.PowerLimiter.SolarPassThroughEnabled) { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc,
|
if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc,
|
||||||
config.PowerLimiter.FullSolarPassThroughStartVoltage,
|
config.PowerLimiter.FullSolarPassThroughStartVoltage,
|
||||||
|
|||||||
@ -4,20 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
#include "PowerMeter.h"
|
#include "PowerMeter.h"
|
||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
|
#include "PinMapping.h"
|
||||||
#include "HttpPowerMeter.h"
|
#include "HttpPowerMeter.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "NetworkSettings.h"
|
#include "NetworkSettings.h"
|
||||||
#include "SDM.h"
|
|
||||||
#include "MessageOutput.h"
|
#include "MessageOutput.h"
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <SoftwareSerial.h>
|
#include <SMA_HM.h>
|
||||||
|
|
||||||
PowerMeterClass PowerMeter;
|
PowerMeterClass PowerMeter;
|
||||||
|
|
||||||
SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN);
|
|
||||||
|
|
||||||
SoftwareSerial inputSerial;
|
|
||||||
|
|
||||||
void PowerMeterClass::init(Scheduler& scheduler)
|
void PowerMeterClass::init(Scheduler& scheduler)
|
||||||
{
|
{
|
||||||
scheduler.addTask(_loopTask);
|
scheduler.addTask(_loopTask);
|
||||||
@ -37,8 +33,12 @@ void PowerMeterClass::init(Scheduler& scheduler)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(config.PowerMeter.Source) {
|
const PinMapping_t& pin = PinMapping.get();
|
||||||
case SOURCE_MQTT: {
|
MessageOutput.printf("[PowerMeter] rx = %d, tx = %d, dere = %d\r\n",
|
||||||
|
pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere);
|
||||||
|
|
||||||
|
switch(static_cast<Source>(config.PowerMeter.Source)) {
|
||||||
|
case Source::MQTT: {
|
||||||
auto subscribe = [this](char const* topic, float* target) {
|
auto subscribe = [this](char const* topic, float* target) {
|
||||||
if (strlen(topic) == 0) { return; }
|
if (strlen(topic) == 0) { return; }
|
||||||
MqttSettings.subscribe(topic, 0,
|
MqttSettings.subscribe(topic, 0,
|
||||||
@ -56,21 +56,38 @@ void PowerMeterClass::init(Scheduler& scheduler)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case SOURCE_SDM1PH:
|
case Source::SDM1PH:
|
||||||
case SOURCE_SDM3PH:
|
case Source::SDM3PH:
|
||||||
sdm.begin();
|
if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) {
|
||||||
|
MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_upSdm = std::make_unique<SDM>(Serial2, 9600, pin.powermeter_dere,
|
||||||
|
SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx);
|
||||||
|
_upSdm->begin();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SOURCE_HTTP:
|
case Source::HTTP:
|
||||||
HttpPowerMeter.init();
|
HttpPowerMeter.init();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SOURCE_SML:
|
case Source::SML:
|
||||||
pinMode(SML_RX_PIN, INPUT);
|
if (pin.powermeter_rx < 0) {
|
||||||
inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95);
|
MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)");
|
||||||
inputSerial.enableRx(true);
|
return;
|
||||||
inputSerial.enableTx(false);
|
}
|
||||||
inputSerial.flush();
|
|
||||||
|
pinMode(pin.powermeter_rx, INPUT);
|
||||||
|
_upSmlSerial = std::make_unique<SoftwareSerial>();
|
||||||
|
_upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95);
|
||||||
|
_upSmlSerial->enableRx(true);
|
||||||
|
_upSmlSerial->enableTx(false);
|
||||||
|
_upSmlSerial->flush();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Source::SMAHM2:
|
||||||
|
SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,30 +125,34 @@ float PowerMeterClass::getPowerTotal(bool forceUpdate)
|
|||||||
readPowerMeter();
|
readPowerMeter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
|
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
|
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
return _lastPowerMeterUpdate;
|
return _lastPowerMeterUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerMeterClass::mqtt()
|
void PowerMeterClass::mqtt()
|
||||||
{
|
{
|
||||||
if (!MqttSettings.getConnected()) {
|
if (!MqttSettings.getConnected()) { return; }
|
||||||
return;
|
|
||||||
} else {
|
String topic = "powermeter";
|
||||||
String topic = "powermeter";
|
auto totalPower = getPowerTotal();
|
||||||
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
|
|
||||||
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power));
|
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
|
||||||
MqttSettings.publish(topic + "/powertotal", String(getPowerTotal()));
|
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
|
||||||
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage));
|
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power));
|
||||||
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage));
|
MqttSettings.publish(topic + "/powertotal", String(totalPower));
|
||||||
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage));
|
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage));
|
||||||
MqttSettings.publish(topic + "/import", String(_powerMeterImport));
|
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage));
|
||||||
MqttSettings.publish(topic + "/export", String(_powerMeterExport));
|
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage));
|
||||||
}
|
MqttSettings.publish(topic + "/import", String(_powerMeterImport));
|
||||||
|
MqttSettings.publish(topic + "/export", String(_powerMeterExport));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerMeterClass::loop()
|
void PowerMeterClass::loop()
|
||||||
@ -141,12 +162,10 @@ void PowerMeterClass::loop()
|
|||||||
|
|
||||||
if (!config.PowerMeter.Enabled) { return; }
|
if (!config.PowerMeter.Enabled) { return; }
|
||||||
|
|
||||||
if (config.PowerMeter.Source == SOURCE_SML) {
|
if (static_cast<Source>(config.PowerMeter.Source) == Source::SML &&
|
||||||
if (!smlReadLoop()) {
|
nullptr != _upSmlSerial) {
|
||||||
return;
|
if (!smlReadLoop()) { return; }
|
||||||
} else {
|
_lastPowerMeterUpdate = millis();
|
||||||
_lastPowerMeterUpdate = millis();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) {
|
if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) {
|
||||||
@ -167,44 +186,77 @@ void PowerMeterClass::readPowerMeter()
|
|||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
uint8_t _address = config.PowerMeter.SdmAddress;
|
uint8_t _address = config.PowerMeter.SdmAddress;
|
||||||
|
Source configuredSource = static_cast<Source>(config.PowerMeter.Source);
|
||||||
|
|
||||||
if (config.PowerMeter.Source == SOURCE_SDM1PH) {
|
if (configuredSource == Source::SDM1PH) {
|
||||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
if (!_upSdm) { return; }
|
||||||
_powerMeter2Power = 0.0;
|
|
||||||
_powerMeter3Power = 0.0;
|
// this takes a "very long" time as each readVal() is a synchronous
|
||||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
// exchange of serial messages. cache the values and write later.
|
||||||
_powerMeter2Voltage = 0.0;
|
auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address);
|
||||||
_powerMeter3Voltage = 0.0;
|
auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address);
|
||||||
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
|
||||||
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
|
_powerMeter1Power = static_cast<float>(phase1Power);
|
||||||
|
_powerMeter2Power = 0;
|
||||||
|
_powerMeter3Power = 0;
|
||||||
|
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
|
||||||
|
_powerMeter2Voltage = 0;
|
||||||
|
_powerMeter3Voltage = 0;
|
||||||
|
_powerMeterImport = static_cast<float>(energyImport);
|
||||||
|
_powerMeterExport = static_cast<float>(energyExport);
|
||||||
_lastPowerMeterUpdate = millis();
|
_lastPowerMeterUpdate = millis();
|
||||||
}
|
}
|
||||||
else if (config.PowerMeter.Source == SOURCE_SDM3PH) {
|
else if (configuredSource == Source::SDM3PH) {
|
||||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
if (!_upSdm) { return; }
|
||||||
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
|
|
||||||
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
|
// this takes a "very long" time as each readVal() is a synchronous
|
||||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
// exchange of serial messages. cache the values and write later.
|
||||||
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
|
auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address);
|
||||||
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
|
auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address);
|
||||||
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address);
|
||||||
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address);
|
||||||
|
auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address);
|
||||||
|
auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address);
|
||||||
|
auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
|
||||||
|
auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
|
_powerMeter1Power = static_cast<float>(phase1Power);
|
||||||
|
_powerMeter2Power = static_cast<float>(phase2Power);
|
||||||
|
_powerMeter3Power = static_cast<float>(phase3Power);
|
||||||
|
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
|
||||||
|
_powerMeter2Voltage = static_cast<float>(phase2Voltage);
|
||||||
|
_powerMeter3Voltage = static_cast<float>(phase3Voltage);
|
||||||
|
_powerMeterImport = static_cast<float>(energyImport);
|
||||||
|
_powerMeterExport = static_cast<float>(energyExport);
|
||||||
_lastPowerMeterUpdate = millis();
|
_lastPowerMeterUpdate = millis();
|
||||||
}
|
}
|
||||||
else if (config.PowerMeter.Source == SOURCE_HTTP) {
|
else if (configuredSource == Source::HTTP) {
|
||||||
if (HttpPowerMeter.updateValues()) {
|
if (HttpPowerMeter.updateValues()) {
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
_powerMeter1Power = HttpPowerMeter.getPower(1);
|
_powerMeter1Power = HttpPowerMeter.getPower(1);
|
||||||
_powerMeter2Power = HttpPowerMeter.getPower(2);
|
_powerMeter2Power = HttpPowerMeter.getPower(2);
|
||||||
_powerMeter3Power = HttpPowerMeter.getPower(3);
|
_powerMeter3Power = HttpPowerMeter.getPower(3);
|
||||||
_lastPowerMeterUpdate = millis();
|
_lastPowerMeterUpdate = millis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (configuredSource == Source::SMAHM2) {
|
||||||
|
std::lock_guard<std::mutex> l(_mutex);
|
||||||
|
_powerMeter1Power = SMA_HM.getPowerL1();
|
||||||
|
_powerMeter2Power = SMA_HM.getPowerL2();
|
||||||
|
_powerMeter3Power = SMA_HM.getPowerL3();
|
||||||
|
_lastPowerMeterUpdate = millis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PowerMeterClass::smlReadLoop()
|
bool PowerMeterClass::smlReadLoop()
|
||||||
{
|
{
|
||||||
while (inputSerial.available()) {
|
while (_upSmlSerial->available()) {
|
||||||
double readVal = 0;
|
double readVal = 0;
|
||||||
unsigned char smlCurrentChar = inputSerial.read();
|
unsigned char smlCurrentChar = _upSmlSerial->read();
|
||||||
sml_states_t smlCurrentState = smlState(smlCurrentChar);
|
sml_states_t smlCurrentState = smlState(smlCurrentChar);
|
||||||
if (smlCurrentState == SML_LISTEND) {
|
if (smlCurrentState == SML_LISTEND) {
|
||||||
for (auto& handler: smlHandlerList) {
|
for (auto& handler: smlHandlerList) {
|
||||||
|
|||||||
203
src/SMA_HM.cpp
Normal file
203
src/SMA_HM.cpp
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Holger-Steffen Stapf
|
||||||
|
*/
|
||||||
|
#include "SMA_HM.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "NetworkSettings.h"
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
|
||||||
|
unsigned int multicastPort = 9522; // local port to listen on
|
||||||
|
IPAddress multicastIP(239, 12, 255, 254);
|
||||||
|
WiFiUDP SMAUdp;
|
||||||
|
|
||||||
|
constexpr uint32_t interval = 1000;
|
||||||
|
|
||||||
|
SMA_HMClass SMA_HM;
|
||||||
|
|
||||||
|
void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif,
|
||||||
|
char const* name, float value, uint32_t timestamp)
|
||||||
|
{
|
||||||
|
if (!_verboseLogging) { return; }
|
||||||
|
|
||||||
|
MessageOutput.printf("SMA_HM: %s = %.1f (timestamp %d)\r\n",
|
||||||
|
name, value, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SMA_HMClass::init(Scheduler& scheduler, bool verboseLogging)
|
||||||
|
{
|
||||||
|
_verboseLogging = verboseLogging;
|
||||||
|
scheduler.addTask(_loopTask);
|
||||||
|
_loopTask.setCallback(std::bind(&SMA_HMClass::loop, this));
|
||||||
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
|
_loopTask.enable();
|
||||||
|
SMAUdp.begin(multicastPort);
|
||||||
|
SMAUdp.beginMulticast(multicastIP, multicastPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SMA_HMClass::loop()
|
||||||
|
{
|
||||||
|
uint32_t currentMillis = millis();
|
||||||
|
if (currentMillis - _previousMillis >= interval) {
|
||||||
|
_previousMillis = currentMillis;
|
||||||
|
event1();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen)
|
||||||
|
{
|
||||||
|
float Pbezug = 0;
|
||||||
|
float BezugL1 = 0;
|
||||||
|
float BezugL2 = 0;
|
||||||
|
float BezugL3 = 0;
|
||||||
|
float Peinspeisung = 0;
|
||||||
|
float EinspeisungL1 = 0;
|
||||||
|
float EinspeisungL2 = 0;
|
||||||
|
float EinspeisungL3 = 0;
|
||||||
|
|
||||||
|
uint8_t* endOfGroup = offset + grouplen;
|
||||||
|
|
||||||
|
// not used: uint16_t protocolID = (offset[0] << 8) + offset[1];
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
// not used: uint16_t susyID = (offset[0] << 8) + offset[1];
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
_serial = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3];
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
uint32_t timestamp = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3];
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
unsigned count = 0;
|
||||||
|
while (offset < endOfGroup) {
|
||||||
|
uint8_t kanal = offset[0];
|
||||||
|
uint8_t index = offset[1];
|
||||||
|
uint8_t art = offset[2];
|
||||||
|
uint8_t tarif = offset[3];
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (kanal == 144) {
|
||||||
|
// Optional: Versionsnummer auslesen... aber interessiert die?
|
||||||
|
offset += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (art == 8) {
|
||||||
|
offset += 8;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (art == 4) {
|
||||||
|
uint32_t data = (offset[0] << 24) +
|
||||||
|
(offset[1] << 16) +
|
||||||
|
(offset[2] << 8) +
|
||||||
|
offset[3];
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
switch (index) {
|
||||||
|
case (1):
|
||||||
|
Pbezug = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (2):
|
||||||
|
Peinspeisung = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (21):
|
||||||
|
BezugL1 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (22):
|
||||||
|
EinspeisungL1 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (41):
|
||||||
|
BezugL2 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (42):
|
||||||
|
EinspeisungL2 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (61):
|
||||||
|
BezugL3 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case (62):
|
||||||
|
EinspeisungL3 = data * 0.1;
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 8) {
|
||||||
|
_powerMeterPower = Pbezug - Peinspeisung;
|
||||||
|
_powerMeterL1 = BezugL1 - EinspeisungL1;
|
||||||
|
_powerMeterL2 = BezugL2 - EinspeisungL2;
|
||||||
|
_powerMeterL3 = BezugL3 - EinspeisungL3;
|
||||||
|
Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp);
|
||||||
|
Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp);
|
||||||
|
Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp);
|
||||||
|
Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp);
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageOutput.printf("SMA_HM: Skipped unknown measurement: %d %d %d %d\r\n",
|
||||||
|
kanal, index, art, tarif);
|
||||||
|
offset += art;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SMA_HMClass::event1()
|
||||||
|
{
|
||||||
|
int packetSize = SMAUdp.parsePacket();
|
||||||
|
if (!packetSize) { return; }
|
||||||
|
|
||||||
|
uint8_t buffer[1024];
|
||||||
|
int rSize = SMAUdp.read(buffer, 1024);
|
||||||
|
if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') {
|
||||||
|
MessageOutput.println("SMA_HM: Not an SMA packet?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t grouplen;
|
||||||
|
uint16_t grouptag;
|
||||||
|
uint8_t* offset = buffer + 4; // skips the header 'SMA\0'
|
||||||
|
|
||||||
|
do {
|
||||||
|
grouplen = (offset[0] << 8) + offset[1];
|
||||||
|
grouptag = (offset[2] << 8) + offset[3];
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (grouplen == 0xffff) return;
|
||||||
|
|
||||||
|
if (grouptag == 0x02A0 && grouplen == 4) {
|
||||||
|
offset += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grouptag == 0x0010) {
|
||||||
|
offset = decodeGroup(offset, grouplen);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grouptag == 0) {
|
||||||
|
// end marker
|
||||||
|
offset += grouplen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageOutput.printf("SMA_HM: Unhandled group 0x%04x with length %d\r\n",
|
||||||
|
grouptag, grouplen);
|
||||||
|
offset += grouplen;
|
||||||
|
} while (grouplen > 0 && offset + 4 < buffer + rSize);
|
||||||
|
}
|
||||||
60
src/SerialPortManager.cpp
Normal file
60
src/SerialPortManager.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include "SerialPortManager.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
|
||||||
|
#define MAX_CONTROLLERS 3
|
||||||
|
|
||||||
|
SerialPortManagerClass SerialPortManager;
|
||||||
|
|
||||||
|
bool SerialPortManagerClass::allocateBatteryPort(int port)
|
||||||
|
{
|
||||||
|
return allocatePort(port, Owner::BATTERY);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SerialPortManagerClass::allocateMpptPort(int port)
|
||||||
|
{
|
||||||
|
return allocatePort(port, Owner::MPPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SerialPortManagerClass::allocatePort(uint8_t port, Owner owner)
|
||||||
|
{
|
||||||
|
if (port >= MAX_CONTROLLERS) {
|
||||||
|
MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allocatedPorts.insert({port, owner}).second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialPortManagerClass::invalidateBatteryPort()
|
||||||
|
{
|
||||||
|
invalidate(Owner::BATTERY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialPortManagerClass::invalidateMpptPorts()
|
||||||
|
{
|
||||||
|
invalidate(Owner::MPPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialPortManagerClass::invalidate(Owner owner)
|
||||||
|
{
|
||||||
|
for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) {
|
||||||
|
if (it->second == owner) {
|
||||||
|
MessageOutput.printf("[SerialPortManager] Removing port = %d, owner = %s \r\n", it->first, print(owner));
|
||||||
|
it = allocatedPorts.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* SerialPortManagerClass::print(Owner owner)
|
||||||
|
{
|
||||||
|
switch (owner) {
|
||||||
|
case BATTERY:
|
||||||
|
return "BATTERY";
|
||||||
|
case MPPT:
|
||||||
|
return "MPPT";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
@ -79,6 +79,16 @@ bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Utils::checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line)
|
||||||
|
{
|
||||||
|
if (doc.overflowed()) {
|
||||||
|
MessageOutput.printf("DynamicJsonDocument overflowed: %s, %d\r\n", function, line);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// @brief Remove all files but the PINMAPPING_FILENAME
|
/// @brief Remove all files but the PINMAPPING_FILENAME
|
||||||
void Utils::removeAllFiles()
|
void Utils::removeAllFiles()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,13 +3,14 @@
|
|||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "PinMapping.h"
|
#include "PinMapping.h"
|
||||||
#include "MessageOutput.h"
|
#include "MessageOutput.h"
|
||||||
|
#include "SerialPortManager.h"
|
||||||
|
|
||||||
VictronMpptClass VictronMppt;
|
VictronMpptClass VictronMppt;
|
||||||
|
|
||||||
void VictronMpptClass::init(Scheduler& scheduler)
|
void VictronMpptClass::init(Scheduler& scheduler)
|
||||||
{
|
{
|
||||||
scheduler.addTask(_loopTask);
|
scheduler.addTask(_loopTask);
|
||||||
_loopTask.setCallback(std::bind(&VictronMpptClass::loop, this));
|
_loopTask.setCallback([this] { loop(); });
|
||||||
_loopTask.setIterations(TASK_FOREVER);
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
_loopTask.enable();
|
_loopTask.enable();
|
||||||
|
|
||||||
@ -21,24 +22,41 @@ void VictronMpptClass::updateSettings()
|
|||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
_controllers.clear();
|
_controllers.clear();
|
||||||
|
SerialPortManager.invalidateMpptPorts();
|
||||||
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
if (!config.Vedirect.Enabled) { return; }
|
if (!config.Vedirect.Enabled) { return; }
|
||||||
|
|
||||||
const PinMapping_t& pin = PinMapping.get();
|
const PinMapping_t& pin = PinMapping.get();
|
||||||
int8_t rx = pin.victron_rx;
|
|
||||||
int8_t tx = pin.victron_tx;
|
|
||||||
|
|
||||||
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx);
|
int hwSerialPort = 1;
|
||||||
|
bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort);
|
||||||
|
if (initSuccess) {
|
||||||
|
hwSerialPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort)
|
||||||
|
{
|
||||||
|
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort);
|
||||||
|
|
||||||
if (rx < 0) {
|
if (rx < 0) {
|
||||||
MessageOutput.println("[VictronMppt] invalid pin config");
|
MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx);
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SerialPortManager.allocateMpptPort(hwSerialPort)) {
|
||||||
|
MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n",
|
||||||
|
hwSerialPort);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto upController = std::make_unique<VeDirectMpptController>();
|
auto upController = std::make_unique<VeDirectMpptController>();
|
||||||
upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging);
|
upController->init(rx, tx, &MessageOutput, logging, hwSerialPort);
|
||||||
_controllers.push_back(std::move(upController));
|
_controllers.push_back(std::move(upController));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VictronMpptClass::loop()
|
void VictronMpptClass::loop()
|
||||||
@ -54,13 +72,24 @@ bool VictronMpptClass::isDataValid() const
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
for (auto const& upController : _controllers) {
|
for (auto const& upController: _controllers) {
|
||||||
if (!upController->isDataValid()) { return false; }
|
if (!upController->isDataValid()) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
return !_controllers.empty();
|
return !_controllers.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VictronMpptClass::isDataValid(size_t idx) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
if (_controllers.empty() || idx >= _controllers.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _controllers[idx]->isDataValid();
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t VictronMpptClass::getDataAgeMillis() const
|
uint32_t VictronMpptClass::getDataAgeMillis() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
@ -81,17 +110,26 @@ uint32_t VictronMpptClass::getDataAgeMillis() const
|
|||||||
return age;
|
return age;
|
||||||
}
|
}
|
||||||
|
|
||||||
VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const
|
uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
if (_controllers.empty() || idx >= _controllers.size()) { return 0; }
|
||||||
|
|
||||||
|
return millis() - _controllers[idx]->getLastUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<VeDirectMpptController::spData_t> VictronMpptClass::getData(size_t idx) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
if (_controllers.empty() || idx >= _controllers.size()) {
|
if (_controllers.empty() || idx >= _controllers.size()) {
|
||||||
MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n",
|
MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n",
|
||||||
idx, _controllers.size());
|
idx, _controllers.size());
|
||||||
return std::make_shared<VeDirectMpptController::veMpptStruct>();
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _controllers[idx]->getData();
|
return std::optional<VeDirectMpptController::spData_t>{_controllers[idx]->getData()};
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t VictronMpptClass::getPowerOutputWatts() const
|
int32_t VictronMpptClass::getPowerOutputWatts() const
|
||||||
@ -99,6 +137,7 @@ int32_t VictronMpptClass::getPowerOutputWatts() const
|
|||||||
int32_t sum = 0;
|
int32_t sum = 0;
|
||||||
|
|
||||||
for (const auto& upController : _controllers) {
|
for (const auto& upController : _controllers) {
|
||||||
|
if (!upController->isDataValid()) { continue; }
|
||||||
sum += upController->getData()->P;
|
sum += upController->getData()->P;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +149,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const
|
|||||||
int32_t sum = 0;
|
int32_t sum = 0;
|
||||||
|
|
||||||
for (const auto& upController : _controllers) {
|
for (const auto& upController : _controllers) {
|
||||||
|
if (!upController->isDataValid()) { continue; }
|
||||||
sum += upController->getData()->PPV;
|
sum += upController->getData()->PPV;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +161,7 @@ double VictronMpptClass::getYieldTotal() const
|
|||||||
double sum = 0;
|
double sum = 0;
|
||||||
|
|
||||||
for (const auto& upController : _controllers) {
|
for (const auto& upController : _controllers) {
|
||||||
|
if (!upController->isDataValid()) { continue; }
|
||||||
sum += upController->getData()->H19;
|
sum += upController->getData()->H19;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +173,7 @@ double VictronMpptClass::getYieldDay() const
|
|||||||
double sum = 0;
|
double sum = 0;
|
||||||
|
|
||||||
for (const auto& upController : _controllers) {
|
for (const auto& upController : _controllers) {
|
||||||
|
if (!upController->isDataValid()) { continue; }
|
||||||
sum += upController->getData()->H20;
|
sum += upController->getData()->H20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +185,7 @@ double VictronMpptClass::getOutputVoltage() const
|
|||||||
double min = -1;
|
double min = -1;
|
||||||
|
|
||||||
for (const auto& upController : _controllers) {
|
for (const auto& upController : _controllers) {
|
||||||
|
if (!upController->isDataValid()) { continue; }
|
||||||
double volts = upController->getData()->V;
|
double volts = upController->getData()->V;
|
||||||
if (min == -1) { min = volts; }
|
if (min == -1) { min = volts; }
|
||||||
min = std::min(min, volts);
|
min = std::min(min, volts);
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "MqttHandleBatteryHass.h"
|
#include "MqttHandleBatteryHass.h"
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
#include "WebApi.h"
|
#include "WebApi.h"
|
||||||
#include "WebApi_battery.h"
|
#include "WebApi_battery.h"
|
||||||
#include "WebApi_errors.h"
|
#include "WebApi_errors.h"
|
||||||
@ -114,4 +115,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
|
|
||||||
Battery.updateSettings();
|
Battery.updateSettings();
|
||||||
MqttHandleBatteryHass.forceUpdate();
|
MqttHandleBatteryHass.forceUpdate();
|
||||||
|
|
||||||
|
// potentially make SoC thresholds auto-discoverable
|
||||||
|
MqttHandlePowerLimiterHass.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,9 +86,11 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
|||||||
led["brightness"] = config.Led_Single[i].Brightness;
|
led["brightness"] = config.Led_Single[i].Brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject victronPinObj = curPin.createNestedObject("victron");
|
auto victronPinObj = curPin.createNestedObject("victron");
|
||||||
victronPinObj["rx"] = pin.victron_rx;
|
victronPinObj["rx"] = pin.victron_rx;
|
||||||
victronPinObj["tx"] = pin.victron_tx;
|
victronPinObj["tx"] = pin.victron_tx;
|
||||||
|
victronPinObj["rx2"] = pin.victron_rx2;
|
||||||
|
victronPinObj["tx2"] = pin.victron_tx2;
|
||||||
|
|
||||||
JsonObject batteryPinObj = curPin.createNestedObject("battery");
|
JsonObject batteryPinObj = curPin.createNestedObject("battery");
|
||||||
batteryPinObj["rx"] = pin.battery_rx;
|
batteryPinObj["rx"] = pin.battery_rx;
|
||||||
|
|||||||
@ -131,7 +131,10 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["serial"].as<uint64_t>() == 0) {
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
|
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||||
|
|
||||||
|
if (serial == 0) {
|
||||||
retMsg["message"] = "Serial cannot be zero!";
|
retMsg["message"] = "Serial cannot be zero!";
|
||||||
retMsg["code"] = WebApiError::DtuSerialZero;
|
retMsg["code"] = WebApiError::DtuSerialZero;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -187,8 +190,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
|||||||
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
// Interpret the string as a hex value and convert it to uint64_t
|
config.Dtu.Serial = serial;
|
||||||
config.Dtu.Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
|
||||||
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
|
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
|
||||||
config.Dtu.VerboseLogging = root["verbose_logging"].as<bool>();
|
config.Dtu.VerboseLogging = root["verbose_logging"].as<bool>();
|
||||||
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
|
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
|
||||||
|
|||||||
@ -129,7 +129,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["serial"].as<uint64_t>() == 0) {
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
|
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||||
|
|
||||||
|
if (serial == 0) {
|
||||||
retMsg["message"] = "Serial must be a number > 0!";
|
retMsg["message"] = "Serial must be a number > 0!";
|
||||||
retMsg["code"] = WebApiError::InverterSerialZero;
|
retMsg["code"] = WebApiError::InverterSerialZero;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -158,7 +161,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Interpret the string as a hex value and convert it to uint64_t
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
inverter->Serial = serial;
|
||||||
|
|
||||||
strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||||
|
|
||||||
@ -233,7 +236,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["serial"].as<uint64_t>() == 0) {
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
|
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||||
|
|
||||||
|
if (serial == 0) {
|
||||||
retMsg["message"] = "Serial must be a number > 0!";
|
retMsg["message"] = "Serial must be a number > 0!";
|
||||||
retMsg["code"] = WebApiError::InverterSerialZero;
|
retMsg["code"] = WebApiError::InverterSerialZero;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -261,7 +267,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
|||||||
|
|
||||||
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];
|
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];
|
||||||
|
|
||||||
uint64_t new_serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
uint64_t new_serial = serial;
|
||||||
uint64_t old_serial = inverter.Serial;
|
uint64_t old_serial = inverter.Serial;
|
||||||
|
|
||||||
// Interpret the string as a hex value and convert it to uint64_t
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
@ -380,8 +386,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
|||||||
|
|
||||||
Hoymiles.removeInverterBySerial(inverter.Serial);
|
Hoymiles.removeInverterBySerial(inverter.Serial);
|
||||||
|
|
||||||
inverter.Serial = 0;
|
Configuration.deleteInverterById(inverter_id);
|
||||||
strncpy(inverter.Name, "", sizeof(inverter.Name));
|
|
||||||
|
|
||||||
WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
|
WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
|
||||||
|
|
||||||
|
|||||||
@ -100,7 +100,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["serial"].as<uint64_t>() == 0) {
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
|
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||||
|
|
||||||
|
if (serial == 0) {
|
||||||
retMsg["message"] = "Serial must be a number > 0!";
|
retMsg["message"] = "Serial must be a number > 0!";
|
||||||
retMsg["code"] = WebApiError::LimitSerialZero;
|
retMsg["code"] = WebApiError::LimitSerialZero;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -129,7 +132,6 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
|
||||||
uint16_t limit = root["limit_value"].as<uint16_t>();
|
uint16_t limit = root["limit_value"].as<uint16_t>();
|
||||||
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
|
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,10 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["serial"].as<uint64_t>() == 0) {
|
// Interpret the string as a hex value and convert it to uint64_t
|
||||||
|
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||||
|
|
||||||
|
if (serial == 0) {
|
||||||
retMsg["message"] = "Serial must be a number > 0!";
|
retMsg["message"] = "Serial must be a number > 0!";
|
||||||
retMsg["code"] = WebApiError::PowerSerialZero;
|
retMsg["code"] = WebApiError::PowerSerialZero;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -101,7 +104,6 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
|
||||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||||
if (inv == nullptr) {
|
if (inv == nullptr) {
|
||||||
retMsg["message"] = "Invalid inverter specified!";
|
retMsg["message"] = "Invalid inverter specified!";
|
||||||
|
|||||||
@ -7,10 +7,7 @@
|
|||||||
#include "ArduinoJson.h"
|
#include "ArduinoJson.h"
|
||||||
#include "AsyncJson.h"
|
#include "AsyncJson.h"
|
||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "MqttHandleHass.h"
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
#include "MqttHandleVedirectHass.h"
|
|
||||||
#include "MqttSettings.h"
|
|
||||||
#include "PowerMeter.h"
|
|
||||||
#include "PowerLimiter.h"
|
#include "PowerLimiter.h"
|
||||||
#include "WebApi.h"
|
#include "WebApi.h"
|
||||||
#include "helper.h"
|
#include "helper.h"
|
||||||
@ -25,21 +22,24 @@ void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
|||||||
_server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1));
|
_server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1));
|
||||||
_server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1));
|
_server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1));
|
||||||
_server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1));
|
_server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1));
|
||||||
|
_server->on("/api/powerlimiter/metadata", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onMetaData, this, _1));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
|
void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
|
||||||
{
|
{
|
||||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse(false, 512);
|
||||||
auto& root = response->getRoot();
|
auto& root = response->getRoot();
|
||||||
const CONFIG_T& config = Configuration.get();
|
|
||||||
|
|
||||||
root["enabled"] = config.PowerLimiter.Enabled;
|
root["enabled"] = config.PowerLimiter.Enabled;
|
||||||
root["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
root["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
||||||
root["solar_passthrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
root["solar_passthrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
||||||
root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
||||||
root["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
|
root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
|
||||||
root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
||||||
root["inverter_id"] = config.PowerLimiter.InverterId;
|
root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
|
||||||
|
root["inverter_serial"] = String(config.PowerLimiter.InverterId);
|
||||||
root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
|
root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
|
||||||
root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
|
root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
|
||||||
root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
||||||
@ -60,6 +60,54 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
if (!WebApi.checkCredentials(request)) { return; }
|
||||||
|
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
size_t invAmount = 0;
|
||||||
|
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||||
|
if (config.Inverter[i].Serial != 0) { ++invAmount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse(false, 256 + 256 * invAmount);
|
||||||
|
auto& root = response->getRoot();
|
||||||
|
|
||||||
|
root["power_meter_enabled"] = config.PowerMeter.Enabled;
|
||||||
|
root["battery_enabled"] = config.Battery.Enabled;
|
||||||
|
root["charge_controller_enabled"] = config.Vedirect.Enabled;
|
||||||
|
|
||||||
|
JsonObject inverters = root.createNestedObject("inverters");
|
||||||
|
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||||
|
if (config.Inverter[i].Serial == 0) { continue; }
|
||||||
|
|
||||||
|
// we use the integer (base 10) representation of the inverter serial,
|
||||||
|
// rather than the hex represenation as used when handling the inverter
|
||||||
|
// serial elsewhere in the web application, because in this case, the
|
||||||
|
// serial is actually not displayed but only used as a value/index.
|
||||||
|
JsonObject obj = inverters.createNestedObject(String(config.Inverter[i].Serial));
|
||||||
|
obj["pos"] = i;
|
||||||
|
obj["name"] = String(config.Inverter[i].Name);
|
||||||
|
obj["poll_enable"] = config.Inverter[i].Poll_Enable;
|
||||||
|
obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
|
||||||
|
obj["command_enable"] = config.Inverter[i].Command_Enable;
|
||||||
|
obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
|
||||||
|
|
||||||
|
obj["type"] = "Unknown";
|
||||||
|
obj["channels"] = 1;
|
||||||
|
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
|
||||||
|
if (inv != nullptr) {
|
||||||
|
obj["type"] = inv->typeName();
|
||||||
|
auto channels = inv->Statistics()->getChannelsByType(TYPE_DC);
|
||||||
|
obj["channels"] = channels.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request)
|
void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request)
|
||||||
{
|
{
|
||||||
if (!WebApi.checkCredentials(request)) {
|
if (!WebApi.checkCredentials(request)) {
|
||||||
@ -105,13 +153,14 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(root.containsKey("enabled")
|
// we were not actually checking for all the keys we (unconditionally)
|
||||||
&& root.containsKey("lower_power_limit")
|
// access below for a long time, and it is technically not needed if users
|
||||||
&& root.containsKey("inverter_id")
|
// use the web application to submit settings. the web app will always
|
||||||
&& root.containsKey("inverter_channel_id")
|
// submit all keys. users who send HTTP requests manually need to beware
|
||||||
&& root.containsKey("target_power_consumption")
|
// anyways to always include the keys accessed below. if we wanted to
|
||||||
&& root.containsKey("target_power_consumption_hysteresis")
|
// support a simpler API, like only sending the "enabled" key which only
|
||||||
)) {
|
// changes that key, we need to refactor all of the code below.
|
||||||
|
if (!root.containsKey("enabled")) {
|
||||||
retMsg["message"] = "Values are missing!";
|
retMsg["message"] = "Values are missing!";
|
||||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||||
response->setLength();
|
response->setLength();
|
||||||
@ -119,33 +168,43 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
config.PowerLimiter.Enabled = root["enabled"].as<bool>();
|
config.PowerLimiter.Enabled = root["enabled"].as<bool>();
|
||||||
PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation
|
PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation
|
||||||
config.PowerLimiter.VerboseLogging = root["verbose_logging"].as<bool>();
|
config.PowerLimiter.VerboseLogging = root["verbose_logging"].as<bool>();
|
||||||
config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as<bool>();
|
|
||||||
config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as<uint8_t>();
|
if (config.Vedirect.Enabled) {
|
||||||
config.PowerLimiter.BatteryDrainStategy= root["battery_drain_strategy"].as<uint8_t>();
|
config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as<bool>();
|
||||||
|
config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as<uint8_t>();
|
||||||
|
config.PowerLimiter.BatteryAlwaysUseAtNight= root["battery_always_use_at_night"].as<bool>();
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast<int>(root["full_solar_passthrough_start_voltage"].as<float>() * 100) / 100.0;
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast<int>(root["full_solar_passthrough_stop_voltage"].as<float>() * 100) / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as<bool>();
|
config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as<bool>();
|
||||||
config.PowerLimiter.InverterId = root["inverter_id"].as<uint8_t>();
|
config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as<bool>();
|
||||||
|
config.PowerLimiter.InverterId = root["inverter_serial"].as<uint64_t>();
|
||||||
config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as<uint8_t>();
|
config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as<uint8_t>();
|
||||||
config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();
|
config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();
|
||||||
config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as<int32_t>();
|
config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as<int32_t>();
|
||||||
config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as<int32_t>();
|
config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as<int32_t>();
|
||||||
config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();
|
config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();
|
||||||
config.PowerLimiter.IgnoreSoc = root["ignore_soc"].as<bool>();
|
|
||||||
config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as<uint32_t>();
|
if (config.Battery.Enabled) {
|
||||||
config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as<uint32_t>();
|
config.PowerLimiter.IgnoreSoc = root["ignore_soc"].as<bool>();
|
||||||
|
config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as<uint32_t>();
|
||||||
|
config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as<uint32_t>();
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as<uint32_t>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as<float>();
|
config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as<float>();
|
||||||
config.PowerLimiter.VoltageStartThreshold = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0;
|
config.PowerLimiter.VoltageStartThreshold = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0;
|
||||||
config.PowerLimiter.VoltageStopThreshold = root["voltage_stop_threshold"].as<float>();
|
config.PowerLimiter.VoltageStopThreshold = root["voltage_stop_threshold"].as<float>();
|
||||||
config.PowerLimiter.VoltageStopThreshold = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0;
|
config.PowerLimiter.VoltageStopThreshold = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0;
|
||||||
config.PowerLimiter.VoltageLoadCorrectionFactor = root["voltage_load_correction_factor"].as<float>();
|
config.PowerLimiter.VoltageLoadCorrectionFactor = root["voltage_load_correction_factor"].as<float>();
|
||||||
config.PowerLimiter.RestartHour = root["inverter_restart_hour"].as<int8_t>();
|
config.PowerLimiter.RestartHour = root["inverter_restart_hour"].as<int8_t>();
|
||||||
config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as<uint32_t>();
|
|
||||||
config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast<int>(root["full_solar_passthrough_start_voltage"].as<float>() * 100) / 100.0;
|
|
||||||
config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast<int>(root["full_solar_passthrough_stop_voltage"].as<float>() * 100) / 100.0;
|
|
||||||
|
|
||||||
WebApi.writeConfig(retMsg);
|
WebApi.writeConfig(retMsg);
|
||||||
|
|
||||||
@ -153,4 +212,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
|
|
||||||
PowerLimiter.calcNextInverterRestart();
|
PowerLimiter.calcNextInverterRestart();
|
||||||
|
|
||||||
|
// potentially make thresholds auto-discoverable
|
||||||
|
MqttHandlePowerLimiterHass.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,7 +118,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["source"].as<uint8_t>() == PowerMeter.SOURCE_HTTP) {
|
if (static_cast<PowerMeterClass::Source>(root["source"].as<uint8_t>()) == PowerMeterClass::Source::HTTP) {
|
||||||
JsonArray http_phases = root["http_phases"];
|
JsonArray http_phases = root["http_phases"];
|
||||||
for (uint8_t i = 0; i < http_phases.size(); i++) {
|
for (uint8_t i = 0; i < http_phases.size(); i++) {
|
||||||
JsonObject phase = http_phases[i].as<JsonObject>();
|
JsonObject phase = http_phases[i].as<JsonObject>();
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include "WebApi.h"
|
#include "WebApi.h"
|
||||||
#include "WebApi_errors.h"
|
#include "WebApi_errors.h"
|
||||||
#include "helper.h"
|
#include "helper.h"
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
|
|
||||||
void WebApiVedirectClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
void WebApiVedirectClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||||
{
|
{
|
||||||
@ -118,4 +119,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
|
|
||||||
VictronMppt.updateSettings();
|
VictronMppt.updateSettings();
|
||||||
|
|
||||||
|
// potentially make solar passthrough thresholds auto-discoverable
|
||||||
|
MqttHandlePowerLimiterHass.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,6 +64,8 @@ void WebApiWsHuaweiLiveClass::sendDataTaskCb()
|
|||||||
JsonVariant var = root;
|
JsonVariant var = root;
|
||||||
generateJsonResponse(var);
|
generateJsonResponse(var);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
String buffer;
|
String buffer;
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,11 @@ void WebApiWsBatteryLiveClass::sendDataTaskCb()
|
|||||||
JsonVariant var = root;
|
JsonVariant var = root;
|
||||||
generateJsonResponse(var);
|
generateJsonResponse(var);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
|
// battery provider does not generate a card, e.g., MQTT provider
|
||||||
|
if (root.isNull()) { return; }
|
||||||
|
|
||||||
String buffer;
|
String buffer;
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
|
|
||||||
|
|||||||
@ -56,25 +56,32 @@ void WebApiWsLiveClass::wsCleanupTaskCb()
|
|||||||
|
|
||||||
void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all)
|
void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all)
|
||||||
{
|
{
|
||||||
|
auto const& config = Configuration.get();
|
||||||
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
|
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
|
||||||
|
|
||||||
if (all || (millis() - _lastPublishVictron) > VictronMppt.getDataAgeMillis()) {
|
auto victronAge = VictronMppt.getDataAgeMillis();
|
||||||
|
if (all || (victronAge > 0 && (millis() - _lastPublishVictron) > victronAge)) {
|
||||||
JsonObject vedirectObj = root.createNestedObject("vedirect");
|
JsonObject vedirectObj = root.createNestedObject("vedirect");
|
||||||
vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled;
|
vedirectObj["enabled"] = config.Vedirect.Enabled;
|
||||||
JsonObject totalVeObj = vedirectObj.createNestedObject("total");
|
|
||||||
|
|
||||||
addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1);
|
if (config.Vedirect.Enabled) {
|
||||||
addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0);
|
JsonObject totalVeObj = vedirectObj.createNestedObject("total");
|
||||||
addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2);
|
addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1);
|
||||||
|
addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0);
|
||||||
|
addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2);
|
||||||
|
}
|
||||||
|
|
||||||
if (!all) { _lastPublishVictron = millis(); }
|
if (!all) { _lastPublishVictron = millis(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) {
|
if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) {
|
||||||
JsonObject huaweiObj = root.createNestedObject("huawei");
|
JsonObject huaweiObj = root.createNestedObject("huawei");
|
||||||
huaweiObj["enabled"] = Configuration.get().Huawei.Enabled;
|
huaweiObj["enabled"] = config.Huawei.Enabled;
|
||||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
|
||||||
addTotalField(huaweiObj, "Power", rp->output_power, "W", 2);
|
if (config.Huawei.Enabled) {
|
||||||
|
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||||
|
addTotalField(huaweiObj, "Power", rp->output_power, "W", 2);
|
||||||
|
}
|
||||||
|
|
||||||
if (!all) { _lastPublishHuawei = millis(); }
|
if (!all) { _lastPublishHuawei = millis(); }
|
||||||
}
|
}
|
||||||
@ -82,16 +89,22 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al
|
|||||||
auto spStats = Battery.getStats();
|
auto spStats = Battery.getStats();
|
||||||
if (all || spStats->updateAvailable(_lastPublishBattery)) {
|
if (all || spStats->updateAvailable(_lastPublishBattery)) {
|
||||||
JsonObject batteryObj = root.createNestedObject("battery");
|
JsonObject batteryObj = root.createNestedObject("battery");
|
||||||
batteryObj["enabled"] = Configuration.get().Battery.Enabled;
|
batteryObj["enabled"] = config.Battery.Enabled;
|
||||||
addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0);
|
|
||||||
|
if (config.Battery.Enabled) {
|
||||||
|
addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (!all) { _lastPublishBattery = millis(); }
|
if (!all) { _lastPublishBattery = millis(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) {
|
if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) {
|
||||||
JsonObject powerMeterObj = root.createNestedObject("power_meter");
|
JsonObject powerMeterObj = root.createNestedObject("power_meter");
|
||||||
powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled;
|
powerMeterObj["enabled"] = config.PowerMeter.Enabled;
|
||||||
addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1);
|
|
||||||
|
if (config.PowerMeter.Enabled) {
|
||||||
|
addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!all) { _lastPublishPowerMeter = millis(); }
|
if (!all) { _lastPublishPowerMeter = millis(); }
|
||||||
}
|
}
|
||||||
@ -99,7 +112,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al
|
|||||||
|
|
||||||
void WebApiWsLiveClass::sendOnBatteryStats()
|
void WebApiWsLiveClass::sendOnBatteryStats()
|
||||||
{
|
{
|
||||||
DynamicJsonDocument root(512);
|
DynamicJsonDocument root(1024);
|
||||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; }
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
JsonVariant var = root;
|
JsonVariant var = root;
|
||||||
@ -108,6 +121,10 @@ void WebApiWsLiveClass::sendOnBatteryStats()
|
|||||||
if (all) { _lastPublishOnBatteryFull = millis(); }
|
if (all) { _lastPublishOnBatteryFull = millis(); }
|
||||||
generateOnBatteryJsonResponse(var, all);
|
generateOnBatteryJsonResponse(var, all);
|
||||||
|
|
||||||
|
if (root.isNull()) { return; }
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
String buffer;
|
String buffer;
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
|
|
||||||
|
|||||||
@ -52,28 +52,46 @@ void WebApiWsVedirectLiveClass::wsCleanupTaskCb()
|
|||||||
_ws.cleanupClients();
|
_ws.cleanupClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WebApiWsVedirectLiveClass::hasUpdate(size_t idx)
|
||||||
|
{
|
||||||
|
auto dataAgeMillis = VictronMppt.getDataAgeMillis(idx);
|
||||||
|
if (dataAgeMillis == 0) { return false; }
|
||||||
|
auto publishAgeMillis = millis() - _lastPublish;
|
||||||
|
return dataAgeMillis < publishAgeMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t WebApiWsVedirectLiveClass::responseSize() const
|
||||||
|
{
|
||||||
|
// estimated with ArduinoJson assistant
|
||||||
|
return VictronMppt.controllerAmount() * (1024 + 512) + 128/*DPL status and structure*/;
|
||||||
|
}
|
||||||
|
|
||||||
void WebApiWsVedirectLiveClass::sendDataTaskCb()
|
void WebApiWsVedirectLiveClass::sendDataTaskCb()
|
||||||
{
|
{
|
||||||
// do nothing if no WS client is connected
|
// do nothing if no WS client is connected
|
||||||
if (_ws.count() == 0) {
|
if (_ws.count() == 0) { return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we assume this loop to be running at least twice for every
|
|
||||||
// update from a VE.Direct MPPT data producer, so _dataAgeMillis
|
|
||||||
// acutally grows in between updates.
|
|
||||||
auto lastDataAgeMillis = _dataAgeMillis;
|
|
||||||
_dataAgeMillis = VictronMppt.getDataAgeMillis();
|
|
||||||
|
|
||||||
// Update on ve.direct change or at least after 10 seconds
|
// Update on ve.direct change or at least after 10 seconds
|
||||||
if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) {
|
bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000));
|
||||||
|
bool updateAvailable = false;
|
||||||
|
if (!fullUpdate) {
|
||||||
|
for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
|
if (hasUpdate(idx)) {
|
||||||
|
updateAvailable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullUpdate || updateAvailable) {
|
||||||
try {
|
try {
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
DynamicJsonDocument root(_responseSize);
|
DynamicJsonDocument root(responseSize());
|
||||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
JsonVariant var = root;
|
JsonVariant var = root;
|
||||||
generateJsonResponse(var);
|
generateJsonResponse(var, fullUpdate);
|
||||||
|
|
||||||
|
if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; }
|
||||||
|
|
||||||
String buffer;
|
String buffer;
|
||||||
serializeJson(root, buffer);
|
serializeJson(root, buffer);
|
||||||
@ -92,22 +110,50 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb()
|
|||||||
} catch (const std::exception& exc) {
|
} catch (const std::exception& exc) {
|
||||||
MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what());
|
MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_lastWsPublish = millis();
|
if (fullUpdate) {
|
||||||
|
_lastFullPublish = millis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
|
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool fullUpdate)
|
||||||
{
|
{
|
||||||
auto spMpptData = VictronMppt.getData();
|
const JsonObject &array = root["vedirect"].createNestedObject("instances");
|
||||||
|
root["vedirect"]["full_update"] = fullUpdate;
|
||||||
|
|
||||||
|
for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
|
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
|
||||||
|
if (!spOptMpptData.has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullUpdate && !hasUpdate(idx)) { continue; }
|
||||||
|
|
||||||
|
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
|
||||||
|
|
||||||
|
String serial(spMpptData->SER);
|
||||||
|
if (serial.isEmpty()) { continue; } // serial required as index
|
||||||
|
|
||||||
|
const JsonObject &nested = array.createNestedObject(serial);
|
||||||
|
nested["data_age_ms"] = VictronMppt.getDataAgeMillis(idx);
|
||||||
|
populateJson(nested, spMpptData);
|
||||||
|
_lastPublish = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// power limiter state
|
||||||
|
root["dpl"]["PLSTATE"] = -1;
|
||||||
|
if (Configuration.get().PowerLimiter.Enabled)
|
||||||
|
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
|
||||||
|
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) {
|
||||||
// device info
|
// device info
|
||||||
root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000;
|
|
||||||
root["device"]["age_critical"] = !VictronMppt.isDataValid();
|
|
||||||
root["device"]["PID"] = spMpptData->getPidAsString();
|
root["device"]["PID"] = spMpptData->getPidAsString();
|
||||||
root["device"]["SER"] = spMpptData->SER;
|
root["device"]["SER"] = spMpptData->SER;
|
||||||
root["device"]["FW"] = spMpptData->FW;
|
root["device"]["FW"] = spMpptData->FW;
|
||||||
root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF";
|
root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF";
|
||||||
root["device"]["CS"] = spMpptData->getCsAsString();
|
root["device"]["CS"] = spMpptData->getCsAsString();
|
||||||
root["device"]["ERR"] = spMpptData->getErrAsString();
|
root["device"]["ERR"] = spMpptData->getErrAsString();
|
||||||
root["device"]["OR"] = spMpptData->getOrAsString();
|
root["device"]["OR"] = spMpptData->getOrAsString();
|
||||||
@ -154,12 +200,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
|
|||||||
root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23;
|
root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23;
|
||||||
root["input"]["MaximumPowerYesterday"]["u"] = "W";
|
root["input"]["MaximumPowerYesterday"]["u"] = "W";
|
||||||
root["input"]["MaximumPowerYesterday"]["d"] = 0;
|
root["input"]["MaximumPowerYesterday"]["d"] = 0;
|
||||||
|
|
||||||
// power limiter state
|
|
||||||
root["dpl"]["PLSTATE"] = -1;
|
|
||||||
if (Configuration.get().PowerLimiter.Enabled)
|
|
||||||
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
|
|
||||||
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||||
@ -184,10 +224,10 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize);
|
AsyncJsonResponse* response = new AsyncJsonResponse(false, responseSize());
|
||||||
auto& root = response->getRoot();
|
auto& root = response->getRoot();
|
||||||
|
|
||||||
generateJsonResponse(root);
|
generateJsonResponse(root, true/*fullUpdate*/);
|
||||||
|
|
||||||
response->setLength();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include "MqttHandleVedirect.h"
|
#include "MqttHandleVedirect.h"
|
||||||
#include "MqttHandleHuawei.h"
|
#include "MqttHandleHuawei.h"
|
||||||
#include "MqttHandlePowerLimiter.h"
|
#include "MqttHandlePowerLimiter.h"
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "NetworkSettings.h"
|
#include "NetworkSettings.h"
|
||||||
#include "NtpSettings.h"
|
#include "NtpSettings.h"
|
||||||
@ -123,6 +124,7 @@ void setup()
|
|||||||
MqttHandleBatteryHass.init(scheduler);
|
MqttHandleBatteryHass.init(scheduler);
|
||||||
MqttHandleHuawei.init(scheduler);
|
MqttHandleHuawei.init(scheduler);
|
||||||
MqttHandlePowerLimiter.init(scheduler);
|
MqttHandlePowerLimiter.init(scheduler);
|
||||||
|
MqttHandlePowerLimiterHass.init(scheduler);
|
||||||
MessageOutput.println("done");
|
MessageOutput.println("done");
|
||||||
|
|
||||||
// Initialize WebApi
|
// Initialize WebApi
|
||||||
|
|||||||
@ -13,37 +13,37 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap-icons-vue": "^1.11.3",
|
"bootstrap-icons-vue": "^1.11.3",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"vue": "^3.4.19",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.9.1",
|
"vue-i18n": "^9.10.2",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
"@intlify/unplugin-vue-i18n": "^3.0.1",
|
||||||
"@rushstack/eslint-patch": "^1.7.2",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@tsconfig/node18": "^18.2.2",
|
"@tsconfig/node18": "^18.2.2",
|
||||||
"@types/bootstrap": "^5.2.10",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.30",
|
||||||
"@types/pulltorefreshjs": "^0.1.7",
|
"@types/pulltorefreshjs": "^0.1.7",
|
||||||
"@types/sortablejs": "^1.15.7",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/spark-md5": "^3.0.4",
|
"@types/spark-md5": "^3.0.4",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.21.1",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"pulltorefreshjs": "^0.1.22",
|
"pulltorefreshjs": "^0.1.22",
|
||||||
"sass": "^1.71.0",
|
"sass": "^1.72.0",
|
||||||
"terser": "^5.27.1",
|
"terser": "^5.29.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.3",
|
||||||
"vite": "^5.1.3",
|
"vite": "^5.2.3",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-css-injected-by-js": "^3.4.0",
|
"vite-plugin-css-injected-by-js": "^3.5.0",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^2.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,12 +76,12 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
productionYear() {
|
productionYear() {
|
||||||
return() => {
|
return() => {
|
||||||
return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014;
|
return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
productionWeek() {
|
productionWeek() {
|
||||||
return() => {
|
return() => {
|
||||||
return ((parseInt(this.devInfoList.serial.toString(), 16) >> (5 * 4)) & 0xFF).toString(16);
|
return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xFF).toString(16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,17 +32,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
|
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
|
||||||
<td v-if="modelAllowVersionInfo">
|
<td>
|
||||||
<a :href="systemStatus.update_url" target="_blank" v-tooltip
|
<div class="form-check form-check-inline form-switch">
|
||||||
:title="$t('firmwareinfo.FirmwareUpdateHint')">
|
|
||||||
<span class="badge" :class="systemStatus.update_status">
|
|
||||||
{{ systemStatus.update_text }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td v-else>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
|
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
|
||||||
|
<label class="form-check-label">
|
||||||
|
<a v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined" :href="systemStatus.update_url" target="_blank" v-tooltip
|
||||||
|
:title="$t('firmwareinfo.FirmwareUpdateHint')">
|
||||||
|
<span class="badge" :class="systemStatus.update_status">
|
||||||
|
{{ systemStatus.update_text }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<span v-else-if="modelAllowVersionInfo" class="badge" :class="systemStatus.update_status">
|
||||||
|
{{ systemStatus.update_text }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
114
webapp/src/components/InputSerial.vue
Normal file
114
webapp/src/components/InputSerial.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<input v-model="inputSerial" type="text" :id="id" :required="required" class="form-control" :class="inputClass" />
|
||||||
|
<BootstrapAlert show :variant="formatShow" v-if="formatHint">{{ formatHint }}</BootstrapAlert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import BootstrapAlert from './BootstrapAlert.vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
const chars32 = '0123456789ABCDEFGHJKLMNPRSTUVWXY';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BootstrapAlert,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
'modelValue': { type: [String, Number], required: true },
|
||||||
|
'id': String,
|
||||||
|
'inputClass': String,
|
||||||
|
'required': Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputSerial: "",
|
||||||
|
formatHint: "",
|
||||||
|
formatShow: "info",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get(): any {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value: any) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue: function (val) {
|
||||||
|
this.inputSerial = val;
|
||||||
|
},
|
||||||
|
inputSerial: function (val) {
|
||||||
|
const serial = val.toString().toUpperCase(); // Convert to lowercase for case-insensitivity
|
||||||
|
|
||||||
|
if (serial == "") {
|
||||||
|
this.formatHint = "";
|
||||||
|
this.model = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formatShow = "info";
|
||||||
|
|
||||||
|
// Contains only numbers
|
||||||
|
if (/^1{1}[\dA-F]{11}$/.test(serial)) {
|
||||||
|
this.model = serial;
|
||||||
|
this.formatHint = this.$t('inputserial.format_hoymiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains numbers and hex characters but at least one number
|
||||||
|
else if (/^(?=.*\d)[\dA-F]{12}$/.test(serial)) {
|
||||||
|
this.model = serial;
|
||||||
|
this.formatHint = this.$t('inputserial.format_converted');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has format: xxxxxxxxx-xxx
|
||||||
|
else if (/^((A01)|(A11)|(A21))[\dA-HJ-NR-YP]{6}-[\dA-HJ-NP-Z]{3}$/.test(serial)) {
|
||||||
|
if (this.checkHerfChecksum(serial)) {
|
||||||
|
this.model = this.convertHerfToHoy(serial);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.formatHint = this.$t('inputserial.format_herf_valid', { serial: this.model });
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.formatHint = this.$t('inputserial.format_herf_invalid');
|
||||||
|
this.formatShow = "danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other format
|
||||||
|
} else {
|
||||||
|
this.formatHint = this.$t('inputserial.format_unknown');
|
||||||
|
this.formatShow = "danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkHerfChecksum(sn: string) {
|
||||||
|
const chars64 = 'HMFLGW5XC301234567899Z67YRT2S8ABCDEFGHJKDVEJ4KQPUALMNPRSTUVWXYNB';
|
||||||
|
|
||||||
|
const checksum = sn.substring(sn.indexOf("-") + 1);
|
||||||
|
const serial = sn.substring(0, sn.indexOf("-"));
|
||||||
|
|
||||||
|
const first_char = '1';
|
||||||
|
const i = chars32.indexOf(first_char)
|
||||||
|
const sum1: number = Array.from(serial).reduce((sum, c) => sum + c.charCodeAt(0), 0) & 31;
|
||||||
|
const sum2: number = Array.from(serial).reduce((sum, c) => sum + chars32.indexOf(c), 0) & 31;
|
||||||
|
const ext = first_char + chars64[sum1 + i] + chars64[sum2 + i];
|
||||||
|
|
||||||
|
return checksum == ext;
|
||||||
|
},
|
||||||
|
convertHerfToHoy(sn: string) {
|
||||||
|
let sn_int: bigint = 0n;
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
const pos: bigint = BigInt(chars32.indexOf(sn[i].toUpperCase()));
|
||||||
|
const shift: bigint = BigInt(42 - 5 * i - (i <= 2 ? 0 : 2));
|
||||||
|
sn_int |= (pos << shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sn_int.toString(16);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="totalBattData.enabled || powerMeterData.enabled || huaweiData.enabled">
|
<div v-show="totalBattData.enabled || powerMeterData.enabled || huaweiData.enabled">
|
||||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||||
<div class="col" v-show="totalBattData.enabled">
|
<div class="col" v-if="totalBattData.enabled">
|
||||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.BatterySoc')">
|
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.BatterySoc')">
|
||||||
<h2>
|
<h2>
|
||||||
{{ $n(totalBattData.soc.v, 'decimal', {
|
{{ $n(totalBattData.soc.v, 'decimal', {
|
||||||
@ -84,7 +84,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</CardElement>
|
</CardElement>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" v-show="powerMeterData.enabled">
|
<div class="col" v-if="powerMeterData.enabled">
|
||||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')">
|
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')">
|
||||||
<h2>
|
<h2>
|
||||||
{{ $n(powerMeterData.Power.v, 'decimal', {
|
{{ $n(powerMeterData.Power.v, 'decimal', {
|
||||||
@ -95,7 +95,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</CardElement>
|
</CardElement>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" v-show="huaweiData.enabled">
|
<div class="col" v-if="huaweiData.enabled">
|
||||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HuaweiPower')">
|
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HuaweiPower')">
|
||||||
<h2>
|
<h2>
|
||||||
{{ $n(huaweiData.Power.v, 'decimal', {
|
{{ $n(huaweiData.Power.v, 'decimal', {
|
||||||
|
|||||||
@ -9,25 +9,25 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||||
<div class="card">
|
<div class="card" v-for="(item, serial) in vedirect.instances" :key="serial">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center"
|
<div class="card-header d-flex justify-content-between align-items-center"
|
||||||
:class="{
|
:class="{
|
||||||
'text-bg-danger': vedirectData.age_critical,
|
'text-bg-danger': item.data_age_ms >= 10000,
|
||||||
'text-bg-primary': !vedirectData.age_critical,
|
'text-bg-primary': item.data_age_ms < 10000,
|
||||||
}">
|
}">
|
||||||
<div class="p-1 flex-grow-1">
|
<div class="p-1 flex-grow-1">
|
||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
<div style="padding-right: 2em;">
|
<div style="padding-right: 2em;">
|
||||||
{{ vedirectData.PID }}
|
{{ item.device.PID }}
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-right: 2em;">
|
<div style="padding-right: 2em;">
|
||||||
{{ $t('vedirecthome.SerialNumber') }} {{ vedirectData.SER }}
|
{{ $t('vedirecthome.SerialNumber') }} {{ item.device.SER }}
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-right: 2em;">
|
<div style="padding-right: 2em;">
|
||||||
{{ $t('vedirecthome.FirmwareNumber') }} {{ vedirectData.FW }}
|
{{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }}
|
||||||
</div>
|
</div>
|
||||||
<div style="padding-right: 2em;">
|
<div style="padding-right: 2em;">
|
||||||
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': vedirectData.data_age }) }}
|
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,33 +71,33 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.LoadOutputState') }}</th>
|
<th scope="row">{{ $t('vedirecthome.LoadOutputState') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.LOAD}}</td>
|
<td style="text-align: right">{{item.device.LOAD}}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.StateOfOperation') }}</th>
|
<th scope="row">{{ $t('vedirecthome.StateOfOperation') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.CS}}</td>
|
<td style="text-align: right">{{item.device.CS}}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.TrackerOperationMode') }}</th>
|
<th scope="row">{{ $t('vedirecthome.TrackerOperationMode') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.MPPT}}</td>
|
<td style="text-align: right">{{item.device.MPPT}}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.OffReason') }}</th>
|
<th scope="row">{{ $t('vedirecthome.OffReason') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.OR}}</td>
|
<td style="text-align: right">{{item.device.OR}}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.ErrorCode') }}</th>
|
<th scope="row">{{ $t('vedirecthome.ErrorCode') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.ERR}}</td>
|
<td style="text-align: right">{{item.device.ERR}}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ $t('vedirecthome.DaySequenceNumber') }}</th>
|
<th scope="row">{{ $t('vedirecthome.DaySequenceNumber') }}</th>
|
||||||
<td style="text-align: right">{{vedirectData.HSDS.v}}</td>
|
<td style="text-align: right">{{item.device.HSDS.v}}</td>
|
||||||
<td>{{vedirectData.HSDS.u}}</td>
|
<td>{{item.device.HSDS.u}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -119,7 +119,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(prop, key) in vedirectOutput" v-bind:key="key">
|
<tr v-for="(prop, key) in item.output" v-bind:key="key">
|
||||||
<th scope="row">{{ $t('vedirecthome.output.' + key) }}</th>
|
<th scope="row">{{ $t('vedirecthome.output.' + key) }}</th>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ $n(prop.v, 'decimal', {
|
{{ $n(prop.v, 'decimal', {
|
||||||
@ -149,7 +149,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(prop, key) in vedirectInput" v-bind:key="key">
|
<tr v-for="(prop, key) in item.input" v-bind:key="key">
|
||||||
<th scope="row">{{ $t('vedirecthome.input.' + key) }}</th>
|
<th scope="row">{{ $t('vedirecthome.input.' + key) }}</th>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ $n(prop.v, 'decimal', {
|
{{ $n(prop.v, 'decimal', {
|
||||||
@ -178,7 +178,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import type { DynamicPowerLimiter, VedirectDevice, VedirectOutput, VedirectInput } from '@/types/VedirectLiveDataStatus';
|
import type { DynamicPowerLimiter, Vedirect } from '@/types/VedirectLiveDataStatus';
|
||||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||||
import {
|
import {
|
||||||
BIconSun,
|
BIconSun,
|
||||||
@ -199,19 +199,16 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
socket: {} as WebSocket,
|
socket: {} as WebSocket,
|
||||||
heartInterval: 0,
|
heartInterval: 0,
|
||||||
dataAgeInterval: 0,
|
dataAgeTimers: {} as Record<string, number>,
|
||||||
dataLoading: true,
|
dataLoading: true,
|
||||||
dplData: {} as DynamicPowerLimiter,
|
dplData: {} as DynamicPowerLimiter,
|
||||||
vedirectData: {} as VedirectDevice,
|
vedirect: {} as Vedirect,
|
||||||
vedirectOutput: {} as VedirectOutput,
|
|
||||||
vedirectInput: {} as VedirectInput,
|
|
||||||
isFirstFetchAfterConnect: true,
|
isFirstFetchAfterConnect: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getInitialData();
|
this.getInitialData();
|
||||||
this.initSocket();
|
this.initSocket();
|
||||||
this.initDataAgeing();
|
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.closeSocket();
|
this.closeSocket();
|
||||||
@ -224,10 +221,9 @@ export default defineComponent({
|
|||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
.then((root) => {
|
.then((root) => {
|
||||||
this.dplData = root["dpl"];
|
this.dplData = root["dpl"];
|
||||||
this.vedirectData = root["device"];
|
this.vedirect = root["vedirect"];
|
||||||
this.vedirectOutput = root["output"];
|
|
||||||
this.vedirectInput = root["input"];
|
|
||||||
this.dataLoading = false;
|
this.dataLoading = false;
|
||||||
|
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initSocket() {
|
initSocket() {
|
||||||
@ -244,9 +240,12 @@ export default defineComponent({
|
|||||||
console.log(event);
|
console.log(event);
|
||||||
var root = JSON.parse(event.data);
|
var root = JSON.parse(event.data);
|
||||||
this.dplData = root["dpl"];
|
this.dplData = root["dpl"];
|
||||||
this.vedirectData = root["device"];
|
if (root["vedirect"]["full_update"] === true) {
|
||||||
this.vedirectOutput = root["output"];
|
this.vedirect = root["vedirect"];
|
||||||
this.vedirectInput = root["input"];
|
} else {
|
||||||
|
Object.assign(this.vedirect.instances, root["vedirect"]["instances"]);
|
||||||
|
}
|
||||||
|
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||||
this.dataLoading = false;
|
this.dataLoading = false;
|
||||||
this.heartCheck(); // Reset heartbeat detection
|
this.heartCheck(); // Reset heartbeat detection
|
||||||
};
|
};
|
||||||
@ -261,11 +260,25 @@ export default defineComponent({
|
|||||||
this.closeSocket();
|
this.closeSocket();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
initDataAgeing() {
|
resetDataAging(serials: Array<string>) {
|
||||||
this.dataAgeInterval = setInterval(() => {
|
serials.forEach((serial) => {
|
||||||
if (this.vedirectData) {
|
if (this.dataAgeTimers[serial] !== undefined) {
|
||||||
this.vedirectData.data_age++;
|
clearTimeout(this.dataAgeTimers[serial]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000);
|
||||||
|
this.dataAgeTimers[serial] = setTimeout(() => {
|
||||||
|
this.doDataAging(serial);
|
||||||
|
}, nextMs);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
doDataAging(serial: string) {
|
||||||
|
if (this.vedirect?.instances?.[serial] === undefined) { return; }
|
||||||
|
|
||||||
|
this.vedirect.instances[serial].data_age_ms += 1000;
|
||||||
|
|
||||||
|
this.dataAgeTimers[serial] = setTimeout(() => {
|
||||||
|
this.doDataAging(serial);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||||
@ -286,11 +299,6 @@ export default defineComponent({
|
|||||||
this.heartInterval && clearTimeout(this.heartInterval);
|
this.heartInterval && clearTimeout(this.heartInterval);
|
||||||
this.isFirstFetchAfterConnect = true;
|
this.isFirstFetchAfterConnect = true;
|
||||||
},
|
},
|
||||||
formatNumber(num: number) {
|
|
||||||
return new Intl.NumberFormat(
|
|
||||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
|
||||||
).format(num);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"DTUSettings": "DTU",
|
"DTUSettings": "DTU",
|
||||||
"DeviceManager": "Hardware",
|
"DeviceManager": "Hardware",
|
||||||
"VedirectSettings": "VE.Direct",
|
"VedirectSettings": "VE.Direct",
|
||||||
"PowerMeterSettings": "Power Meter",
|
"PowerMeterSettings": "Stromzähler",
|
||||||
"BatterySettings": "Batterie",
|
"BatterySettings": "Batterie",
|
||||||
"AcChargerSettings": "AC Ladegerät",
|
"AcChargerSettings": "AC Ladegerät",
|
||||||
"ConfigManagement": "Konfigurationsverwaltung",
|
"ConfigManagement": "Konfigurationsverwaltung",
|
||||||
@ -553,6 +553,7 @@
|
|||||||
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
||||||
"typeHTTP": "HTTP(S) + JSON",
|
"typeHTTP": "HTTP(S) + JSON",
|
||||||
"typeSML": "SML (OBIS 16.7.0)",
|
"typeSML": "SML (OBIS 16.7.0)",
|
||||||
|
"typeSMAHM2": "SMA Homemanager 2.0",
|
||||||
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1",
|
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1",
|
||||||
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)",
|
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)",
|
||||||
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)",
|
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)",
|
||||||
@ -574,47 +575,52 @@
|
|||||||
"testHttpRequest": "Testen"
|
"testHttpRequest": "Testen"
|
||||||
},
|
},
|
||||||
"powerlimiteradmin": {
|
"powerlimiteradmin": {
|
||||||
"PowerLimiterSettings": "Power Limiter Einstellungen",
|
"PowerLimiterSettings": "Dynamic Power Limiter Einstellungen",
|
||||||
"PowerLimiterConfiguration": "Power Limiter Konfiguration",
|
"ConfigAlertMessage": "Eine oder mehrere Voraussetzungen zum Betrieb des Dynamic Power Limiter sind nicht erfüllt.",
|
||||||
|
"ConfigHints": "Konfigurationshinweise",
|
||||||
|
"ConfigHintRequirement": "Erforderlich",
|
||||||
|
"ConfigHintOptional": "Optional",
|
||||||
|
"ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:",
|
||||||
|
"ConfigHintPowerMeterDisabled": "Zum Betrieb des DPL muss der Power Meter konfiguriert sein und Daten liefern.",
|
||||||
|
"ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.",
|
||||||
|
"ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für den zu regelnden Wechselrichter aktiviert sein.",
|
||||||
|
"ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.",
|
||||||
|
"ConfigHintNoBatteryInterface": "SoC-basierte Schwellwerte können nur mit konfigurierter Batteriekommunikationsschnittstelle genutzt werden.",
|
||||||
"General": "Allgemein",
|
"General": "Allgemein",
|
||||||
"Enable": "Aktiviert",
|
"Enable": "Aktiviert",
|
||||||
"VerboseLogging": "@:base.VerboseLogging",
|
"VerboseLogging": "@:base.VerboseLogging",
|
||||||
|
"SolarPassthrough": "Solar-Passthrough",
|
||||||
"EnableSolarPassthrough": "Aktiviere Solar-Passthrough",
|
"EnableSolarPassthrough": "Aktiviere Solar-Passthrough",
|
||||||
"SolarPassthroughLosses": "(Full) Solar-Passthrough Verluste:",
|
"SolarPassthroughLosses": "(Full) Solar-Passthrough Verluste",
|
||||||
"SolarPassthroughLossesInfo": "<b>Hinweis:</b> Bei der Übertragung von Energie vom Solarladeregler zum Inverter sind Leitungsverluste zu erwarten. Um eine schleichende Entladung der Batterie im (Full) Solar-Passthrough Modus zu unterbinden, können diese Verluste berücksichtigt werden. Das am Inverter einzustellende Power Limit wird nach Berücksichtigung von dessen Effizienz zusätzlich um diesen Faktor verringert.",
|
"SolarPassthroughLossesInfo": "<b>Hinweis:</b> Bei der Übertragung von Energie vom Solarladeregler zum Inverter sind Leitungsverluste zu erwarten. Um eine schleichende Entladung der Batterie im (Full) Solar-Passthrough Modus zu unterbinden, können diese Verluste berücksichtigt werden. Das am Inverter einzustellende Power Limit wird nach Berücksichtigung von dessen Effizienz zusätzlich um diesen Faktor verringert.",
|
||||||
"BatteryDrainStrategy": "Strategie zur Batterieentleerung",
|
"BatteryDischargeAtNight": "Batterie nachts sogar teilweise geladen nutzen",
|
||||||
"BatteryDrainWhenFull": "Leeren, wenn voll",
|
"SolarpassthroughInfo": "Diese Funktion ermöglicht den unmittelbaren Verbauch der verfügbaren Solarleistung. Dazu wird die aktuell vom Laderegler gemeldete Solarleistung am Wechselrichter als Limit eingestellt, selbst wenn sich die Batterie in einem Ladezyklus befindet. Somit wird eine unnötige Speicherung vermieden, die verlustbehaftet wäre.",
|
||||||
"BatteryDrainAtNight": "Leeren zur Nacht",
|
"InverterSettings": "Wechselrichter",
|
||||||
"SolarpassthroughInfo": "Diese Einstellung aktiviert die direkte Weitergabe der aktuell vom Laderegler gemeldeten Solarleistung an den Wechselrichter um eine unnötige Speicherung zu vermeiden und die Energieverluste zu minimieren.",
|
"Inverter": "Zu regelnder Wechselrichter",
|
||||||
"InverterId": "Wechselrichter ID",
|
"SelectInverter": "Inverter auswählen...",
|
||||||
"InverterIdHint": "Wähle den Wechselrichter an dem die Batterie hängt.",
|
"InverterChannelId": "Eingang für Spannungsmessungen",
|
||||||
"InverterChannelId": "Kanal ID",
|
|
||||||
"InverterChannelIdHint": "Wähle den Kanal an dem die Batterie hängt.",
|
|
||||||
"TargetPowerConsumption": "Angestrebter Netzbezug",
|
"TargetPowerConsumption": "Angestrebter Netzbezug",
|
||||||
"TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz.",
|
"TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.",
|
||||||
"TargetPowerConsumptionHysteresis": "Hysterese für das berechnete Limit",
|
"TargetPowerConsumptionHysteresis": "Hysterese",
|
||||||
"TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.",
|
"TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.",
|
||||||
"LowerPowerLimit": "Unteres Leistungslimit",
|
"LowerPowerLimit": "Unteres Leistungslimit",
|
||||||
"UpperPowerLimit": "Oberes Leistungslimit",
|
"UpperPowerLimit": "Oberes Leistungslimit",
|
||||||
"PowerMeters": "Leistungsmesser",
|
"SocThresholds": "Batterie State of Charge (SoC) Schwellwerte",
|
||||||
"IgnoreSoc": "Batterie SoC ignorieren",
|
"IgnoreSoc": "Batterie SoC ignorieren",
|
||||||
"BatterySocStartThreshold": "Akku SoC - Start",
|
"StartThreshold": "Batterienutzung Start-Schwellwert",
|
||||||
"BatterySocStopThreshold": "Akku SoC - Stop",
|
"StopThreshold": "Batterienutzung Stop-Schwellwert",
|
||||||
"BatterySocSolarPassthroughStartThreshold": "Akku SoC - Start solar passthrough",
|
"FullSolarPassthroughStartThreshold": "Full-Solar-Passthrough Start-Schwellwert",
|
||||||
"BatterySocSolarPassthroughStartThresholdHint": "Wenn der Batterie SoC über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.",
|
"FullSolarPassthroughStartThresholdHint": "Oberhalb dieses Schwellwertes wird die Inverterleistung der Victron-MPPT-Leistung gleichgesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.",
|
||||||
"VoltageStartThreshold": "DC Spannung - Start",
|
"VoltageSolarPassthroughStopThreshold": "Full-Solar-Passthrough Stop-Schwellwert",
|
||||||
"VoltageStopThreshold": "DC Spannung - Stop",
|
"VoltageLoadCorrectionFactor": "Lastkorrekturfaktor",
|
||||||
"VoltageSolarPassthroughStartThreshold": "DC Spannung - Start Solar-Passthrough",
|
|
||||||
"VoltageSolarPassthroughStopThreshold": "DC Spannung - Stop Solar-Passthrough",
|
|
||||||
"VoltageSolarPassthroughStartThresholdHint": "Wenn der Batteriespannung über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist. Dieser Mode wird aktiv wenn das Start Spannungslimit überschritten wird und inaktiv wenn das Stop Spannungslimit unterschritten wird.",
|
|
||||||
"VoltageLoadCorrectionFactor": "DC Spannung - Lastkorrekturfaktor",
|
|
||||||
"BatterySocInfo": "<b>Hinweis:</b> Die Akku SoC (State of Charge) Werte werden nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte geschickt hat. Andernfalls werden als Fallback-Option die Spannungseinstellungen verwendet.",
|
"BatterySocInfo": "<b>Hinweis:</b> Die Akku SoC (State of Charge) Werte werden nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte geschickt hat. Andernfalls werden als Fallback-Option die Spannungseinstellungen verwendet.",
|
||||||
"InverterIsBehindPowerMeter": "Welchselrichter ist hinter Leistungsmesser",
|
"InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichterleistung",
|
||||||
"Battery": "DC / Akku",
|
"InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist",
|
||||||
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht normalerweise die Spannung etwas ein. Damit nicht vorzeitig der Wechelrichter ausgeschaltet wird sobald der \"Stop\"-Schwellenwert erreicht wird, wird der hier angegebene Korrekturfaktor mit einberechnet. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
|
"VoltageThresholds": "Batterie Spannungs-Schwellwerte ",
|
||||||
"InverterRestart": "Wechselrichter Neustart",
|
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit nicht vorzeitig der Wechselrichter ausgeschaltet wird sobald der Stop-Schwellenwert unterschritten wurde, wird der hier angegebene Korrekturfaktor mit einberechnet um die Spannung zu errechnen die der Akku in Ruhe hätte. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
|
||||||
"InverterRestartHour": "Stunde für Neustart",
|
"InverterRestartHour": "Uhrzeit für geplanten Neustart",
|
||||||
"InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen."
|
"InverterRestartDisabled": "Keinen automatischen Neustart planen",
|
||||||
|
"InverterRestartHint": "Der Tagesertrag des Wechselrichters wird normalerweise nachts zurückgesetzt, wenn sich der Wechselrichter mangels Licht abschaltet. Um den Tageserstrag zurückzusetzen obwohl der Wechselrichter dauerhaft von der Batterie gespeist wird, kann der Inverter täglich zur gewünschten Uhrzeit automatisch neu gestartet werden."
|
||||||
},
|
},
|
||||||
"batteryadmin": {
|
"batteryadmin": {
|
||||||
"BatterySettings": "Batterie Einstellungen",
|
"BatterySettings": "Batterie Einstellungen",
|
||||||
@ -724,11 +730,11 @@
|
|||||||
"UploadProgress": "Hochlade-Fortschritt"
|
"UploadProgress": "Hochlade-Fortschritt"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"AboutOpendtu": "Über OpenDTU",
|
"AboutOpendtu": "Über OpenDTU-OnBattery",
|
||||||
"Documentation": "Dokumentation",
|
"Documentation": "Dokumentation",
|
||||||
"DocumentationBody": "Die Firmware- und Hardware-Dokumentation ist hier zu finden: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
|
"DocumentationBody": "Die Firmware- und Hardware-Dokumentation des <b>Basis-Projektes</b> ist hier zu finden: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a><br>Zusätzliche Informationen, insbesondere zu OpenDTU-OnBattery-spezifischen Funktionen, gibt es im <a href=\"https://github.com/helgeerbe/OpenDTU-OnBattery/wiki\" target=\"_blank\">Wiki auf Github</a>.",
|
||||||
"ProjectOrigin": "Projekt Ursprung",
|
"ProjectOrigin": "Projekt Ursprung",
|
||||||
"ProjectOriginBody1": "Das Projekt wurde aus <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">dieser Diskussion (mikrocontroller.net)</a> heraus gestartet.",
|
"ProjectOriginBody1": "OpenDTU-OnBattery ist eine Erweiterung von OpenDTU. Das Basis-Projekt OpenDTU wurde aus <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">dieser Diskussion (mikrocontroller.net)</a> heraus gestartet.",
|
||||||
"ProjectOriginBody2": "Das Hoymiles-Protokoll wurde durch die freiwilligen Bemühungen vieler Teilnehmer entschlüsselt. OpenDTU wurde unter anderem auf der Grundlage dieser Arbeit entwickelt. Das Projekt ist unter einer Open-Source-Lizenz lizenziert (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
"ProjectOriginBody2": "Das Hoymiles-Protokoll wurde durch die freiwilligen Bemühungen vieler Teilnehmer entschlüsselt. OpenDTU wurde unter anderem auf der Grundlage dieser Arbeit entwickelt. Das Projekt ist unter einer Open-Source-Lizenz lizenziert (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
||||||
"ProjectOriginBody3": "Die Software wurde nach bestem Wissen und Gewissen entwickelt. Dennoch kann keine Haftung für eine Fehlfunktion oder einen Garantieverlust des Wechselrichters übernommen werden.",
|
"ProjectOriginBody3": "Die Software wurde nach bestem Wissen und Gewissen entwickelt. Dennoch kann keine Haftung für eine Fehlfunktion oder einen Garantieverlust des Wechselrichters übernommen werden.",
|
||||||
"ProjectOriginBody4": "OpenDTU ist frei verfügbar. Wenn Sie Geld für die Software bezahlt haben, wurden Sie wahrscheinlich abgezockt.",
|
"ProjectOriginBody4": "OpenDTU ist frei verfügbar. Wenn Sie Geld für die Software bezahlt haben, wurden Sie wahrscheinlich abgezockt.",
|
||||||
@ -785,8 +791,15 @@
|
|||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"ValueSelected": "Ausgewählt",
|
"ValueSelected": "Ausgewählt",
|
||||||
"ValueActive": "Aktiv"
|
"ValueActive": "Aktiv"
|
||||||
},
|
},
|
||||||
"huawei": {
|
"inputserial": {
|
||||||
|
"format_hoymiles": "Hoymiles Seriennummerformat",
|
||||||
|
"format_converted": "Bereits konvertierte Seriennummer",
|
||||||
|
"format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}",
|
||||||
|
"format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme",
|
||||||
|
"format_unknown": "Unbekanntes Format"
|
||||||
|
},
|
||||||
|
"huawei": {
|
||||||
"DataAge": "letzte Aktualisierung: ",
|
"DataAge": "letzte Aktualisierung: ",
|
||||||
"Seconds": "vor {val} Sekunden",
|
"Seconds": "vor {val} Sekunden",
|
||||||
"Input": "Eingang",
|
"Input": "Eingang",
|
||||||
@ -813,8 +826,8 @@
|
|||||||
"SetVoltageLimit": "Spannungslimit:",
|
"SetVoltageLimit": "Spannungslimit:",
|
||||||
"SetCurrentLimit": "Stromlimit:",
|
"SetCurrentLimit": "Stromlimit:",
|
||||||
"CurrentLimit": "Aktuelles Limit: "
|
"CurrentLimit": "Aktuelles Limit: "
|
||||||
},
|
},
|
||||||
"acchargeradmin": {
|
"acchargeradmin": {
|
||||||
"ChargerSettings": "AC Ladegerät Einstellungen",
|
"ChargerSettings": "AC Ladegerät Einstellungen",
|
||||||
"Configuration": "AC Ladegerät Konfiguration",
|
"Configuration": "AC Ladegerät Konfiguration",
|
||||||
"EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv",
|
"EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv",
|
||||||
@ -827,8 +840,8 @@
|
|||||||
"lowerPowerLimit": "Minimale Leistung",
|
"lowerPowerLimit": "Minimale Leistung",
|
||||||
"upperPowerLimit": "Maximale Leistung",
|
"upperPowerLimit": "Maximale Leistung",
|
||||||
"Seconds": "@:base.Seconds"
|
"Seconds": "@:base.Seconds"
|
||||||
},
|
},
|
||||||
"battery": {
|
"battery": {
|
||||||
"battery": "Batterie",
|
"battery": "Batterie",
|
||||||
"DataAge": "letzte Aktualisierung: ",
|
"DataAge": "letzte Aktualisierung: ",
|
||||||
"Seconds": "vor {val} Sekunden",
|
"Seconds": "vor {val} Sekunden",
|
||||||
@ -895,6 +908,9 @@
|
|||||||
"bmsInternal": "BMS intern",
|
"bmsInternal": "BMS intern",
|
||||||
"chargeCycles": "Ladezyklen",
|
"chargeCycles": "Ladezyklen",
|
||||||
"chargedEnergy": "Geladene Energie",
|
"chargedEnergy": "Geladene Energie",
|
||||||
"dischargedEnergy": "Entladene Energie"
|
"dischargedEnergy": "Entladene Energie",
|
||||||
}
|
"instantaneousPower": "Aktuelle Leistung",
|
||||||
|
"consumedAmpHours": "Verbrauche Amperestunden",
|
||||||
|
"lastFullCharge": "Letztes mal Vollgeladen"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -555,6 +555,7 @@
|
|||||||
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
||||||
"typeHTTP": "HTTP(s) + JSON",
|
"typeHTTP": "HTTP(s) + JSON",
|
||||||
"typeSML": "SML (OBIS 16.7.0)",
|
"typeSML": "SML (OBIS 16.7.0)",
|
||||||
|
"typeSMAHM2": "SMA Homemanager 2.0",
|
||||||
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
|
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
|
||||||
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
|
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
|
||||||
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
|
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
|
||||||
@ -580,47 +581,52 @@
|
|||||||
"milliSeconds": "ms"
|
"milliSeconds": "ms"
|
||||||
},
|
},
|
||||||
"powerlimiteradmin": {
|
"powerlimiteradmin": {
|
||||||
"PowerLimiterSettings": "Power Limiter Settings",
|
"PowerLimiterSettings": "Dynamic Power Limiter Settings",
|
||||||
"PowerLimiterConfiguration": "Power Limiter Configuration",
|
"ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.",
|
||||||
|
"ConfigHints": "Configuration Notes",
|
||||||
|
"ConfigHintRequirement": "Required",
|
||||||
|
"ConfigHintOptional": "Optional",
|
||||||
|
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
|
||||||
|
"ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.",
|
||||||
|
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
|
||||||
|
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.",
|
||||||
|
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
|
||||||
|
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
|
||||||
"General": "General",
|
"General": "General",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
"VerboseLogging": "@:base.VerboseLogging",
|
"VerboseLogging": "@:base.VerboseLogging",
|
||||||
|
"SolarPassthrough": "Solar-Passthrough",
|
||||||
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
||||||
"SolarPassthroughLosses": "(Full) Solar Passthrough Losses:",
|
"SolarPassthroughLosses": "(Full) Solar-Passthrough Losses",
|
||||||
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
||||||
"BatteryDrainStrategy": "Battery drain strategy",
|
"BatteryDischargeAtNight": "Use battery at night even if only partially charged",
|
||||||
"BatteryDrainWhenFull": "Empty when full",
|
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
|
||||||
"BatteryDrainAtNight": "Empty at night",
|
"InverterSettings": "Inverter",
|
||||||
"SolarpassthroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.",
|
"Inverter": "Target Inverter",
|
||||||
"InverterId": "Inverter ID",
|
"SelectInverter": "Select an inverter...",
|
||||||
"InverterIdHint": "Select proper inverter ID where battery is connected to.",
|
"InverterChannelId": "Input used for voltage measurements",
|
||||||
"InverterChannelId": "Channel ID",
|
"TargetPowerConsumption": "Target Grid Consumption",
|
||||||
"InverterChannelIdHint": "Select proper channel where battery is connected to.",
|
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
|
||||||
"TargetPowerConsumption": "Target power consumption from grid",
|
"TargetPowerConsumptionHysteresis": "Hysteresis",
|
||||||
"TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.",
|
|
||||||
"TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit",
|
|
||||||
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
|
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
|
||||||
"LowerPowerLimit": "Lower power limit",
|
"LowerPowerLimit": "Lower Power Limit",
|
||||||
"UpperPowerLimit": "Upper power limit",
|
"UpperPowerLimit": "Upper Power Limit",
|
||||||
"PowerMeters": "Power meter",
|
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
|
||||||
"IgnoreSoc": "Ignore Battery SoC",
|
"IgnoreSoc": "Ignore Battery SoC",
|
||||||
"BatterySocStartThreshold": "Battery SoC - Start threshold",
|
"StartThreshold": "Start Threshold for Battery Discharging",
|
||||||
"BatterySocStopThreshold": "Battery SoC - Stop threshold",
|
"StopThreshold": "Stop Threshold for Battery Discharging",
|
||||||
"BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough",
|
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
|
||||||
"BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SoC is over this limit. Use this if you like to supply excess power to the grid when battery is full",
|
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
|
||||||
"VoltageStartThreshold": "DC Voltage - Start threshold",
|
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
|
||||||
"VoltageStopThreshold": "DC Voltage - Stop threshold",
|
"VoltageLoadCorrectionFactor": "Load correction factor",
|
||||||
"VoltageSolarPassthroughStartThreshold": "DC Voltage - Start threshold for full solar passthrough",
|
|
||||||
"VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough",
|
|
||||||
"VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.",
|
|
||||||
"VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor",
|
|
||||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
||||||
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
|
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
|
||||||
"Battery": "DC / Battery",
|
"InverterIsSolarPowered": "Inverter is powered by solar modules",
|
||||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
|
"VoltageThresholds": "Battery Voltage Thresholds",
|
||||||
"InverterRestart": "Inverter Restart",
|
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).",
|
||||||
"InverterRestartHour": "Restart Hour",
|
"InverterRestartHour": "Automatic Restart Time",
|
||||||
"InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values."
|
"InverterRestartDisabled": "Do not execute automatic restart",
|
||||||
|
"InverterRestartHint": "The daily yield of the inverter is usually reset at night when the inverter turns off due to lack of light. To reset the daily yield even though the inverter is continuously powered by the battery, the inverter can be automatically restarted daily at the desired time."
|
||||||
},
|
},
|
||||||
"batteryadmin": {
|
"batteryadmin": {
|
||||||
"BatterySettings": "Battery Settings",
|
"BatterySettings": "Battery Settings",
|
||||||
@ -730,11 +736,11 @@
|
|||||||
"UploadProgress": "Upload Progress"
|
"UploadProgress": "Upload Progress"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"AboutOpendtu": "About OpenDTU",
|
"AboutOpendtu": "About OpenDTU-OnBattery",
|
||||||
"Documentation": "Documentation",
|
"Documentation": "Documentation",
|
||||||
"DocumentationBody": "The firmware and hardware documentation can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
|
"DocumentationBody": "The firmware and hardware documentation of the <b>upstream</b> project can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a><br>Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the <a href=\"https://github.com/helgeerbe/OpenDTU-OnBattery/wiki\" target=\"_blank\">Github Wiki</a>.",
|
||||||
"ProjectOrigin": "Project Origin",
|
"ProjectOrigin": "Project Origin",
|
||||||
"ProjectOriginBody1": "This project was started from <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">this discussion. (Mikrocontroller.net)</a>",
|
"ProjectOriginBody1": "OpenDTU-OnBattery is a fork of OpenDTU. The upstream project OpenDTU was started from <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">this discussion. (Mikrocontroller.net)</a>",
|
||||||
"ProjectOriginBody2": "The Hoymiles protocol was decrypted through the voluntary efforts of many participants. OpenDTU, among others, was developed based on this work. The project is licensed under an Open Source License (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
"ProjectOriginBody2": "The Hoymiles protocol was decrypted through the voluntary efforts of many participants. OpenDTU, among others, was developed based on this work. The project is licensed under an Open Source License (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
||||||
"ProjectOriginBody3": "The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.",
|
"ProjectOriginBody3": "The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.",
|
||||||
"ProjectOriginBody4": "OpenDTU is freely available. If you paid money for the software, you probably got ripped off.",
|
"ProjectOriginBody4": "OpenDTU is freely available. If you paid money for the software, you probably got ripped off.",
|
||||||
@ -792,8 +798,15 @@
|
|||||||
"Number": "Number",
|
"Number": "Number",
|
||||||
"ValueSelected": "Selected",
|
"ValueSelected": "Selected",
|
||||||
"ValueActive": "Active"
|
"ValueActive": "Active"
|
||||||
|
},
|
||||||
|
"inputserial": {
|
||||||
|
"format_hoymiles": "Hoymiles serial number format",
|
||||||
|
"format_converted": "Already converted serial number",
|
||||||
|
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
|
||||||
|
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
|
||||||
|
"format_unknown": "Unknown format"
|
||||||
},
|
},
|
||||||
"huawei": {
|
"huawei": {
|
||||||
"DataAge": "Data Age: ",
|
"DataAge": "Data Age: ",
|
||||||
"Seconds": " {val} seconds",
|
"Seconds": " {val} seconds",
|
||||||
"Input": "Input",
|
"Input": "Input",
|
||||||
@ -820,8 +833,8 @@
|
|||||||
"SetVoltageLimit": "Voltage limit:",
|
"SetVoltageLimit": "Voltage limit:",
|
||||||
"SetCurrentLimit": "Current limit:",
|
"SetCurrentLimit": "Current limit:",
|
||||||
"CurrentLimit": "Current limit:"
|
"CurrentLimit": "Current limit:"
|
||||||
},
|
},
|
||||||
"acchargeradmin": {
|
"acchargeradmin": {
|
||||||
"ChargerSettings": "AC Charger Settings",
|
"ChargerSettings": "AC Charger Settings",
|
||||||
"Configuration": "AC Charger Configuration",
|
"Configuration": "AC Charger Configuration",
|
||||||
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
||||||
@ -834,8 +847,8 @@
|
|||||||
"lowerPowerLimit": "Minimum output power",
|
"lowerPowerLimit": "Minimum output power",
|
||||||
"upperPowerLimit": "Maximum output power",
|
"upperPowerLimit": "Maximum output power",
|
||||||
"Seconds": "@:base.Seconds"
|
"Seconds": "@:base.Seconds"
|
||||||
},
|
},
|
||||||
"battery": {
|
"battery": {
|
||||||
"battery": "Battery",
|
"battery": "Battery",
|
||||||
"DataAge": "Data Age: ",
|
"DataAge": "Data Age: ",
|
||||||
"Seconds": " {val} seconds",
|
"Seconds": " {val} seconds",
|
||||||
@ -902,6 +915,9 @@
|
|||||||
"bmsInternal": "BMS internal",
|
"bmsInternal": "BMS internal",
|
||||||
"chargeCycles": "Charge cycles",
|
"chargeCycles": "Charge cycles",
|
||||||
"chargedEnergy": "Charged energy",
|
"chargedEnergy": "Charged energy",
|
||||||
"dischargedEnergy": "Discharged energy"
|
"dischargedEnergy": "Discharged energy",
|
||||||
}
|
"instantaneousPower": "Instantaneous Power",
|
||||||
|
"consumedAmpHours": "Consumed Amp Hours",
|
||||||
|
"lastFullCharge": "Last full Charge"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -663,44 +663,49 @@
|
|||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel"
|
||||||
},
|
},
|
||||||
"powerlimiteradmin": {
|
"powerlimiteradmin": {
|
||||||
"PowerLimiterSettings": "Power Limiter Settings",
|
"PowerLimiterSettings": "Dynamic Power Limiter Settings",
|
||||||
"PowerLimiterConfiguration": "Power Limiter Configuration",
|
"ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.",
|
||||||
|
"ConfigHints": "Configuration Notes",
|
||||||
|
"ConfigHintRequirement": "Required",
|
||||||
|
"ConfigHintOptional": "Optional",
|
||||||
|
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
|
||||||
|
"ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.",
|
||||||
|
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
|
||||||
|
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.",
|
||||||
|
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
|
||||||
|
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
|
||||||
"General": "General",
|
"General": "General",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
"VerboseLogging": "@:base.VerboseLogging",
|
"VerboseLogging": "@:base.VerboseLogging",
|
||||||
|
"SolarPassthrough": "Solar-Passthrough",
|
||||||
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
||||||
"SolarPassthroughLosses": "(Full) Solar Passthrough Losses:",
|
"SolarPassthroughLosses": "(Full) Solar-Passthrough Losses",
|
||||||
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
||||||
"BatteryDrainStrategy": "Battery drain strategy",
|
"BatteryDischargeAtNight": "Use battery at night even if only partially charged",
|
||||||
"BatteryDrainWhenFull": "Empty when full",
|
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
|
||||||
"BatteryDrainAtNight": "Empty at night",
|
"InverterSettings": "Inverter",
|
||||||
"SolarpassthroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.",
|
"Inverter": "Target Inverter",
|
||||||
"InverterId": "Inverter ID",
|
"SelectInverter": "Select an inverter...",
|
||||||
"InverterIdHint": "Select proper inverter ID where battery is connected to.",
|
"InverterChannelId": "Input used for voltage measurements",
|
||||||
"InverterChannelId": "Channel ID",
|
"TargetPowerConsumption": "Target Grid Consumption",
|
||||||
"InverterChannelIdHint": "Select proper channel where battery is connected to.",
|
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
|
||||||
"TargetPowerConsumption": "Target power consumption from grid",
|
"TargetPowerConsumptionHysteresis": "Hysteresis",
|
||||||
"TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.",
|
|
||||||
"TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit",
|
|
||||||
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
|
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
|
||||||
"LowerPowerLimit": "Lower power limit",
|
"LowerPowerLimit": "Lower Power Limit",
|
||||||
"UpperPowerLimit": "Upper power limit",
|
"UpperPowerLimit": "Upper Power Limit",
|
||||||
"PowerMeters": "Power meter",
|
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
|
||||||
"IgnoreSoc": "Ignore Battery SoC",
|
"IgnoreSoc": "Ignore Battery SoC",
|
||||||
"BatterySocStartThreshold": "Battery SoC - Start threshold",
|
"StartThreshold": "Start Threshold for Battery Discharging",
|
||||||
"BatterySocStopThreshold": "Battery SoC - Stop threshold",
|
"StopThreshold": "Stop Threshold for Battery Discharging",
|
||||||
"BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough",
|
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
|
||||||
"BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SOC is over this limit. Use this if you like to supply excess power to the grid when battery is full",
|
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
|
||||||
"VoltageStartThreshold": "DC Voltage - Start threshold",
|
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
|
||||||
"VoltageStopThreshold": "DC Voltage - Stop threshold",
|
"VoltageLoadCorrectionFactor": "Load correction factor",
|
||||||
"VoltageSolarPassthroughStartThreshold": "DC Voltage - Start threshold for full solar passthrough",
|
|
||||||
"VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough",
|
|
||||||
"VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.",
|
|
||||||
"VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor",
|
|
||||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
||||||
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
|
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
|
||||||
"Battery": "DC / Battery",
|
"InverterIsSolarPowered": "Inverter is powered by solar modules",
|
||||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor)."
|
"VoltageThresholds": "Battery Voltage Thresholds",
|
||||||
|
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Connexion",
|
"Login": "Connexion",
|
||||||
@ -723,9 +728,9 @@
|
|||||||
"UploadProgress": "Progression du téléversement"
|
"UploadProgress": "Progression du téléversement"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"AboutOpendtu": "À propos d'OpenDTU",
|
"AboutOpendtu": "À propos d'OpenDTU-OnBattery",
|
||||||
"Documentation": "Documentation",
|
"Documentation": "Documentation",
|
||||||
"DocumentationBody": "The firmware and hardware documentation can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
|
"DocumentationBody": "The firmware and hardware documentation of the <b>upstream</b> project can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a><br>Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the <a href=\"https://github.com/helgeerbe/OpenDTU-OnBattery/wiki\" target=\"_blank\">Github Wiki</a>.",
|
||||||
"ProjectOrigin": "Origine du projet",
|
"ProjectOrigin": "Origine du projet",
|
||||||
"ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">(Mikrocontroller.net)</a>.",
|
"ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">(Mikrocontroller.net)</a>.",
|
||||||
"ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
"ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
|
||||||
@ -784,8 +789,15 @@
|
|||||||
"Name": "Nom",
|
"Name": "Nom",
|
||||||
"ValueSelected": "Sélectionné",
|
"ValueSelected": "Sélectionné",
|
||||||
"ValueActive": "Activé"
|
"ValueActive": "Activé"
|
||||||
},
|
},
|
||||||
"huawei": {
|
"inputserial": {
|
||||||
|
"format_hoymiles": "Hoymiles serial number format",
|
||||||
|
"format_converted": "Already converted serial number",
|
||||||
|
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
|
||||||
|
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
|
||||||
|
"format_unknown": "Unknown format"
|
||||||
|
},
|
||||||
|
"huawei": {
|
||||||
"DataAge": "Data Age: ",
|
"DataAge": "Data Age: ",
|
||||||
"Seconds": " {val} seconds",
|
"Seconds": " {val} seconds",
|
||||||
"Input": "Input",
|
"Input": "Input",
|
||||||
@ -812,8 +824,8 @@
|
|||||||
"SetVoltageLimit": "Voltage limit:",
|
"SetVoltageLimit": "Voltage limit:",
|
||||||
"SetCurrentLimit": "Current limit:",
|
"SetCurrentLimit": "Current limit:",
|
||||||
"CurrentLimit": "Current limit:"
|
"CurrentLimit": "Current limit:"
|
||||||
},
|
},
|
||||||
"acchargeradmin": {
|
"acchargeradmin": {
|
||||||
"ChargerSettings": "AC Charger Settings",
|
"ChargerSettings": "AC Charger Settings",
|
||||||
"Configuration": "AC Charger Configuration",
|
"Configuration": "AC Charger Configuration",
|
||||||
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
||||||
@ -826,8 +838,8 @@
|
|||||||
"lowerPowerLimit": "Minimum output power",
|
"lowerPowerLimit": "Minimum output power",
|
||||||
"upperPowerLimit": "Maximum output power",
|
"upperPowerLimit": "Maximum output power",
|
||||||
"Seconds": "@:base.Seconds"
|
"Seconds": "@:base.Seconds"
|
||||||
},
|
},
|
||||||
"battery": {
|
"battery": {
|
||||||
"battery": "Battery",
|
"battery": "Battery",
|
||||||
"DataAge": "Data Age: ",
|
"DataAge": "Data Age: ",
|
||||||
"Seconds": " {val} seconds",
|
"Seconds": " {val} seconds",
|
||||||
@ -894,6 +906,9 @@
|
|||||||
"bmsInternal": "BMS internal",
|
"bmsInternal": "BMS internal",
|
||||||
"chargeCycles": "Charge cycles",
|
"chargeCycles": "Charge cycles",
|
||||||
"chargedEnergy": "Charged energy",
|
"chargedEnergy": "Charged energy",
|
||||||
"dischargedEnergy": "Discharged energy"
|
"dischargedEnergy": "Discharged energy",
|
||||||
}
|
"instantaneousPower": "Instantaneous Power",
|
||||||
|
"consumedAmpHours": "Consumed Amp Hours",
|
||||||
|
"lastFullCharge": "Last full Charge"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export interface DevInfoStatus {
|
export interface DevInfoStatus {
|
||||||
serial: number;
|
serial: string;
|
||||||
valid_data: boolean;
|
valid_data: boolean;
|
||||||
fw_bootloader_version: number;
|
fw_bootloader_version: number;
|
||||||
fw_build_version: number;
|
fw_build_version: number;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export interface InverterChannel {
|
|||||||
|
|
||||||
export interface Inverter {
|
export interface Inverter {
|
||||||
id: string;
|
id: string;
|
||||||
serial: number;
|
serial: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
order: number;
|
order: number;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export interface LimitConfig {
|
export interface LimitConfig {
|
||||||
serial: number;
|
serial: string;
|
||||||
limit_value: number;
|
limit_value: number;
|
||||||
limit_type: number;
|
limit_type: number;
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ export interface InverterStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Inverter {
|
export interface Inverter {
|
||||||
serial: number;
|
serial: string;
|
||||||
name: string;
|
name: string;
|
||||||
order: number;
|
order: number;
|
||||||
data_age: number;
|
data_age: number;
|
||||||
|
|||||||
@ -1,11 +1,32 @@
|
|||||||
|
export interface PowerLimiterInverterInfo {
|
||||||
|
pos: number;
|
||||||
|
name: string;
|
||||||
|
poll_enable: boolean;
|
||||||
|
poll_enable_night: boolean;
|
||||||
|
command_enable: boolean;
|
||||||
|
command_enable_night: boolean;
|
||||||
|
type: string;
|
||||||
|
channels: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// meta-data not directly part of the DPL settings,
|
||||||
|
// to control visibility of DPL settings
|
||||||
|
export interface PowerLimiterMetaData {
|
||||||
|
power_meter_enabled: boolean;
|
||||||
|
battery_enabled: boolean;
|
||||||
|
charge_controller_enabled: boolean;
|
||||||
|
inverters: { [key: string]: PowerLimiterInverterInfo };
|
||||||
|
}
|
||||||
|
|
||||||
export interface PowerLimiterConfig {
|
export interface PowerLimiterConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
verbose_logging: boolean;
|
verbose_logging: boolean;
|
||||||
solar_passthrough_enabled: boolean;
|
solar_passthrough_enabled: boolean;
|
||||||
solar_passthrough_losses: number;
|
solar_passthrough_losses: number;
|
||||||
battery_drain_strategy: number;
|
battery_always_use_at_night: boolean;
|
||||||
is_inverter_behind_powermeter: boolean;
|
is_inverter_behind_powermeter: boolean;
|
||||||
inverter_id: number;
|
is_inverter_solar_powered: boolean;
|
||||||
|
inverter_serial: string;
|
||||||
inverter_channel_id: number;
|
inverter_channel_id: number;
|
||||||
target_power_consumption: number;
|
target_power_consumption: number;
|
||||||
target_power_consumption_hysteresis: number;
|
target_power_consumption_hysteresis: number;
|
||||||
|
|||||||
@ -5,12 +5,22 @@ export interface DynamicPowerLimiter {
|
|||||||
PLLIMIT: number;
|
PLLIMIT: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Vedirect {
|
||||||
|
full_update: boolean;
|
||||||
|
instances: { [key: string]: VedirectInstance };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VedirectInstance {
|
||||||
|
data_age_ms: number;
|
||||||
|
device: VedirectDevice;
|
||||||
|
output: VedirectOutput;
|
||||||
|
input: VedirectInput;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VedirectDevice {
|
export interface VedirectDevice {
|
||||||
SER: string;
|
SER: string;
|
||||||
PID: string;
|
PID: string;
|
||||||
FW: string;
|
FW: string;
|
||||||
age_critical: boolean;
|
|
||||||
data_age: 0;
|
|
||||||
LOAD: ValueObject;
|
LOAD: ValueObject;
|
||||||
CS: ValueObject;
|
CS: ValueObject;
|
||||||
MPPT: ValueObject;
|
MPPT: ValueObject;
|
||||||
|
|||||||
@ -355,7 +355,7 @@ export default defineComponent({
|
|||||||
showAlertLimit: false,
|
showAlertLimit: false,
|
||||||
|
|
||||||
powerSettingView: {} as bootstrap.Modal,
|
powerSettingView: {} as bootstrap.Modal,
|
||||||
powerSettingSerial: 0,
|
powerSettingSerial: "",
|
||||||
powerSettingLoading: true,
|
powerSettingLoading: true,
|
||||||
alertMessagePower: "",
|
alertMessagePower: "",
|
||||||
alertTypePower: "info",
|
alertTypePower: "info",
|
||||||
@ -532,7 +532,7 @@ export default defineComponent({
|
|||||||
this.heartInterval && clearTimeout(this.heartInterval);
|
this.heartInterval && clearTimeout(this.heartInterval);
|
||||||
this.isFirstFetchAfterConnect = true;
|
this.isFirstFetchAfterConnect = true;
|
||||||
},
|
},
|
||||||
onShowEventlog(serial: number) {
|
onShowEventlog(serial: string) {
|
||||||
this.eventLogLoading = true;
|
this.eventLogLoading = true;
|
||||||
fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() })
|
fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() })
|
||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
@ -543,7 +543,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.eventLogView.show();
|
this.eventLogView.show();
|
||||||
},
|
},
|
||||||
onShowDevInfo(serial: number) {
|
onShowDevInfo(serial: string) {
|
||||||
this.devInfoLoading = true;
|
this.devInfoLoading = true;
|
||||||
fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() })
|
fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() })
|
||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
@ -555,7 +555,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.devInfoView.show();
|
this.devInfoView.show();
|
||||||
},
|
},
|
||||||
onShowGridProfile(serial: number) {
|
onShowGridProfile(serial: string) {
|
||||||
this.gridProfileLoading = true;
|
this.gridProfileLoading = true;
|
||||||
fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() })
|
fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() })
|
||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
@ -572,9 +572,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.gridProfileView.show();
|
this.gridProfileView.show();
|
||||||
},
|
},
|
||||||
onShowLimitSettings(serial: number) {
|
onShowLimitSettings(serial: string) {
|
||||||
this.showAlertLimit = false;
|
this.showAlertLimit = false;
|
||||||
this.targetLimitList.serial = 0;
|
this.targetLimitList.serial = "";
|
||||||
this.targetLimitList.limit_value = 0;
|
this.targetLimitList.limit_value = 0;
|
||||||
this.targetLimitType = 1;
|
this.targetLimitType = 1;
|
||||||
this.targetLimitTypeText = this.$t('home.Relative');
|
this.targetLimitTypeText = this.$t('home.Relative');
|
||||||
@ -628,9 +628,9 @@ export default defineComponent({
|
|||||||
this.targetLimitType = type;
|
this.targetLimitType = type;
|
||||||
},
|
},
|
||||||
|
|
||||||
onShowPowerSettings(serial: number) {
|
onShowPowerSettings(serial: string) {
|
||||||
this.showAlertPower = false;
|
this.showAlertPower = false;
|
||||||
this.powerSettingSerial = 0;
|
this.powerSettingSerial = "";
|
||||||
this.powerSettingLoading = true;
|
this.powerSettingLoading = true;
|
||||||
fetch("/api/power/status", { headers: authHeader() })
|
fetch("/api/power/status", { headers: authHeader() })
|
||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
|
|||||||
@ -8,8 +8,7 @@
|
|||||||
<form class="form-inline" v-on:submit.prevent="onSubmit">
|
<form class="form-inline" v-on:submit.prevent="onSubmit">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ $t('inverteradmin.Serial') }}</label>
|
<label>{{ $t('inverteradmin.Serial') }}</label>
|
||||||
<input v-model="newInverterData.serial" type="number" class="form-control ml-sm-2 mr-sm-4 my-2"
|
<InputSerial v-model="newInverterData.serial" inputClass="ml-sm-2 mr-sm-4 my-2" required />
|
||||||
required />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ $t('inverteradmin.Name') }}</label>
|
<label>{{ $t('inverteradmin.Name') }}</label>
|
||||||
@ -91,7 +90,7 @@
|
|||||||
<label for="inverter-serial" class="col-form-label">
|
<label for="inverter-serial" class="col-form-label">
|
||||||
{{ $t('inverteradmin.InverterSerial') }}
|
{{ $t('inverteradmin.InverterSerial') }}
|
||||||
</label>
|
</label>
|
||||||
<input v-model="selectedInverterData.serial" type="number" id="inverter-serial" class="form-control" />
|
<InputSerial v-model="selectedInverterData.serial" id="inverter-serial" />
|
||||||
<label for="inverter-name" class="col-form-label">{{ $t('inverteradmin.InverterName') }}
|
<label for="inverter-name" class="col-form-label">{{ $t('inverteradmin.InverterName') }}
|
||||||
<BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" />
|
<BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" />
|
||||||
</label>
|
</label>
|
||||||
@ -207,6 +206,7 @@ import BasePage from '@/components/BasePage.vue';
|
|||||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||||
import CardElement from '@/components/CardElement.vue';
|
import CardElement from '@/components/CardElement.vue';
|
||||||
import InputElement from '@/components/InputElement.vue';
|
import InputElement from '@/components/InputElement.vue';
|
||||||
|
import InputSerial from '@/components/InputSerial.vue';
|
||||||
import ModalDialog from '@/components/ModalDialog.vue';
|
import ModalDialog from '@/components/ModalDialog.vue';
|
||||||
import type { Inverter } from '@/types/InverterConfig';
|
import type { Inverter } from '@/types/InverterConfig';
|
||||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||||
@ -235,6 +235,7 @@ export default defineComponent({
|
|||||||
BootstrapAlert,
|
BootstrapAlert,
|
||||||
CardElement,
|
CardElement,
|
||||||
InputElement,
|
InputElement,
|
||||||
|
InputSerial,
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
BIconInfoCircle,
|
BIconInfoCircle,
|
||||||
BIconPencil,
|
BIconPencil,
|
||||||
|
|||||||
@ -1,276 +1,200 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasePage :title="'Dynamic Power limiter Settings'" :isLoading="dataLoading">
|
<BasePage :title="$t('powerlimiteradmin.PowerLimiterSettings')" :isLoading="dataLoading">
|
||||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||||
{{ alertMessage }}
|
{{ alertMessage }}
|
||||||
</BootstrapAlert>
|
</BootstrapAlert>
|
||||||
|
|
||||||
<form @submit="savePowerLimiterConfig">
|
<BootstrapAlert v-model="configAlert" variant="warning">
|
||||||
<CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary">
|
{{ $t('powerlimiteradmin.ConfigAlertMessage') }}
|
||||||
|
</BootstrapAlert>
|
||||||
|
|
||||||
|
<CardElement :text="$t('powerlimiteradmin.ConfigHints')" textVariant="text-bg-primary" v-if="getConfigHints().length">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{{ $t('powerlimiteradmin.ConfigHintsIntro') }}
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li v-for="(hint, idx) in getConfigHints()" :key="idx">
|
||||||
|
<b v-if="hint.severity === 'requirement'">{{ $t('powerlimiteradmin.ConfigHintRequirement') }}:</b>
|
||||||
|
<b v-if="hint.severity === 'optional'">{{ $t('powerlimiteradmin.ConfigHintOptional') }}:</b>
|
||||||
|
{{ $t('powerlimiteradmin.ConfigHint' + hint.subject) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardElement>
|
||||||
|
|
||||||
|
<form @submit="savePowerLimiterConfig" v-if="!configAlert">
|
||||||
|
<CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary" add-space>
|
||||||
<InputElement :label="$t('powerlimiteradmin.Enable')"
|
<InputElement :label="$t('powerlimiteradmin.Enable')"
|
||||||
v-model="powerLimiterConfigList.enabled"
|
v-model="powerLimiterConfigList.enabled"
|
||||||
type="checkbox" wide/>
|
type="checkbox" wide/>
|
||||||
|
|
||||||
<InputElement v-show="powerLimiterConfigList.enabled"
|
<InputElement v-show="isEnabled()"
|
||||||
:label="$t('powerlimiteradmin.VerboseLogging')"
|
:label="$t('powerlimiteradmin.VerboseLogging')"
|
||||||
v-model="powerLimiterConfigList.verbose_logging"
|
v-model="powerLimiterConfigList.verbose_logging"
|
||||||
type="checkbox" wide/>
|
type="checkbox" wide/>
|
||||||
|
|
||||||
<InputElement v-show="powerLimiterConfigList.enabled"
|
<InputElement v-show="isEnabled()"
|
||||||
:label="$t('powerlimiteradmin.EnableSolarPassthrough')"
|
:label="$t('powerlimiteradmin.TargetPowerConsumption')"
|
||||||
v-model="powerLimiterConfigList.solar_passthrough_enabled"
|
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')"
|
||||||
type="checkbox" wide/>
|
v-model="powerLimiterConfigList.target_power_consumption"
|
||||||
|
postfix="W"
|
||||||
|
type="number" wide/>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled && powerLimiterConfigList.solar_passthrough_enabled">
|
<InputElement v-show="isEnabled()"
|
||||||
<label for="inputTimezone" class="col-sm-2 col-form-label">
|
:label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')"
|
||||||
{{ $t('powerlimiteradmin.BatteryDrainStrategy') }}:
|
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')"
|
||||||
|
v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
|
||||||
|
postfix="W"
|
||||||
|
type="number" wide/>
|
||||||
|
</CardElement>
|
||||||
|
|
||||||
|
<CardElement :text="$t('powerlimiteradmin.InverterSettings')" textVariant="text-bg-primary" add-space v-if="isEnabled()">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="inverter_serial" class="col-sm-4 col-form-label">
|
||||||
|
{{ $t('powerlimiteradmin.Inverter') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<select class="form-select" v-model="powerLimiterConfigList.battery_drain_strategy">
|
<select id="inverter_serial" class="form-select" v-model="powerLimiterConfigList.inverter_serial" required>
|
||||||
<option v-for="batteryDrainStrategy in batteryDrainStrategyList" :key="batteryDrainStrategy.key" :value="batteryDrainStrategy.key">
|
<option value="" disabled hidden selected>{{ $t('powerlimiteradmin.SelectInverter') }}</option>
|
||||||
{{ $t(batteryDrainStrategy.value) }}
|
<option v-for="(inv, serial) in powerLimiterMetaData.inverters" :key="serial" :value="serial">
|
||||||
|
{{ inv.name }} ({{ inv.type }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-secondary" v-show="powerLimiterConfigList.enabled" role="alert" v-html="$t('powerlimiteradmin.SolarpassthroughInfo')"></div>
|
<InputElement :label="$t('powerlimiteradmin.InverterIsSolarPowered')"
|
||||||
|
v-model="powerLimiterConfigList.is_inverter_solar_powered"
|
||||||
|
type="checkbox" wide/>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled && powerLimiterConfigList.solar_passthrough_enabled">
|
<div class="row mb-3" v-if="needsChannelSelection()">
|
||||||
<label for="solarPassthroughLosses" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.SolarPassthroughLosses') }}</label>
|
<label for="inverter_channel" class="col-sm-4 col-form-label">
|
||||||
<div class="col-sm-10">
|
{{ $t('powerlimiteradmin.InverterChannelId') }}
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="solarPassthroughLosses"
|
|
||||||
placeholder="3" v-model="powerLimiterConfigList.solar_passthrough_losses"
|
|
||||||
aria-describedby="solarPassthroughLossesDescription" min="0" max="10" required/>
|
|
||||||
<span class="input-group-text" id="solarPassthroughLossesDescription">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-secondary" role="alert" v-show="powerLimiterConfigList.enabled && powerLimiterConfigList.solar_passthrough_enabled" v-html="$t('powerlimiteradmin.SolarPassthroughLossesInfo')"></div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="inputTimezone" class="col-sm-2 col-form-label">
|
|
||||||
{{ $t('powerlimiteradmin.InverterId') }}:
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterIdHint')" />
|
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<select class="form-select" v-model="powerLimiterConfigList.inverter_id">
|
<select id="inverter_channel" class="form-select" v-model="powerLimiterConfigList.inverter_channel_id">
|
||||||
<option v-for="inverter in inverterList" :key="inverter.key" :value="inverter.key">
|
<option v-for="channel in range(powerLimiterMetaData.inverters[powerLimiterConfigList.inverter_serial].channels)" :key="channel" :value="channel">
|
||||||
{{ inverter.value }}
|
{{ channel + 1 }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
<InputElement :label="$t('powerlimiteradmin.LowerPowerLimit')"
|
||||||
<label for="inputTimezone" class="col-sm-2 col-form-label">
|
v-model="powerLimiterConfigList.lower_power_limit"
|
||||||
{{ $t('powerlimiteradmin.InverterChannelId') }}:
|
placeholder="50" min="10" postfix="W"
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterChannelIdHint')" />
|
type="number" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.UpperPowerLimit')"
|
||||||
|
v-model="powerLimiterConfigList.upper_power_limit"
|
||||||
|
placeholder="800" min="20" postfix="W"
|
||||||
|
type="number" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
|
||||||
|
v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
|
||||||
|
type="checkbox" wide/>
|
||||||
|
|
||||||
|
<div class="row mb-3" v-if="!powerLimiterConfigList.is_inverter_solar_powered">
|
||||||
|
<label for="inverter_restart" class="col-sm-4 col-form-label">
|
||||||
|
{{ $t('powerlimiteradmin.InverterRestartHour') }}
|
||||||
|
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" />
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<select class="form-select" v-model="powerLimiterConfigList.inverter_channel_id">
|
<select id="inverter_restart" class="form-select" v-model="powerLimiterConfigList.inverter_restart_hour">
|
||||||
<option v-for="inverterChannel in inverterChannelList" :key="inverterChannel.key" :value="inverterChannel.key">
|
<option value="-1">
|
||||||
{{ inverterChannel.value }}
|
{{ $t('powerlimiteradmin.InverterRestartDisabled') }}
|
||||||
|
</option>
|
||||||
|
<option v-for="hour in range(24)" :key="hour" :value="hour">
|
||||||
|
{{ (hour > 9) ? hour : "0"+hour }}:00
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="targetPowerConsumption" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.TargetPowerConsumption') }}:
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.TargetPowerConsumptionHint')" />
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="targetPowerConsumption"
|
|
||||||
placeholder="75" v-model="powerLimiterConfigList.target_power_consumption"
|
|
||||||
aria-describedby="targetPowerConsumptionDescription" required/>
|
|
||||||
<span class="input-group-text" id="targetPowerConsumptionDescription">W</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="targetPowerConsumptionHyteresis" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.TargetPowerConsumptionHysteresis') }}:
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')" required/>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="targetPowerConsumptionHysteresis"
|
|
||||||
placeholder="30" min="0" v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
|
|
||||||
aria-describedby="targetPowerConsumptionHysteresisDescription" />
|
|
||||||
<span class="input-group-text" id="targetPowerConsumptionHysteresisDescription">W</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="inputLowerPowerLimit" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.LowerPowerLimit') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="inputLowerPowerLimit"
|
|
||||||
placeholder="50" min="10" v-model="powerLimiterConfigList.lower_power_limit"
|
|
||||||
aria-describedby="lowerPowerLimitDescription" required/>
|
|
||||||
<span class="input-group-text" id="lowerPowerLimitDescription">W</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="inputUpperPowerLimit" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.UpperPowerLimit') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="inputUpperPowerLimit"
|
|
||||||
placeholder="800" v-model="powerLimiterConfigList.upper_power_limit"
|
|
||||||
aria-describedby="upperPowerLimitDescription" required/>
|
|
||||||
<span class="input-group-text" id="upperPowerLimitDescription">W</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardElement>
|
</CardElement>
|
||||||
|
|
||||||
<CardElement :text="$t('powerlimiteradmin.PowerMeters')" textVariant="text-bg-primary" add-space
|
<CardElement :text="$t('powerlimiteradmin.SolarPassthrough')" textVariant="text-bg-primary" add-space v-if="canUseSolarPassthrough()">
|
||||||
v-show="powerLimiterConfigList.enabled"
|
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarpassthroughInfo')"></div>
|
||||||
>
|
|
||||||
<InputElement
|
<InputElement :label="$t('powerlimiteradmin.EnableSolarPassthrough')"
|
||||||
:label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
|
v-model="powerLimiterConfigList.solar_passthrough_enabled"
|
||||||
v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
|
type="checkbox" wide/>
|
||||||
type="checkbox" wide/>
|
|
||||||
|
<div v-if="powerLimiterConfigList.solar_passthrough_enabled">
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.BatteryDischargeAtNight')"
|
||||||
|
v-model="powerLimiterConfigList.battery_always_use_at_night"
|
||||||
|
type="checkbox" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.SolarPassthroughLosses')"
|
||||||
|
v-model="powerLimiterConfigList.solar_passthrough_losses"
|
||||||
|
placeholder="3" min="0" max="10" postfix="%"
|
||||||
|
type="number" wide/>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarPassthroughLossesInfo')"></div>
|
||||||
|
</div>
|
||||||
</CardElement>
|
</CardElement>
|
||||||
|
|
||||||
<CardElement :text="$t('powerlimiteradmin.Battery')" textVariant="text-bg-primary" add-space
|
<CardElement :text="$t('powerlimiteradmin.SocThresholds')" textVariant="text-bg-primary" add-space v-if="canUseSoCThresholds()">
|
||||||
v-show="powerLimiterConfigList.enabled"
|
|
||||||
>
|
|
||||||
<InputElement
|
<InputElement
|
||||||
:label="$t('powerlimiteradmin.IgnoreSoc')"
|
:label="$t('powerlimiteradmin.IgnoreSoc')"
|
||||||
v-model="powerLimiterConfigList.ignore_soc"
|
v-model="powerLimiterConfigList.ignore_soc"
|
||||||
type="checkbox"/>
|
type="checkbox" wide/>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="!powerLimiterConfigList.ignore_soc">
|
<div v-if="!powerLimiterConfigList.ignore_soc">
|
||||||
<label for="batterySocStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.BatterySocStartThreshold') }}:</label>
|
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.BatterySocInfo')"></div>
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
<InputElement :label="$t('powerlimiteradmin.StartThreshold')"
|
||||||
<input type="number" class="form-control" id="batterySocStartThreshold"
|
v-model="powerLimiterConfigList.battery_soc_start_threshold"
|
||||||
placeholder="80" v-model="powerLimiterConfigList.battery_soc_start_threshold"
|
placeholder="80" min="0" max="100" postfix="%"
|
||||||
aria-describedby="batterySocStartThresholdDescription" min="0" max="100" required/>
|
type="number" wide/>
|
||||||
<span class="input-group-text" id="batterySocStartThresholdDescription">%</span>
|
|
||||||
</div>
|
<InputElement :label="$t('powerlimiteradmin.StopThreshold')"
|
||||||
</div>
|
v-model="powerLimiterConfigList.battery_soc_stop_threshold"
|
||||||
|
placeholder="20" min="0" max="100" postfix="%"
|
||||||
|
type="number" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
|
||||||
|
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
|
||||||
|
v-model="powerLimiterConfigList.full_solar_passthrough_soc"
|
||||||
|
v-if="isSolarPassthroughEnabled()"
|
||||||
|
placeholder="80" min="0" max="100" postfix="%"
|
||||||
|
type="number" wide/>
|
||||||
|
</div>
|
||||||
|
</CardElement>
|
||||||
|
|
||||||
|
<CardElement :text="$t('powerlimiteradmin.VoltageThresholds')" textVariant="text-bg-primary" add-space v-if="canUseVoltageThresholds()">
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.StartThreshold')"
|
||||||
|
v-model="powerLimiterConfigList.voltage_start_threshold"
|
||||||
|
placeholder="50" min="16" max="66" postfix="V"
|
||||||
|
type="number" step="0.01" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.StopThreshold')"
|
||||||
|
v-model="powerLimiterConfigList.voltage_stop_threshold"
|
||||||
|
placeholder="49" min="16" max="66" postfix="V"
|
||||||
|
type="number" step="0.01" wide/>
|
||||||
|
|
||||||
|
<div v-if="isSolarPassthroughEnabled()">
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
|
||||||
|
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
|
||||||
|
v-model="powerLimiterConfigList.full_solar_passthrough_start_voltage"
|
||||||
|
placeholder="49" min="16" max="66" postfix="V"
|
||||||
|
type="number" step="0.01" wide/>
|
||||||
|
|
||||||
|
<InputElement :label="$t('powerlimiteradmin.VoltageSolarPassthroughStopThreshold')"
|
||||||
|
v-model="powerLimiterConfigList.full_solar_passthrough_stop_voltage"
|
||||||
|
placeholder="49" min="16" max="66" postfix="V"
|
||||||
|
type="number" step="0.01" wide/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3" v-show="!powerLimiterConfigList.ignore_soc">
|
<InputElement :label="$t('powerlimiteradmin.VoltageLoadCorrectionFactor')"
|
||||||
<label for="batterySocStopThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.BatterySocStopThreshold') }}</label>
|
v-model="powerLimiterConfigList.voltage_load_correction_factor"
|
||||||
<div class="col-sm-10">
|
placeholder="0.0001" postfix="1/A"
|
||||||
<div class="input-group">
|
type="number" step="0.0001" wide/>
|
||||||
<input type="number" class="form-control" id="batterySocStopThreshold"
|
|
||||||
placeholder="20" v-model="powerLimiterConfigList.battery_soc_stop_threshold"
|
|
||||||
aria-describedby="batterySocStopThresholdDescription" min="0" max="100" required/>
|
|
||||||
<span class="input-group-text" id="batterySocStopThresholdDescription">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.solar_passthrough_enabled && !powerLimiterConfigList.ignore_soc">
|
|
||||||
<label for="batterySocSolarPassthroughStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.BatterySocSolarPassthroughStartThreshold') }}
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.BatterySocSolarPassthroughStartThresholdHint')" />
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" id="batterySocSolarPassthroughStartThreshold"
|
|
||||||
placeholder="20" v-model="powerLimiterConfigList.full_solar_passthrough_soc"
|
|
||||||
aria-describedby="batterySocSolarPassthroughStartThresholdDescription" min="0" max="100" required/>
|
|
||||||
<span class="input-group-text" id="batterySocSolarPassthroughStartThresholdDescription">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.BatterySocInfo')" v-show="!powerLimiterConfigList.ignore_soc"></div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="inputVoltageStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageStartThreshold') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" step="0.01" class="form-control" id="inputVoltageStartThreshold"
|
|
||||||
placeholder="50" v-model="powerLimiterConfigList.voltage_start_threshold"
|
|
||||||
aria-describedby="voltageStartThresholdDescription" required/>
|
|
||||||
<span class="input-group-text" id="voltageStartThresholdDescription">V</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="inputVoltageStopThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageStopThreshold') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" step="0.01" class="form-control" id="inputVoltageStopThreshold"
|
|
||||||
placeholder="49" v-model="powerLimiterConfigList.voltage_stop_threshold"
|
|
||||||
aria-describedby="voltageStopThresholdDescription" required/>
|
|
||||||
<span class="input-group-text" id="voltageStopThresholdDescription">V</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.solar_passthrough_enabled">
|
|
||||||
<label for="inputVoltageSolarPassthroughStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageSolarPassthroughStartThreshold') }}:
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.VoltageSolarPassthroughStartThresholdHint')" />
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" step="0.01" class="form-control" id="inputVoltageSolarPassthroughStartThreshold"
|
|
||||||
placeholder="49" v-model="powerLimiterConfigList.full_solar_passthrough_start_voltage"
|
|
||||||
aria-describedby="voltageSolarPassthroughStartThresholdDescription" required/>
|
|
||||||
<span class="input-group-text" id="voltageSolarPassthroughStartThresholdDescription">V</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.solar_passthrough_enabled">
|
|
||||||
<label for="inputVoltageSolarPassthroughStopThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageSolarPassthroughStopThreshold') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" step="0.01" class="form-control" id="inputVoltageSolarPassthroughStopThreshold"
|
|
||||||
placeholder="49" v-model="powerLimiterConfigList.full_solar_passthrough_stop_voltage"
|
|
||||||
aria-describedby="voltageSolarPassthroughStopThresholdDescription" required/>
|
|
||||||
<span class="input-group-text" id="voltageSolarPassthroughStopThresholdDescription">V</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="inputVoltageLoadCorrectionFactor" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageLoadCorrectionFactor') }}:</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" step="0.0001" class="form-control" id="inputVoltageLoadCorrectionFactor"
|
|
||||||
placeholder="49" v-model="powerLimiterConfigList.voltage_load_correction_factor"
|
|
||||||
aria-describedby="voltageLoadCorrectionFactorDescription" required/>
|
|
||||||
<span class="input-group-text" id="voltageLoadCorrectionFactorDescription">V</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.VoltageLoadCorrectionInfo')"></div>
|
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.VoltageLoadCorrectionInfo')"></div>
|
||||||
</CardElement>
|
</CardElement>
|
||||||
|
|
||||||
<CardElement :text="$t('powerlimiteradmin.InverterRestart')" textVariant="text-bg-primary" add-space
|
<FormFooter @reload="getAllData"/>
|
||||||
v-show="powerLimiterConfigList.enabled"
|
|
||||||
>
|
|
||||||
<div class="row mb-3" v-show="powerLimiterConfigList.enabled">
|
|
||||||
<label for="inputTimezone" class="col-sm-2 col-form-label">
|
|
||||||
{{ $t('powerlimiteradmin.InverterRestartHour') }}:
|
|
||||||
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" />
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<select class="form-select" v-model="powerLimiterConfigList.inverter_restart_hour">
|
|
||||||
<option v-for="hour in restartHourList" :key="hour.key" :value="hour.key">
|
|
||||||
{{ hour.value }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardElement>
|
|
||||||
|
|
||||||
<FormFooter @reload="getPowerLimiterConfig"/>
|
|
||||||
</form>
|
</form>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
</template>
|
</template>
|
||||||
@ -284,7 +208,7 @@ import CardElement from '@/components/CardElement.vue';
|
|||||||
import FormFooter from '@/components/FormFooter.vue';
|
import FormFooter from '@/components/FormFooter.vue';
|
||||||
import InputElement from '@/components/InputElement.vue';
|
import InputElement from '@/components/InputElement.vue';
|
||||||
import { BIconInfoCircle } from 'bootstrap-icons-vue';
|
import { BIconInfoCircle } from 'bootstrap-icons-vue';
|
||||||
import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig";
|
import type { PowerLimiterConfig, PowerLimiterMetaData } from "@/types/PowerLimiterConfig";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -299,72 +223,136 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
dataLoading: true,
|
dataLoading: true,
|
||||||
powerLimiterConfigList: {} as PowerLimiterConfig,
|
powerLimiterConfigList: {} as PowerLimiterConfig,
|
||||||
inverterList: [
|
powerLimiterMetaData: {} as PowerLimiterMetaData,
|
||||||
{ key: 0, value: "ID 00" },
|
|
||||||
{ key: 1, value: "ID 01" },
|
|
||||||
{ key: 2, value: "ID 02" },
|
|
||||||
{ key: 3, value: "ID 03" },
|
|
||||||
{ key: 4, value: "ID 04" },
|
|
||||||
{ key: 5, value: "ID 05" },
|
|
||||||
{ key: 6, value: "ID 06" },
|
|
||||||
{ key: 7, value: "ID 07" },
|
|
||||||
{ key: 8, value: "ID 08" },
|
|
||||||
{ key: 9, value: "ID 09" },
|
|
||||||
{ key: 10, value: "ID 10" },
|
|
||||||
],
|
|
||||||
inverterChannelList: [
|
|
||||||
{ key: 0, value: "CH 0" },
|
|
||||||
{ key: 1, value: "CH 1" },
|
|
||||||
{ key: 2, value: "CH 2" },
|
|
||||||
{ key: 3, value: "CH 3" },
|
|
||||||
],
|
|
||||||
batteryDrainStrategyList: [
|
|
||||||
{ key: 0, value: "powerlimiteradmin.BatteryDrainWhenFull"},
|
|
||||||
{ key: 1, value: "powerlimiteradmin.BatteryDrainAtNight" },
|
|
||||||
],
|
|
||||||
restartHourList: [
|
|
||||||
{ key: -1, value: "- - - -" },
|
|
||||||
{ key: 0, value: "0:00" },
|
|
||||||
{ key: 1, value: "1:00" },
|
|
||||||
{ key: 2, value: "2:00" },
|
|
||||||
{ key: 3, value: "3:00" },
|
|
||||||
{ key: 4, value: "4:00" },
|
|
||||||
{ key: 5, value: "5:00" },
|
|
||||||
{ key: 6, value: "6:00" },
|
|
||||||
{ key: 7, value: "7:00" },
|
|
||||||
{ key: 8, value: "8:00" },
|
|
||||||
{ key: 9, value: "9:00" },
|
|
||||||
{ key: 10, value: "10:00" },
|
|
||||||
{ key: 11, value: "11:00" },
|
|
||||||
{ key: 12, value: "12:00" },
|
|
||||||
{ key: 13, value: "13:00" },
|
|
||||||
{ key: 14, value: "14:00" },
|
|
||||||
{ key: 15, value: "15:00" },
|
|
||||||
{ key: 16, value: "16:00" },
|
|
||||||
{ key: 17, value: "17:00" },
|
|
||||||
{ key: 18, value: "18:00" },
|
|
||||||
{ key: 19, value: "19:00" },
|
|
||||||
{ key: 20, value: "20:00" },
|
|
||||||
{ key: 21, value: "21:00" },
|
|
||||||
{ key: 22, value: "22:00" },
|
|
||||||
{ key: 23, value: "23:00" },
|
|
||||||
],
|
|
||||||
alertMessage: "",
|
alertMessage: "",
|
||||||
alertType: "info",
|
alertType: "info",
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
|
configAlert: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getPowerLimiterConfig();
|
this.getAllData();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'powerLimiterConfigList.inverter_serial'(newVal) {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
var meta = this.powerLimiterMetaData;
|
||||||
|
|
||||||
|
if (newVal === "") { return; } // do not try to convert the placeholder value
|
||||||
|
|
||||||
|
if (meta.inverters[newVal] !== undefined) { return; }
|
||||||
|
|
||||||
|
for (const [serial, inverter] of Object.entries(meta.inverters)) {
|
||||||
|
// cfg.inverter_serial might be too large to parse as a 32 bit
|
||||||
|
// int, so we make sure to only try to parse two characters. if
|
||||||
|
// cfg.inverter_serial is indeed an old position based index,
|
||||||
|
// it is only one character.
|
||||||
|
if (inverter.pos == Number(cfg.inverter_serial.substr(0, 2))) {
|
||||||
|
// inverter_serial uses the old position-based
|
||||||
|
// value to identify the inverter. convert to serial.
|
||||||
|
cfg.inverter_serial = serial;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// previously selected inverter was deleted. marks serial as
|
||||||
|
// invalid, selects placeholder option.
|
||||||
|
cfg.inverter_serial = '';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getPowerLimiterConfig() {
|
getConfigHints() {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
var meta = this.powerLimiterMetaData;
|
||||||
|
var hints = [];
|
||||||
|
|
||||||
|
if (meta.power_meter_enabled !== true) {
|
||||||
|
hints.push({severity: "requirement", subject: "PowerMeterDisabled"});
|
||||||
|
this.configAlert = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) {
|
||||||
|
hints.push({severity: "requirement", subject: "NoInverter"});
|
||||||
|
this.configAlert = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var inv = meta.inverters[cfg.inverter_serial];
|
||||||
|
if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) {
|
||||||
|
hints.push({severity: "requirement", subject: "InverterCommunication"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.is_inverter_solar_powered) {
|
||||||
|
if (!meta.charge_controller_enabled) {
|
||||||
|
hints.push({severity: "optional", subject: "NoChargeController"});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.battery_enabled) {
|
||||||
|
hints.push({severity: "optional", subject: "NoBatteryInterface"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
},
|
||||||
|
isEnabled() {
|
||||||
|
return this.powerLimiterConfigList.enabled;
|
||||||
|
},
|
||||||
|
canUseSolarPassthrough() {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
var meta = this.powerLimiterMetaData;
|
||||||
|
var canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered;
|
||||||
|
if (!canUse) { cfg.solar_passthrough_enabled = false; }
|
||||||
|
return canUse;
|
||||||
|
},
|
||||||
|
canUseSoCThresholds() {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
var meta = this.powerLimiterMetaData;
|
||||||
|
return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered;
|
||||||
|
},
|
||||||
|
canUseVoltageThresholds() {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
return this.isEnabled() && !cfg.is_inverter_solar_powered;
|
||||||
|
},
|
||||||
|
isSolarPassthroughEnabled() {
|
||||||
|
return this.powerLimiterConfigList.solar_passthrough_enabled;
|
||||||
|
},
|
||||||
|
range(end: number) {
|
||||||
|
return Array.from(Array(end).keys());
|
||||||
|
},
|
||||||
|
needsChannelSelection() {
|
||||||
|
var cfg = this.powerLimiterConfigList;
|
||||||
|
var meta = this.powerLimiterMetaData;
|
||||||
|
|
||||||
|
var reset = function() {
|
||||||
|
cfg.inverter_channel_id = 0;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cfg.inverter_serial === '') { return reset(); }
|
||||||
|
|
||||||
|
if (cfg.is_inverter_solar_powered) { return reset(); }
|
||||||
|
|
||||||
|
var inverter = meta.inverters[cfg.inverter_serial];
|
||||||
|
if (inverter === undefined) { return reset(); }
|
||||||
|
|
||||||
|
if (cfg.inverter_channel_id >= inverter.channels) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return inverter.channels > 1;
|
||||||
|
},
|
||||||
|
getAllData() {
|
||||||
this.dataLoading = true;
|
this.dataLoading = true;
|
||||||
fetch("/api/powerlimiter/config", { headers: authHeader() })
|
fetch("/api/powerlimiter/metadata", { headers: authHeader() })
|
||||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.powerLimiterConfigList = data;
|
this.powerLimiterMetaData = data;
|
||||||
this.dataLoading = false;
|
fetch("/api/powerlimiter/config", { headers: authHeader() })
|
||||||
|
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||||
|
.then((data) => {
|
||||||
|
this.powerLimiterConfigList = data;
|
||||||
|
this.dataLoading = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
savePowerLimiterConfig(e: Event) {
|
savePowerLimiterConfig(e: Event) {
|
||||||
|
|||||||
@ -236,6 +236,7 @@ export default defineComponent({
|
|||||||
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') },
|
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') },
|
||||||
{ key: 3, value: this.$t('powermeteradmin.typeHTTP') },
|
{ key: 3, value: this.$t('powermeteradmin.typeHTTP') },
|
||||||
{ key: 4, value: this.$t('powermeteradmin.typeSML') },
|
{ key: 4, value: this.$t('powermeteradmin.typeSML') },
|
||||||
|
{ key: 5, value: this.$t('powermeteradmin.typeSMAHM2') },
|
||||||
],
|
],
|
||||||
powerMeterAuthList: [
|
powerMeterAuthList: [
|
||||||
{ key: 0, value: "None" },
|
{ key: 0, value: "None" },
|
||||||
|
|||||||
@ -58,12 +58,16 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
getUpdateInfo() {
|
getUpdateInfo() {
|
||||||
|
if (this.systemDataList.git_hash === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If the left char is a "g" the value is the git hash (remove the "g")
|
// If the left char is a "g" the value is the git hash (remove the "g")
|
||||||
this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g';
|
this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g';
|
||||||
this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash;
|
this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash;
|
||||||
|
|
||||||
// Handle format "v0.1-5-gabcdefh"
|
// Handle format "v0.1-5-gabcdefh"
|
||||||
if (this.systemDataList.git_hash.lastIndexOf("-") >= 0) {
|
if (this.systemDataList.git_hash?.lastIndexOf("-") >= 0) {
|
||||||
this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2)
|
this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2)
|
||||||
this.systemDataList.git_is_hash = true;
|
this.systemDataList.git_is_hash = true;
|
||||||
}
|
}
|
||||||
@ -96,8 +100,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
allowVersionInfo(allow: Boolean) {
|
allowVersionInfo(allow: Boolean) {
|
||||||
|
localStorage.setItem("allowVersionInfo", allow ? "1" : "0");
|
||||||
if (allow) {
|
if (allow) {
|
||||||
localStorage.setItem("allowVersionInfo", this.allowVersionInfo ? "1" : "0");
|
|
||||||
this.getUpdateInfo();
|
this.getUpdateInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export default defineConfig({
|
|||||||
fullInstall: false,
|
fullInstall: false,
|
||||||
forceStringify: true,
|
forceStringify: true,
|
||||||
strictMessage: false,
|
strictMessage: false,
|
||||||
|
jitCompilation: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
971
webapp/yarn.lock
971
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user