convert files with CRLF endings to LF endings
this only changes line endings. inspect this commit with command `git
show <commit-sha> --ignore-space-at-eol` and it will tell you that the
commit appears to be "empty" (since all changes are whitespace changes
near the end of a line, which are ignored in that git show command).
the files to be changed were found and updated using this command:
find lib src include webapp/src -type f | \
xargs grep --binary-files=without-match --files-with-matches \
$(printf '\r\n') | xargs dos2unix
the following files were restored afterwards, as they are using CRLF
line endings in the upstream as well:
- lib/CMT2300a/cmt2300a_defs.h
- lib/README
- include/README
This commit is contained in:
parent
2cc086335f
commit
83c59d7811
@ -1,158 +1,158 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "SPI.h"
|
||||
#include <mcp_can.h>
|
||||
#include <mutex>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
#define HUAWEI_PIN_MISO 12
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MOSI
|
||||
#define HUAWEI_PIN_MOSI 13
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_SCLK
|
||||
#define HUAWEI_PIN_SCLK 26
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_IRQ
|
||||
#define HUAWEI_PIN_IRQ 25
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_CS
|
||||
#define HUAWEI_PIN_CS 15
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_POWER
|
||||
#define HUAWEI_PIN_POWER 33
|
||||
#endif
|
||||
|
||||
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
|
||||
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
|
||||
|
||||
#define MAX_CURRENT_MULTIPLIER 20
|
||||
|
||||
// Index values for rec_values array
|
||||
#define HUAWEI_INPUT_POWER_IDX 0
|
||||
#define HUAWEI_INPUT_FREQ_IDX 1
|
||||
#define HUAWEI_INPUT_CURRENT_IDX 2
|
||||
#define HUAWEI_OUTPUT_POWER_IDX 3
|
||||
#define HUAWEI_EFFICIENCY_IDX 4
|
||||
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
|
||||
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
|
||||
#define HUAWEI_INPUT_VOLTAGE_IDX 7
|
||||
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
|
||||
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
|
||||
#define HUAWEI_OUTPUT_CURRENT_IDX 10
|
||||
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
|
||||
|
||||
// Defines and index values for tx_values array
|
||||
#define HUAWEI_OFFLINE_VOLTAGE 0x01
|
||||
#define HUAWEI_ONLINE_VOLTAGE 0x00
|
||||
#define HUAWEI_OFFLINE_CURRENT 0x04
|
||||
#define HUAWEI_ONLINE_CURRENT 0x03
|
||||
|
||||
// Modes of operation
|
||||
#define HUAWEI_MODE_OFF 0
|
||||
#define HUAWEI_MODE_ON 1
|
||||
#define HUAWEI_MODE_AUTO_EXT 2
|
||||
#define HUAWEI_MODE_AUTO_INT 3
|
||||
|
||||
// Error codes
|
||||
#define HUAWEI_ERROR_CODE_RX 0x01
|
||||
#define HUAWEI_ERROR_CODE_TX 0x02
|
||||
|
||||
// Wait time/current before shuting down the PSU / charger
|
||||
// This is set to allow the fan to run for some time
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
|
||||
|
||||
// Updateinterval used to request new values from the PSU
|
||||
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
|
||||
|
||||
typedef struct RectifierParameters {
|
||||
float input_voltage;
|
||||
float input_frequency;
|
||||
float input_current;
|
||||
float input_power;
|
||||
float input_temp;
|
||||
float efficiency;
|
||||
float output_voltage;
|
||||
float output_current;
|
||||
float max_output_current;
|
||||
float output_power;
|
||||
float output_temp;
|
||||
float amp_hour;
|
||||
} RectifierParameters_t;
|
||||
|
||||
class HuaweiCanCommClass {
|
||||
public:
|
||||
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
|
||||
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
|
||||
void loop();
|
||||
bool gotNewRxDataFrame(bool clear);
|
||||
uint8_t getErrorCode(bool clear);
|
||||
uint32_t getParameterValue(uint8_t parameter);
|
||||
void setParameterValue(uint16_t in, uint8_t parameterType);
|
||||
|
||||
private:
|
||||
void sendRequest();
|
||||
|
||||
SPIClass *SPI;
|
||||
MCP_CAN *_CAN;
|
||||
uint8_t _huaweiIrq; // IRQ pin
|
||||
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
uint32_t _recValues[12];
|
||||
uint16_t _txValues[5];
|
||||
bool _hasNewTxValue[5];
|
||||
|
||||
uint8_t _errorCode;
|
||||
bool _completeUpdateReceived;
|
||||
};
|
||||
|
||||
class HuaweiCanClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void setValue(float in, uint8_t parameterType);
|
||||
void setMode(uint8_t mode);
|
||||
|
||||
RectifierParameters_t * get();
|
||||
uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; };
|
||||
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
|
||||
uint8_t getMode() const { return _mode; };
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void processReceivedParameters();
|
||||
void _setValue(float in, uint8_t parameterType);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
|
||||
bool _initialized = false;
|
||||
uint8_t _huaweiPower; // Power pin
|
||||
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
|
||||
|
||||
RectifierParameters_t _rp;
|
||||
|
||||
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
|
||||
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
|
||||
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
|
||||
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
|
||||
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
|
||||
|
||||
uint8_t _autoPowerEnabledCounter = 0;
|
||||
bool _autoPowerEnabled = false;
|
||||
bool _batteryEmergencyCharging = false;
|
||||
};
|
||||
|
||||
extern HuaweiCanClass HuaweiCan;
|
||||
extern HuaweiCanCommClass HuaweiCanComm;
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "SPI.h"
|
||||
#include <mcp_can.h>
|
||||
#include <mutex>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
#define HUAWEI_PIN_MISO 12
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MOSI
|
||||
#define HUAWEI_PIN_MOSI 13
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_SCLK
|
||||
#define HUAWEI_PIN_SCLK 26
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_IRQ
|
||||
#define HUAWEI_PIN_IRQ 25
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_CS
|
||||
#define HUAWEI_PIN_CS 15
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_POWER
|
||||
#define HUAWEI_PIN_POWER 33
|
||||
#endif
|
||||
|
||||
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
|
||||
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
|
||||
|
||||
#define MAX_CURRENT_MULTIPLIER 20
|
||||
|
||||
// Index values for rec_values array
|
||||
#define HUAWEI_INPUT_POWER_IDX 0
|
||||
#define HUAWEI_INPUT_FREQ_IDX 1
|
||||
#define HUAWEI_INPUT_CURRENT_IDX 2
|
||||
#define HUAWEI_OUTPUT_POWER_IDX 3
|
||||
#define HUAWEI_EFFICIENCY_IDX 4
|
||||
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
|
||||
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
|
||||
#define HUAWEI_INPUT_VOLTAGE_IDX 7
|
||||
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
|
||||
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
|
||||
#define HUAWEI_OUTPUT_CURRENT_IDX 10
|
||||
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
|
||||
|
||||
// Defines and index values for tx_values array
|
||||
#define HUAWEI_OFFLINE_VOLTAGE 0x01
|
||||
#define HUAWEI_ONLINE_VOLTAGE 0x00
|
||||
#define HUAWEI_OFFLINE_CURRENT 0x04
|
||||
#define HUAWEI_ONLINE_CURRENT 0x03
|
||||
|
||||
// Modes of operation
|
||||
#define HUAWEI_MODE_OFF 0
|
||||
#define HUAWEI_MODE_ON 1
|
||||
#define HUAWEI_MODE_AUTO_EXT 2
|
||||
#define HUAWEI_MODE_AUTO_INT 3
|
||||
|
||||
// Error codes
|
||||
#define HUAWEI_ERROR_CODE_RX 0x01
|
||||
#define HUAWEI_ERROR_CODE_TX 0x02
|
||||
|
||||
// Wait time/current before shuting down the PSU / charger
|
||||
// This is set to allow the fan to run for some time
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
|
||||
|
||||
// Updateinterval used to request new values from the PSU
|
||||
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
|
||||
|
||||
typedef struct RectifierParameters {
|
||||
float input_voltage;
|
||||
float input_frequency;
|
||||
float input_current;
|
||||
float input_power;
|
||||
float input_temp;
|
||||
float efficiency;
|
||||
float output_voltage;
|
||||
float output_current;
|
||||
float max_output_current;
|
||||
float output_power;
|
||||
float output_temp;
|
||||
float amp_hour;
|
||||
} RectifierParameters_t;
|
||||
|
||||
class HuaweiCanCommClass {
|
||||
public:
|
||||
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
|
||||
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
|
||||
void loop();
|
||||
bool gotNewRxDataFrame(bool clear);
|
||||
uint8_t getErrorCode(bool clear);
|
||||
uint32_t getParameterValue(uint8_t parameter);
|
||||
void setParameterValue(uint16_t in, uint8_t parameterType);
|
||||
|
||||
private:
|
||||
void sendRequest();
|
||||
|
||||
SPIClass *SPI;
|
||||
MCP_CAN *_CAN;
|
||||
uint8_t _huaweiIrq; // IRQ pin
|
||||
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
uint32_t _recValues[12];
|
||||
uint16_t _txValues[5];
|
||||
bool _hasNewTxValue[5];
|
||||
|
||||
uint8_t _errorCode;
|
||||
bool _completeUpdateReceived;
|
||||
};
|
||||
|
||||
class HuaweiCanClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void setValue(float in, uint8_t parameterType);
|
||||
void setMode(uint8_t mode);
|
||||
|
||||
RectifierParameters_t * get();
|
||||
uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; };
|
||||
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
|
||||
uint8_t getMode() const { return _mode; };
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void processReceivedParameters();
|
||||
void _setValue(float in, uint8_t parameterType);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
|
||||
bool _initialized = false;
|
||||
uint8_t _huaweiPower; // Power pin
|
||||
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
|
||||
|
||||
RectifierParameters_t _rp;
|
||||
|
||||
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
|
||||
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
|
||||
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
|
||||
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
|
||||
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
|
||||
|
||||
uint8_t _autoPowerEnabledCounter = 0;
|
||||
bool _autoPowerEnabled = false;
|
||||
bool _batteryEmergencyCharging = false;
|
||||
};
|
||||
|
||||
extern HuaweiCanClass HuaweiCan;
|
||||
extern HuaweiCanCommClass HuaweiCanComm;
|
||||
|
||||
@ -1,41 +1,41 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <AsyncWebSocket.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <Print.h>
|
||||
#include <freertos/task.h>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
|
||||
class MessageOutputClass : public Print {
|
||||
public:
|
||||
MessageOutputClass();
|
||||
void init(Scheduler& scheduler);
|
||||
size_t write(uint8_t c) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
void register_ws_output(AsyncWebSocket* output);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
using message_t = std::vector<uint8_t>;
|
||||
|
||||
// we keep a buffer for every task and only write complete lines to the
|
||||
// serial output and then move them to be pushed through the websocket.
|
||||
// this way we prevent mangling of messages from different contexts.
|
||||
std::unordered_map<TaskHandle_t, message_t> _task_messages;
|
||||
std::queue<message_t> _lines;
|
||||
|
||||
AsyncWebSocket* _ws = nullptr;
|
||||
|
||||
std::mutex _msgLock;
|
||||
|
||||
void serialWrite(message_t const& m);
|
||||
};
|
||||
|
||||
extern MessageOutputClass MessageOutput;
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <AsyncWebSocket.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <Print.h>
|
||||
#include <freertos/task.h>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
|
||||
class MessageOutputClass : public Print {
|
||||
public:
|
||||
MessageOutputClass();
|
||||
void init(Scheduler& scheduler);
|
||||
size_t write(uint8_t c) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
void register_ws_output(AsyncWebSocket* output);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
using message_t = std::vector<uint8_t>;
|
||||
|
||||
// we keep a buffer for every task and only write complete lines to the
|
||||
// serial output and then move them to be pushed through the websocket.
|
||||
// this way we prevent mangling of messages from different contexts.
|
||||
std::unordered_map<TaskHandle_t, message_t> _task_messages;
|
||||
std::queue<message_t> _lines;
|
||||
|
||||
AsyncWebSocket* _ws = nullptr;
|
||||
|
||||
std::mutex _msgLock;
|
||||
|
||||
void serialWrite(message_t const& m);
|
||||
};
|
||||
|
||||
extern MessageOutputClass MessageOutput;
|
||||
|
||||
@ -1,44 +1,44 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <Huawei_can.h>
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandleHuaweiClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
enum class Topic : unsigned {
|
||||
LimitOnlineVoltage,
|
||||
LimitOnlineCurrent,
|
||||
LimitOfflineVoltage,
|
||||
LimitOfflineCurrent,
|
||||
Mode
|
||||
};
|
||||
|
||||
void onMqttMessage(Topic t,
|
||||
const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len,
|
||||
size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <Huawei_can.h>
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandleHuaweiClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
enum class Topic : unsigned {
|
||||
LimitOnlineVoltage,
|
||||
LimitOnlineCurrent,
|
||||
LimitOfflineVoltage,
|
||||
LimitOfflineCurrent,
|
||||
Mode
|
||||
};
|
||||
|
||||
void onMqttMessage(Topic t,
|
||||
const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len,
|
||||
size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
extern MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
@ -1,45 +1,45 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandlePowerLimiterClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
enum class MqttPowerLimiterCommand : unsigned {
|
||||
Mode,
|
||||
BatterySoCStartThreshold,
|
||||
BatterySoCStopThreshold,
|
||||
FullSolarPassthroughSoC,
|
||||
VoltageStartThreshold,
|
||||
VoltageStopThreshold,
|
||||
FullSolarPassThroughStartVoltage,
|
||||
FullSolarPassThroughStopVoltage,
|
||||
UpperPowerLimit,
|
||||
TargetPowerConsumption
|
||||
};
|
||||
|
||||
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandlePowerLimiterClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
enum class MqttPowerLimiterCommand : unsigned {
|
||||
Mode,
|
||||
BatterySoCStartThreshold,
|
||||
BatterySoCStopThreshold,
|
||||
FullSolarPassthroughSoC,
|
||||
VoltageStartThreshold,
|
||||
VoltageStopThreshold,
|
||||
FullSolarPassThroughStartVoltage,
|
||||
FullSolarPassThroughStopVoltage,
|
||||
UpperPowerLimit,
|
||||
TargetPowerConsumption
|
||||
};
|
||||
|
||||
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiHuaweiClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
void getJsonData(JsonVariant& root);
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiHuaweiClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
void getJsonData(JsonVariant& root);
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -1,29 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsHuaweiLiveClass {
|
||||
public:
|
||||
WebApiWsHuaweiLiveClass();
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateCommonJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
Task _wsCleanupTask;
|
||||
void wsCleanupTaskCb();
|
||||
|
||||
Task _sendDataTask;
|
||||
void sendDataTaskCb();
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsHuaweiLiveClass {
|
||||
public:
|
||||
WebApiWsHuaweiLiveClass();
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateCommonJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
Task _wsCleanupTask;
|
||||
void wsCleanupTaskCb();
|
||||
|
||||
Task _sendDataTask;
|
||||
void sendDataTaskCb();
|
||||
};
|
||||
@ -1,32 +1,32 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsBatteryLiveClass {
|
||||
public:
|
||||
WebApiWsBatteryLiveClass();
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateCommonJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
static constexpr uint16_t _responseSize = 1024 + 512;
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
Task _wsCleanupTask;
|
||||
void wsCleanupTaskCb();
|
||||
|
||||
Task _sendDataTask;
|
||||
void sendDataTaskCb();
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsBatteryLiveClass {
|
||||
public:
|
||||
WebApiWsBatteryLiveClass();
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateCommonJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
static constexpr uint16_t _responseSize = 1024 + 512;
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
Task _wsCleanupTask;
|
||||
void wsCleanupTaskCb();
|
||||
|
||||
Task _sendDataTask;
|
||||
void sendDataTaskCb();
|
||||
};
|
||||
1052
src/Huawei_can.cpp
1052
src/Huawei_can.cpp
File diff suppressed because it is too large
Load Diff
@ -1,114 +1,114 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include <HardwareSerial.h>
|
||||
#include "MessageOutput.h"
|
||||
|
||||
MessageOutputClass MessageOutput;
|
||||
|
||||
MessageOutputClass::MessageOutputClass()
|
||||
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MessageOutputClass::loop, this))
|
||||
{
|
||||
}
|
||||
|
||||
void MessageOutputClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.enable();
|
||||
}
|
||||
|
||||
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
_ws = output;
|
||||
}
|
||||
|
||||
void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
|
||||
{
|
||||
// operator bool() of HWCDC returns false if the device is not attached to
|
||||
// a USB host. in general it makes sense to skip writing entirely if the
|
||||
// default serial port is not ready.
|
||||
if (!Serial) { return; }
|
||||
|
||||
size_t written = 0;
|
||||
while (written < m.size()) {
|
||||
written += Serial.write(m.data() + written, m.size() - written);
|
||||
}
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(uint8_t c)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
_task_messages.erase(iter);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(const uint8_t *buffer, size_t size)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.reserve(message.size() + size);
|
||||
|
||||
for (size_t idx = 0; idx < size; ++idx) {
|
||||
uint8_t c = buffer[idx];
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
message.clear();
|
||||
message.reserve(size - idx - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.empty()) { _task_messages.erase(iter); }
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void MessageOutputClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
// clean up (possibly filled) buffers of deleted tasks
|
||||
auto map_iter = _task_messages.begin();
|
||||
while (map_iter != _task_messages.end()) {
|
||||
if (eTaskGetState(map_iter->first) == eDeleted) {
|
||||
map_iter = _task_messages.erase(map_iter);
|
||||
continue;
|
||||
}
|
||||
|
||||
++map_iter;
|
||||
}
|
||||
|
||||
if (!_ws) {
|
||||
while (!_lines.empty()) {
|
||||
_lines.pop(); // do not hog memory
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (!_lines.empty() && _ws->availableForWriteAll()) {
|
||||
_ws->textAll(std::make_shared<message_t>(std::move(_lines.front())));
|
||||
_lines.pop();
|
||||
}
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include <HardwareSerial.h>
|
||||
#include "MessageOutput.h"
|
||||
|
||||
MessageOutputClass MessageOutput;
|
||||
|
||||
MessageOutputClass::MessageOutputClass()
|
||||
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MessageOutputClass::loop, this))
|
||||
{
|
||||
}
|
||||
|
||||
void MessageOutputClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.enable();
|
||||
}
|
||||
|
||||
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
_ws = output;
|
||||
}
|
||||
|
||||
void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
|
||||
{
|
||||
// operator bool() of HWCDC returns false if the device is not attached to
|
||||
// a USB host. in general it makes sense to skip writing entirely if the
|
||||
// default serial port is not ready.
|
||||
if (!Serial) { return; }
|
||||
|
||||
size_t written = 0;
|
||||
while (written < m.size()) {
|
||||
written += Serial.write(m.data() + written, m.size() - written);
|
||||
}
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(uint8_t c)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
_task_messages.erase(iter);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(const uint8_t *buffer, size_t size)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.reserve(message.size() + size);
|
||||
|
||||
for (size_t idx = 0; idx < size; ++idx) {
|
||||
uint8_t c = buffer[idx];
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
message.clear();
|
||||
message.reserve(size - idx - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.empty()) { _task_messages.erase(iter); }
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void MessageOutputClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
// clean up (possibly filled) buffers of deleted tasks
|
||||
auto map_iter = _task_messages.begin();
|
||||
while (map_iter != _task_messages.end()) {
|
||||
if (eTaskGetState(map_iter->first) == eDeleted) {
|
||||
map_iter = _task_messages.erase(map_iter);
|
||||
continue;
|
||||
}
|
||||
|
||||
++map_iter;
|
||||
}
|
||||
|
||||
if (!_ws) {
|
||||
while (!_lines.empty()) {
|
||||
_lines.pop(); // do not hog memory
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (!_lines.empty() && _ws->availableForWriteAll()) {
|
||||
_ws->textAll(std::make_shared<message_t>(std::move(_lines.front())));
|
||||
_lines.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,162 +1,162 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "Huawei_can.h"
|
||||
// #include "Failsafe.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include <ctime>
|
||||
|
||||
MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
|
||||
void MqttHandleHuaweiClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
String const& prefix = MqttSettings.getPrefix();
|
||||
|
||||
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
|
||||
String fullTopic(prefix + "huawei/cmd/" + subTopic);
|
||||
MqttSettings.subscribe(fullTopic.c_str(), 0,
|
||||
std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
|
||||
std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4,
|
||||
std::placeholders::_5, std::placeholders::_6));
|
||||
};
|
||||
|
||||
subscribe("limit_online_voltage", Topic::LimitOnlineVoltage);
|
||||
subscribe("limit_online_current", Topic::LimitOnlineCurrent);
|
||||
subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage);
|
||||
subscribe("limit_offline_current", Topic::LimitOfflineCurrent);
|
||||
subscribe("mode", Topic::Mode);
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::loop()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RectifierParameters_t *rp = HuaweiCan.get();
|
||||
|
||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
||||
MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000));
|
||||
MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage));
|
||||
MqttSettings.publish("huawei/input_current", String(rp->input_current));
|
||||
MqttSettings.publish("huawei/input_power", String(rp->input_power));
|
||||
MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage));
|
||||
MqttSettings.publish("huawei/output_current", String(rp->output_current));
|
||||
MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current));
|
||||
MqttSettings.publish("huawei/output_power", String(rp->output_power));
|
||||
MqttSettings.publish("huawei/input_temp", String(rp->input_temp));
|
||||
MqttSettings.publish("huawei/output_temp", String(rp->output_temp));
|
||||
MqttSettings.publish("huawei/efficiency", String(rp->efficiency));
|
||||
MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode()));
|
||||
|
||||
|
||||
yield();
|
||||
_lastPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::onMqttMessage(Topic t,
|
||||
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);
|
||||
float payload_val = -1;
|
||||
try {
|
||||
payload_val = std::stof(strValue);
|
||||
}
|
||||
catch (std::invalid_argument const& e) {
|
||||
MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||
topic, strValue.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
switch (t) {
|
||||
case Topic::LimitOnlineVoltage:
|
||||
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineVoltage:
|
||||
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOnlineCurrent:
|
||||
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineCurrent:
|
||||
MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::Mode:
|
||||
switch (static_cast<int>(payload_val)) {
|
||||
case 3:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_INT));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_EXT));
|
||||
break;
|
||||
|
||||
case 1:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_ON));
|
||||
break;
|
||||
|
||||
case 0:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_OFF));
|
||||
break;
|
||||
|
||||
default:
|
||||
MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "Huawei_can.h"
|
||||
// #include "Failsafe.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include <ctime>
|
||||
|
||||
MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
|
||||
void MqttHandleHuaweiClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
String const& prefix = MqttSettings.getPrefix();
|
||||
|
||||
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
|
||||
String fullTopic(prefix + "huawei/cmd/" + subTopic);
|
||||
MqttSettings.subscribe(fullTopic.c_str(), 0,
|
||||
std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
|
||||
std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4,
|
||||
std::placeholders::_5, std::placeholders::_6));
|
||||
};
|
||||
|
||||
subscribe("limit_online_voltage", Topic::LimitOnlineVoltage);
|
||||
subscribe("limit_online_current", Topic::LimitOnlineCurrent);
|
||||
subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage);
|
||||
subscribe("limit_offline_current", Topic::LimitOfflineCurrent);
|
||||
subscribe("mode", Topic::Mode);
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::loop()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RectifierParameters_t *rp = HuaweiCan.get();
|
||||
|
||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
||||
MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000));
|
||||
MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage));
|
||||
MqttSettings.publish("huawei/input_current", String(rp->input_current));
|
||||
MqttSettings.publish("huawei/input_power", String(rp->input_power));
|
||||
MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage));
|
||||
MqttSettings.publish("huawei/output_current", String(rp->output_current));
|
||||
MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current));
|
||||
MqttSettings.publish("huawei/output_power", String(rp->output_power));
|
||||
MqttSettings.publish("huawei/input_temp", String(rp->input_temp));
|
||||
MqttSettings.publish("huawei/output_temp", String(rp->output_temp));
|
||||
MqttSettings.publish("huawei/efficiency", String(rp->efficiency));
|
||||
MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode()));
|
||||
|
||||
|
||||
yield();
|
||||
_lastPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::onMqttMessage(Topic t,
|
||||
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);
|
||||
float payload_val = -1;
|
||||
try {
|
||||
payload_val = std::stof(strValue);
|
||||
}
|
||||
catch (std::invalid_argument const& e) {
|
||||
MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||
topic, strValue.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
switch (t) {
|
||||
case Topic::LimitOnlineVoltage:
|
||||
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineVoltage:
|
||||
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOnlineCurrent:
|
||||
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineCurrent:
|
||||
MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::Mode:
|
||||
switch (static_cast<int>(payload_val)) {
|
||||
case 3:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_INT));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_EXT));
|
||||
break;
|
||||
|
||||
case 1:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_ON));
|
||||
break;
|
||||
|
||||
case 0:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_OFF));
|
||||
break;
|
||||
|
||||
default:
|
||||
MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,197 +1,197 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler, Malte Schmidt and others
|
||||
*/
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MqttHandlePowerLimiter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
|
||||
void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
String const& prefix = MqttSettings.getPrefix();
|
||||
|
||||
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);
|
||||
subscribe("upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit);
|
||||
subscribe("target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption);
|
||||
|
||||
_lastPublish = millis();
|
||||
}
|
||||
|
||||
|
||||
void MqttHandlePowerLimiterClass::loop()
|
||||
{
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.PowerLimiter.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) { return; }
|
||||
|
||||
if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
|
||||
|
||||
// no thresholds are relevant for setups without a battery
|
||||
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
|
||||
|
||||
if (config.Vedirect.Enabled) {
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage));
|
||||
}
|
||||
|
||||
if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; }
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold));
|
||||
|
||||
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;
|
||||
case MqttPowerLimiterCommand::UpperPowerLimit:
|
||||
if (config.PowerLimiter.UpperPowerLimit == intValue) { return; }
|
||||
MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue);
|
||||
config.PowerLimiter.UpperPowerLimit = intValue;
|
||||
break;
|
||||
case MqttPowerLimiterCommand::TargetPowerConsumption:
|
||||
if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; }
|
||||
MessageOutput.printf("Setting target power consumption to: %d W\r\n", intValue);
|
||||
config.PowerLimiter.TargetPowerConsumption = intValue;
|
||||
break;
|
||||
}
|
||||
|
||||
// not reached if the value did not change
|
||||
Configuration.write();
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler, Malte Schmidt and others
|
||||
*/
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MqttHandlePowerLimiter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
|
||||
void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
String const& prefix = MqttSettings.getPrefix();
|
||||
|
||||
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);
|
||||
subscribe("upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit);
|
||||
subscribe("target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption);
|
||||
|
||||
_lastPublish = millis();
|
||||
}
|
||||
|
||||
|
||||
void MqttHandlePowerLimiterClass::loop()
|
||||
{
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.PowerLimiter.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) { return; }
|
||||
|
||||
if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption));
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
|
||||
|
||||
// no thresholds are relevant for setups without a battery
|
||||
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
|
||||
|
||||
if (config.Vedirect.Enabled) {
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage));
|
||||
}
|
||||
|
||||
if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; }
|
||||
|
||||
MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold));
|
||||
MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold));
|
||||
|
||||
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;
|
||||
case MqttPowerLimiterCommand::UpperPowerLimit:
|
||||
if (config.PowerLimiter.UpperPowerLimit == intValue) { return; }
|
||||
MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue);
|
||||
config.PowerLimiter.UpperPowerLimit = intValue;
|
||||
break;
|
||||
case MqttPowerLimiterCommand::TargetPowerConsumption:
|
||||
if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; }
|
||||
MessageOutput.printf("Setting target power consumption to: %d W\r\n", intValue);
|
||||
config.PowerLimiter.TargetPowerConsumption = intValue;
|
||||
break;
|
||||
}
|
||||
|
||||
// not reached if the value did not change
|
||||
Configuration.write();
|
||||
}
|
||||
|
||||
@ -1,259 +1,259 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PinMapping.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiHuaweiClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = &server;
|
||||
|
||||
_server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1));
|
||||
_server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::getJsonData(JsonVariant& root) {
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root["input_voltage"]["v"] = rp->input_voltage;
|
||||
root["input_voltage"]["u"] = "V";
|
||||
root["input_current"]["v"] = rp->input_current;
|
||||
root["input_current"]["u"] = "A";
|
||||
root["input_power"]["v"] = rp->input_power;
|
||||
root["input_power"]["u"] = "W";
|
||||
root["output_voltage"]["v"] = rp->output_voltage;
|
||||
root["output_voltage"]["u"] = "V";
|
||||
root["output_current"]["v"] = rp->output_current;
|
||||
root["output_current"]["u"] = "A";
|
||||
root["max_output_current"]["v"] = rp->max_output_current;
|
||||
root["max_output_current"]["u"] = "A";
|
||||
root["output_power"]["v"] = rp->output_power;
|
||||
root["output_power"]["u"] = "W";
|
||||
root["input_temp"]["v"] = rp->input_temp;
|
||||
root["input_temp"]["u"] = "°C";
|
||||
root["output_temp"]["v"] = rp->output_temp;
|
||||
root["output_temp"]["u"] = "°C";
|
||||
root["efficiency"]["v"] = rp->efficiency * 100;
|
||||
root["efficiency"]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float value;
|
||||
uint8_t online = true;
|
||||
float minimal_voltage;
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
|
||||
if (root.containsKey("online")) {
|
||||
online = root["online"].as<bool>();
|
||||
if (online) {
|
||||
minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE;
|
||||
} else {
|
||||
minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE;
|
||||
}
|
||||
} else {
|
||||
retMsg["message"] = "Could not read info if data should be set for online/offline operation!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("voltage_valid")) {
|
||||
if (root["voltage_valid"].as<bool>()) {
|
||||
if (root["voltage"].as<float>() < minimal_voltage || root["voltage"].as<float>() > 58) {
|
||||
retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 58;
|
||||
retMsg["param"]["min"] = minimal_voltage;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["voltage"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("current_valid")) {
|
||||
if (root["current_valid"].as<bool>()) {
|
||||
if (root["current"].as<float>() < 0 || root["current"].as<float>() > 60) {
|
||||
retMsg["message"] = "current must be in range between 0 and 60!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 60;
|
||||
retMsg["param"]["min"] = 0;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["current"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root["enabled"] = config.Huawei.Enabled;
|
||||
root["verbose_logging"] = config.Huawei.VerboseLogging;
|
||||
root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
|
||||
root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
|
||||
root["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled;
|
||||
root["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled;
|
||||
root["voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0;
|
||||
root["enable_voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0;
|
||||
root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit;
|
||||
root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit;
|
||||
root["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold;
|
||||
root["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
|
||||
if (!(root.containsKey("enabled")) ||
|
||||
!(root.containsKey("can_controller_frequency")) ||
|
||||
!(root.containsKey("auto_power_enabled")) ||
|
||||
!(root.containsKey("emergency_charge_enabled")) ||
|
||||
!(root.containsKey("voltage_limit")) ||
|
||||
!(root.containsKey("lower_power_limit")) ||
|
||||
!(root.containsKey("upper_power_limit"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Huawei.Enabled = root["enabled"].as<bool>();
|
||||
config.Huawei.VerboseLogging = root["verbose_logging"];
|
||||
config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as<uint32_t>();
|
||||
config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as<bool>();
|
||||
config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as<bool>();
|
||||
config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as<bool>();
|
||||
config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"];
|
||||
config.Huawei.Auto_Power_Target_Power_Consumption = root["target_power_consumption"];
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
// TODO(schlimmchen): HuaweiCan has no real concept of the fact that the
|
||||
// config might change. at least not regarding CAN parameters. until that
|
||||
// changes, the ESP must restart for configuration changes to take effect.
|
||||
yield();
|
||||
delay(1000);
|
||||
yield();
|
||||
ESP.restart();
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
// Properly turn this on
|
||||
if (config.Huawei.Enabled) {
|
||||
MessageOutput.println("Initialize Huawei AC charger interface... ");
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
}
|
||||
|
||||
// Properly turn this off
|
||||
if (!config.Huawei.Enabled) {
|
||||
HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||
delay(500);
|
||||
HuaweiCan.setMode(HUAWEI_MODE_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Huawei.Auto_Power_Enabled) {
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT);
|
||||
return;
|
||||
}
|
||||
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT);
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PinMapping.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiHuaweiClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = &server;
|
||||
|
||||
_server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1));
|
||||
_server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::getJsonData(JsonVariant& root) {
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root["input_voltage"]["v"] = rp->input_voltage;
|
||||
root["input_voltage"]["u"] = "V";
|
||||
root["input_current"]["v"] = rp->input_current;
|
||||
root["input_current"]["u"] = "A";
|
||||
root["input_power"]["v"] = rp->input_power;
|
||||
root["input_power"]["u"] = "W";
|
||||
root["output_voltage"]["v"] = rp->output_voltage;
|
||||
root["output_voltage"]["u"] = "V";
|
||||
root["output_current"]["v"] = rp->output_current;
|
||||
root["output_current"]["u"] = "A";
|
||||
root["max_output_current"]["v"] = rp->max_output_current;
|
||||
root["max_output_current"]["u"] = "A";
|
||||
root["output_power"]["v"] = rp->output_power;
|
||||
root["output_power"]["u"] = "W";
|
||||
root["input_temp"]["v"] = rp->input_temp;
|
||||
root["input_temp"]["u"] = "°C";
|
||||
root["output_temp"]["v"] = rp->output_temp;
|
||||
root["output_temp"]["u"] = "°C";
|
||||
root["efficiency"]["v"] = rp->efficiency * 100;
|
||||
root["efficiency"]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float value;
|
||||
uint8_t online = true;
|
||||
float minimal_voltage;
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
|
||||
if (root.containsKey("online")) {
|
||||
online = root["online"].as<bool>();
|
||||
if (online) {
|
||||
minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE;
|
||||
} else {
|
||||
minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE;
|
||||
}
|
||||
} else {
|
||||
retMsg["message"] = "Could not read info if data should be set for online/offline operation!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("voltage_valid")) {
|
||||
if (root["voltage_valid"].as<bool>()) {
|
||||
if (root["voltage"].as<float>() < minimal_voltage || root["voltage"].as<float>() > 58) {
|
||||
retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 58;
|
||||
retMsg["param"]["min"] = minimal_voltage;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["voltage"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("current_valid")) {
|
||||
if (root["current_valid"].as<bool>()) {
|
||||
if (root["current"].as<float>() < 0 || root["current"].as<float>() > 60) {
|
||||
retMsg["message"] = "current must be in range between 0 and 60!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 60;
|
||||
retMsg["param"]["min"] = 0;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["current"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root["enabled"] = config.Huawei.Enabled;
|
||||
root["verbose_logging"] = config.Huawei.VerboseLogging;
|
||||
root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
|
||||
root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
|
||||
root["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled;
|
||||
root["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled;
|
||||
root["voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0;
|
||||
root["enable_voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0;
|
||||
root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit;
|
||||
root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit;
|
||||
root["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold;
|
||||
root["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
|
||||
if (!(root.containsKey("enabled")) ||
|
||||
!(root.containsKey("can_controller_frequency")) ||
|
||||
!(root.containsKey("auto_power_enabled")) ||
|
||||
!(root.containsKey("emergency_charge_enabled")) ||
|
||||
!(root.containsKey("voltage_limit")) ||
|
||||
!(root.containsKey("lower_power_limit")) ||
|
||||
!(root.containsKey("upper_power_limit"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Huawei.Enabled = root["enabled"].as<bool>();
|
||||
config.Huawei.VerboseLogging = root["verbose_logging"];
|
||||
config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as<uint32_t>();
|
||||
config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as<bool>();
|
||||
config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as<bool>();
|
||||
config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as<bool>();
|
||||
config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"];
|
||||
config.Huawei.Auto_Power_Target_Power_Consumption = root["target_power_consumption"];
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
// TODO(schlimmchen): HuaweiCan has no real concept of the fact that the
|
||||
// config might change. at least not regarding CAN parameters. until that
|
||||
// changes, the ESP must restart for configuration changes to take effect.
|
||||
yield();
|
||||
delay(1000);
|
||||
yield();
|
||||
ESP.restart();
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
// Properly turn this on
|
||||
if (config.Huawei.Enabled) {
|
||||
MessageOutput.println("Initialize Huawei AC charger interface... ");
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
}
|
||||
|
||||
// Properly turn this off
|
||||
if (!config.Huawei.Enabled) {
|
||||
HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||
delay(500);
|
||||
HuaweiCan.setMode(HUAWEI_MODE_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Huawei.Auto_Power_Enabled) {
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT);
|
||||
return;
|
||||
}
|
||||
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT);
|
||||
}
|
||||
|
||||
@ -1,144 +1,144 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "Utils.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass()
|
||||
: _ws("/huaweilivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = &server;
|
||||
_server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsHuaweiLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
scheduler.addTask(_wsCleanupTask);
|
||||
_wsCleanupTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::wsCleanupTaskCb, this));
|
||||
_wsCleanupTask.setIterations(TASK_FOREVER);
|
||||
_wsCleanupTask.setInterval(1 * TASK_SECOND);
|
||||
_wsCleanupTask.enable();
|
||||
|
||||
scheduler.addTask(_sendDataTask);
|
||||
_sendDataTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::sendDataTaskCb, this));
|
||||
_sendDataTask.setIterations(TASK_FOREVER);
|
||||
_sendDataTask.setInterval(1 * TASK_SECOND);
|
||||
_sendDataTask.enable();
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::wsCleanupTaskCb()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
_ws.cleanupClients();
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::sendDataTaskCb()
|
||||
{
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
JsonDocument root;
|
||||
JsonVariant var = root;
|
||||
|
||||
generateCommonJsonResponse(var);
|
||||
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root["input_voltage"]["v"] = rp->input_voltage;
|
||||
root["input_voltage"]["u"] = "V";
|
||||
root["input_current"]["v"] = rp->input_current;
|
||||
root["input_current"]["u"] = "A";
|
||||
root["input_power"]["v"] = rp->input_power;
|
||||
root["input_power"]["u"] = "W";
|
||||
root["output_voltage"]["v"] = rp->output_voltage;
|
||||
root["output_voltage"]["u"] = "V";
|
||||
root["output_current"]["v"] = rp->output_current;
|
||||
root["output_current"]["u"] = "A";
|
||||
root["max_output_current"]["v"] = rp->max_output_current;
|
||||
root["max_output_current"]["u"] = "A";
|
||||
root["output_power"]["v"] = rp->output_power;
|
||||
root["output_power"]["u"] = "W";
|
||||
root["input_temp"]["v"] = rp->input_temp;
|
||||
root["input_temp"]["u"] = "°C";
|
||||
root["output_temp"]["v"] = rp->output_temp;
|
||||
root["output_temp"]["u"] = "°C";
|
||||
root["efficiency"]["v"] = rp->efficiency * 100;
|
||||
root["efficiency"]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
generateCommonJsonResponse(root);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "Utils.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass()
|
||||
: _ws("/huaweilivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = &server;
|
||||
_server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsHuaweiLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
scheduler.addTask(_wsCleanupTask);
|
||||
_wsCleanupTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::wsCleanupTaskCb, this));
|
||||
_wsCleanupTask.setIterations(TASK_FOREVER);
|
||||
_wsCleanupTask.setInterval(1 * TASK_SECOND);
|
||||
_wsCleanupTask.enable();
|
||||
|
||||
scheduler.addTask(_sendDataTask);
|
||||
_sendDataTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::sendDataTaskCb, this));
|
||||
_sendDataTask.setIterations(TASK_FOREVER);
|
||||
_sendDataTask.setInterval(1 * TASK_SECOND);
|
||||
_sendDataTask.enable();
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::wsCleanupTaskCb()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
_ws.cleanupClients();
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::sendDataTaskCb()
|
||||
{
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
JsonDocument root;
|
||||
JsonVariant var = root;
|
||||
|
||||
generateCommonJsonResponse(var);
|
||||
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root["input_voltage"]["v"] = rp->input_voltage;
|
||||
root["input_voltage"]["u"] = "V";
|
||||
root["input_current"]["v"] = rp->input_current;
|
||||
root["input_current"]["u"] = "A";
|
||||
root["input_power"]["v"] = rp->input_power;
|
||||
root["input_power"]["u"] = "W";
|
||||
root["output_voltage"]["v"] = rp->output_voltage;
|
||||
root["output_voltage"]["u"] = "V";
|
||||
root["output_current"]["v"] = rp->output_current;
|
||||
root["output_current"]["u"] = "A";
|
||||
root["max_output_current"]["v"] = rp->max_output_current;
|
||||
root["max_output_current"]["u"] = "A";
|
||||
root["output_power"]["v"] = rp->output_power;
|
||||
root["output_power"]["u"] = "W";
|
||||
root["input_temp"]["v"] = rp->input_temp;
|
||||
root["input_temp"]["u"] = "°C";
|
||||
root["output_temp"]["v"] = rp->output_temp;
|
||||
root["output_temp"]["u"] = "°C";
|
||||
root["efficiency"]["v"] = rp->efficiency * 100;
|
||||
root["efficiency"]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
generateCommonJsonResponse(root);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
@ -1,126 +1,126 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_battery.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
#include "Utils.h"
|
||||
|
||||
WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass()
|
||||
: _ws("/batterylivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = &server;
|
||||
_server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsBatteryLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
scheduler.addTask(_wsCleanupTask);
|
||||
_wsCleanupTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::wsCleanupTaskCb, this));
|
||||
_wsCleanupTask.setIterations(TASK_FOREVER);
|
||||
_wsCleanupTask.setInterval(1 * TASK_SECOND);
|
||||
_wsCleanupTask.enable();
|
||||
|
||||
scheduler.addTask(_sendDataTask);
|
||||
_sendDataTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::sendDataTaskCb, this));
|
||||
_sendDataTask.setIterations(TASK_FOREVER);
|
||||
_sendDataTask.setInterval(1 * TASK_SECOND);
|
||||
_sendDataTask.enable();
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::wsCleanupTaskCb()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
_ws.cleanupClients();
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::sendDataTaskCb()
|
||||
{
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Battery.getStats()->updateAvailable(_lastUpdateCheck)) { return; }
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
JsonDocument root;
|
||||
JsonVariant var = root;
|
||||
|
||||
generateCommonJsonResponse(var);
|
||||
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
|
||||
// battery provider does not generate a card, e.g., MQTT provider
|
||||
if (root.isNull()) { return; }
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
if (Configuration.get().Security.AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
Battery.getStats()->getLiveViewData(root);
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id());
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
generateCommonJsonResponse(root);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_battery.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
#include "Utils.h"
|
||||
|
||||
WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass()
|
||||
: _ws("/batterylivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = &server;
|
||||
_server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsBatteryLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
scheduler.addTask(_wsCleanupTask);
|
||||
_wsCleanupTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::wsCleanupTaskCb, this));
|
||||
_wsCleanupTask.setIterations(TASK_FOREVER);
|
||||
_wsCleanupTask.setInterval(1 * TASK_SECOND);
|
||||
_wsCleanupTask.enable();
|
||||
|
||||
scheduler.addTask(_sendDataTask);
|
||||
_sendDataTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::sendDataTaskCb, this));
|
||||
_sendDataTask.setIterations(TASK_FOREVER);
|
||||
_sendDataTask.setInterval(1 * TASK_SECOND);
|
||||
_sendDataTask.enable();
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::wsCleanupTaskCb()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
_ws.cleanupClients();
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::sendDataTaskCb()
|
||||
{
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Battery.getStats()->updateAvailable(_lastUpdateCheck)) { return; }
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
JsonDocument root;
|
||||
JsonVariant var = root;
|
||||
|
||||
generateCommonJsonResponse(var);
|
||||
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
|
||||
// battery provider does not generate a card, e.g., MQTT provider
|
||||
if (root.isNull()) { return; }
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
if (Configuration.get().Security.AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
Battery.getStats()->getLiveViewData(root);
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id());
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
generateCommonJsonResponse(root);
|
||||
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
406
src/main.cpp
406
src/main.cpp
@ -1,203 +1,203 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "Configuration.h"
|
||||
#include "Datastore.h"
|
||||
#include "Display_Graphic.h"
|
||||
#include "InverterSettings.h"
|
||||
#include "Led_Single.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "SerialPortManager.h"
|
||||
#include "VictronMppt.h"
|
||||
#include "Battery.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MqttHandleDtu.h"
|
||||
#include "MqttHandleHass.h"
|
||||
#include "MqttHandleVedirectHass.h"
|
||||
#include "MqttHandleBatteryHass.h"
|
||||
#include "MqttHandleInverter.h"
|
||||
#include "MqttHandleInverterTotal.h"
|
||||
#include "MqttHandleVedirect.h"
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MqttHandlePowerLimiter.h"
|
||||
#include "MqttHandlePowerLimiterHass.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "NtpSettings.h"
|
||||
#include "PinMapping.h"
|
||||
#include "Scheduler.h"
|
||||
#include "SunPosition.h"
|
||||
#include "Utils.h"
|
||||
#include "WebApi.h"
|
||||
#include "PowerMeter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include "defaults.h"
|
||||
#include <Arduino.h>
|
||||
#include <LittleFS.h>
|
||||
#include <TaskScheduler.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
void setup()
|
||||
{
|
||||
// Move all dynamic allocations >512byte to psram (if available)
|
||||
heap_caps_malloc_extmem_enable(512);
|
||||
|
||||
// Initialize serial output
|
||||
Serial.begin(SERIAL_BAUDRATE);
|
||||
#if ARDUINO_USB_CDC_ON_BOOT
|
||||
Serial.setTxTimeoutMs(0);
|
||||
delay(100);
|
||||
#else
|
||||
while (!Serial)
|
||||
yield();
|
||||
#endif
|
||||
MessageOutput.init(scheduler);
|
||||
MessageOutput.println();
|
||||
MessageOutput.println("Starting OpenDTU");
|
||||
|
||||
// Initialize file system
|
||||
MessageOutput.print("Initialize FS... ");
|
||||
if (!LittleFS.begin(false)) { // Do not format if mount failed
|
||||
MessageOutput.print("failed... trying to format...");
|
||||
if (!LittleFS.begin(true)) {
|
||||
MessageOutput.print("success");
|
||||
} else {
|
||||
MessageOutput.print("failed");
|
||||
}
|
||||
} else {
|
||||
MessageOutput.println("done");
|
||||
}
|
||||
|
||||
// Read configuration values
|
||||
MessageOutput.print("Reading configuration... ");
|
||||
if (!Configuration.read()) {
|
||||
MessageOutput.print("initializing... ");
|
||||
Configuration.init();
|
||||
if (Configuration.write()) {
|
||||
MessageOutput.print("written... ");
|
||||
} else {
|
||||
MessageOutput.print("failed... ");
|
||||
}
|
||||
}
|
||||
if (Configuration.get().Cfg.Version != CONFIG_VERSION) {
|
||||
MessageOutput.print("migrated... ");
|
||||
Configuration.migrate();
|
||||
}
|
||||
auto& config = Configuration.get();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Load PinMapping
|
||||
MessageOutput.print("Reading PinMapping... ");
|
||||
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
|
||||
MessageOutput.print("found valid mapping ");
|
||||
} else {
|
||||
MessageOutput.print("using default config ");
|
||||
}
|
||||
const auto& pin = PinMapping.get();
|
||||
MessageOutput.println("done");
|
||||
|
||||
SerialPortManager.init();
|
||||
|
||||
// Initialize WiFi
|
||||
MessageOutput.print("Initialize Network... ");
|
||||
NetworkSettings.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
NetworkSettings.applyConfig();
|
||||
|
||||
// Initialize NTP
|
||||
MessageOutput.print("Initialize NTP... ");
|
||||
NtpSettings.init();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize SunPosition
|
||||
MessageOutput.print("Initialize SunPosition... ");
|
||||
SunPosition.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize MqTT
|
||||
MessageOutput.print("Initialize MqTT... ");
|
||||
MqttSettings.init();
|
||||
MqttHandleDtu.init(scheduler);
|
||||
MqttHandleInverter.init(scheduler);
|
||||
MqttHandleInverterTotal.init(scheduler);
|
||||
MqttHandleVedirect.init(scheduler);
|
||||
MqttHandleHass.init(scheduler);
|
||||
MqttHandleVedirectHass.init(scheduler);
|
||||
MqttHandleBatteryHass.init(scheduler);
|
||||
MqttHandleHuawei.init(scheduler);
|
||||
MqttHandlePowerLimiter.init(scheduler);
|
||||
MqttHandlePowerLimiterHass.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize WebApi
|
||||
MessageOutput.print("Initialize WebApi... ");
|
||||
WebApi.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Display
|
||||
MessageOutput.print("Initialize Display... ");
|
||||
Display.init(
|
||||
scheduler,
|
||||
static_cast<DisplayType_t>(pin.display_type),
|
||||
pin.display_data,
|
||||
pin.display_clk,
|
||||
pin.display_cs,
|
||||
pin.display_reset);
|
||||
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
|
||||
Display.setOrientation(config.Display.Rotation);
|
||||
Display.enablePowerSafe = config.Display.PowerSafe;
|
||||
Display.enableScreensaver = config.Display.ScreenSaver;
|
||||
Display.setContrast(config.Display.Contrast);
|
||||
Display.setLanguage(config.Display.Language);
|
||||
Display.setStartupDisplay();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Single LEDs
|
||||
MessageOutput.print("Initialize LEDs... ");
|
||||
LedSingle.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Check for default DTU serial
|
||||
MessageOutput.print("Check for default DTU serial... ");
|
||||
if (config.Dtu.Serial == DTU_SERIAL) {
|
||||
MessageOutput.print("generate serial based on ESP chip id: ");
|
||||
const uint64_t dtuId = Utils::generateDtuSerial();
|
||||
MessageOutput.printf("%0x%08x... ",
|
||||
((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(dtuId & 0xFFFFFFFF)));
|
||||
config.Dtu.Serial = dtuId;
|
||||
Configuration.write();
|
||||
}
|
||||
MessageOutput.println("done");
|
||||
MessageOutput.println("done");
|
||||
|
||||
InverterSettings.init(scheduler);
|
||||
|
||||
Datastore.init(scheduler);
|
||||
|
||||
VictronMppt.init(scheduler);
|
||||
|
||||
// Power meter
|
||||
PowerMeter.init(scheduler);
|
||||
|
||||
// Dynamic power limiter
|
||||
PowerLimiter.init(scheduler);
|
||||
|
||||
// Initialize Huawei AC-charger PSU / CAN bus
|
||||
MessageOutput.println("Initialize Huawei AC charger interface... ");
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.init(scheduler, pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
|
||||
Battery.init(scheduler);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
scheduler.execute();
|
||||
}
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||
*/
|
||||
#include "Configuration.h"
|
||||
#include "Datastore.h"
|
||||
#include "Display_Graphic.h"
|
||||
#include "InverterSettings.h"
|
||||
#include "Led_Single.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "SerialPortManager.h"
|
||||
#include "VictronMppt.h"
|
||||
#include "Battery.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MqttHandleDtu.h"
|
||||
#include "MqttHandleHass.h"
|
||||
#include "MqttHandleVedirectHass.h"
|
||||
#include "MqttHandleBatteryHass.h"
|
||||
#include "MqttHandleInverter.h"
|
||||
#include "MqttHandleInverterTotal.h"
|
||||
#include "MqttHandleVedirect.h"
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MqttHandlePowerLimiter.h"
|
||||
#include "MqttHandlePowerLimiterHass.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "NtpSettings.h"
|
||||
#include "PinMapping.h"
|
||||
#include "Scheduler.h"
|
||||
#include "SunPosition.h"
|
||||
#include "Utils.h"
|
||||
#include "WebApi.h"
|
||||
#include "PowerMeter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include "defaults.h"
|
||||
#include <Arduino.h>
|
||||
#include <LittleFS.h>
|
||||
#include <TaskScheduler.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
void setup()
|
||||
{
|
||||
// Move all dynamic allocations >512byte to psram (if available)
|
||||
heap_caps_malloc_extmem_enable(512);
|
||||
|
||||
// Initialize serial output
|
||||
Serial.begin(SERIAL_BAUDRATE);
|
||||
#if ARDUINO_USB_CDC_ON_BOOT
|
||||
Serial.setTxTimeoutMs(0);
|
||||
delay(100);
|
||||
#else
|
||||
while (!Serial)
|
||||
yield();
|
||||
#endif
|
||||
MessageOutput.init(scheduler);
|
||||
MessageOutput.println();
|
||||
MessageOutput.println("Starting OpenDTU");
|
||||
|
||||
// Initialize file system
|
||||
MessageOutput.print("Initialize FS... ");
|
||||
if (!LittleFS.begin(false)) { // Do not format if mount failed
|
||||
MessageOutput.print("failed... trying to format...");
|
||||
if (!LittleFS.begin(true)) {
|
||||
MessageOutput.print("success");
|
||||
} else {
|
||||
MessageOutput.print("failed");
|
||||
}
|
||||
} else {
|
||||
MessageOutput.println("done");
|
||||
}
|
||||
|
||||
// Read configuration values
|
||||
MessageOutput.print("Reading configuration... ");
|
||||
if (!Configuration.read()) {
|
||||
MessageOutput.print("initializing... ");
|
||||
Configuration.init();
|
||||
if (Configuration.write()) {
|
||||
MessageOutput.print("written... ");
|
||||
} else {
|
||||
MessageOutput.print("failed... ");
|
||||
}
|
||||
}
|
||||
if (Configuration.get().Cfg.Version != CONFIG_VERSION) {
|
||||
MessageOutput.print("migrated... ");
|
||||
Configuration.migrate();
|
||||
}
|
||||
auto& config = Configuration.get();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Load PinMapping
|
||||
MessageOutput.print("Reading PinMapping... ");
|
||||
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
|
||||
MessageOutput.print("found valid mapping ");
|
||||
} else {
|
||||
MessageOutput.print("using default config ");
|
||||
}
|
||||
const auto& pin = PinMapping.get();
|
||||
MessageOutput.println("done");
|
||||
|
||||
SerialPortManager.init();
|
||||
|
||||
// Initialize WiFi
|
||||
MessageOutput.print("Initialize Network... ");
|
||||
NetworkSettings.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
NetworkSettings.applyConfig();
|
||||
|
||||
// Initialize NTP
|
||||
MessageOutput.print("Initialize NTP... ");
|
||||
NtpSettings.init();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize SunPosition
|
||||
MessageOutput.print("Initialize SunPosition... ");
|
||||
SunPosition.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize MqTT
|
||||
MessageOutput.print("Initialize MqTT... ");
|
||||
MqttSettings.init();
|
||||
MqttHandleDtu.init(scheduler);
|
||||
MqttHandleInverter.init(scheduler);
|
||||
MqttHandleInverterTotal.init(scheduler);
|
||||
MqttHandleVedirect.init(scheduler);
|
||||
MqttHandleHass.init(scheduler);
|
||||
MqttHandleVedirectHass.init(scheduler);
|
||||
MqttHandleBatteryHass.init(scheduler);
|
||||
MqttHandleHuawei.init(scheduler);
|
||||
MqttHandlePowerLimiter.init(scheduler);
|
||||
MqttHandlePowerLimiterHass.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize WebApi
|
||||
MessageOutput.print("Initialize WebApi... ");
|
||||
WebApi.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Display
|
||||
MessageOutput.print("Initialize Display... ");
|
||||
Display.init(
|
||||
scheduler,
|
||||
static_cast<DisplayType_t>(pin.display_type),
|
||||
pin.display_data,
|
||||
pin.display_clk,
|
||||
pin.display_cs,
|
||||
pin.display_reset);
|
||||
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
|
||||
Display.setOrientation(config.Display.Rotation);
|
||||
Display.enablePowerSafe = config.Display.PowerSafe;
|
||||
Display.enableScreensaver = config.Display.ScreenSaver;
|
||||
Display.setContrast(config.Display.Contrast);
|
||||
Display.setLanguage(config.Display.Language);
|
||||
Display.setStartupDisplay();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Single LEDs
|
||||
MessageOutput.print("Initialize LEDs... ");
|
||||
LedSingle.init(scheduler);
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Check for default DTU serial
|
||||
MessageOutput.print("Check for default DTU serial... ");
|
||||
if (config.Dtu.Serial == DTU_SERIAL) {
|
||||
MessageOutput.print("generate serial based on ESP chip id: ");
|
||||
const uint64_t dtuId = Utils::generateDtuSerial();
|
||||
MessageOutput.printf("%0x%08x... ",
|
||||
((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(dtuId & 0xFFFFFFFF)));
|
||||
config.Dtu.Serial = dtuId;
|
||||
Configuration.write();
|
||||
}
|
||||
MessageOutput.println("done");
|
||||
MessageOutput.println("done");
|
||||
|
||||
InverterSettings.init(scheduler);
|
||||
|
||||
Datastore.init(scheduler);
|
||||
|
||||
VictronMppt.init(scheduler);
|
||||
|
||||
// Power meter
|
||||
PowerMeter.init(scheduler);
|
||||
|
||||
// Dynamic power limiter
|
||||
PowerLimiter.init(scheduler);
|
||||
|
||||
// Initialize Huawei AC-charger PSU / CAN bus
|
||||
MessageOutput.println("Initialize Huawei AC charger interface... ");
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.init(scheduler, pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
|
||||
Battery.init(scheduler);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
scheduler.execute();
|
||||
}
|
||||
|
||||
@ -1,217 +1,217 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="'values' in batteryData"> <!-- suppress the card for MQTT battery provider -->
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': batteryData.data_age >= 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'fwversion' in batteryData">
|
||||
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'hwversion' in batteryData">
|
||||
{{ $t('battery.HwVersion') }}: {{ batteryData.hwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.DataAge') }} {{ $t('battery.Seconds', { 'val': batteryData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div v-for="(values, section) in batteryData.values" v-bind:key="section" class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('battery.Value') }}</th>
|
||||
<th scope="col">{{ $t('battery.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
<template v-if="typeof prop === 'string'">
|
||||
{{ $t('battery.' + prop) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="typeof prop === 'string'"></td>
|
||||
<td v-else>{{prop.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card">
|
||||
<div :class="{'card-header': true, 'border-bottom-0': maxIssueValue === 0}">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
{{ $t('battery.issues') }}
|
||||
<div v-if="maxIssueValue === 0" class="badge text-bg-success">{{ $t('battery.noIssues') }}</div>
|
||||
<div v-else-if="maxIssueValue === 1" class="badge text-bg-warning text-dark">{{ $t('battery.warning') }}</div>
|
||||
<div v-else-if="maxIssueValue === 2" class="badge text-bg-danger">{{ $t('battery.alarm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" v-if="'issues' in batteryData">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.issueName') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueType') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in batteryData.issues" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': prop === 1,
|
||||
'text-bg-danger': prop === 2
|
||||
}">
|
||||
<template v-if="prop === 1">{{ $t('battery.warning') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Battery } from '@/types/BatteryDataStatus';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
batteryData: {} as Battery,
|
||||
isFirstFetchAfterConnect: true,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Battery");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/batterylivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Battery WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/batterylivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.batteryData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Battery websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.batteryData) {
|
||||
this.batteryData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxIssueValue() {
|
||||
return ('issues' in this.batteryData)?Math.max(...Object.values(this.batteryData.issues)):0;
|
||||
},
|
||||
},
|
||||
});
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="'values' in batteryData"> <!-- suppress the card for MQTT battery provider -->
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': batteryData.data_age >= 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'fwversion' in batteryData">
|
||||
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'hwversion' in batteryData">
|
||||
{{ $t('battery.HwVersion') }}: {{ batteryData.hwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.DataAge') }} {{ $t('battery.Seconds', { 'val': batteryData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div v-for="(values, section) in batteryData.values" v-bind:key="section" class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('battery.Value') }}</th>
|
||||
<th scope="col">{{ $t('battery.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
<template v-if="typeof prop === 'string'">
|
||||
{{ $t('battery.' + prop) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="typeof prop === 'string'"></td>
|
||||
<td v-else>{{prop.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card">
|
||||
<div :class="{'card-header': true, 'border-bottom-0': maxIssueValue === 0}">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
{{ $t('battery.issues') }}
|
||||
<div v-if="maxIssueValue === 0" class="badge text-bg-success">{{ $t('battery.noIssues') }}</div>
|
||||
<div v-else-if="maxIssueValue === 1" class="badge text-bg-warning text-dark">{{ $t('battery.warning') }}</div>
|
||||
<div v-else-if="maxIssueValue === 2" class="badge text-bg-danger">{{ $t('battery.alarm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" v-if="'issues' in batteryData">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.issueName') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueType') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in batteryData.issues" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': prop === 1,
|
||||
'text-bg-danger': prop === 2
|
||||
}">
|
||||
<template v-if="prop === 1">{{ $t('battery.warning') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Battery } from '@/types/BatteryDataStatus';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
batteryData: {} as Battery,
|
||||
isFirstFetchAfterConnect: true,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Battery");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/batterylivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Battery WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/batterylivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.batteryData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Battery websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.batteryData) {
|
||||
this.batteryData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxIssueValue() {
|
||||
return ('issues' in this.batteryData)?Math.max(...Object.values(this.batteryData.issues)):0;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,358 +1,358 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': huaweiData.data_age > 20,
|
||||
'text-bg-primary': huaweiData.data_age < 19,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
Huawei R4850G2
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('huawei.DataAge') }} {{ $t('huawei.Seconds', { 'val': huaweiData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button :disabled="false" type="button" class="btn btn-sm btn-danger" @click="onShowLimitSettings()"
|
||||
v-tooltip :title="$t('huawei.ShowSetLimit')">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Input') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_voltage') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_voltage.v) }}</td>
|
||||
<td>{{ huaweiData.input_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_current') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_current.v) }}</td>
|
||||
<td>{{ huaweiData.input_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_power') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_power.v) }}</td>
|
||||
<td>{{ huaweiData.input_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.input_temp.v) }}</td>
|
||||
<td>{{ huaweiData.input_temp.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.efficiency') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.efficiency.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.efficiency.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Output') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_voltage') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_current.v.toFixed(2) }}</td>
|
||||
<td>{{ huaweiData.output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.max_output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.max_output_current.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.max_output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_power') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_power.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.output_temp.v) }}</td>
|
||||
<td>{{ huaweiData.output_temp.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('huawei.CurrentLimit') }} </label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputVoltageTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetVoltageLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchVoltage"
|
||||
v-model="targetLimitList.voltage_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.01" name="inputVoltageTargetLimit" class="form-control" id="inputVoltageTargetLimit"
|
||||
:min="targetVoltageLimitMin" :max="targetVoltageLimitMax" v-model="targetLimitList.voltage"
|
||||
:disabled=!targetLimitList.voltage_valid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9">
|
||||
<div v-if="targetLimitList.voltage < targetVoltageLimitMinOffline" class="alert alert-secondary mt-3"
|
||||
role="alert" v-html="$t('huawei.LimitHint')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputCurrentTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetCurrentLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCurrentt"
|
||||
v-model="targetLimitList.current_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.1" name="inputCurrentTargetLimit" class="form-control" id="inputCurrentTargetLimit"
|
||||
:min="targetCurrentLimitMin" :max="targetCurrentLimitMax" v-model="targetLimitList.current"
|
||||
:disabled=!targetLimitList.current_valid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">{{ $t('huawei.SetOnline')
|
||||
}}</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">{{
|
||||
$t('huawei.SetOffline')
|
||||
}}</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('huawei.Close') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Huawei } from '@/types/HuaweiDataStatus';
|
||||
import type { HuaweiLimitConfig } from '@/types/HuaweiLimitConfig';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconSpeedometer,
|
||||
} from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconSpeedometer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
huaweiData: {} as Huawei,
|
||||
isFirstFetchAfterConnect: true,
|
||||
targetVoltageLimitMin: 42,
|
||||
targetVoltageLimitMinOffline: 48,
|
||||
targetVoltageLimitMax: 58,
|
||||
targetCurrentLimitMin: 0,
|
||||
targetCurrentLimitMax: 60,
|
||||
targetLimitList: {} as HuaweiLimitConfig,
|
||||
targetLimitPersistent: false,
|
||||
huaweiLimitSettingView: {} as bootstrap.Modal,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Huawei");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/huaweilivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.huaweiData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Huawei WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/huaweilivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.huaweiData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Huawei websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.huaweiData) {
|
||||
this.huaweiData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num);
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings() {
|
||||
this.huaweiLimitSettingView = new bootstrap.Modal('#huaweiLimitSettingView');
|
||||
this.huaweiLimitSettingView.show();
|
||||
},
|
||||
onSetLimitSettings(online: boolean) {
|
||||
this.targetLimitList.online = online;
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.targetLimitList));
|
||||
|
||||
console.log(this.targetLimitList);
|
||||
|
||||
fetch("/api/huawei/limit/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.huaweiLimitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': huaweiData.data_age > 20,
|
||||
'text-bg-primary': huaweiData.data_age < 19,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
Huawei R4850G2
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('huawei.DataAge') }} {{ $t('huawei.Seconds', { 'val': huaweiData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button :disabled="false" type="button" class="btn btn-sm btn-danger" @click="onShowLimitSettings()"
|
||||
v-tooltip :title="$t('huawei.ShowSetLimit')">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Input') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_voltage') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_voltage.v) }}</td>
|
||||
<td>{{ huaweiData.input_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_current') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_current.v) }}</td>
|
||||
<td>{{ huaweiData.input_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_power') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_power.v) }}</td>
|
||||
<td>{{ huaweiData.input_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.input_temp.v) }}</td>
|
||||
<td>{{ huaweiData.input_temp.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.efficiency') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.efficiency.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.efficiency.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Output') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_voltage') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_current.v.toFixed(2) }}</td>
|
||||
<td>{{ huaweiData.output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.max_output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.max_output_current.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.max_output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_power') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_power.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.output_temp.v) }}</td>
|
||||
<td>{{ huaweiData.output_temp.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('huawei.CurrentLimit') }} </label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputVoltageTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetVoltageLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchVoltage"
|
||||
v-model="targetLimitList.voltage_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.01" name="inputVoltageTargetLimit" class="form-control" id="inputVoltageTargetLimit"
|
||||
:min="targetVoltageLimitMin" :max="targetVoltageLimitMax" v-model="targetLimitList.voltage"
|
||||
:disabled=!targetLimitList.voltage_valid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9">
|
||||
<div v-if="targetLimitList.voltage < targetVoltageLimitMinOffline" class="alert alert-secondary mt-3"
|
||||
role="alert" v-html="$t('huawei.LimitHint')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputCurrentTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetCurrentLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCurrentt"
|
||||
v-model="targetLimitList.current_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.1" name="inputCurrentTargetLimit" class="form-control" id="inputCurrentTargetLimit"
|
||||
:min="targetCurrentLimitMin" :max="targetCurrentLimitMax" v-model="targetLimitList.current"
|
||||
:disabled=!targetLimitList.current_valid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">{{ $t('huawei.SetOnline')
|
||||
}}</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">{{
|
||||
$t('huawei.SetOffline')
|
||||
}}</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('huawei.Close') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Huawei } from '@/types/HuaweiDataStatus';
|
||||
import type { HuaweiLimitConfig } from '@/types/HuaweiLimitConfig';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconSpeedometer,
|
||||
} from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconSpeedometer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
huaweiData: {} as Huawei,
|
||||
isFirstFetchAfterConnect: true,
|
||||
targetVoltageLimitMin: 42,
|
||||
targetVoltageLimitMinOffline: 48,
|
||||
targetVoltageLimitMax: 58,
|
||||
targetCurrentLimitMin: 0,
|
||||
targetCurrentLimitMax: 60,
|
||||
targetLimitList: {} as HuaweiLimitConfig,
|
||||
targetLimitPersistent: false,
|
||||
huaweiLimitSettingView: {} as bootstrap.Modal,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Huawei");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/huaweilivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.huaweiData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Huawei WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/huaweilivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.huaweiData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Huawei websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.huaweiData) {
|
||||
this.huaweiData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num);
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings() {
|
||||
this.huaweiLimitSettingView = new bootstrap.Modal('#huaweiLimitSettingView');
|
||||
this.huaweiLimitSettingView.show();
|
||||
},
|
||||
onSetLimitSettings(online: boolean) {
|
||||
this.targetLimitList.online = online;
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.targetLimitList));
|
||||
|
||||
console.log(this.targetLimitList);
|
||||
|
||||
fetch("/api/huawei/limit/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.huaweiLimitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,14 @@
|
||||
export interface AcChargerConfig {
|
||||
enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
can_controller_frequency: number;
|
||||
auto_power_enabled: boolean;
|
||||
auto_power_batterysoc_limits_enabled: boolean;
|
||||
voltage_limit: number;
|
||||
enable_voltage_limit: number;
|
||||
lower_power_limit: number;
|
||||
upper_power_limit: number;
|
||||
emergency_charge_enabled: boolean;
|
||||
stop_batterysoc_threshold: number;
|
||||
target_power_consumption: number;
|
||||
}
|
||||
export interface AcChargerConfig {
|
||||
enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
can_controller_frequency: number;
|
||||
auto_power_enabled: boolean;
|
||||
auto_power_batterysoc_limits_enabled: boolean;
|
||||
voltage_limit: number;
|
||||
enable_voltage_limit: number;
|
||||
lower_power_limit: number;
|
||||
upper_power_limit: number;
|
||||
emergency_charge_enabled: boolean;
|
||||
stop_batterysoc_threshold: number;
|
||||
target_power_consumption: number;
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
type BatteryData = (ValueObject | string)[];
|
||||
|
||||
export interface Battery {
|
||||
manufacturer: string;
|
||||
fwversion: string;
|
||||
hwversion: string;
|
||||
data_age: number;
|
||||
values: BatteryData[];
|
||||
issues: number[];
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
type BatteryData = (ValueObject | string)[];
|
||||
|
||||
export interface Battery {
|
||||
manufacturer: string;
|
||||
fwversion: string;
|
||||
hwversion: string;
|
||||
data_age: number;
|
||||
values: BatteryData[];
|
||||
issues: number[];
|
||||
}
|
||||
@ -1,18 +1,18 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
// Huawei
|
||||
export interface Huawei {
|
||||
data_age: 0;
|
||||
input_voltage: ValueObject;
|
||||
input_frequency: ValueObject;
|
||||
input_current: ValueObject;
|
||||
input_power: ValueObject;
|
||||
input_temp: ValueObject;
|
||||
efficiency: ValueObject;
|
||||
output_voltage: ValueObject;
|
||||
output_current: ValueObject;
|
||||
max_output_current: ValueObject;
|
||||
output_power: ValueObject;
|
||||
output_temp: ValueObject;
|
||||
amp_hour: ValueObject;
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
// Huawei
|
||||
export interface Huawei {
|
||||
data_age: 0;
|
||||
input_voltage: ValueObject;
|
||||
input_frequency: ValueObject;
|
||||
input_current: ValueObject;
|
||||
input_power: ValueObject;
|
||||
input_temp: ValueObject;
|
||||
efficiency: ValueObject;
|
||||
output_voltage: ValueObject;
|
||||
output_current: ValueObject;
|
||||
max_output_current: ValueObject;
|
||||
output_power: ValueObject;
|
||||
output_temp: ValueObject;
|
||||
amp_hour: ValueObject;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export interface HuaweiLimitConfig {
|
||||
voltage: number;
|
||||
voltage_valid: boolean;
|
||||
current: number;
|
||||
current_valid: boolean;
|
||||
online: boolean;
|
||||
export interface HuaweiLimitConfig {
|
||||
voltage: number;
|
||||
voltage_valid: boolean;
|
||||
current: number;
|
||||
current_valid: boolean;
|
||||
online: boolean;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user