From 83c59d7811e049b3e83e676f760a12aa2f94db41 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 24 Jun 2024 21:53:22 +0200 Subject: [PATCH] convert files with CRLF endings to LF endings this only changes line endings. inspect this commit with command `git show --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 --- include/Huawei_can.h | 316 ++--- include/MessageOutput.h | 82 +- include/MqttHandleHuawei.h | 86 +- include/MqttHandlePowerLimiter.h | 90 +- include/WebApi_Huawei.h | 36 +- include/WebApi_ws_Huawei.h | 56 +- include/WebApi_ws_battery.h | 62 +- src/Huawei_can.cpp | 1052 +++++++------- src/MessageOutput.cpp | 228 +-- src/MqttHandleHuawei.cpp | 324 ++--- src/MqttHandlePowerLimiter.cpp | 394 +++--- src/WebApi_Huawei.cpp | 518 +++---- src/WebApi_ws_Huawei.cpp | 286 ++-- src/WebApi_ws_battery.cpp | 250 ++-- src/main.cpp | 406 +++--- webapp/src/components/BatteryView.vue | 432 +++--- webapp/src/components/HuaweiView.vue | 714 +++++----- webapp/src/locales/fr.json | 1886 ++++++++++++------------- webapp/src/types/AcChargerConfig.ts | 28 +- webapp/src/types/BatteryDataStatus.ts | 22 +- webapp/src/types/HuaweiDataStatus.ts | 34 +- webapp/src/types/HuaweiLimitConfig.ts | 12 +- 22 files changed, 3657 insertions(+), 3657 deletions(-) diff --git a/include/Huawei_can.h b/include/Huawei_can.h index 3a699cd7d..e84f2f092 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -1,158 +1,158 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include "SPI.h" -#include -#include -#include - -#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 +#include "SPI.h" +#include +#include +#include + +#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; diff --git a/include/MessageOutput.h b/include/MessageOutput.h index 6f2daa529..c68c116be 100644 --- a/include/MessageOutput.h +++ b/include/MessageOutput.h @@ -1,41 +1,41 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -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; - - // 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 _task_messages; - std::queue _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 +#include +#include +#include +#include +#include +#include +#include + +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; + + // 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 _task_messages; + std::queue _lines; + + AsyncWebSocket* _ws = nullptr; + + std::mutex _msgLock; + + void serialWrite(message_t const& m); +}; + +extern MessageOutputClass MessageOutput; diff --git a/include/MqttHandleHuawei.h b/include/MqttHandleHuawei.h index f518ed9d4..f7f6f4c20 100644 --- a/include/MqttHandleHuawei.h +++ b/include/MqttHandleHuawei.h @@ -1,44 +1,44 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "Configuration.h" -#include -#include -#include -#include -#include -#include - -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> _mqttCallbacks; -}; - +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include +#include + +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> _mqttCallbacks; +}; + extern MqttHandleHuaweiClass MqttHandleHuawei; \ No newline at end of file diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index a3e78ab73..12f2a714c 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -1,45 +1,45 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "Configuration.h" -#include -#include -#include -#include -#include - -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> _mqttCallbacks; -}; - -extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include + +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> _mqttCallbacks; +}; + +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; diff --git a/include/WebApi_Huawei.h b/include/WebApi_Huawei.h index 57d00ed83..5cda423b8 100644 --- a/include/WebApi_Huawei.h +++ b/include/WebApi_Huawei.h @@ -1,19 +1,19 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include - -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 +#include +#include + +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; }; \ No newline at end of file diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h index 38025bb61..43e528e83 100644 --- a/include/WebApi_ws_Huawei.h +++ b/include/WebApi_ws_Huawei.h @@ -1,29 +1,29 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "ArduinoJson.h" -#include -#include -#include - -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 +#include +#include + +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(); }; \ No newline at end of file diff --git a/include/WebApi_ws_battery.h b/include/WebApi_ws_battery.h index 17c63d42f..d89e01aec 100644 --- a/include/WebApi_ws_battery.h +++ b/include/WebApi_ws_battery.h @@ -1,32 +1,32 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "ArduinoJson.h" -#include -#include -#include - -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 +#include +#include + +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(); }; \ No newline at end of file diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 20b013881..5f378602e 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -1,526 +1,526 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2023 Malte Schmidt and others - */ -#include "Battery.h" -#include "Huawei_can.h" -#include "MessageOutput.h" -#include "PowerMeter.h" -#include "PowerLimiter.h" -#include "Configuration.h" -#include "Battery.h" -#include -#include - -#include -#include -#include -#include -#include - -HuaweiCanClass HuaweiCan; -HuaweiCanCommClass HuaweiCanComm; - -// ******************************************************* -// Huawei CAN Communication -// ******************************************************* - -// Using a C function to avoid static C++ member -void HuaweiCanCommunicationTask(void* parameter) { - for( ;; ) { - HuaweiCanComm.loop(); - yield(); - } -} - -bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, - uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { - SPI = new SPIClass(HSPI); - SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); - pinMode(huawei_cs, OUTPUT); - digitalWrite(huawei_cs, HIGH); - - pinMode(huawei_irq, INPUT_PULLUP); - _huaweiIrq = huawei_irq; - - auto mcp_frequency = MCP_8MHZ; - if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; } - else if (8000000UL != frequency) { - MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency); - } - - _CAN = new MCP_CAN(SPI, huawei_cs); - if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) { - return false; - } - - const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and... - const uint32_t myFilter = 0x1081407F; // filter for this message only - _CAN->init_Mask(0, 1, myMask); - _CAN->init_Filt(0, 1, myFilter); - _CAN->init_Mask(1, 1, myMask); - - // Change to normal mode to allow messages to be transmitted - _CAN->setMode(MCP_NORMAL); - - return true; -} - -// Public methods need to obtain semaphore - -void HuaweiCanCommClass::loop() -{ - std::lock_guard lock(_mutex); - - INT32U rxId; - unsigned char len = 0; - unsigned char rxBuf[8]; - uint8_t i; - - if (!digitalRead(_huaweiIrq)) { - // If CAN_INT pin is low, read receive buffer - _CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s) - if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits) - if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) { - - uint32_t value = __bswap32(* reinterpret_cast (rxBuf + 4)); - - // Input power 0x70, Input frequency 0x71, Input current 0x72 - // Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76 - if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) { - _recValues[rxBuf[1] - 0x70] = value; - } - - // Input voltage - if(rxBuf[1] == 0x78 ) { - _recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value; - } - - // Output Temperature - if(rxBuf[1] == 0x7F ) { - _recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value; - } - - // Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82 - if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) { - _recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value; - } - - // This is the last value that is send - if(rxBuf[1] == 0x81) { - _completeUpdateReceived = true; - } - } - } - // Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See: - // https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c - // https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/ - } - - // Transmit values - for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) { - if ( _hasNewTxValue[i] == true) { - uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)}; - - // Send extended message - byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); - if (sndStat == CAN_OK) { - _hasNewTxValue[i] = false; - } else { - _errorCode |= HUAWEI_ERROR_CODE_TX; - } - } - } - - if (_nextRequestMillis < millis()) { - sendRequest(); - _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; - } - -} - -uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) -{ - std::lock_guard lock(_mutex); - uint32_t v = 0; - if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { - v = _recValues[parameter]; - } - return v; -} - -bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) -{ - std::lock_guard lock(_mutex); - bool b = false; - b = _completeUpdateReceived; - if (clear) { - _completeUpdateReceived = false; - } - return b; -} - -uint8_t HuaweiCanCommClass::getErrorCode(bool clear) -{ - std::lock_guard lock(_mutex); - uint8_t e = 0; - e = _errorCode; - if (clear) { - _errorCode = 0; - } - return e; -} - -void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) -{ - std::lock_guard lock(_mutex); - if (parameterType < HUAWEI_OFFLINE_CURRENT) { - _txValues[parameterType] = in; - _hasNewTxValue[parameterType] = true; - } -} - -// Private methods -// Requests current values from Huawei unit. Response is handled in onReceive -void HuaweiCanCommClass::sendRequest() -{ - uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - //Send extended message - byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); - if(sndStat != CAN_OK) { - _errorCode |= HUAWEI_ERROR_CODE_RX; - } -} - -// ******************************************************* -// Huawei CAN Controller -// ******************************************************* - -void HuaweiCanClass::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) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power); -} - -void HuaweiCanClass::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) -{ - if (_initialized) { - return; - } - - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) { - MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); - return; - }; - - pinMode(huawei_power, OUTPUT); - digitalWrite(huawei_power, HIGH); - _huaweiPower = huawei_power; - - if (config.Huawei.Auto_Power_Enabled) { - _mode = HUAWEI_MODE_AUTO_INT; - } - - xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl); - - MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!"); - _initialized = true; -} - -RectifierParameters_t * HuaweiCanClass::get() -{ - return &_rp; -} - - -void HuaweiCanClass::processReceivedParameters() -{ - _rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0; - _rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0; - _rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0; - _rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0; - _rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0; - _rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0; - _rp.max_output_current = static_cast(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER; - _rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0; - _rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0; - _rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0; - _rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0; - - if (HuaweiCanComm.gotNewRxDataFrame(true)) { - _lastUpdateReceivedMillis = millis(); - } -} - - -void HuaweiCanClass::loop() -{ - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled || !_initialized) { - return; - } - - bool verboseLogging = config.Huawei.VerboseLogging; - - processReceivedParameters(); - - uint8_t com_error = HuaweiCanComm.getErrorCode(true); - if (com_error & HUAWEI_ERROR_CODE_RX) { - MessageOutput.println("[HuaweiCanClass::loop] Data request error"); - } - if (com_error & HUAWEI_ERROR_CODE_TX) { - MessageOutput.println("[HuaweiCanClass::loop] Data set error"); - } - - // Print updated data - if (HuaweiCanComm.gotNewRxDataFrame(false) && verboseLogging) { - MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power); - MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power); - MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp); - } - - // Internal PSU power pin (slot detect) control - if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { - _outputCurrentOnSinceMillis = millis(); - } - if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && - (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { - digitalWrite(_huaweiPower, 1); - } - - - if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { - - // Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested. - if ( _nextAutoModePeriodicIntMillis < millis()) { - MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit); - _setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); - _nextAutoModePeriodicIntMillis = millis() + 60000; - } - } - // *********************** - // Emergency charge - // *********************** - auto stats = Battery.getStats(); - if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) { - _batteryEmergencyCharging = true; - - // Set output current - float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); - float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage); - MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent); - _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); - return; - } - - if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) { - // Battery request has changed. Set current to 0, wait for PSU to respond and then clear state - _setValue(0, HUAWEI_ONLINE_CURRENT); - if (_rp.output_current < 1) { - _batteryEmergencyCharging = false; - } - return; - } - - // *********************** - // Automatic power control - // *********************** - - if (_mode == HUAWEI_MODE_AUTO_INT ) { - - // Check if we should run automatic power calculation at all. - // We may have set a value recently and still wait for output stabilization - if (_autoModeBlockedTillMillis > millis()) { - return; - } - - // Re-enable automatic power control if the output voltage has dropped below threshold - if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) { - _autoPowerEnabledCounter = 10; - } - - - // Check if inverter used by the power limiter is active - std::shared_ptr inverter = - Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); - - if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { - // we previously had an index saved as InverterId. fall back to the - // respective positional lookup if InverterId is not a known serial. - inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); - } - - if (inverter != nullptr) { - if(inverter->isProducing()) { - _setValue(0.0, HUAWEI_ONLINE_CURRENT); - // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus - _autoModeBlockedTillMillis = millis() + 1000; - MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); - return; - } - } - - if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && - _autoPowerEnabledCounter > 0) { - // We have received a new PowerMeter value. Also we're _autoPowerEnabled - // So we're good to calculate a new limit - - _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); - - // Calculate new power limit - float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); - float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); - - // Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor - newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency; - - if (verboseLogging){ - MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power); - } - - // Check whether the battery SoC limit setting is enabled - if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { - uint8_t _batterySoC = Battery.getStats()->getSoC(); - // Sets power limit to 0 if the BMS reported SoC reaches or exceeds the user configured value - if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) { - newPowerLimit = 0; - if (verboseLogging) { - MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached " - "stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC, - config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit); - } - } - } - - if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) { - - // Check if the output power has dropped below the lower limit (i.e. the battery is full) - // and if the PSU should be turned off. Also we use a simple counter mechanism here to be able - // to ramp up from zero output power when starting up - if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) { - MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n"); - _autoPowerEnabledCounter--; - if (_autoPowerEnabledCounter == 0) { - _autoPowerEnabled = false; - _setValue(0, HUAWEI_ONLINE_CURRENT); - return; - } - } else { - _autoPowerEnabledCounter = 10; - } - - // Limit power to maximum - if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) { - newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; - } - - // Calculate output current - float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage); - - // Limit output current to value requested by BMS - float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources, e.g. Victron MPPT charger - float outputCurrent = std::min(calculatedCurrent, permissableCurrent); - outputCurrent= outputCurrent > 0 ? outputCurrent : 0; - - if (verboseLogging) { - MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); - } - _autoPowerEnabled = true; - _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); - - // Don't run auto mode some time to allow for output stabilization after issuing a new value - _autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS; - } else { - // requested PL is below minium. Set current to 0 - _autoPowerEnabled = false; - _setValue(0.0, HUAWEI_ONLINE_CURRENT); - } - } - } -} - -void HuaweiCanClass::setValue(float in, uint8_t parameterType) -{ - if (_mode != HUAWEI_MODE_AUTO_INT) { - _setValue(in, parameterType); - } -} - -void HuaweiCanClass::_setValue(float in, uint8_t parameterType) -{ - - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - uint16_t value; - - if (in < 0) { - MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in); - return; - } - - // Start PSU if needed - if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && - (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { - digitalWrite(_huaweiPower, 0); - _outputCurrentOnSinceMillis = millis(); - } - - if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) { - value = in * 1024; - } else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) { - value = in * MAX_CURRENT_MULTIPLIER; - } else { - return; - } - - HuaweiCanComm.setParameterValue(value, parameterType); -} - -void HuaweiCanClass::setMode(uint8_t mode) { - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - if(mode == HUAWEI_MODE_OFF) { - digitalWrite(_huaweiPower, 1); - _mode = HUAWEI_MODE_OFF; - } - if(mode == HUAWEI_MODE_ON) { - digitalWrite(_huaweiPower, 0); - _mode = HUAWEI_MODE_ON; - } - - if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) { - MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command"); - return; - } - - if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) { - _autoPowerEnabled = false; - _setValue(0, HUAWEI_ONLINE_CURRENT); - } - - if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) { - _mode = mode; - } -} - - - +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Malte Schmidt and others + */ +#include "Battery.h" +#include "Huawei_can.h" +#include "MessageOutput.h" +#include "PowerMeter.h" +#include "PowerLimiter.h" +#include "Configuration.h" +#include "Battery.h" +#include +#include + +#include +#include +#include +#include +#include + +HuaweiCanClass HuaweiCan; +HuaweiCanCommClass HuaweiCanComm; + +// ******************************************************* +// Huawei CAN Communication +// ******************************************************* + +// Using a C function to avoid static C++ member +void HuaweiCanCommunicationTask(void* parameter) { + for( ;; ) { + HuaweiCanComm.loop(); + yield(); + } +} + +bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, + uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { + SPI = new SPIClass(HSPI); + SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); + pinMode(huawei_cs, OUTPUT); + digitalWrite(huawei_cs, HIGH); + + pinMode(huawei_irq, INPUT_PULLUP); + _huaweiIrq = huawei_irq; + + auto mcp_frequency = MCP_8MHZ; + if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; } + else if (8000000UL != frequency) { + MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency); + } + + _CAN = new MCP_CAN(SPI, huawei_cs); + if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) { + return false; + } + + const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and... + const uint32_t myFilter = 0x1081407F; // filter for this message only + _CAN->init_Mask(0, 1, myMask); + _CAN->init_Filt(0, 1, myFilter); + _CAN->init_Mask(1, 1, myMask); + + // Change to normal mode to allow messages to be transmitted + _CAN->setMode(MCP_NORMAL); + + return true; +} + +// Public methods need to obtain semaphore + +void HuaweiCanCommClass::loop() +{ + std::lock_guard lock(_mutex); + + INT32U rxId; + unsigned char len = 0; + unsigned char rxBuf[8]; + uint8_t i; + + if (!digitalRead(_huaweiIrq)) { + // If CAN_INT pin is low, read receive buffer + _CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s) + if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits) + if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) { + + uint32_t value = __bswap32(* reinterpret_cast (rxBuf + 4)); + + // Input power 0x70, Input frequency 0x71, Input current 0x72 + // Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76 + if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) { + _recValues[rxBuf[1] - 0x70] = value; + } + + // Input voltage + if(rxBuf[1] == 0x78 ) { + _recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value; + } + + // Output Temperature + if(rxBuf[1] == 0x7F ) { + _recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value; + } + + // Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82 + if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) { + _recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value; + } + + // This is the last value that is send + if(rxBuf[1] == 0x81) { + _completeUpdateReceived = true; + } + } + } + // Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See: + // https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c + // https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/ + } + + // Transmit values + for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) { + if ( _hasNewTxValue[i] == true) { + uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)}; + + // Send extended message + byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); + if (sndStat == CAN_OK) { + _hasNewTxValue[i] = false; + } else { + _errorCode |= HUAWEI_ERROR_CODE_TX; + } + } + } + + if (_nextRequestMillis < millis()) { + sendRequest(); + _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; + } + +} + +uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) +{ + std::lock_guard lock(_mutex); + uint32_t v = 0; + if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { + v = _recValues[parameter]; + } + return v; +} + +bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) +{ + std::lock_guard lock(_mutex); + bool b = false; + b = _completeUpdateReceived; + if (clear) { + _completeUpdateReceived = false; + } + return b; +} + +uint8_t HuaweiCanCommClass::getErrorCode(bool clear) +{ + std::lock_guard lock(_mutex); + uint8_t e = 0; + e = _errorCode; + if (clear) { + _errorCode = 0; + } + return e; +} + +void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) +{ + std::lock_guard lock(_mutex); + if (parameterType < HUAWEI_OFFLINE_CURRENT) { + _txValues[parameterType] = in; + _hasNewTxValue[parameterType] = true; + } +} + +// Private methods +// Requests current values from Huawei unit. Response is handled in onReceive +void HuaweiCanCommClass::sendRequest() +{ + uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + //Send extended message + byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); + if(sndStat != CAN_OK) { + _errorCode |= HUAWEI_ERROR_CODE_RX; + } +} + +// ******************************************************* +// Huawei CAN Controller +// ******************************************************* + +void HuaweiCanClass::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) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power); +} + +void HuaweiCanClass::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) +{ + if (_initialized) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) { + MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); + return; + }; + + pinMode(huawei_power, OUTPUT); + digitalWrite(huawei_power, HIGH); + _huaweiPower = huawei_power; + + if (config.Huawei.Auto_Power_Enabled) { + _mode = HUAWEI_MODE_AUTO_INT; + } + + xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl); + + MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!"); + _initialized = true; +} + +RectifierParameters_t * HuaweiCanClass::get() +{ + return &_rp; +} + + +void HuaweiCanClass::processReceivedParameters() +{ + _rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0; + _rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0; + _rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0; + _rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0; + _rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0; + _rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0; + _rp.max_output_current = static_cast(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER; + _rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0; + _rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0; + _rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0; + _rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0; + + if (HuaweiCanComm.gotNewRxDataFrame(true)) { + _lastUpdateReceivedMillis = millis(); + } +} + + +void HuaweiCanClass::loop() +{ + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled || !_initialized) { + return; + } + + bool verboseLogging = config.Huawei.VerboseLogging; + + processReceivedParameters(); + + uint8_t com_error = HuaweiCanComm.getErrorCode(true); + if (com_error & HUAWEI_ERROR_CODE_RX) { + MessageOutput.println("[HuaweiCanClass::loop] Data request error"); + } + if (com_error & HUAWEI_ERROR_CODE_TX) { + MessageOutput.println("[HuaweiCanClass::loop] Data set error"); + } + + // Print updated data + if (HuaweiCanComm.gotNewRxDataFrame(false) && verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power); + MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power); + MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp); + } + + // Internal PSU power pin (slot detect) control + if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { + _outputCurrentOnSinceMillis = millis(); + } + if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && + (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { + digitalWrite(_huaweiPower, 1); + } + + + if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { + + // Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested. + if ( _nextAutoModePeriodicIntMillis < millis()) { + MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit); + _setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); + _nextAutoModePeriodicIntMillis = millis() + 60000; + } + } + // *********************** + // Emergency charge + // *********************** + auto stats = Battery.getStats(); + if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) { + _batteryEmergencyCharging = true; + + // Set output current + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage); + MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent); + _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); + return; + } + + if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) { + // Battery request has changed. Set current to 0, wait for PSU to respond and then clear state + _setValue(0, HUAWEI_ONLINE_CURRENT); + if (_rp.output_current < 1) { + _batteryEmergencyCharging = false; + } + return; + } + + // *********************** + // Automatic power control + // *********************** + + if (_mode == HUAWEI_MODE_AUTO_INT ) { + + // Check if we should run automatic power calculation at all. + // We may have set a value recently and still wait for output stabilization + if (_autoModeBlockedTillMillis > millis()) { + return; + } + + // Re-enable automatic power control if the output voltage has dropped below threshold + if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) { + _autoPowerEnabledCounter = 10; + } + + + // Check if inverter used by the power limiter is active + std::shared_ptr inverter = + Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); + + if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { + // we previously had an index saved as InverterId. fall back to the + // respective positional lookup if InverterId is not a known serial. + inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + } + + if (inverter != nullptr) { + if(inverter->isProducing()) { + _setValue(0.0, HUAWEI_ONLINE_CURRENT); + // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus + _autoModeBlockedTillMillis = millis() + 1000; + MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); + return; + } + } + + if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && + _autoPowerEnabledCounter > 0) { + // We have received a new PowerMeter value. Also we're _autoPowerEnabled + // So we're good to calculate a new limit + + _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); + + // Calculate new power limit + float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + + // Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor + newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency; + + if (verboseLogging){ + MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power); + } + + // Check whether the battery SoC limit setting is enabled + if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { + uint8_t _batterySoC = Battery.getStats()->getSoC(); + // Sets power limit to 0 if the BMS reported SoC reaches or exceeds the user configured value + if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) { + newPowerLimit = 0; + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached " + "stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC, + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit); + } + } + } + + if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) { + + // Check if the output power has dropped below the lower limit (i.e. the battery is full) + // and if the PSU should be turned off. Also we use a simple counter mechanism here to be able + // to ramp up from zero output power when starting up + if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) { + MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n"); + _autoPowerEnabledCounter--; + if (_autoPowerEnabledCounter == 0) { + _autoPowerEnabled = false; + _setValue(0, HUAWEI_ONLINE_CURRENT); + return; + } + } else { + _autoPowerEnabledCounter = 10; + } + + // Limit power to maximum + if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) { + newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; + } + + // Calculate output current + float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage); + + // Limit output current to value requested by BMS + float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources, e.g. Victron MPPT charger + float outputCurrent = std::min(calculatedCurrent, permissableCurrent); + outputCurrent= outputCurrent > 0 ? outputCurrent : 0; + + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); + } + _autoPowerEnabled = true; + _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); + + // Don't run auto mode some time to allow for output stabilization after issuing a new value + _autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS; + } else { + // requested PL is below minium. Set current to 0 + _autoPowerEnabled = false; + _setValue(0.0, HUAWEI_ONLINE_CURRENT); + } + } + } +} + +void HuaweiCanClass::setValue(float in, uint8_t parameterType) +{ + if (_mode != HUAWEI_MODE_AUTO_INT) { + _setValue(in, parameterType); + } +} + +void HuaweiCanClass::_setValue(float in, uint8_t parameterType) +{ + + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + uint16_t value; + + if (in < 0) { + MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in); + return; + } + + // Start PSU if needed + if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && + (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { + digitalWrite(_huaweiPower, 0); + _outputCurrentOnSinceMillis = millis(); + } + + if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) { + value = in * 1024; + } else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) { + value = in * MAX_CURRENT_MULTIPLIER; + } else { + return; + } + + HuaweiCanComm.setParameterValue(value, parameterType); +} + +void HuaweiCanClass::setMode(uint8_t mode) { + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + if(mode == HUAWEI_MODE_OFF) { + digitalWrite(_huaweiPower, 1); + _mode = HUAWEI_MODE_OFF; + } + if(mode == HUAWEI_MODE_ON) { + digitalWrite(_huaweiPower, 0); + _mode = HUAWEI_MODE_ON; + } + + if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) { + MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command"); + return; + } + + if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) { + _autoPowerEnabled = false; + _setValue(0, HUAWEI_ONLINE_CURRENT); + } + + if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) { + _mode = mode; + } +} + + + diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index d490ca48b..04e9ddd44 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -1,114 +1,114 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include -#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 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 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 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 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(std::move(_lines.front()))); - _lines.pop(); - } -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include +#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 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 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 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 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(std::move(_lines.front()))); + _lines.pop(); + } +} diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 1f0f7ddb7..2afab15dc 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -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 - -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 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(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 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(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 + +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 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(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 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(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; + } +} diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index ab35fff97..bb3dd00a8 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -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 -#include - -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 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(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(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(payload_val); - - std::lock_guard mqttLock(_mqttMutex); - - switch (command) { - case MqttPowerLimiterCommand::Mode: - { - using Mode = PowerLimiterClass::Mode; - Mode mode = static_cast(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 +#include + +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 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(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(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(payload_val); + + std::lock_guard mqttLock(_mqttMutex); + + switch (command) { + case MqttPowerLimiterCommand::Mode: + { + using Mode = PowerLimiterClass::Mode; + Mode mode = static_cast(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(); +} diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index da6b38b39..685ab5949 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -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 -#include - -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(); - 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()) { - if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 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(); - 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()) { - if (root["current"].as() < 0 || root["current"].as() > 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(); - 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(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; - root["enable_voltage_limit"] = static_cast(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(); - config.Huawei.VerboseLogging = root["verbose_logging"]; - config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); - config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); - config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as(); - config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); - config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); - config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); - config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); - config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); - 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 +#include + +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(); + 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()) { + if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 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(); + 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()) { + if (root["current"].as() < 0 || root["current"].as() > 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(); + 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(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; + root["enable_voltage_limit"] = static_cast(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(); + config.Huawei.VerboseLogging = root["verbose_logging"]; + config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); + config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); + config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as(); + config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); + config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); + config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); + config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); + config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); + 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); +} diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp index e8e23615a..f171a18ab 100644 --- a/src/WebApi_ws_Huawei.cpp +++ b/src/WebApi_ws_Huawei.cpp @@ -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 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 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 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 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); + } } \ No newline at end of file diff --git a/src/WebApi_ws_battery.cpp b/src/WebApi_ws_battery.cpp index 466540228..42913abc6 100644 --- a/src/WebApi_ws_battery.cpp +++ b/src/WebApi_ws_battery.cpp @@ -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 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 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 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 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); + } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c23087aae..7489a27d8 100644 --- a/src/main.cpp +++ b/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 -#include -#include -#include - -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(pin.display_type), - pin.display_data, - pin.display_clk, - pin.display_cs, - pin.display_reset); - Display.setDiagramMode(static_cast(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 +#include +#include +#include + +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(pin.display_type), + pin.display_data, + pin.display_clk, + pin.display_cs, + pin.display_reset); + Display.setDiagramMode(static_cast(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(); +} diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue index 171e5bd8a..b381859a1 100644 --- a/webapp/src/components/BatteryView.vue +++ b/webapp/src/components/BatteryView.vue @@ -1,217 +1,217 @@ - - - \ No newline at end of file diff --git a/webapp/src/components/HuaweiView.vue b/webapp/src/components/HuaweiView.vue index c9e1d3964..c4659a3f2 100644 --- a/webapp/src/components/HuaweiView.vue +++ b/webapp/src/components/HuaweiView.vue @@ -1,358 +1,358 @@ - - - \ No newline at end of file diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index c91fe3a25..d93471d2e 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -1,943 +1,943 @@ -{ - "menu": { - "LiveView": "Direct", - "Settings": "Paramètres", - "NetworkSettings": "Réseau", - "NTPSettings": "Heure locale", - "MQTTSettings": "MQTT", - "InverterSettings": "Onduleurs", - "SecuritySettings": "Sécurité", - "DTUSettings": "DTU", - "DeviceManager": "Périphériques", - "VedirectSettings": "VE.Direct", - "PowerMeterSettings": "Power Meter", - "BatterySettings": "Battery", - "AcChargerSettings": "AC Charger", - "ConfigManagement": "Gestion de la configuration", - "FirmwareUpgrade": "Mise à jour du firmware", - "DeviceReboot": "Redémarrage de l'appareil", - "Info": "Informations", - "System": "Système", - "Network": "Réseau", - "NTP": "NTP", - "MQTT": "MQTT", - "Console": "Console", - "Vedirect": "VE.Direct", - "About": "A propos", - "Logout": "Déconnexion", - "Login": "Connexion" - }, - "base": { - "Yes": "Oui", - "No": "Non", - "VerboseLogging": "Journalisation Détaillée", - "Seconds": "Secondes", - "Loading": "Chargement...", - "Reload": "Reload", - "Cancel": "Annuler", - "Save": "Sauvegarder", - "Refreshing": "Refreshing", - "Pull": "Pull down to refresh", - "Release": "Release to refresh", - "Close": "Fermer" - }, - "Error": { - "Oops": "Oops!" - }, - "localeswitcher": { - "Dark": "Sombre", - "Light": "Clair", - "Auto": "Auto" - }, - "apiresponse": { - "1001": "Paramètres enregistrés !", - "1002": "Aucune valeur trouvée !", - "1003": "Données trop importantes !", - "1004": "Échec de l'analyse des données !", - "1005": "Certaines valeurs sont manquantes !", - "1006": "Write failed!", - "2001": "Le numéro de série ne peut pas être nul !", - "2002": "L'intervalle de sondage doit être supérieur à zéro !", - "2003": "Réglage du niveau de puissance invalide !", - "2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!", - "2005": "Invalid country selection !", - "3001": "Rien n'a été supprimé !", - "3002": "Configuration réinitialisée. Redémarrage maintenant...", - "4001": "@:apiresponse.2001", - "4002": "Le nom doit comporter entre 1 et {max} caractères !", - "4003": "Seulement {max} onduleurs sont supportés !", - "4004": "Onduleur créé !", - "4005": "Identifiant spécifié invalide !", - "4006": "Réglage du montant maximal de canaux invalide !", - "4007": "Onduleur modifié !", - "4008": "Onduleur supprimé !", - "4009": "Inverter order saved!", - "5001": "@:apiresponse.2001", - "5002": "La limite doit être comprise entre 1 et {max} !", - "5003": "Type spécifié invalide !", - "5004": "Onduleur spécifié invalide !", - "6001": "Redémarrage déclenché !", - "6002": "Redémarrage annulé !", - "7001": "Le nom du serveur MQTT doit comporter entre 1 et {max} caractères !", - "7002": "Le nom d'utilisateur ne doit pas comporter plus de {max} caractères !", - "7003": "Le mot de passe ne doit pas comporter plus de {max} caractères !", - "7004": "Le sujet ne doit pas comporter plus de {max} caractères !", - "7005": "Le sujet ne doit pas contenir d'espace !", - "7006": "Le sujet doit se terminer par une barre oblique (/) !", - "7007": "Le port doit être un nombre entre 1 et 65535 !", - "7008": "Le certificat ne doit pas comporter plus de {max} caractères !", - "7009": "Le sujet LWT ne doit pas comporter plus de {max} caractères !", - "7010": "Le sujet LWT ne doit pas contenir de caractères d'espacement !", - "7011": "La valeur LWT en ligne ne doit pas dépasser {max} caractères !", - "7012": "La valeur LWT hors ligne ne doit pas dépasser {max} caractères !", - "7013": "L'intervalle de publication doit être un nombre compris entre {min} et {max} !", - "7014": "Le sujet Hass ne doit pas dépasser {max} caractères !", - "7015": "Le sujet Hass ne doit pas contenir d'espace !", - "7016": "LWT QOS ne doit pas être supérieur à {max}!", - "8001": "L'adresse IP n'est pas valide !", - "8002": "Le masque de réseau n'est pas valide !", - "8003": "La passerelle n'est pas valide !", - "8004": "L'adresse IP du serveur DNS primaire n'est pas valide !", - "8005": "L'adresse IP du serveur DNS secondaire n'est pas valide !", - "8006": "La valeur du délai d'attente du point d'accès administratif n'est pas valide !", - "9001": "Le serveur NTP doit avoir une longueur comprise entre 1 et {max} caractères !", - "9002": "Le fuseau horaire doit comporter entre 1 et {max} caractères !", - "9003": "La description du fuseau horaire doit comporter entre 1 et {max} caractères !", - "9004": "L'année doit être un nombre compris entre {min} et {max} !", - "9005": "Le mois doit être un nombre compris entre {min} et {max} !", - "9006": "Le jour doit être un nombre compris entre {min} et {max} !", - "9007": "Les heures doivent être un nombre compris entre {min} et {max} !", - "9008": "Les minutes doivent être un nombre compris entre {min} et {max} !", - "9009": "Les secondes doivent être un nombre compris entre {min} et {max} !", - "9010": "Heure mise à jour !", - "10001": "Le mot de passe doit comporter entre 8 et {max} caractères !", - "10002": "Authentification réussie !", - "11001": "@:apiresponse.2001", - "11002": "@:apiresponse:5004", - "12001": "Le profil doit comporter entre 1 et {max} caractères !" - }, - "home": { - "LiveData": "Données en direct", - "SerialNumber": "Numéro de série : ", - "CurrentLimit": "Limite de courant : ", - "DataAge": "Âge des données : ", - "Seconds": "{val} secondes", - "ShowSetInverterLimit": "Afficher / Régler la limite de l'onduleur", - "TurnOnOff": "Allumer / Eteindre l'onduleur", - "ShowInverterInfo": "Afficher les informations sur l'onduleur", - "ShowEventlog": "Afficher le journal des événements", - "UnreadMessages": "messages non lus", - "Loading": "@:base.Loading", - "EventLog": "Journal des événements", - "InverterInfo": "Informations sur l'onduleur", - "LimitSettings": "Paramètres de la limite", - "LastLimitSetStatus": "Statut de la dernière limite fixée", - "SetLimit": "Fixer la limite", - "Relative": "Relative (%)", - "Absolute": "Absolue (W)", - "LimitHint": "Astuce : Si vous définissez la limite en valeur absolue, l'affichage de la valeur actuelle ne sera mis à jour qu'après environ 4 minutes.", - "SetPersistent": "Fixer une limite persistante", - "SetNonPersistent": "Fixer une limite non persistante", - "PowerSettings": "Paramètres d'alimentation", - "LastPowerSetStatus": "État du dernier réglage de l'alimentation", - "TurnOn": "Allumer", - "TurnOff": "Eteindre", - "Restart": "Redémarrer", - "Failure": "Échec", - "Pending": "En attente", - "Ok": "OK", - "Unknown": "Inconnu", - "ShowGridProfile": "Show Grid Profile", - "GridProfile": "Grid Profile", - "LoadingInverter": "Waiting for data... (can take up to 10 seconds)" - }, - "vedirecthome": { - "SerialNumber": "Numéro de série", - "FirmwareVersion": "Version du Firmware", - "DataAge": "Âge des données", - "Seconds": "{val} secondes", - "Property": "Property", - "Value": "Value", - "Unit": "Unit", - "section_device": "Device Info", - "device": { - "LOAD": "Load output state", - "CS": "State of operation", - "MPPT": "Tracker operation mode", - "OR": "Off reason", - "ERR": "Error code", - "HSDS": "Day sequence number (0..364)", - "MpptTemperature": "Charge controller temperature" - }, - "section_output": "Output (Battery)", - "output": { - "P": "Power (calculated)", - "V": "Voltage", - "I": "Current", - "E": "Efficiency (calculated)" - }, - "section_input": "Input (Solar Panels)", - "input": { - "NetworkPower": "VE.Smart network total power", - "PPV": "Power", - "VPV": "Voltage", - "IPV": "Current (calculated)", - "YieldToday": "Yield today", - "YieldYesterday": "Yield yesterday", - "YieldTotal": "Yield total (user resettable counter)", - "MaximumPowerToday": "Maximum power today", - "MaximumPowerYesterday": "Maximum power yesterday" - }, - "PowerLimiterState": "Power limiter state [off (charging), solar passthrough, on battery]" - }, - "vedirecthome": { - "SerialNumber": "Serial Number: ", - "FirmwareNumber": "Firmware Number: ", - "DataAge": "Data Age: ", - "Seconds": "{val} seconds", - "DeviceInfo": "Device Info", - "Property": "Property", - "Value": "Value", - "Unit": "Unit", - "LoadOutputState": "Load output state", - "StateOfOperation": "State of operation", - "TrackerOperationMode": "Tracker operation mode", - "OffReason": "Off reason", - "ErrorCode": "Error code", - "DaySequenceNumber": "Day sequence number (0..364)", - "Battery": "Output (Battery)", - "output": { - "P": "Power (calculated)", - "V": "Voltage", - "I": "Current", - "E": "Efficiency (calculated)" - }, - "Panel": "Input (Solar Panels)", - "input": { - "PPV": "Power", - "VPV": "Voltage", - "IPV": "Current (calculated)", - "YieldToday": "Yield today", - "YieldYesterday": "Yield yesterday", - "YieldTotal": "Yield total (user resettable counter)", - "MaximumPowerToday": "Maximum power today", - "MaximumPowerYesterday": "Maximum power yesterday" - }, - "PowerLimiterState": "Power limiter state [off (charging), solar passthrough, on battery]" - }, - "eventlog": { - "Start": "Départ", - "Stop": "Arrêt", - "Id": "ID", - "Message": "Message" - }, - "devinfo": { - "NoInfo": "Aucune information disponible", - "NoInfoLong": "N'a pas reçu de données valides de l'onduleur jusqu'à présent. J'essaie toujours...", - "UnknownModel": "Modèle inconnu ! Veuillez signaler le \"Numéro d'article matériel\" et le modèle (par exemple, HM-350) comme un problème ici.", - "Serial": "Serial", - "ProdYear": "Production Year", - "ProdWeek": "Production Week", - "Model": "Modèle", - "DetectedMaxPower": "Puissance maximale détectée", - "BootloaderVersion": "Version du bootloader", - "FirmwareVersion": "Version du firmware", - "FirmwareBuildDate": "Date de création du firmware", - "HardwarePartNumber": "Numéro d'article matériel", - "HardwareVersion": "Version du matériel" - }, - "gridprofile": { - "NoInfo": "@:devinfo.NoInfo", - "NoInfoLong": "@:devinfo.NoInfoLong", - "Name": "Name", - "Version": "Version", - "Enabled": "@:wifistationinfo.Enabled", - "Disabled": "@:wifistationinfo.Disabled", - "GridprofileSupport": "Support the development", - "GridprofileSupportLong": "Please see here for further information." - }, - "systeminfo": { - "SystemInfo": "Informations sur le système", - "VersionError": "Erreur de récupération des informations de version", - "VersionNew": "Nouvelle version disponible ! Montrer les changements !", - "VersionOk": "À jour !" - }, - "firmwareinfo": { - "FirmwareInformation": "Informations sur le firmware", - "Hostname": "Nom d'hôte", - "SdkVersion": "Version du SDK", - "ConfigVersion": "Version de la configuration", - "FirmwareVersion": "Version du firmware / Hash Git", - "PioEnv": "PIO Environment", - "FirmwareVersionHint": "Cliquez ici pour afficher des informations sur votre version actuelle", - "FirmwareUpdate": "Mise à jour du firmware", - "FirmwareUpdateHint": "Cliquez ici pour voir les changements entre votre version et la dernière version", - "FrmwareUpdateAllow": "En activant le contrôle de mise à jour, une demande est envoyée à GitHub.com à chaque fois que la page est consultée afin de récupérer la dernière version disponible. Si tu n'es pas d'accord, laisse cette fonction désactivée.", - "ResetReason0": "Raison de la réinitialisation CPU 0", - "ResetReason1": "Raison de la réinitialisation CPU 1", - "ConfigSaveCount": "Nombre d'enregistrements de la configuration", - "Uptime": "Durée de fonctionnement", - "UptimeValue": "0 jour {time} | 1 jour {time} | {count} jours {time}" - }, - "hardwareinfo": { - "HardwareInformation": "Informations sur le matériel", - "ChipModel": "Modèle de puce", - "ChipRevision": "Révision de la puce", - "ChipCores": "Nombre de cœurs", - "CpuFrequency": "Fréquence du CPU", - "Mhz": "MHz", - "CpuTemperature": "CPU Temperature", - "FlashSize": "Taille de la mémoire flash" - }, - "memoryinfo": { - "MemoryInformation": "Informations sur la mémoire", - "Type": "Type", - "Usage": "Utilisation", - "Free": "Libre", - "Used": "Utilisée", - "Size": "Taille", - "Heap": "Heap", - "PsRam": "PSRAM", - "LittleFs": "LittleFs", - "Sketch": "Sketch" - }, - "heapdetails": { - "HeapDetails": "Heap Details", - "TotalFree": "Total free", - "LargestFreeBlock": "Biggest contiguous free block", - "MaxUsage": "Maximum usage since start", - "Fragmentation": "Level of fragmentation" - }, - "radioinfo": { - "RadioInformation": "Informations sur la radio", - "Status": "{module} Statut", - "ChipStatus": "{module} État de la puce", - "ChipType": "{module} Type de puce", - "Connected": "connectée", - "NotConnected": "non connectée", - "Configured": "configurée", - "NotConfigured": "non configurée", - "Unknown": "Inconnue" - }, - "networkinfo": { - "NetworkInformation": "Informations sur le réseau" - }, - "wifistationinfo": { - "WifiStationInfo": "Informations sur le WiFi (Station)", - "Status": "Statut", - "Enabled": "activé", - "Disabled": "désactivé", - "Ssid": "SSID", - "Bssid": "BSSID", - "Quality": "Qualité", - "Rssi": "RSSI" - }, - "wifiapinfo": { - "WifiApInfo": "Informations sur le WiFi (Point d'accès)", - "Status": "@:wifistationinfo.Status", - "Enabled": "@:wifistationinfo.Enabled", - "Disabled": "@:wifistationinfo.Disabled", - "Ssid": "@:wifistationinfo.Ssid", - "Stations": "# Stations" - }, - "interfacenetworkinfo": { - "NetworkInterface": "Interface réseau ({iface})", - "Hostname": "@:firmwareinfo.Hostname", - "IpAddress": "Adresse IP", - "Netmask": "Masque de réseau", - "DefaultGateway": "Passerelle par défaut", - "Dns": "DNS {num}", - "MacAddress": "Addresse MAC" - }, - "interfaceapinfo": { - "NetworkInterface": "Interface réseau (Point d'accès)", - "IpAddress": "@:interfacenetworkinfo.IpAddress", - "MacAddress": "@:interfacenetworkinfo.MacAddress" - }, - "ntpinfo": { - "NtpInformation": "Informations sur le NTP", - "ConfigurationSummary": "Résumé de la configuration", - "Server": "Serveur", - "Timezone": "Fuseau horaire", - "TimezoneDescription": "Description du fuseau horaire", - "CurrentTime": "Heure actuelle", - "Status": "Statut", - "Synced": "synchronisée", - "NotSynced": "pas synchronisée", - "LocalTime": "Heure locale", - "Sunrise": "Lever du soleil", - "Sunset": "Coucher du soleil", - "NotAvailable": "Not Available", - "Mode": "Mode", - "Day": "Jour", - "Night": "Nuit" - }, - "mqttinfo": { - "MqttInformation": "MQTT Information", - "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", - "Status": "@:ntpinfo.Status", - "Enabled": "Activé", - "Disabled": "Désactivé", - "Server": "@:ntpinfo.Server", - "Port": "Port", - "Username": "Nom d'utilisateur", - "BaseTopic": "Sujet de base", - "PublishInterval": "Intervalle de publication", - "Seconds": "{sec} secondes", - "CleanSession": "CleanSession Flag", - "Retain": "Conserver", - "Tls": "TLS", - "RootCertifcateInfo": "Informations sur le certificat de l'autorité de certification racine", - "TlsCertLogin": "Connexion avec un certificat TLS", - "ClientCertifcateInfo": "Informations sur le certificat du client", - "HassSummary": "Résumé de la configuration de la découverte automatique du MQTT de Home Assistant", - "Expire": "Expiration", - "IndividualPanels": "Panneaux individuels", - "RuntimeSummary": "Résumé du temps de fonctionnement", - "ConnectionStatus": "État de la connexion", - "Connected": "connecté", - "Disconnected": "déconnecté" - }, - "vedirectinfo": { - "VedirectInformation" : "VE.Direct Info", - "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", - "Status": "@:ntpinfo.Status", - "Enabled": "@:mqttinfo.Enabled", - "Disabled": "@:mqttinfo.Disabled", - "VerboseLogging": "@:base.VerboseLogging", - "UpdatesOnly": "@:vedirectadmin.UpdatesOnly", - "UpdatesEnabled": "@:mqttinfo.Enabled", - "UpdatesDisabled": "@:mqttinfo.Disabled" - }, - "console": { - "Console": "Console", - "VirtualDebugConsole": "Console de débogage", - "EnableAutoScroll": "Activer le défilement automatique", - "ClearConsole": "Vider la console", - "CopyToClipboard": "Copier dans le presse-papiers" - }, - "inverterchannelinfo": { - "String": "Ligne {num}", - "Phase": "Phase {num}", - "General": "General" - }, - "invertertotalinfo": { - "InverterTotalYieldTotal": "Onduleurs rendement total", - "InverterTotalYieldDay": "Onduleurs rendement du jour", - "InverterTotalPower": "Onduleurs puissance de l'installation", - "MpptTotalYieldTotal": "MPPT rendement total", - "MpptTotalYieldDay": "MPPT rendement du jour", - "MpptTotalPower": "MPPT puissance de l'installation", - "BatterySoc": "State of charge", - "HomePower": "Grid Power", - "HuaweiPower": "Huawei AC Power" - }, - "inverterchannelproperty": { - "Power": "Puissance", - "Voltage": "Tension", - "Current": "Courant", - "Power DC": "Puissance continue", - "YieldDay": "Rendement du jour", - "YieldTotal": "Rendement total", - "Frequency": "Fréquence", - "Temperature": "Température", - "PowerFactor": "Facteur de puissance", - "ReactivePower": "Puissance réactive", - "Efficiency": "Efficacité", - "Irradiation": "Irradiation" - }, - "maintenancereboot": { - "DeviceReboot": "Redémarrage de l'appareil", - "PerformReboot": "Effectuer un redémarrage", - "Reboot": "Redémarrer !", - "Cancel": "@:base.Cancel", - "RebootOpenDTU": "Redémarrer OpenDTU", - "RebootQuestion": "Voulez-vous vraiment redémarrer l'appareil ?", - "RebootHint": "Astuce : Normalement, il n'est pas nécessaire de procéder à un redémarrage manuel. OpenDTU effectue automatiquement tout redémarrage nécessaire (par exemple, après une mise à jour du firmware). Les paramètres sont également adoptés sans redémarrage. Si vous devez redémarrer en raison d'une erreur, veuillez envisager de la signaler à l'adresse suivante Github." - }, - "dtuadmin": { - "DtuSettings": "Paramètres du DTU", - "DtuConfiguration": "Configuration du DTU", - "Serial": "Numéro de série", - "SerialHint": "L'onduleur et le DTU ont tous deux un numéro de série. Le numéro de série du DTU est généré de manière aléatoire lors du premier démarrage et ne doit normalement pas être modifié.", - "PollInterval": "Intervalle de sondage", - "VerboseLogging": "@:base.VerboseLogging", - "Seconds": "Secondes", - "NrfPaLevel": "NRF24 Niveau de puissance d'émission", - "CmtPaLevel": "CMT2300A Niveau de puissance d'émission", - "NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", - "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", - "CmtCountry": "CMT2300A Region/Country:", - "CmtCountryHint": "Each country has different frequency allocations.", - "country_0": "Europe ({min}MHz - {max}MHz)", - "country_1": "North America ({min}MHz - {max}MHz)", - "country_2": "Brazil ({min}MHz - {max}MHz)", - "CmtFrequency": "CMT2300A Frequency:", - "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", - "CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.", - "MHz": "{mhz} MHz", - "dBm": "{dbm} dBm", - "Min": "Minimum ({db} dBm)", - "Low": "Bas ({db} dBm)", - "High": "Haut ({db} dBm)", - "Max": "Maximum ({db} dBm)" - }, - "securityadmin": { - "SecuritySettings": "Paramètres de sécurité", - "AdminPassword": "Mot de passe administrateur", - "Password": "Mot de passe", - "RepeatPassword": "Répéter le mot de passe", - "PasswordHint": "Astuce : Le mot de passe administrateur est utilisé pour accéder à cette interface web (utilisateur 'admin'), mais aussi pour se connecter à l'appareil en mode AP. Il doit comporter de 8 à 64 caractères.", - "Permissions": "Autorisations", - "ReadOnly": "Autoriser l'accès en lecture seule à l'interface web sans mot de passe" - }, - "ntpadmin": { - "NtpSettings": "Paramètres NTP", - "NtpConfiguration": "Configuration du protocole NTP", - "TimeServer": "Serveur horaire", - "TimeServerHint": "La valeur par défaut convient tant que OpenDTU a un accès direct à Internet.", - "Timezone": "Fuseau horaire", - "TimezoneConfig": "Configuration du fuseau horaire", - "LocationConfiguration": "Géolocalisation", - "Longitude": "Longitude", - "Latitude": "Latitude", - "SunSetType": "Sunset type", - "SunSetTypeHint": "Affects the day/night calculation. It can take up to one minute until the new type will be applied.", - "OFFICIAL": "Standard dawn (90.8°)", - "NAUTICAL": "Nautical dawn (102°)", - "CIVIL": "Civil dawn (96°)", - "ASTONOMICAL": "Astronomical dawn (108°)", - "ManualTimeSynchronization": "Synchronisation manuelle de l'heure", - "CurrentOpenDtuTime": "Heure actuelle de l'OpenDTU", - "CurrentLocalTime": "Heure locale actuelle", - "SynchronizeTime": "Synchroniser l'heure", - "SynchronizeTimeHint": "Astuce : Vous pouvez utiliser la synchronisation horaire manuelle pour définir l'heure actuelle d'OpenDTU si aucun serveur NTP n'est disponible. Mais attention, en cas de mise sous tension, l'heure est perdue. Notez également que la précision de l'heure sera faussée, car elle ne peut pas être resynchronisée régulièrement et le microcontrôleur ESP32 ne dispose pas d'une horloge temps réel." - }, - "networkadmin": { - "NetworkSettings": "Paramètres réseau", - "WifiConfiguration": "Configuration du réseau WiFi", - "WifiSsid": "SSID", - "WifiPassword": "Mot de passe", - "Hostname": "Nom d'hôte", - "HostnameHint": "Astuce : Le texte %06X sera remplacé par les 6 derniers chiffres de l'ESP ChipID au format hexadécimal.", - "EnableDhcp": "Activer le DHCP", - "StaticIpConfiguration": "Configuration de l'IP statique", - "IpAddress": "Adresse IP", - "Netmask": "Masque de réseau", - "DefaultGateway": "Passerelle par défaut", - "Dns": "Serveur DNS {num}", - "AdminAp": "Configuration du réseau WiFi (Point d'accès)", - "ApTimeout": "Délai d'attente du point d'accès", - "ApTimeoutHint": "Durée pendant laquelle le point d'accès reste ouvert. Une valeur de 0 signifie infini.", - "Minutes": "minutes", - "EnableMdns": "Activer mDNS", - "MdnsSettings": "mDNS Settings" - }, - "mqttadmin": { - "MqttSettings": "Paramètres MQTT", - "MqttConfiguration": "Configuration du système MQTT", - "EnableMqtt": "Activer le MQTT", - "VerboseLogging": "@:base.VerboseLogging", - "EnableHass": "Activer la découverte automatique du MQTT de Home Assistant", - "MqttBrokerParameter": "Paramètre du Broker MQTT", - "Hostname": "Nom d'hôte", - "HostnameHint": "Nom d'hôte ou adresse IP", - "Port": "Port", - "Username": "Nom d'utilisateur", - "UsernameHint": "Nom d'utilisateur, laisser vide pour une connexion anonyme", - "Password": "Mot de passe:", - "PasswordHint": "Mot de passe, laissez vide pour une connexion anonyme", - "BaseTopic": "Sujet de base", - "BaseTopicHint": "Sujet de base, qui sera ajouté en préambule à tous les sujets publiés (par exemple, inverter/).", - "PublishInterval": "Intervalle de publication", - "Seconds": "secondes", - "CleanSession": "Enable CleanSession flag", - "EnableRetain": "Activation du maintien", - "EnableTls": "Activer le TLS", - "RootCa": "Certificat CA-Root (par défaut Letsencrypt)", - "TlsCertLoginEnable": "Activer la connexion par certificat TLS", - "ClientCert": "Certificat client TLS:", - "ClientKey": "Clé client TLS:", - "LwtParameters": "Paramètres LWT", - "LwtTopic": "Sujet LWT", - "LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base", - "LwtOnline": "Message en ligne de LWT", - "LwtOnlineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera en ligne", - "LwtOffline": "Message hors ligne de LWT", - "LwtOfflineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera hors ligne", - "LwtQos": "QoS (Quality of Service):", - "QOS0": "0 (Au maximum une fois)", - "QOS1": "1 (Au moins une fois)", - "QOS2": "2 (Exactement une fois)", - "HassParameters": "Paramètres de découverte automatique MQTT de Home Assistant", - "HassPrefixTopic": "Préfixe du sujet", - "HassPrefixTopicHint": "Le préfixe de découverte du sujet", - "HassRetain": "Activer du maintien", - "HassExpire": "Activer l'expiration", - "HassIndividual": "Panneaux individuels" - }, - "vedirectadmin": { - "VedirectSettings": "VE.Direct Settings", - "VedirectConfiguration": "VE.Direct Configuration", - "EnableVedirect": "Enable VE.Direct", - "VedirectParameter": "VE.Direct Parameter", - "VerboseLogging": "@:base.VerboseLogging", - "UpdatesOnly": "Publish values to MQTT only when they change" - }, - "batteryadmin": { - "BatterySettings": "Battery Settings", - "BatteryConfiguration": "General Interface Settings", - "EnableBattery": "Enable Interface", - "VerboseLogging": "@:base.VerboseLogging", - "Provider": "Data Provider", - "ProviderPylontechCan": "Pylontech using CAN bus", - "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", - "ProviderMqtt": "Battery data from MQTT broker", - "ProviderVictron": "Victron SmartShunt using VE.Direct interface", - "MqttConfiguration": "MQTT Settings", - "MqttSocTopic": "SoC value topic", - "MqttVoltageTopic": "Voltage value topic", - "JkBmsConfiguration": "JK BMS Settings", - "JkBmsInterface": "Interface Type", - "JkBmsInterfaceUart": "TTL-UART on MCU", - "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", - "PollingInterval": "Polling Interval", - "Seconds": "@:base.Seconds" - }, - "inverteradmin": { - "InverterSettings": "Paramètres des onduleurs", - "AddInverter": "Ajouter un nouvel onduleur", - "Serial": "Numéro de série", - "Name": "Nom", - "Add": "Ajouter", - "AddHint": " Astuce : Vous pouvez définir des paramètres supplémentaires après avoir créé l'onduleur. Utilisez l'icône du stylo dans la liste des onduleurs.", - "InverterList": "Liste des onduleurs", - "Status": "État", - "Send": "Envoyer", - "Receive": "Recevoir", - "StatusHint": "Astuce : L'onduleur est alimenté par son entrée courant continu. S'il n'y a pas de soleil, l'onduleur est éteint, mais les requêtes peuvent toujours être envoyées.", - "Type": "Type", - "Action": "Action", - "SaveOrder": "Save order", - "DeleteInverter": "Supprimer l'onduleur", - "EditInverter": "Modifier l'onduleur", - "General": "Général", - "String": "Ligne", - "Advanced": "Advanced", - "InverterSerial": "Numéro de série de l'onduleur", - "InverterName": "Nom de l'onduleur :", - "InverterNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour votre onduleur.", - "InverterStatus": "Recevoir / Envoyer", - "PollEnable": "Interroger les données de l'onduleur", - "PollEnableNight": "Interroger les données de l'onduleur la nuit", - "CommandEnable": "Envoyer des commandes", - "CommandEnableNight": "Envoyer des commandes la nuit", - "StringName": "Nom de la ligne {num}:", - "StringNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour le port respectif de votre onduleur.", - "StringMaxPower": "Puissance maximale de la ligne {num}:", - "StringMaxPowerHint": "Entrez la puissance maximale des panneaux solaires connectés.", - "StringYtOffset": "Décalage du rendement total de la ligne {num} :", - "StringYtOffsetHint": "Ce décalage est appliqué à la valeur de rendement total lue sur le variateur. Il peut être utilisé pour mettre le rendement total du variateur à zéro si un variateur usagé est utilisé.", - "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", - "ReachableThreshold": "Reachable Threshold:", - "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", - "ZeroRuntime": "Zero runtime data", - "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", - "ZeroDay": "Zero daily yield at midnight", - "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", - "ClearEventlog": "Clear Eventlog at midnight", - "Cancel": "@:base.Cancel", - "Save": "@:base.Save", - "DeleteMsg": "Êtes-vous sûr de vouloir supprimer l'onduleur \"{name}\" avec le numéro de série \"{serial}\" ?", - "Delete": "Supprimer", - "YieldDayCorrection": "Yield Day Correction", - "YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight" - }, - "configadmin": { - "ConfigManagement": "Gestion de la configuration", - "BackupHeader": "Sauvegarder le fichier de configuration", - "BackupConfig": "Fichier de configuration", - "Backup": "Sauvegarder", - "Restore": "Restaurer", - "NoFileSelected": "Aucun fichier sélectionné", - "RestoreHeader": "Restaurer le fichier de configuration", - "Back": "Retour", - "UploadSuccess": "Succès du téléversement", - "RestoreHint": "Note : Cette opération remplace le fichier de configuration par la configuration restaurée et redémarre OpenDTU pour appliquer tous les paramètres.", - "ResetHeader": "Effectuer une réinitialisation d'usine", - "FactoryResetButton": "Restaurer les paramètres d'usine", - "ResetHint": "Note : Cliquez sur \"Restaurer les paramètres d'usine\" pour restaurer et initialiser les paramètres d'usine par défaut et redémarrer.", - "FactoryReset": "Remise à zéro", - "ResetMsg": "Êtes-vous sûr de vouloir supprimer la configuration actuelle et réinitialiser tous les paramètres à leurs valeurs par défaut ?", - "ResetConfirm": "Remise à zéro !", - "Cancel": "@:base.Cancel" - }, - "powerlimiteradmin": { - "PowerLimiterSettings": "Dynamic Power Limiter Settings", - "ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.", - "ConfigHints": "Configuration Notes", - "ConfigHintRequirement": "Required", - "ConfigHintOptional": "Optional", - "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", - "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", - "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", - "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", - "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", - "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", - "General": "General", - "Enable": "Enable", - "VerboseLogging": "@:base.VerboseLogging", - "SolarPassthrough": "Solar-Passthrough", - "EnableSolarPassthrough": "Enable Solar-Passthrough", - "SolarPassthroughLosses": "(Full) Solar-Passthrough Losses", - "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", - "BatteryDischargeAtNight": "Use battery at night even if only partially charged", - "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", - "InverterSettings": "Inverter", - "Inverter": "Target Inverter", - "SelectInverter": "Select an inverter...", - "InverterChannelId": "Input used for voltage measurements", - "TargetPowerConsumption": "Target Grid Consumption", - "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", - "TargetPowerConsumptionHysteresis": "Hysteresis", - "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", - "LowerPowerLimit": "Minimum Power Limit", - "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", - "BaseLoadLimit": "Base Load", - "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", - "UpperPowerLimit": "Maximum Power Limit", - "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", - "SocThresholds": "Battery State of Charge (SoC) Thresholds", - "IgnoreSoc": "Ignore Battery SoC", - "StartThreshold": "Start Threshold for Battery Discharging", - "StopThreshold": "Stop Threshold for Battery Discharging", - "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", - "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", - "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", - "VoltageLoadCorrectionFactor": "Load correction factor", - "BatterySocInfo": "Hint: The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", - "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", - "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", - "InverterIsSolarPowered": "Inverter is powered by solar modules", - "VoltageThresholds": "Battery Voltage Thresholds", - "VoltageLoadCorrectionInfo": "Hint: When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)." - }, - "login": { - "Login": "Connexion", - "SystemLogin": "Connexion au système", - "Username": "Nom d'utilisateur", - "UsernameRequired": "Le nom d'utilisateur est requis", - "Password": "Mot de passe", - "PasswordRequired": "Le mot de passe est requis", - "LoginButton": "Connexion" - }, - "firmwareupgrade": { - "FirmwareUpgrade": "Mise à jour du firmware", - "Loading": "@:base.Loading", - "OtaError": "Erreur OTA", - "Back": "Retour", - "Retry": "Réessayer", - "OtaStatus": "Statut OTA", - "OtaSuccess": "Le téléchargement du firmware a réussi. L'appareil a été redémarré automatiquement. Lorsque l'appareil est à nouveau accessible, l'interface est automatiquement rechargée.", - "FirmwareUpload": "Téléversement du firmware", - "UploadProgress": "Progression du téléversement" - }, - "about": { - "AboutOpendtu": "À propos d'OpenDTU-OnBattery", - "Documentation": "Documentation", - "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", - "ProjectOrigin": "Origine du projet", - "ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion (Mikrocontroller.net).", - "ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (GNU General Public License version 2).", - "ProjectOriginBody3": "Le logiciel a été développé au mieux de nos connaissances et de nos convictions. Néanmoins, aucune responsabilité ne peut être acceptée en cas de dysfonctionnement ou de perte de garantie de l'onduleur.", - "ProjectOriginBody4": "OpenDTU est disponible gratuitement. Si vous avez payé pour le logiciel, vous avez probablement été arnaqué.", - "NewsUpdates": "Actualités et mises à jour", - "NewsUpdatesBody": "Les nouvelles mises à jour peuvent être trouvées sur Github.", - "ErrorReporting": "Rapport d'erreurs", - "ErrorReportingBody": "Veuillez signaler les problèmes en utilisant la fonction fournie par Github.", - "Discussion": "Discussion", - "DiscussionBody": "Discutez avec nous sur Discord ou sur Github." - }, - "hints": { - "RadioProblem": "Impossible de se connecter à un module radio configuré.. Veuillez vérifier le câblage.", - "TimeSync": "L'horloge n'a pas encore été synchronisée. Sans une horloge correctement réglée, aucune demande n'est adressée à l'onduleur. Ceci est normal peu de temps après le démarrage. Cependant, après un temps de fonctionnement plus long (>1 minute), cela indique que le serveur NTP n'est pas accessible.", - "TimeSyncLink": "Veuillez vérifier vos paramètres horaires.", - "DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.", - "DefaultPasswordLink": "Merci de changer le mot de passe." - }, - "deviceadmin": { - "DeviceManager": "Gestionnaire de périphériques", - "ParseError": "Erreur d'analyse dans 'pin_mapping.json': {error}", - "PinAssignment": "Paramètres de connexion", - "SelectedProfile": "Profil sélectionné", - "DefaultProfile": "(Réglages par défaut)", - "ProfileHint": "Votre appareil peut cesser de répondre si vous sélectionnez un profil incompatible. Dans ce cas, vous devez effectuer une suppression via l'interface série.", - "Display": "Affichage", - "PowerSafe": "Economiseur d'énergie", - "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", - "Screensaver": "OLED Anti burn-in", - "ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)", - "DiagramMode": "Diagram mode:", - "off": "Off", - "small": "Small", - "fullscreen": "Fullscreen", - "DiagramDuration": "Diagram duration:", - "DiagramDurationHint": "The time period which is shown in the diagram.", - "Seconds": "Seconds", - "Contrast": "Contraste ({contrast}):", - "Rotation": "Rotation:", - "rot0": "Pas de rotation", - "rot90": "Rotation de 90 degrés", - "rot180": "Rotation de 180 degrés", - "rot270": "Rotation de 270 degrés", - "DisplayLanguage": "Langue d'affichage", - "en": "Anglais", - "de": "Allemand", - "fr": "Français", - "Leds": "LEDs", - "EqualBrightness": "Même luminosité:", - "LedBrightness": "LED {led} luminosité ({brightness}):" - }, - "pininfo": { - "PinOverview": "Vue d'ensemble des connexions", - "Category": "Catégorie", - "Name": "Nom", - "ValueSelected": "Sélectionné", - "ValueActive": "Activé" - }, - "inputserial": { - "format_hoymiles": "Hoymiles serial number format", - "format_converted": "Already converted serial number", - "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", - "format_herf_invalid": "E-Star HERF format: Invalid checksum", - "format_unknown": "Unknown format" - }, - "huawei": { - "DataAge": "Data Age: ", - "Seconds": " {val} seconds", - "Input": "Input", - "Output": "Output", - "Property": "Property", - "Value": "Value", - "Unit": "Unit", - "input_voltage": "Input voltage", - "input_current": "Input current", - "input_power": "Input power", - "input_temp": "Input temperature", - "efficiency": "Efficiency", - "output_voltage": "Output voltage", - "output_current": "Output current", - "max_output_current": "Maximum output current", - "output_power": "Output power", - "output_temp": "Output temperature", - "ShowSetLimit": "Show / Set Huawei Limit", - "LimitSettings": "Limit Settings", - "SetOffline": "Set limit, CAN bus not connected", - "SetOnline": "Set limit, CAN bus connected", - "LimitHint": "Hint: CAN bus not connected voltage limit is 48V-58.5V.", - "Close": "close", - "SetVoltageLimit": "Voltage limit:", - "SetCurrentLimit": "Current limit:", - "CurrentLimit": "Current limit:" - }, - "acchargeradmin": { - "ChargerSettings": "AC Charger Settings", - "Configuration": "AC Charger Configuration", - "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", - "VerboseLogging": "@:base.VerboseLogging", - "CanControllerFrequency": "CAN controller quarz frequency", - "EnableAutoPower": "Automatic power control", - "EnableBatterySoCLimits": "Use SoC data of a connected battery", - "Limits": "Limits", - "BatterySoCLimits": "Battery SoC Limits", - "VoltageLimit": "Charge Voltage limit", - "enableVoltageLimit": "Re-enable voltage limit", - "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", - "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", - "upperPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", - "lowerPowerLimit": "Minimum output power", - "upperPowerLimit": "Maximum output power", - "StopBatterySoCThreshold": "Stop charging at SoC", - "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", - "Seconds": "@:base.Seconds", - "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS", - "targetPowerConsumption": "Target power consumption", - "targetPowerConsumptionHint": "Postitive values use grid power to charge the battery. Negative values result in early shutdown" - }, - "battery": { - "battery": "Battery", - "FwVersion": "Firmware Version", - "HwVersion": "Hardware Version", - "DataAge": "Data Age: ", - "Seconds": " {val} seconds", - "status": "Status", - "Property": "Property", - "yes": "@:base.Yes", - "no": "@:base.No", - "Value": "Value", - "Unit": "Unit", - "SoC": "State of Charge", - "stateOfHealth": "State of Health", - "voltage": "Voltage", - "current": "Current", - "power": "Power", - "temperature": "Temperature", - "bmsTemp": "BMS temperature", - "chargeVoltage": "Requested charge voltage", - "chargeCurrentLimitation": "Charge current limit", - "dischargeCurrentLimitation": "Discharge current limit", - "chargeEnabled": "Charging possible", - "dischargeEnabled": "Discharging possible", - "chargeImmediately": "Immediate charging requested", - "cells": "Cells", - "batOneTemp": "Battery temperature 1", - "batTwoTemp": "Battery temperature 2", - "cellMinVoltage": "Minimum cell voltage", - "cellAvgVoltage": "Average cell voltage", - "cellMaxVoltage": "Maximum cell voltage", - "cellDiffVoltage": "Cell voltage difference", - "balancingActive": "Balancing active", - "issues": "Issues", - "noIssues": "No Issues", - "issueName": "Name", - "issueType": "Type", - "alarm": "Alarm", - "warning": "Warning", - "JkBmsIssueLowCapacity": "Low Capacity", - "JkBmsIssueBmsOvertemperature": "BMS overtemperature", - "JkBmsIssueChargingOvervoltage": "Overvoltage (sum of all cells)", - "JkBmsIssueDischargeUndervoltage": "Undervoltage (sum of all cells)", - "JkBmsIssueBatteryOvertemperature": "Battery overtemperature", - "JkBmsIssueChargingOvercurrent": "Overcurrent (Charging)", - "JkBmsIssueDischargeOvercurrent": "Overcurrent (Discharging)", - "JkBmsIssueCellVoltageDifference": "Cell voltage difference too high", - "JkBmsIssueBatteryBoxOvertemperature": "Battery (box?) overtemperature", - "JkBmsIssueBatteryUndertemperature": "Battery undertemperature", - "JkBmsIssueCellOvervoltage": "Overvoltage (single cell)", - "JkBmsIssueCellUndervoltage": "Undervoltage (single cell)", - "JkBmsIssueAProtect": "AProtect (meaning?)", - "JkBmsIssueBProtect": "BProtect (meaning?)", - "highCurrentDischarge": "High current (discharge)", - "overCurrentDischarge": "Overcurrent (discharge)", - "highCurrentCharge": "High current (charge)", - "overCurrentCharge": "Overcurrent (charge)", - "lowTemperature": "Low temperature", - "underTemperature": "Undertemperature", - "highTemperature": "High temperature", - "overTemperature": "Overtemperature", - "lowVoltage": "Low voltage", - "lowSOC": "Low state of charge", - "underVoltage": "Undervoltage", - "highVoltage": "High voltage", - "overVoltage": "Overvoltage", - "bmsInternal": "BMS internal", - "chargeCycles": "Charge cycles", - "chargedEnergy": "Charged energy", - "dischargedEnergy": "Discharged energy", - "instantaneousPower": "Instantaneous Power", - "consumedAmpHours": "Consumed Amp Hours", - "midpointVoltage": "Midpoint Voltage", - "midpointDeviation": "Midpoint Deviation", - "lastFullCharge": "Last full Charge" - } -} +{ + "menu": { + "LiveView": "Direct", + "Settings": "Paramètres", + "NetworkSettings": "Réseau", + "NTPSettings": "Heure locale", + "MQTTSettings": "MQTT", + "InverterSettings": "Onduleurs", + "SecuritySettings": "Sécurité", + "DTUSettings": "DTU", + "DeviceManager": "Périphériques", + "VedirectSettings": "VE.Direct", + "PowerMeterSettings": "Power Meter", + "BatterySettings": "Battery", + "AcChargerSettings": "AC Charger", + "ConfigManagement": "Gestion de la configuration", + "FirmwareUpgrade": "Mise à jour du firmware", + "DeviceReboot": "Redémarrage de l'appareil", + "Info": "Informations", + "System": "Système", + "Network": "Réseau", + "NTP": "NTP", + "MQTT": "MQTT", + "Console": "Console", + "Vedirect": "VE.Direct", + "About": "A propos", + "Logout": "Déconnexion", + "Login": "Connexion" + }, + "base": { + "Yes": "Oui", + "No": "Non", + "VerboseLogging": "Journalisation Détaillée", + "Seconds": "Secondes", + "Loading": "Chargement...", + "Reload": "Reload", + "Cancel": "Annuler", + "Save": "Sauvegarder", + "Refreshing": "Refreshing", + "Pull": "Pull down to refresh", + "Release": "Release to refresh", + "Close": "Fermer" + }, + "Error": { + "Oops": "Oops!" + }, + "localeswitcher": { + "Dark": "Sombre", + "Light": "Clair", + "Auto": "Auto" + }, + "apiresponse": { + "1001": "Paramètres enregistrés !", + "1002": "Aucune valeur trouvée !", + "1003": "Données trop importantes !", + "1004": "Échec de l'analyse des données !", + "1005": "Certaines valeurs sont manquantes !", + "1006": "Write failed!", + "2001": "Le numéro de série ne peut pas être nul !", + "2002": "L'intervalle de sondage doit être supérieur à zéro !", + "2003": "Réglage du niveau de puissance invalide !", + "2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!", + "2005": "Invalid country selection !", + "3001": "Rien n'a été supprimé !", + "3002": "Configuration réinitialisée. Redémarrage maintenant...", + "4001": "@:apiresponse.2001", + "4002": "Le nom doit comporter entre 1 et {max} caractères !", + "4003": "Seulement {max} onduleurs sont supportés !", + "4004": "Onduleur créé !", + "4005": "Identifiant spécifié invalide !", + "4006": "Réglage du montant maximal de canaux invalide !", + "4007": "Onduleur modifié !", + "4008": "Onduleur supprimé !", + "4009": "Inverter order saved!", + "5001": "@:apiresponse.2001", + "5002": "La limite doit être comprise entre 1 et {max} !", + "5003": "Type spécifié invalide !", + "5004": "Onduleur spécifié invalide !", + "6001": "Redémarrage déclenché !", + "6002": "Redémarrage annulé !", + "7001": "Le nom du serveur MQTT doit comporter entre 1 et {max} caractères !", + "7002": "Le nom d'utilisateur ne doit pas comporter plus de {max} caractères !", + "7003": "Le mot de passe ne doit pas comporter plus de {max} caractères !", + "7004": "Le sujet ne doit pas comporter plus de {max} caractères !", + "7005": "Le sujet ne doit pas contenir d'espace !", + "7006": "Le sujet doit se terminer par une barre oblique (/) !", + "7007": "Le port doit être un nombre entre 1 et 65535 !", + "7008": "Le certificat ne doit pas comporter plus de {max} caractères !", + "7009": "Le sujet LWT ne doit pas comporter plus de {max} caractères !", + "7010": "Le sujet LWT ne doit pas contenir de caractères d'espacement !", + "7011": "La valeur LWT en ligne ne doit pas dépasser {max} caractères !", + "7012": "La valeur LWT hors ligne ne doit pas dépasser {max} caractères !", + "7013": "L'intervalle de publication doit être un nombre compris entre {min} et {max} !", + "7014": "Le sujet Hass ne doit pas dépasser {max} caractères !", + "7015": "Le sujet Hass ne doit pas contenir d'espace !", + "7016": "LWT QOS ne doit pas être supérieur à {max}!", + "8001": "L'adresse IP n'est pas valide !", + "8002": "Le masque de réseau n'est pas valide !", + "8003": "La passerelle n'est pas valide !", + "8004": "L'adresse IP du serveur DNS primaire n'est pas valide !", + "8005": "L'adresse IP du serveur DNS secondaire n'est pas valide !", + "8006": "La valeur du délai d'attente du point d'accès administratif n'est pas valide !", + "9001": "Le serveur NTP doit avoir une longueur comprise entre 1 et {max} caractères !", + "9002": "Le fuseau horaire doit comporter entre 1 et {max} caractères !", + "9003": "La description du fuseau horaire doit comporter entre 1 et {max} caractères !", + "9004": "L'année doit être un nombre compris entre {min} et {max} !", + "9005": "Le mois doit être un nombre compris entre {min} et {max} !", + "9006": "Le jour doit être un nombre compris entre {min} et {max} !", + "9007": "Les heures doivent être un nombre compris entre {min} et {max} !", + "9008": "Les minutes doivent être un nombre compris entre {min} et {max} !", + "9009": "Les secondes doivent être un nombre compris entre {min} et {max} !", + "9010": "Heure mise à jour !", + "10001": "Le mot de passe doit comporter entre 8 et {max} caractères !", + "10002": "Authentification réussie !", + "11001": "@:apiresponse.2001", + "11002": "@:apiresponse:5004", + "12001": "Le profil doit comporter entre 1 et {max} caractères !" + }, + "home": { + "LiveData": "Données en direct", + "SerialNumber": "Numéro de série : ", + "CurrentLimit": "Limite de courant : ", + "DataAge": "Âge des données : ", + "Seconds": "{val} secondes", + "ShowSetInverterLimit": "Afficher / Régler la limite de l'onduleur", + "TurnOnOff": "Allumer / Eteindre l'onduleur", + "ShowInverterInfo": "Afficher les informations sur l'onduleur", + "ShowEventlog": "Afficher le journal des événements", + "UnreadMessages": "messages non lus", + "Loading": "@:base.Loading", + "EventLog": "Journal des événements", + "InverterInfo": "Informations sur l'onduleur", + "LimitSettings": "Paramètres de la limite", + "LastLimitSetStatus": "Statut de la dernière limite fixée", + "SetLimit": "Fixer la limite", + "Relative": "Relative (%)", + "Absolute": "Absolue (W)", + "LimitHint": "Astuce : Si vous définissez la limite en valeur absolue, l'affichage de la valeur actuelle ne sera mis à jour qu'après environ 4 minutes.", + "SetPersistent": "Fixer une limite persistante", + "SetNonPersistent": "Fixer une limite non persistante", + "PowerSettings": "Paramètres d'alimentation", + "LastPowerSetStatus": "État du dernier réglage de l'alimentation", + "TurnOn": "Allumer", + "TurnOff": "Eteindre", + "Restart": "Redémarrer", + "Failure": "Échec", + "Pending": "En attente", + "Ok": "OK", + "Unknown": "Inconnu", + "ShowGridProfile": "Show Grid Profile", + "GridProfile": "Grid Profile", + "LoadingInverter": "Waiting for data... (can take up to 10 seconds)" + }, + "vedirecthome": { + "SerialNumber": "Numéro de série", + "FirmwareVersion": "Version du Firmware", + "DataAge": "Âge des données", + "Seconds": "{val} secondes", + "Property": "Property", + "Value": "Value", + "Unit": "Unit", + "section_device": "Device Info", + "device": { + "LOAD": "Load output state", + "CS": "State of operation", + "MPPT": "Tracker operation mode", + "OR": "Off reason", + "ERR": "Error code", + "HSDS": "Day sequence number (0..364)", + "MpptTemperature": "Charge controller temperature" + }, + "section_output": "Output (Battery)", + "output": { + "P": "Power (calculated)", + "V": "Voltage", + "I": "Current", + "E": "Efficiency (calculated)" + }, + "section_input": "Input (Solar Panels)", + "input": { + "NetworkPower": "VE.Smart network total power", + "PPV": "Power", + "VPV": "Voltage", + "IPV": "Current (calculated)", + "YieldToday": "Yield today", + "YieldYesterday": "Yield yesterday", + "YieldTotal": "Yield total (user resettable counter)", + "MaximumPowerToday": "Maximum power today", + "MaximumPowerYesterday": "Maximum power yesterday" + }, + "PowerLimiterState": "Power limiter state [off (charging), solar passthrough, on battery]" + }, + "vedirecthome": { + "SerialNumber": "Serial Number: ", + "FirmwareNumber": "Firmware Number: ", + "DataAge": "Data Age: ", + "Seconds": "{val} seconds", + "DeviceInfo": "Device Info", + "Property": "Property", + "Value": "Value", + "Unit": "Unit", + "LoadOutputState": "Load output state", + "StateOfOperation": "State of operation", + "TrackerOperationMode": "Tracker operation mode", + "OffReason": "Off reason", + "ErrorCode": "Error code", + "DaySequenceNumber": "Day sequence number (0..364)", + "Battery": "Output (Battery)", + "output": { + "P": "Power (calculated)", + "V": "Voltage", + "I": "Current", + "E": "Efficiency (calculated)" + }, + "Panel": "Input (Solar Panels)", + "input": { + "PPV": "Power", + "VPV": "Voltage", + "IPV": "Current (calculated)", + "YieldToday": "Yield today", + "YieldYesterday": "Yield yesterday", + "YieldTotal": "Yield total (user resettable counter)", + "MaximumPowerToday": "Maximum power today", + "MaximumPowerYesterday": "Maximum power yesterday" + }, + "PowerLimiterState": "Power limiter state [off (charging), solar passthrough, on battery]" + }, + "eventlog": { + "Start": "Départ", + "Stop": "Arrêt", + "Id": "ID", + "Message": "Message" + }, + "devinfo": { + "NoInfo": "Aucune information disponible", + "NoInfoLong": "N'a pas reçu de données valides de l'onduleur jusqu'à présent. J'essaie toujours...", + "UnknownModel": "Modèle inconnu ! Veuillez signaler le \"Numéro d'article matériel\" et le modèle (par exemple, HM-350) comme un problème ici.", + "Serial": "Serial", + "ProdYear": "Production Year", + "ProdWeek": "Production Week", + "Model": "Modèle", + "DetectedMaxPower": "Puissance maximale détectée", + "BootloaderVersion": "Version du bootloader", + "FirmwareVersion": "Version du firmware", + "FirmwareBuildDate": "Date de création du firmware", + "HardwarePartNumber": "Numéro d'article matériel", + "HardwareVersion": "Version du matériel" + }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "Name": "Name", + "Version": "Version", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "GridprofileSupport": "Support the development", + "GridprofileSupportLong": "Please see here for further information." + }, + "systeminfo": { + "SystemInfo": "Informations sur le système", + "VersionError": "Erreur de récupération des informations de version", + "VersionNew": "Nouvelle version disponible ! Montrer les changements !", + "VersionOk": "À jour !" + }, + "firmwareinfo": { + "FirmwareInformation": "Informations sur le firmware", + "Hostname": "Nom d'hôte", + "SdkVersion": "Version du SDK", + "ConfigVersion": "Version de la configuration", + "FirmwareVersion": "Version du firmware / Hash Git", + "PioEnv": "PIO Environment", + "FirmwareVersionHint": "Cliquez ici pour afficher des informations sur votre version actuelle", + "FirmwareUpdate": "Mise à jour du firmware", + "FirmwareUpdateHint": "Cliquez ici pour voir les changements entre votre version et la dernière version", + "FrmwareUpdateAllow": "En activant le contrôle de mise à jour, une demande est envoyée à GitHub.com à chaque fois que la page est consultée afin de récupérer la dernière version disponible. Si tu n'es pas d'accord, laisse cette fonction désactivée.", + "ResetReason0": "Raison de la réinitialisation CPU 0", + "ResetReason1": "Raison de la réinitialisation CPU 1", + "ConfigSaveCount": "Nombre d'enregistrements de la configuration", + "Uptime": "Durée de fonctionnement", + "UptimeValue": "0 jour {time} | 1 jour {time} | {count} jours {time}" + }, + "hardwareinfo": { + "HardwareInformation": "Informations sur le matériel", + "ChipModel": "Modèle de puce", + "ChipRevision": "Révision de la puce", + "ChipCores": "Nombre de cœurs", + "CpuFrequency": "Fréquence du CPU", + "Mhz": "MHz", + "CpuTemperature": "CPU Temperature", + "FlashSize": "Taille de la mémoire flash" + }, + "memoryinfo": { + "MemoryInformation": "Informations sur la mémoire", + "Type": "Type", + "Usage": "Utilisation", + "Free": "Libre", + "Used": "Utilisée", + "Size": "Taille", + "Heap": "Heap", + "PsRam": "PSRAM", + "LittleFs": "LittleFs", + "Sketch": "Sketch" + }, + "heapdetails": { + "HeapDetails": "Heap Details", + "TotalFree": "Total free", + "LargestFreeBlock": "Biggest contiguous free block", + "MaxUsage": "Maximum usage since start", + "Fragmentation": "Level of fragmentation" + }, + "radioinfo": { + "RadioInformation": "Informations sur la radio", + "Status": "{module} Statut", + "ChipStatus": "{module} État de la puce", + "ChipType": "{module} Type de puce", + "Connected": "connectée", + "NotConnected": "non connectée", + "Configured": "configurée", + "NotConfigured": "non configurée", + "Unknown": "Inconnue" + }, + "networkinfo": { + "NetworkInformation": "Informations sur le réseau" + }, + "wifistationinfo": { + "WifiStationInfo": "Informations sur le WiFi (Station)", + "Status": "Statut", + "Enabled": "activé", + "Disabled": "désactivé", + "Ssid": "SSID", + "Bssid": "BSSID", + "Quality": "Qualité", + "Rssi": "RSSI" + }, + "wifiapinfo": { + "WifiApInfo": "Informations sur le WiFi (Point d'accès)", + "Status": "@:wifistationinfo.Status", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "Ssid": "@:wifistationinfo.Ssid", + "Stations": "# Stations" + }, + "interfacenetworkinfo": { + "NetworkInterface": "Interface réseau ({iface})", + "Hostname": "@:firmwareinfo.Hostname", + "IpAddress": "Adresse IP", + "Netmask": "Masque de réseau", + "DefaultGateway": "Passerelle par défaut", + "Dns": "DNS {num}", + "MacAddress": "Addresse MAC" + }, + "interfaceapinfo": { + "NetworkInterface": "Interface réseau (Point d'accès)", + "IpAddress": "@:interfacenetworkinfo.IpAddress", + "MacAddress": "@:interfacenetworkinfo.MacAddress" + }, + "ntpinfo": { + "NtpInformation": "Informations sur le NTP", + "ConfigurationSummary": "Résumé de la configuration", + "Server": "Serveur", + "Timezone": "Fuseau horaire", + "TimezoneDescription": "Description du fuseau horaire", + "CurrentTime": "Heure actuelle", + "Status": "Statut", + "Synced": "synchronisée", + "NotSynced": "pas synchronisée", + "LocalTime": "Heure locale", + "Sunrise": "Lever du soleil", + "Sunset": "Coucher du soleil", + "NotAvailable": "Not Available", + "Mode": "Mode", + "Day": "Jour", + "Night": "Nuit" + }, + "mqttinfo": { + "MqttInformation": "MQTT Information", + "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", + "Status": "@:ntpinfo.Status", + "Enabled": "Activé", + "Disabled": "Désactivé", + "Server": "@:ntpinfo.Server", + "Port": "Port", + "Username": "Nom d'utilisateur", + "BaseTopic": "Sujet de base", + "PublishInterval": "Intervalle de publication", + "Seconds": "{sec} secondes", + "CleanSession": "CleanSession Flag", + "Retain": "Conserver", + "Tls": "TLS", + "RootCertifcateInfo": "Informations sur le certificat de l'autorité de certification racine", + "TlsCertLogin": "Connexion avec un certificat TLS", + "ClientCertifcateInfo": "Informations sur le certificat du client", + "HassSummary": "Résumé de la configuration de la découverte automatique du MQTT de Home Assistant", + "Expire": "Expiration", + "IndividualPanels": "Panneaux individuels", + "RuntimeSummary": "Résumé du temps de fonctionnement", + "ConnectionStatus": "État de la connexion", + "Connected": "connecté", + "Disconnected": "déconnecté" + }, + "vedirectinfo": { + "VedirectInformation" : "VE.Direct Info", + "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", + "Status": "@:ntpinfo.Status", + "Enabled": "@:mqttinfo.Enabled", + "Disabled": "@:mqttinfo.Disabled", + "VerboseLogging": "@:base.VerboseLogging", + "UpdatesOnly": "@:vedirectadmin.UpdatesOnly", + "UpdatesEnabled": "@:mqttinfo.Enabled", + "UpdatesDisabled": "@:mqttinfo.Disabled" + }, + "console": { + "Console": "Console", + "VirtualDebugConsole": "Console de débogage", + "EnableAutoScroll": "Activer le défilement automatique", + "ClearConsole": "Vider la console", + "CopyToClipboard": "Copier dans le presse-papiers" + }, + "inverterchannelinfo": { + "String": "Ligne {num}", + "Phase": "Phase {num}", + "General": "General" + }, + "invertertotalinfo": { + "InverterTotalYieldTotal": "Onduleurs rendement total", + "InverterTotalYieldDay": "Onduleurs rendement du jour", + "InverterTotalPower": "Onduleurs puissance de l'installation", + "MpptTotalYieldTotal": "MPPT rendement total", + "MpptTotalYieldDay": "MPPT rendement du jour", + "MpptTotalPower": "MPPT puissance de l'installation", + "BatterySoc": "State of charge", + "HomePower": "Grid Power", + "HuaweiPower": "Huawei AC Power" + }, + "inverterchannelproperty": { + "Power": "Puissance", + "Voltage": "Tension", + "Current": "Courant", + "Power DC": "Puissance continue", + "YieldDay": "Rendement du jour", + "YieldTotal": "Rendement total", + "Frequency": "Fréquence", + "Temperature": "Température", + "PowerFactor": "Facteur de puissance", + "ReactivePower": "Puissance réactive", + "Efficiency": "Efficacité", + "Irradiation": "Irradiation" + }, + "maintenancereboot": { + "DeviceReboot": "Redémarrage de l'appareil", + "PerformReboot": "Effectuer un redémarrage", + "Reboot": "Redémarrer !", + "Cancel": "@:base.Cancel", + "RebootOpenDTU": "Redémarrer OpenDTU", + "RebootQuestion": "Voulez-vous vraiment redémarrer l'appareil ?", + "RebootHint": "Astuce : Normalement, il n'est pas nécessaire de procéder à un redémarrage manuel. OpenDTU effectue automatiquement tout redémarrage nécessaire (par exemple, après une mise à jour du firmware). Les paramètres sont également adoptés sans redémarrage. Si vous devez redémarrer en raison d'une erreur, veuillez envisager de la signaler à l'adresse suivante Github." + }, + "dtuadmin": { + "DtuSettings": "Paramètres du DTU", + "DtuConfiguration": "Configuration du DTU", + "Serial": "Numéro de série", + "SerialHint": "L'onduleur et le DTU ont tous deux un numéro de série. Le numéro de série du DTU est généré de manière aléatoire lors du premier démarrage et ne doit normalement pas être modifié.", + "PollInterval": "Intervalle de sondage", + "VerboseLogging": "@:base.VerboseLogging", + "Seconds": "Secondes", + "NrfPaLevel": "NRF24 Niveau de puissance d'émission", + "CmtPaLevel": "CMT2300A Niveau de puissance d'émission", + "NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", + "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", + "CmtCountry": "CMT2300A Region/Country:", + "CmtCountryHint": "Each country has different frequency allocations.", + "country_0": "Europe ({min}MHz - {max}MHz)", + "country_1": "North America ({min}MHz - {max}MHz)", + "country_2": "Brazil ({min}MHz - {max}MHz)", + "CmtFrequency": "CMT2300A Frequency:", + "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", + "CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", + "Min": "Minimum ({db} dBm)", + "Low": "Bas ({db} dBm)", + "High": "Haut ({db} dBm)", + "Max": "Maximum ({db} dBm)" + }, + "securityadmin": { + "SecuritySettings": "Paramètres de sécurité", + "AdminPassword": "Mot de passe administrateur", + "Password": "Mot de passe", + "RepeatPassword": "Répéter le mot de passe", + "PasswordHint": "Astuce : Le mot de passe administrateur est utilisé pour accéder à cette interface web (utilisateur 'admin'), mais aussi pour se connecter à l'appareil en mode AP. Il doit comporter de 8 à 64 caractères.", + "Permissions": "Autorisations", + "ReadOnly": "Autoriser l'accès en lecture seule à l'interface web sans mot de passe" + }, + "ntpadmin": { + "NtpSettings": "Paramètres NTP", + "NtpConfiguration": "Configuration du protocole NTP", + "TimeServer": "Serveur horaire", + "TimeServerHint": "La valeur par défaut convient tant que OpenDTU a un accès direct à Internet.", + "Timezone": "Fuseau horaire", + "TimezoneConfig": "Configuration du fuseau horaire", + "LocationConfiguration": "Géolocalisation", + "Longitude": "Longitude", + "Latitude": "Latitude", + "SunSetType": "Sunset type", + "SunSetTypeHint": "Affects the day/night calculation. It can take up to one minute until the new type will be applied.", + "OFFICIAL": "Standard dawn (90.8°)", + "NAUTICAL": "Nautical dawn (102°)", + "CIVIL": "Civil dawn (96°)", + "ASTONOMICAL": "Astronomical dawn (108°)", + "ManualTimeSynchronization": "Synchronisation manuelle de l'heure", + "CurrentOpenDtuTime": "Heure actuelle de l'OpenDTU", + "CurrentLocalTime": "Heure locale actuelle", + "SynchronizeTime": "Synchroniser l'heure", + "SynchronizeTimeHint": "Astuce : Vous pouvez utiliser la synchronisation horaire manuelle pour définir l'heure actuelle d'OpenDTU si aucun serveur NTP n'est disponible. Mais attention, en cas de mise sous tension, l'heure est perdue. Notez également que la précision de l'heure sera faussée, car elle ne peut pas être resynchronisée régulièrement et le microcontrôleur ESP32 ne dispose pas d'une horloge temps réel." + }, + "networkadmin": { + "NetworkSettings": "Paramètres réseau", + "WifiConfiguration": "Configuration du réseau WiFi", + "WifiSsid": "SSID", + "WifiPassword": "Mot de passe", + "Hostname": "Nom d'hôte", + "HostnameHint": "Astuce : Le texte %06X sera remplacé par les 6 derniers chiffres de l'ESP ChipID au format hexadécimal.", + "EnableDhcp": "Activer le DHCP", + "StaticIpConfiguration": "Configuration de l'IP statique", + "IpAddress": "Adresse IP", + "Netmask": "Masque de réseau", + "DefaultGateway": "Passerelle par défaut", + "Dns": "Serveur DNS {num}", + "AdminAp": "Configuration du réseau WiFi (Point d'accès)", + "ApTimeout": "Délai d'attente du point d'accès", + "ApTimeoutHint": "Durée pendant laquelle le point d'accès reste ouvert. Une valeur de 0 signifie infini.", + "Minutes": "minutes", + "EnableMdns": "Activer mDNS", + "MdnsSettings": "mDNS Settings" + }, + "mqttadmin": { + "MqttSettings": "Paramètres MQTT", + "MqttConfiguration": "Configuration du système MQTT", + "EnableMqtt": "Activer le MQTT", + "VerboseLogging": "@:base.VerboseLogging", + "EnableHass": "Activer la découverte automatique du MQTT de Home Assistant", + "MqttBrokerParameter": "Paramètre du Broker MQTT", + "Hostname": "Nom d'hôte", + "HostnameHint": "Nom d'hôte ou adresse IP", + "Port": "Port", + "Username": "Nom d'utilisateur", + "UsernameHint": "Nom d'utilisateur, laisser vide pour une connexion anonyme", + "Password": "Mot de passe:", + "PasswordHint": "Mot de passe, laissez vide pour une connexion anonyme", + "BaseTopic": "Sujet de base", + "BaseTopicHint": "Sujet de base, qui sera ajouté en préambule à tous les sujets publiés (par exemple, inverter/).", + "PublishInterval": "Intervalle de publication", + "Seconds": "secondes", + "CleanSession": "Enable CleanSession flag", + "EnableRetain": "Activation du maintien", + "EnableTls": "Activer le TLS", + "RootCa": "Certificat CA-Root (par défaut Letsencrypt)", + "TlsCertLoginEnable": "Activer la connexion par certificat TLS", + "ClientCert": "Certificat client TLS:", + "ClientKey": "Clé client TLS:", + "LwtParameters": "Paramètres LWT", + "LwtTopic": "Sujet LWT", + "LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base", + "LwtOnline": "Message en ligne de LWT", + "LwtOnlineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera en ligne", + "LwtOffline": "Message hors ligne de LWT", + "LwtOfflineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera hors ligne", + "LwtQos": "QoS (Quality of Service):", + "QOS0": "0 (Au maximum une fois)", + "QOS1": "1 (Au moins une fois)", + "QOS2": "2 (Exactement une fois)", + "HassParameters": "Paramètres de découverte automatique MQTT de Home Assistant", + "HassPrefixTopic": "Préfixe du sujet", + "HassPrefixTopicHint": "Le préfixe de découverte du sujet", + "HassRetain": "Activer du maintien", + "HassExpire": "Activer l'expiration", + "HassIndividual": "Panneaux individuels" + }, + "vedirectadmin": { + "VedirectSettings": "VE.Direct Settings", + "VedirectConfiguration": "VE.Direct Configuration", + "EnableVedirect": "Enable VE.Direct", + "VedirectParameter": "VE.Direct Parameter", + "VerboseLogging": "@:base.VerboseLogging", + "UpdatesOnly": "Publish values to MQTT only when they change" + }, + "batteryadmin": { + "BatterySettings": "Battery Settings", + "BatteryConfiguration": "General Interface Settings", + "EnableBattery": "Enable Interface", + "VerboseLogging": "@:base.VerboseLogging", + "Provider": "Data Provider", + "ProviderPylontechCan": "Pylontech using CAN bus", + "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", + "ProviderMqtt": "Battery data from MQTT broker", + "ProviderVictron": "Victron SmartShunt using VE.Direct interface", + "MqttConfiguration": "MQTT Settings", + "MqttSocTopic": "SoC value topic", + "MqttVoltageTopic": "Voltage value topic", + "JkBmsConfiguration": "JK BMS Settings", + "JkBmsInterface": "Interface Type", + "JkBmsInterfaceUart": "TTL-UART on MCU", + "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", + "PollingInterval": "Polling Interval", + "Seconds": "@:base.Seconds" + }, + "inverteradmin": { + "InverterSettings": "Paramètres des onduleurs", + "AddInverter": "Ajouter un nouvel onduleur", + "Serial": "Numéro de série", + "Name": "Nom", + "Add": "Ajouter", + "AddHint": " Astuce : Vous pouvez définir des paramètres supplémentaires après avoir créé l'onduleur. Utilisez l'icône du stylo dans la liste des onduleurs.", + "InverterList": "Liste des onduleurs", + "Status": "État", + "Send": "Envoyer", + "Receive": "Recevoir", + "StatusHint": "Astuce : L'onduleur est alimenté par son entrée courant continu. S'il n'y a pas de soleil, l'onduleur est éteint, mais les requêtes peuvent toujours être envoyées.", + "Type": "Type", + "Action": "Action", + "SaveOrder": "Save order", + "DeleteInverter": "Supprimer l'onduleur", + "EditInverter": "Modifier l'onduleur", + "General": "Général", + "String": "Ligne", + "Advanced": "Advanced", + "InverterSerial": "Numéro de série de l'onduleur", + "InverterName": "Nom de l'onduleur :", + "InverterNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour votre onduleur.", + "InverterStatus": "Recevoir / Envoyer", + "PollEnable": "Interroger les données de l'onduleur", + "PollEnableNight": "Interroger les données de l'onduleur la nuit", + "CommandEnable": "Envoyer des commandes", + "CommandEnableNight": "Envoyer des commandes la nuit", + "StringName": "Nom de la ligne {num}:", + "StringNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour le port respectif de votre onduleur.", + "StringMaxPower": "Puissance maximale de la ligne {num}:", + "StringMaxPowerHint": "Entrez la puissance maximale des panneaux solaires connectés.", + "StringYtOffset": "Décalage du rendement total de la ligne {num} :", + "StringYtOffsetHint": "Ce décalage est appliqué à la valeur de rendement total lue sur le variateur. Il peut être utilisé pour mettre le rendement total du variateur à zéro si un variateur usagé est utilisé.", + "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", + "ReachableThreshold": "Reachable Threshold:", + "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", + "ZeroRuntime": "Zero runtime data", + "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", + "ZeroDay": "Zero daily yield at midnight", + "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", + "ClearEventlog": "Clear Eventlog at midnight", + "Cancel": "@:base.Cancel", + "Save": "@:base.Save", + "DeleteMsg": "Êtes-vous sûr de vouloir supprimer l'onduleur \"{name}\" avec le numéro de série \"{serial}\" ?", + "Delete": "Supprimer", + "YieldDayCorrection": "Yield Day Correction", + "YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight" + }, + "configadmin": { + "ConfigManagement": "Gestion de la configuration", + "BackupHeader": "Sauvegarder le fichier de configuration", + "BackupConfig": "Fichier de configuration", + "Backup": "Sauvegarder", + "Restore": "Restaurer", + "NoFileSelected": "Aucun fichier sélectionné", + "RestoreHeader": "Restaurer le fichier de configuration", + "Back": "Retour", + "UploadSuccess": "Succès du téléversement", + "RestoreHint": "Note : Cette opération remplace le fichier de configuration par la configuration restaurée et redémarre OpenDTU pour appliquer tous les paramètres.", + "ResetHeader": "Effectuer une réinitialisation d'usine", + "FactoryResetButton": "Restaurer les paramètres d'usine", + "ResetHint": "Note : Cliquez sur \"Restaurer les paramètres d'usine\" pour restaurer et initialiser les paramètres d'usine par défaut et redémarrer.", + "FactoryReset": "Remise à zéro", + "ResetMsg": "Êtes-vous sûr de vouloir supprimer la configuration actuelle et réinitialiser tous les paramètres à leurs valeurs par défaut ?", + "ResetConfirm": "Remise à zéro !", + "Cancel": "@:base.Cancel" + }, + "powerlimiteradmin": { + "PowerLimiterSettings": "Dynamic Power Limiter Settings", + "ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.", + "ConfigHints": "Configuration Notes", + "ConfigHintRequirement": "Required", + "ConfigHintOptional": "Optional", + "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", + "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", + "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", + "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", + "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", + "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", + "General": "General", + "Enable": "Enable", + "VerboseLogging": "@:base.VerboseLogging", + "SolarPassthrough": "Solar-Passthrough", + "EnableSolarPassthrough": "Enable Solar-Passthrough", + "SolarPassthroughLosses": "(Full) Solar-Passthrough Losses", + "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", + "BatteryDischargeAtNight": "Use battery at night even if only partially charged", + "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", + "InverterSettings": "Inverter", + "Inverter": "Target Inverter", + "SelectInverter": "Select an inverter...", + "InverterChannelId": "Input used for voltage measurements", + "TargetPowerConsumption": "Target Grid Consumption", + "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", + "TargetPowerConsumptionHysteresis": "Hysteresis", + "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", + "LowerPowerLimit": "Minimum Power Limit", + "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", + "BaseLoadLimit": "Base Load", + "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", + "UpperPowerLimit": "Maximum Power Limit", + "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", + "SocThresholds": "Battery State of Charge (SoC) Thresholds", + "IgnoreSoc": "Ignore Battery SoC", + "StartThreshold": "Start Threshold for Battery Discharging", + "StopThreshold": "Stop Threshold for Battery Discharging", + "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", + "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", + "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", + "VoltageLoadCorrectionFactor": "Load correction factor", + "BatterySocInfo": "Hint: The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", + "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", + "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", + "InverterIsSolarPowered": "Inverter is powered by solar modules", + "VoltageThresholds": "Battery Voltage Thresholds", + "VoltageLoadCorrectionInfo": "Hint: When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)." + }, + "login": { + "Login": "Connexion", + "SystemLogin": "Connexion au système", + "Username": "Nom d'utilisateur", + "UsernameRequired": "Le nom d'utilisateur est requis", + "Password": "Mot de passe", + "PasswordRequired": "Le mot de passe est requis", + "LoginButton": "Connexion" + }, + "firmwareupgrade": { + "FirmwareUpgrade": "Mise à jour du firmware", + "Loading": "@:base.Loading", + "OtaError": "Erreur OTA", + "Back": "Retour", + "Retry": "Réessayer", + "OtaStatus": "Statut OTA", + "OtaSuccess": "Le téléchargement du firmware a réussi. L'appareil a été redémarré automatiquement. Lorsque l'appareil est à nouveau accessible, l'interface est automatiquement rechargée.", + "FirmwareUpload": "Téléversement du firmware", + "UploadProgress": "Progression du téléversement" + }, + "about": { + "AboutOpendtu": "À propos d'OpenDTU-OnBattery", + "Documentation": "Documentation", + "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", + "ProjectOrigin": "Origine du projet", + "ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion (Mikrocontroller.net).", + "ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (GNU General Public License version 2).", + "ProjectOriginBody3": "Le logiciel a été développé au mieux de nos connaissances et de nos convictions. Néanmoins, aucune responsabilité ne peut être acceptée en cas de dysfonctionnement ou de perte de garantie de l'onduleur.", + "ProjectOriginBody4": "OpenDTU est disponible gratuitement. Si vous avez payé pour le logiciel, vous avez probablement été arnaqué.", + "NewsUpdates": "Actualités et mises à jour", + "NewsUpdatesBody": "Les nouvelles mises à jour peuvent être trouvées sur Github.", + "ErrorReporting": "Rapport d'erreurs", + "ErrorReportingBody": "Veuillez signaler les problèmes en utilisant la fonction fournie par Github.", + "Discussion": "Discussion", + "DiscussionBody": "Discutez avec nous sur Discord ou sur Github." + }, + "hints": { + "RadioProblem": "Impossible de se connecter à un module radio configuré.. Veuillez vérifier le câblage.", + "TimeSync": "L'horloge n'a pas encore été synchronisée. Sans une horloge correctement réglée, aucune demande n'est adressée à l'onduleur. Ceci est normal peu de temps après le démarrage. Cependant, après un temps de fonctionnement plus long (>1 minute), cela indique que le serveur NTP n'est pas accessible.", + "TimeSyncLink": "Veuillez vérifier vos paramètres horaires.", + "DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.", + "DefaultPasswordLink": "Merci de changer le mot de passe." + }, + "deviceadmin": { + "DeviceManager": "Gestionnaire de périphériques", + "ParseError": "Erreur d'analyse dans 'pin_mapping.json': {error}", + "PinAssignment": "Paramètres de connexion", + "SelectedProfile": "Profil sélectionné", + "DefaultProfile": "(Réglages par défaut)", + "ProfileHint": "Votre appareil peut cesser de répondre si vous sélectionnez un profil incompatible. Dans ce cas, vous devez effectuer une suppression via l'interface série.", + "Display": "Affichage", + "PowerSafe": "Economiseur d'énergie", + "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", + "Screensaver": "OLED Anti burn-in", + "ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)", + "DiagramMode": "Diagram mode:", + "off": "Off", + "small": "Small", + "fullscreen": "Fullscreen", + "DiagramDuration": "Diagram duration:", + "DiagramDurationHint": "The time period which is shown in the diagram.", + "Seconds": "Seconds", + "Contrast": "Contraste ({contrast}):", + "Rotation": "Rotation:", + "rot0": "Pas de rotation", + "rot90": "Rotation de 90 degrés", + "rot180": "Rotation de 180 degrés", + "rot270": "Rotation de 270 degrés", + "DisplayLanguage": "Langue d'affichage", + "en": "Anglais", + "de": "Allemand", + "fr": "Français", + "Leds": "LEDs", + "EqualBrightness": "Même luminosité:", + "LedBrightness": "LED {led} luminosité ({brightness}):" + }, + "pininfo": { + "PinOverview": "Vue d'ensemble des connexions", + "Category": "Catégorie", + "Name": "Nom", + "ValueSelected": "Sélectionné", + "ValueActive": "Activé" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" + }, + "huawei": { + "DataAge": "Data Age: ", + "Seconds": " {val} seconds", + "Input": "Input", + "Output": "Output", + "Property": "Property", + "Value": "Value", + "Unit": "Unit", + "input_voltage": "Input voltage", + "input_current": "Input current", + "input_power": "Input power", + "input_temp": "Input temperature", + "efficiency": "Efficiency", + "output_voltage": "Output voltage", + "output_current": "Output current", + "max_output_current": "Maximum output current", + "output_power": "Output power", + "output_temp": "Output temperature", + "ShowSetLimit": "Show / Set Huawei Limit", + "LimitSettings": "Limit Settings", + "SetOffline": "Set limit, CAN bus not connected", + "SetOnline": "Set limit, CAN bus connected", + "LimitHint": "Hint: CAN bus not connected voltage limit is 48V-58.5V.", + "Close": "close", + "SetVoltageLimit": "Voltage limit:", + "SetCurrentLimit": "Current limit:", + "CurrentLimit": "Current limit:" + }, + "acchargeradmin": { + "ChargerSettings": "AC Charger Settings", + "Configuration": "AC Charger Configuration", + "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "VerboseLogging": "@:base.VerboseLogging", + "CanControllerFrequency": "CAN controller quarz frequency", + "EnableAutoPower": "Automatic power control", + "EnableBatterySoCLimits": "Use SoC data of a connected battery", + "Limits": "Limits", + "BatterySoCLimits": "Battery SoC Limits", + "VoltageLimit": "Charge Voltage limit", + "enableVoltageLimit": "Re-enable voltage limit", + "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", + "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", + "upperPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", + "lowerPowerLimit": "Minimum output power", + "upperPowerLimit": "Maximum output power", + "StopBatterySoCThreshold": "Stop charging at SoC", + "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS", + "targetPowerConsumption": "Target power consumption", + "targetPowerConsumptionHint": "Postitive values use grid power to charge the battery. Negative values result in early shutdown" + }, + "battery": { + "battery": "Battery", + "FwVersion": "Firmware Version", + "HwVersion": "Hardware Version", + "DataAge": "Data Age: ", + "Seconds": " {val} seconds", + "status": "Status", + "Property": "Property", + "yes": "@:base.Yes", + "no": "@:base.No", + "Value": "Value", + "Unit": "Unit", + "SoC": "State of Charge", + "stateOfHealth": "State of Health", + "voltage": "Voltage", + "current": "Current", + "power": "Power", + "temperature": "Temperature", + "bmsTemp": "BMS temperature", + "chargeVoltage": "Requested charge voltage", + "chargeCurrentLimitation": "Charge current limit", + "dischargeCurrentLimitation": "Discharge current limit", + "chargeEnabled": "Charging possible", + "dischargeEnabled": "Discharging possible", + "chargeImmediately": "Immediate charging requested", + "cells": "Cells", + "batOneTemp": "Battery temperature 1", + "batTwoTemp": "Battery temperature 2", + "cellMinVoltage": "Minimum cell voltage", + "cellAvgVoltage": "Average cell voltage", + "cellMaxVoltage": "Maximum cell voltage", + "cellDiffVoltage": "Cell voltage difference", + "balancingActive": "Balancing active", + "issues": "Issues", + "noIssues": "No Issues", + "issueName": "Name", + "issueType": "Type", + "alarm": "Alarm", + "warning": "Warning", + "JkBmsIssueLowCapacity": "Low Capacity", + "JkBmsIssueBmsOvertemperature": "BMS overtemperature", + "JkBmsIssueChargingOvervoltage": "Overvoltage (sum of all cells)", + "JkBmsIssueDischargeUndervoltage": "Undervoltage (sum of all cells)", + "JkBmsIssueBatteryOvertemperature": "Battery overtemperature", + "JkBmsIssueChargingOvercurrent": "Overcurrent (Charging)", + "JkBmsIssueDischargeOvercurrent": "Overcurrent (Discharging)", + "JkBmsIssueCellVoltageDifference": "Cell voltage difference too high", + "JkBmsIssueBatteryBoxOvertemperature": "Battery (box?) overtemperature", + "JkBmsIssueBatteryUndertemperature": "Battery undertemperature", + "JkBmsIssueCellOvervoltage": "Overvoltage (single cell)", + "JkBmsIssueCellUndervoltage": "Undervoltage (single cell)", + "JkBmsIssueAProtect": "AProtect (meaning?)", + "JkBmsIssueBProtect": "BProtect (meaning?)", + "highCurrentDischarge": "High current (discharge)", + "overCurrentDischarge": "Overcurrent (discharge)", + "highCurrentCharge": "High current (charge)", + "overCurrentCharge": "Overcurrent (charge)", + "lowTemperature": "Low temperature", + "underTemperature": "Undertemperature", + "highTemperature": "High temperature", + "overTemperature": "Overtemperature", + "lowVoltage": "Low voltage", + "lowSOC": "Low state of charge", + "underVoltage": "Undervoltage", + "highVoltage": "High voltage", + "overVoltage": "Overvoltage", + "bmsInternal": "BMS internal", + "chargeCycles": "Charge cycles", + "chargedEnergy": "Charged energy", + "dischargedEnergy": "Discharged energy", + "instantaneousPower": "Instantaneous Power", + "consumedAmpHours": "Consumed Amp Hours", + "midpointVoltage": "Midpoint Voltage", + "midpointDeviation": "Midpoint Deviation", + "lastFullCharge": "Last full Charge" + } +} diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 80625d8e1..b06f0236b 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -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; +} diff --git a/webapp/src/types/BatteryDataStatus.ts b/webapp/src/types/BatteryDataStatus.ts index 0143725d8..e905f7372 100644 --- a/webapp/src/types/BatteryDataStatus.ts +++ b/webapp/src/types/BatteryDataStatus.ts @@ -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[]; } \ No newline at end of file diff --git a/webapp/src/types/HuaweiDataStatus.ts b/webapp/src/types/HuaweiDataStatus.ts index ce7b8aabd..b349c207f 100644 --- a/webapp/src/types/HuaweiDataStatus.ts +++ b/webapp/src/types/HuaweiDataStatus.ts @@ -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; } \ No newline at end of file diff --git a/webapp/src/types/HuaweiLimitConfig.ts b/webapp/src/types/HuaweiLimitConfig.ts index f839d83c3..3ce15dabb 100644 --- a/webapp/src/types/HuaweiLimitConfig.ts +++ b/webapp/src/types/HuaweiLimitConfig.ts @@ -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; } \ No newline at end of file