diff --git a/.gitignore b/.gitignore index 506d73ff0..8b14dd998 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .vscode/launch.json .vscode/ipch .vscode/settings.json -platformio-device-monitor*.log \ No newline at end of file +platformio-device-monitor*.log +platformio_override.ini diff --git a/README.md b/README.md index 621122d12..049f7db40 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,15 @@ Sends text raw data as difined in VE.Direct spec. * Hoymiles HM-1000 * Hoymiles HM-1200 * Hoymiles HM-1500 -* TSUN TSOL-M350 (Maybe depending on firmware on the inverter) -* TSUN TSOL-M800 (Maybe depending on firmware on the inverter) -* TSUN TSOL-M1600 (Maybe depending on firmware on the inverter) +* TSUN TSOL-M350 (Maybe depending on firmware/serial number on the inverter) +* TSUN TSOL-M800 (Maybe depending on firmware/serial number on the inverter) +* TSUN TSOL-M1600 (Maybe depending on firmware/serial number on the inverter) + +**TSUN compatibility remark:** +Compatibility with OpenDTU seems to be related to serial numbers. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not. +Firmware version seems to play not a significant role and cannot be read from the stickers. For completeness, the following firmware version have been reported to work with OpenDTU: +* v1.0.10 TSOL-M800 (DE) +* v1.0.12 TSOL-M1600 ## Features for end users * Read live data from inverter @@ -80,6 +86,7 @@ Sends text raw data as difined in VE.Direct spec. * Ve.Direct interface (via web-interface, REST-api, or MQTT) * Ethernet support * Prometheus API endpoint (/api/prometheus/metrics) +* English and german web interface ## Features for developers * The microcontroller part @@ -132,7 +139,7 @@ Use a power suppy with 5 V and 1 A. The USB cable connected to your PC/Notebook ### Change pin assignment Its possible to change all the pins of the NRF24L01+ module. -This can be achieved by editing the 'platformio.ini' file and add/change one or more of the following lines to the 'build_flags' parameter: +This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter: ``` -DHOYMILES_PIN_MISO=19 -DHOYMILES_PIN_MOSI=23 @@ -143,6 +150,7 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or -DVICTRON_PIN_TX=21 -DVICTRON_PIN_RX=22 ``` +It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy. ## Flashing and starting up ### with Visual Studio Code @@ -150,8 +158,8 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or * In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) * Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/) * Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory. -* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" file) -* Adjust the COM port in the file "platformio.ini" for your USB-serial-converter. It occurs twice: +* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" and "platformio_override.ini" file) +* Adjust the COM port in the file "platformio_override.ini" for your USB-to-serial-converter. It occurs twice: * upload_port * monitor_port * Select the arrow button in the blue bottom status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically. @@ -162,7 +170,7 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or ### on the commandline with PlatformIO Core * Install [PlatformIO Core](https://platformio.org/install/cli) * Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur) -* Adjust the COM port in the file "platformio.ini". It occurs twice: +* Adjust the COM port in the file "platformio_override.ini". It occurs twice: * upload_port * monitor_port * build: `platformio run -e generic` diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 46a2b6738..2c6206e39 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -67,7 +67,7 @@ cmd topics are used to set values. Status topics are updated from values set in | [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) | | [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | | [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. | Watt (W) | -| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | -| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. | Watt (W) | +| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. The value must be published non-retained, otherwise it will be ignored! | % | +| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. The value must be published non-retained, otherwise it will be ignored! | Watt (W) | | [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 | | [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 | diff --git a/include/MessageOutput.h b/include/MessageOutput.h new file mode 100644 index 000000000..a47b0278a --- /dev/null +++ b/include/MessageOutput.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +#define BUFFER_SIZE 500 + +class MessageOutputClass : public Print { +public: + MessageOutputClass(); + void loop(); + size_t write(uint8_t c); + void register_ws_output(AsyncWebSocket* output); + +private: + AsyncWebSocket* _ws = NULL; + char _buffer[BUFFER_SIZE]; + uint16_t _buff_pos = 0; + uint32_t _lastSend = 0; + bool _forceSend = false; + + SemaphoreHandle_t _lock; +}; + +extern MessageOutputClass MessageOutput; \ No newline at end of file diff --git a/include/MqttHandleDtu.h b/include/MqttHandleDtu.h new file mode 100644 index 000000000..7a6589249 --- /dev/null +++ b/include/MqttHandleDtu.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class MqttHandleDtuClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastPublish; +}; + +extern MqttHandleDtuClass MqttHandleDtu; \ No newline at end of file diff --git a/include/MqttHassPublishing.h b/include/MqttHandleHass.h similarity index 95% rename from include/MqttHassPublishing.h rename to include/MqttHandleHass.h index ff40dc5ea..33d9df826 100644 --- a/include/MqttHassPublishing.h +++ b/include/MqttHandleHass.h @@ -1,11 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "Configuration.h" -#include #include #include -#include // mqtt discovery device classes enum { @@ -51,7 +48,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { }; #define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass_t)) -class MqttHassPublishingClass { +class MqttHandleHassClass { public: void init(); void loop(); @@ -59,6 +56,7 @@ class MqttHassPublishingClass { void forceUpdate(); private: + void publish(const String& subtopic, const String& payload); void publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, int16_t min = 1, int16_t max = 100); @@ -69,4 +67,4 @@ class MqttHassPublishingClass { bool _updateForced = false; }; -extern MqttHassPublishingClass MqttHassPublishing; \ No newline at end of file +extern MqttHandleHassClass MqttHandleHass; \ No newline at end of file diff --git a/include/MqttPublishing.h b/include/MqttHandleInverter.h similarity index 72% rename from include/MqttPublishing.h rename to include/MqttHandleInverter.h index 5667b40cb..a4e49ce41 100644 --- a/include/MqttPublishing.h +++ b/include/MqttHandleInverter.h @@ -2,11 +2,10 @@ #pragma once #include "Configuration.h" -#include #include -#include +#include -class MqttPublishingClass { +class MqttHandleInverterClass { public: void init(); void loop(); @@ -15,6 +14,7 @@ class MqttPublishingClass { private: void publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); uint32_t _lastPublishStats[INV_MAX_COUNT]; uint32_t _lastPublish; @@ -37,4 +37,4 @@ class MqttPublishingClass { }; }; -extern MqttPublishingClass MqttPublishing; \ No newline at end of file +extern MqttHandleInverterClass MqttHandleInverter; \ No newline at end of file diff --git a/include/MqttVedirectPublishing.h b/include/MqttHandleVedirect.h similarity index 80% rename from include/MqttVedirectPublishing.h rename to include/MqttHandleVedirect.h index 90dba42ae..5c752dd55 100644 --- a/include/MqttVedirectPublishing.h +++ b/include/MqttHandleVedirect.h @@ -14,7 +14,7 @@ #define VICTRON_PIN_TX 21 #endif -class MqttVedirectPublishingClass { +class MqttHandleVedirectClass { public: void init(); void loop(); @@ -23,4 +23,4 @@ class MqttVedirectPublishingClass { uint32_t _lastPublish; }; -extern MqttVedirectPublishingClass MqttVedirectPublishing; \ No newline at end of file +extern MqttHandleVedirectClass MqttHandleVedirect; \ No newline at end of file diff --git a/include/MqttSettings.h b/include/MqttSettings.h index afa3283c8..e68c230f7 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -2,10 +2,9 @@ #pragma once #include "NetworkSettings.h" -#include +#include #include #include -#include class MqttSettingsClass { public: @@ -14,7 +13,10 @@ class MqttSettingsClass { void performReconnect(); bool getConnected(); void publish(const String& subtopic, const String& payload); - void publishHass(const String& subtopic, const String& payload); + void publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos = 0); + + void subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); + void unsubscribe(const String& topic); String getPrefix(); @@ -34,6 +36,7 @@ class MqttSettingsClass { String clientId; String willTopic; Ticker mqttReconnectTimer; + MqttSubscribeParser _mqttSubscribeParser; }; extern MqttSettingsClass MqttSettings; \ No newline at end of file diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index 1ad3c6a43..507167b0b 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -3,7 +3,6 @@ #include #include -#include #include enum class network_mode { diff --git a/include/NtpSettings.h b/include/NtpSettings.h index 08dc06e40..188876e81 100644 --- a/include/NtpSettings.h +++ b/include/NtpSettings.h @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include - class NtpSettingsClass { public: NtpSettingsClass(); diff --git a/include/WebApi.h b/include/WebApi.h index 298dec47b..457d0ba29 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -17,6 +17,7 @@ #include "WebApi_security.h" #include "WebApi_sysstatus.h" #include "WebApi_webapp.h" +#include "WebApi_ws_console.h" #include "WebApi_ws_live.h" #include "WebApi_ws_vedirect_live.h" #include "WebApi_vedirect.h" @@ -51,6 +52,7 @@ class WebApiClass { WebApiSecurityClass _webApiSecurity; WebApiSysstatusClass _webApiSysstatus; WebApiWebappClass _webApiWebapp; + WebApiWsConsoleClass _webApiWsConsole; WebApiWsLiveClass _webApiWsLive; WebApiWsVedirectLiveClass _webApiWsVedirectLive; WebApiVedirectClass _webApiVedirect; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h new file mode 100644 index 000000000..0e4099a0e --- /dev/null +++ b/include/WebApi_errors.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +enum WebApiError { + GenericBase = 1000, + GenericSuccess, + GenericNoValueFound, + GenericDataTooLarge, + GenericParseError, + GenericValueMissing, + + DtuBase = 2000, + DtuSerialZero, + DtuPollZero, + DtuInvalidPowerLevel, + + ConfigBase = 3000, + ConfigNotDeleted, + ConfigSuccess, + + InverterBase = 4000, + InverterSerialZero, + InverterNameLength, + InverterCount, + InverterAdded, + InverterInvalidId, + InverterInvalidMaxChannel, + InverterChanged, + InverterDeleted, + + LimitBase = 5000, + LimitSerialZero, + LimitInvalidLimit, + LimitInvalidType, + LimitInvalidInverter, + + MaintenanceBase = 6000, + MaintenanceRebootTriggered, + MaintenanceRebootCancled, + + MqttBase = 7000, + MqttHostnameLength, + MqttUsernameLength, + MqttPasswordLength, + MqttTopicLength, + MqttTopicCharacter, + MqttTopicTrailingSlash, + MqttPort, + MqttCertificateLength, + MqttLwtTopicLength, + MqttLwtTopicCharacter, + MqttLwtOnlineLength, + MqttLwtOfflineLength, + MqttPublishInterval, + MqttHassTopicLength, + MqttHassTopicCharacter, + + NetworkBase = 8000, + NetworkIpInvalid, + NetworkNetmaskInvalid, + NetworkGatewayInvalid, + NetworkDns1Invalid, + NetworkDns2Invalid, + + NtpBase = 9000, + NtpServerLength, + NtpTimezoneLength, + NtpTimezoneDescriptionLength, + NtpYearInvalid, + NtpMonthInvalid, + NtpDayInvalid, + NtpHourInvalid, + NtpMinuteInvalid, + NtpSecondInvalid, + NtpTimeUpdated, + + SecurityBase = 10000, + SecurityPasswordLength, + SecurityAuthSuccess, + + PowerBase = 11000, + PowerSerialZero, + PowerInvalidInverter, +}; \ No newline at end of file diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 87a406e30..60e8e1026 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -1,7 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "Hoymiles.h" #include +#include class WebApiPrometheusClass { public: diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h new file mode 100644 index 000000000..81df81e90 --- /dev/null +++ b/include/WebApi_ws_console.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiWsConsoleClass { +public: + WebApiWsConsoleClass(); + void init(AsyncWebServer* server); + void loop(); + +private: + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + + AsyncWebServer* _server; + AsyncWebSocket _ws; + + uint32_t _lastWsCleanup = 0; +}; \ No newline at end of file diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 7f21f8034..5da186408 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "ArduinoJson.h" +#include #include #include diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 2b04ddc0d..07b26837f 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "Hoymiles.h" #include "inverters/HM_1CH.h" #include "inverters/HM_2CH.h" @@ -31,8 +35,8 @@ void HoymilesClass::loop() if (_radio->isIdle()) { std::shared_ptr iv = getInverterByPos(inverterPos); if (iv != nullptr) { - Serial.print(F("Fetch inverter: ")); - Serial.println(iv->serial(), HEX); + _messageOutput->print(F("Fetch inverter: ")); + _messageOutput->println(iv->serial(), HEX); iv->sendStatsRequest(_radio.get()); @@ -44,25 +48,25 @@ void HoymilesClass::loop() if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - Serial.println("Request SystemConfigPara"); + _messageOutput->println("Request SystemConfigPara"); iv->sendSystemConfigParaRequest(_radio.get()); } // Set limit if required if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - Serial.println(F("Resend ActivePowerControl")); + _messageOutput->println(F("Resend ActivePowerControl")); iv->resendActivePowerControlRequest(_radio.get()); } // Set power status if required if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - Serial.println(F("Resend PowerCommand")); + _messageOutput->println(F("Resend PowerCommand")); iv->resendPowerControlRequest(_radio.get()); } // Fetch dev info (but first fetch stats) if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { - Serial.println(F("Request device info")); + _messageOutput->println(F("Request device info")); iv->sendDevInfoRequest(_radio.get()); } } @@ -171,4 +175,14 @@ uint32_t HoymilesClass::PollInterval() void HoymilesClass::setPollInterval(uint32_t interval) { _pollInterval = interval; +} + +void HoymilesClass::setMessageOutput(Print* output) +{ + _messageOutput = output; +} + +Print* HoymilesClass::getMessageOutput() +{ + return _messageOutput; } \ No newline at end of file diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 033eabe03..27e3d0338 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "HoymilesRadio.h" #include "inverters/InverterAbstract.h" #include "types.h" +#include #include #include #include @@ -15,6 +17,9 @@ class HoymilesClass { void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); void loop(); + void setMessageOutput(Print* output); + Print* getMessageOutput(); + std::shared_ptr addInverter(const char* name, uint64_t serial); std::shared_ptr getInverterByPos(uint8_t pos); std::shared_ptr getInverterBySerial(uint64_t serial); @@ -35,6 +40,8 @@ class HoymilesClass { uint32_t _pollInterval = 0; uint32_t _lastPoll = 0; + + Print* _messageOutput = &Serial; }; extern HoymilesClass Hoymiles; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 9272b278e..7a463b6bc 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "HoymilesRadio.h" #include "Hoymiles.h" #include "commands/RequestFrameCommand.h" @@ -21,9 +25,9 @@ void HoymilesRadio::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pin _radio->setRetries(0, 0); _radio->maskIRQ(true, true, false); // enable only receiving interrupts if (_radio->isChipConnected()) { - Serial.println(F("Connection successfull")); + Hoymiles.getMessageOutput()->println(F("Connection successfull")); } else { - Serial.println(F("Connection error!!")); + Hoymiles.getMessageOutput()->println(F("Connection error!!")); } attachInterrupt(digitalPinToInterrupt(pinIRQ), std::bind(&HoymilesRadio::handleIntr, this), FALLING); @@ -40,7 +44,7 @@ void HoymilesRadio::loop() } if (_packetReceived) { - Serial.println(F("Interrupt received")); + Hoymiles.getMessageOutput()->println(F("Interrupt received")); while (_radio->available()) { if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { fragment_t f; @@ -52,7 +56,7 @@ void HoymilesRadio::loop() _radio->read(f.fragment, f.len); _rxBuffer.push(f); } else { - Serial.println(F("Buffer full")); + Hoymiles.getMessageOutput()->println(F("Buffer full")); _radio->flush_rx(); } } @@ -72,11 +76,11 @@ void HoymilesRadio::loop() dumpBuf(buf, f.fragment, f.len); inv->addRxFragment(f.fragment, f.len); } else { - Serial.println(F("Inverter Not found!")); + Hoymiles.getMessageOutput()->println(F("Inverter Not found!")); } } else { - Serial.println(F("Frame kaputt")); + Hoymiles.getMessageOutput()->println(F("Frame kaputt")); } // Remove paket from buffer even it was corrupted @@ -85,46 +89,46 @@ void HoymilesRadio::loop() } if (_busyFlag && _rxTimeout.occured()) { - Serial.println(F("RX Period End")); + Hoymiles.getMessageOutput()->println(F("RX Period End")); std::shared_ptr inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress()); if (nullptr != inv) { CommandAbstract* cmd = _commandQueue.front().get(); uint8_t verifyResult = inv->verifyAllFragments(cmd); if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { - Serial.println(F("Nothing received, resend whole request")); + Hoymiles.getMessageOutput()->println(F("Nothing received, resend whole request")); sendLastPacketAgain(); } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { - Serial.println(F("Nothing received, resend count exeeded")); + Hoymiles.getMessageOutput()->println(F("Nothing received, resend count exeeded")); _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { - Serial.println(F("Retransmit timeout")); + Hoymiles.getMessageOutput()->println(F("Retransmit timeout")); _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { - Serial.println(F("Packet handling error")); + Hoymiles.getMessageOutput()->println(F("Packet handling error")); _commandQueue.pop(); _busyFlag = false; } else if (verifyResult > 0) { // Perform Retransmit - Serial.print(F("Request retransmit: ")); - Serial.println(verifyResult); + Hoymiles.getMessageOutput()->print(F("Request retransmit: ")); + Hoymiles.getMessageOutput()->println(verifyResult); sendRetransmitPacket(verifyResult); } else { // Successfull received all packages - Serial.println(F("Success")); + Hoymiles.getMessageOutput()->println(F("Success")); _commandQueue.pop(); _busyFlag = false; } } else { // If inverter was not found, assume the command is invalid - Serial.println(F("RX: Invalid inverter found")); + Hoymiles.getMessageOutput()->println(F("RX: Invalid inverter found")); _commandQueue.pop(); _busyFlag = false; } @@ -138,7 +142,7 @@ void HoymilesRadio::loop() inv->clearRxFragmentBuffer(); sendEsbPacket(cmd); } else { - Serial.println(F("TX: Invalid inverter found")); + Hoymiles.getMessageOutput()->println(F("TX: Invalid inverter found")); _commandQueue.pop(); } } @@ -248,12 +252,12 @@ void HoymilesRadio::sendEsbPacket(CommandAbstract* cmd) openWritingPipe(s); _radio->setRetries(3, 15); - Serial.print(F("TX ")); - Serial.print(cmd->getCommandName()); - Serial.print(F(" Channel: ")); - Serial.print(_radio->getChannel()); - Serial.print(F(" --> ")); - cmd->dumpDataPayload(Serial); + Hoymiles.getMessageOutput()->print(F("TX ")); + Hoymiles.getMessageOutput()->print(cmd->getCommandName()); + Hoymiles.getMessageOutput()->print(F(" Channel: ")); + Hoymiles.getMessageOutput()->print(_radio->getChannel()); + Hoymiles.getMessageOutput()->print(F(" --> ")); + cmd->dumpDataPayload(Hoymiles.getMessageOutput()); _radio->write(cmd->getDataPayload(), cmd->getDataSize()); _radio->setRetries(0, 0); @@ -285,10 +289,10 @@ void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) { if (NULL != info) - Serial.print(String(info)); + Hoymiles.getMessageOutput()->print(String(info)); for (uint8_t i = 0; i < len; i++) { - Serial.printf("%02X ", buf[i]); + Hoymiles.getMessageOutput()->printf("%02X ", buf[i]); } - Serial.println(F("")); + Hoymiles.getMessageOutput()->println(F("")); } \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 189321dfa..6963361c6 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "TimeoutHelper.h" diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 05e671dac..bf4b04de5 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "ActivePowerControlCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h index 67a1c5df5..f3a359ead 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "DevControlCommand.h" diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index e55ddd0f6..d11585974 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "AlarmDataCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.h b/lib/Hoymiles/src/commands/AlarmDataCommand.h index 096648173..1c34a826c 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.h +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "MultiDataCommand.h" diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index c425204ff..e7fb4ab58 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "CommandAbstract.h" #include "crc.h" #include @@ -19,13 +23,13 @@ const uint8_t* CommandAbstract::getDataPayload() return _payload; } -void CommandAbstract::dumpDataPayload(Stream& stream) +void CommandAbstract::dumpDataPayload(Print* stream) { const uint8_t* payload = getDataPayload(); for (uint8_t i = 0; i < getDataSize(); i++) { - stream.printf("%02X ", payload[i]); + stream->printf("%02X ", payload[i]); } - stream.println(""); + stream->println(""); } uint8_t CommandAbstract::getDataSize() diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index c551cfe0d..3baa348ed 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "types.h" @@ -14,7 +15,7 @@ class CommandAbstract { virtual ~CommandAbstract() {}; const uint8_t* getDataPayload(); - void dumpDataPayload(Stream& stream); + void dumpDataPayload(Print* stream); uint8_t getDataSize(); diff --git a/lib/Hoymiles/src/commands/DevControlCommand.cpp b/lib/Hoymiles/src/commands/DevControlCommand.cpp index a8b8c1b49..fce935b82 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.cpp +++ b/lib/Hoymiles/src/commands/DevControlCommand.cpp @@ -1,3 +1,8 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "DevControlCommand.h" #include "crc.h" diff --git a/lib/Hoymiles/src/commands/DevControlCommand.h b/lib/Hoymiles/src/commands/DevControlCommand.h index c9937d02c..f4d0a0495 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.h +++ b/lib/Hoymiles/src/commands/DevControlCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "CommandAbstract.h" diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp index 3a397e8d9..3da57f64a 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "DevInfoAllCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.h b/lib/Hoymiles/src/commands/DevInfoAllCommand.h index 179740514..165563846 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "MultiDataCommand.h" diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp index 7b583c9a4..2985c9a98 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "DevInfoSimpleCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h index 8c46ced75..99b7f503b 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "MultiDataCommand.h" diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index 92fe78b8b..c34ccd001 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "MultiDataCommand.h" #include "crc.h" diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.h b/lib/Hoymiles/src/commands/MultiDataCommand.h index 511d8c75e..ff835d7b9 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.h +++ b/lib/Hoymiles/src/commands/MultiDataCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "CommandAbstract.h" diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.cpp b/lib/Hoymiles/src/commands/ParaSetCommand.cpp index 9e19b0720..4a48cbea9 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.cpp +++ b/lib/Hoymiles/src/commands/ParaSetCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "ParaSetCommand.h" ParaSetCommand::ParaSetCommand(uint64_t target_address, uint64_t router_address) diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.h b/lib/Hoymiles/src/commands/ParaSetCommand.h index caa0a5218..9ca4e8a97 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.h +++ b/lib/Hoymiles/src/commands/ParaSetCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "CommandAbstract.h" diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.cpp b/lib/Hoymiles/src/commands/PowerControlCommand.cpp index 9bd669734..522ad5f2d 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/PowerControlCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "PowerControlCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h index 3b0225650..376d201e9 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.h +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "DevControlCommand.h" diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 314e4c0e1..97d0cebb7 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "RealTimeRunDataCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h index ebbde4560..8cb5be39b 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "MultiDataCommand.h" diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp index 570f03a4c..e2bfb7668 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "RequestFrameCommand.h" RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t router_address, uint8_t frame_no) diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.h b/lib/Hoymiles/src/commands/RequestFrameCommand.h index 7c3e70e3a..5d5e9da15 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.h +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "SingleDataCommand.h" diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.cpp b/lib/Hoymiles/src/commands/SingleDataCommand.cpp index 5d6cb5d62..636ee87ac 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.cpp +++ b/lib/Hoymiles/src/commands/SingleDataCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "SingleDataCommand.h" SingleDataCommand::SingleDataCommand(uint64_t target_address, uint64_t router_address) diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.h b/lib/Hoymiles/src/commands/SingleDataCommand.h index 987225180..c891bda96 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.h +++ b/lib/Hoymiles/src/commands/SingleDataCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "CommandAbstract.h" diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 07c69cdd7..8d310f4c1 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "SystemConfigParaCommand.h" #include "inverters/InverterAbstract.h" diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h index d84d2913f..ef266fffd 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "MultiDataCommand.h" diff --git a/lib/Hoymiles/src/crc.cpp b/lib/Hoymiles/src/crc.cpp index ce3bbf90e..531971328 100644 --- a/lib/Hoymiles/src/crc.cpp +++ b/lib/Hoymiles/src/crc.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "crc.h" uint8_t crc8(const uint8_t buf[], uint8_t len) diff --git a/lib/Hoymiles/src/crc.h b/lib/Hoymiles/src/crc.h index dd640cc3d..a1b01febf 100644 --- a/lib/Hoymiles/src/crc.h +++ b/lib/Hoymiles/src/crc.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 2311330e8..246339c7b 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "HM_1CH.h" HM_1CH::HM_1CH(uint64_t serial) diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index 318bfc79d..6fb345f5c 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "HM_Abstract.h" @@ -12,24 +13,24 @@ class HM_1CH : public HM_Abstract { private: const byteAssign_t byteAssignment[18] = { - { FLD_UDC, UNIT_V, CH1, 2, 2, 10, false }, - { FLD_IDC, UNIT_A, CH1, 4, 2, 100, false }, - { FLD_PDC, UNIT_W, CH1, 6, 2, 10, false }, - { FLD_YD, UNIT_WH, CH1, 12, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH1, 8, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC, false }, + { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { CH1, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, + { CH1, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, + { CH1, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, + { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, - { FLD_UAC, UNIT_V, CH0, 14, 2, 10, false }, - { FLD_IAC, UNIT_A, CH0, 22, 2, 100, false }, - { FLD_PAC, UNIT_W, CH0, 18, 2, 10, false }, - { FLD_PRA, UNIT_VA, CH0, 20, 2, 10, false }, - { FLD_F, UNIT_HZ, CH0, 16, 2, 100, false }, - { FLD_PF, UNIT_NONE, CH0, 24, 2, 1000, false }, - { FLD_T, UNIT_C, CH0, 26, 2, 10, true }, - { FLD_EVT_LOG, UNIT_NONE, CH0, 28, 2, 1, false }, - { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, - { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, - { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, - { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC, false } + { CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, + { CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, + { CH0, FLD_PAC, UNIT_W, 18, 2, 10, false, 1 }, + { CH0, FLD_PRA, UNIT_VA, 20, 2, 10, false, 1 }, + { CH0, FLD_F, UNIT_HZ, 16, 2, 100, false, 2 }, + { CH0, FLD_PF, UNIT_NONE, 24, 2, 1000, false, 3 }, + { CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, + { CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, + { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index cf2c55368..ef148bed2 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -1,3 +1,8 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "HM_2CH.h" HM_2CH::HM_2CH(uint64_t serial) diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index 9f1e4a8cb..d20d43f2e 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "HM_Abstract.h" @@ -12,31 +13,31 @@ class HM_2CH : public HM_Abstract { private: const byteAssign_t byteAssignment[24] = { - { FLD_UDC, UNIT_V, CH1, 2, 2, 10, false }, - { FLD_IDC, UNIT_A, CH1, 4, 2, 100, false }, - { FLD_PDC, UNIT_W, CH1, 6, 2, 10, false }, - { FLD_YD, UNIT_WH, CH1, 22, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH1, 14, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC, false }, + { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { CH1, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, + { CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { CH1, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, - { FLD_UDC, UNIT_V, CH2, 8, 2, 10, false }, - { FLD_IDC, UNIT_A, CH2, 10, 2, 100, false }, - { FLD_PDC, UNIT_W, CH2, 12, 2, 10, false }, - { FLD_YD, UNIT_WH, CH2, 24, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH2, 18, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC, false }, + { CH2, FLD_UDC, UNIT_V, 8, 2, 10, false, 1 }, + { CH2, FLD_IDC, UNIT_A, 10, 2, 100, false, 2 }, + { CH2, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { CH2, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { CH2, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, - { FLD_UAC, UNIT_V, CH0, 26, 2, 10, false }, - { FLD_IAC, UNIT_A, CH0, 34, 2, 100, false }, - { FLD_PAC, UNIT_W, CH0, 30, 2, 10, false }, - { FLD_PRA, UNIT_VA, CH0, 32, 2, 10, false }, - { FLD_F, UNIT_HZ, CH0, 28, 2, 100, false }, - { FLD_PF, UNIT_NONE, CH0, 36, 2, 1000, false }, - { FLD_T, UNIT_C, CH0, 38, 2, 10, true }, - { FLD_EVT_LOG, UNIT_NONE, CH0, 40, 2, 1, false }, - { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, - { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, - { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, - { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC, false } + { CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { CH0, FLD_PRA, UNIT_VA, 32, 2, 10, false, 1 }, + { CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + { CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, + { CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index bda9592d6..afffbeadc 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "HM_4CH.h" HM_4CH::HM_4CH(uint64_t serial) diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index 3d2eefe7f..2a950f2ee 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "HM_Abstract.h" @@ -12,45 +13,45 @@ class HM_4CH : public HM_Abstract { private: const byteAssign_t byteAssignment[36] = { - { FLD_UDC, UNIT_V, CH1, 2, 2, 10, false }, - { FLD_IDC, UNIT_A, CH1, 4, 2, 100, false }, - { FLD_PDC, UNIT_W, CH1, 8, 2, 10, false }, - { FLD_YD, UNIT_WH, CH1, 20, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH1, 12, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC, false }, + { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { CH1, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, + { CH1, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, + { CH1, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, + { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, - { FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC, false }, - { FLD_IDC, UNIT_A, CH2, 6, 2, 100, false }, - { FLD_PDC, UNIT_W, CH2, 10, 2, 10, false }, - { FLD_YD, UNIT_WH, CH2, 22, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH2, 16, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC, false }, + { CH2, FLD_UDC, UNIT_V, CALC_UDC_CH, CH1, CMD_CALC, false, 1 }, + { CH2, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { CH2, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { CH2, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { CH2, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, + { CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, - { FLD_UDC, UNIT_V, CH3, 24, 2, 10, false }, - { FLD_IDC, UNIT_A, CH3, 26, 2, 100, false }, - { FLD_PDC, UNIT_W, CH3, 30, 2, 10, false }, - { FLD_YD, UNIT_WH, CH3, 42, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH3, 34, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC, false }, + { CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, + { CH3, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, + { CH3, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, + { CH3, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, + { CH3, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, + { CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, - { FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC, false }, - { FLD_IDC, UNIT_A, CH4, 28, 2, 100, false }, - { FLD_PDC, UNIT_W, CH4, 32, 2, 10, false }, - { FLD_YD, UNIT_WH, CH4, 44, 2, 1, false }, - { FLD_YT, UNIT_KWH, CH4, 38, 4, 1000, false }, - { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC, false }, + { CH4, FLD_UDC, UNIT_V, CALC_UDC_CH, CH3, CMD_CALC, false, 1 }, + { CH4, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, + { CH4, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, + { CH4, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, + { CH4, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, + { CH4, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH4, CMD_CALC, false, 3 }, - { FLD_UAC, UNIT_V, CH0, 46, 2, 10, false }, - { FLD_IAC, UNIT_A, CH0, 54, 2, 100, false }, - { FLD_PAC, UNIT_W, CH0, 50, 2, 10, false }, - { FLD_PRA, UNIT_VA, CH0, 52, 2, 10, false }, - { FLD_F, UNIT_HZ, CH0, 48, 2, 100, false }, - { FLD_PF, UNIT_NONE, CH0, 56, 2, 1000, false }, - { FLD_T, UNIT_C, CH0, 58, 2, 10, true }, - { FLD_EVT_LOG, UNIT_NONE, CH0, 60, 2, 1, false }, - { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, - { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, - { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, - { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC, false } + { CH0, FLD_UAC, UNIT_V, 46, 2, 10, false, 1 }, + { CH0, FLD_IAC, UNIT_A, 54, 2, 100, false, 2 }, + { CH0, FLD_PAC, UNIT_W, 50, 2, 10, false, 1 }, + { CH0, FLD_PRA, UNIT_VA, 52, 2, 10, false, 1 }, + { CH0, FLD_F, UNIT_HZ, 48, 2, 100, false, 2 }, + { CH0, FLD_PF, UNIT_NONE, 56, 2, 1000, false, 3 }, + { CH0, FLD_T, UNIT_C, 58, 2, 10, true, 1 }, + { CH0, FLD_EVT_LOG, UNIT_NONE, 60, 2, 1, false, 0 }, + { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 36b25af31..73e02cee5 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "HM_Abstract.h" #include "HoymilesRadio.h" #include "commands/ActivePowerControlCommand.h" diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index da3e0d88d..fe89dd24d 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "InverterAbstract.h" diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index bb7216d90..7f6e14a30 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -1,4 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "InverterAbstract.h" +#include "../Hoymiles.h" #include "crc.h" #include @@ -103,18 +108,18 @@ void InverterAbstract::clearRxFragmentBuffer() void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) { if (len < 11) { - Serial.printf("FATAL: (%s, %d) fragment too short\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\n", __FILE__, __LINE__); return; } if (len - 11 > MAX_RF_PAYLOAD_SIZE) { - Serial.printf("FATAL: (%s, %d) fragment too large\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too large\n", __FILE__, __LINE__); return; } uint8_t fragmentCount = fragment[9]; if (fragmentCount == 0) { - Serial.println("ERROR: fragment number zero received and ignored"); + Hoymiles.getMessageOutput()->println("ERROR: fragment number zero received and ignored"); return; } @@ -141,7 +146,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) { // All missing if (_rxFragmentLastPacketId == 0) { - Serial.println(F("All missing")); + Hoymiles.getMessageOutput()->println(F("All missing")); if (cmd->getSendCount() <= MAX_RESEND_COUNT) { return FRAGMENT_ALL_MISSING_RESEND; } else { @@ -152,7 +157,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // Last fragment is missing (thte one with 0x80) if (_rxFragmentMaxPacketId == 0) { - Serial.println(F("Last missing")); + Hoymiles.getMessageOutput()->println(F("Last missing")); if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { return _rxFragmentLastPacketId + 1; } else { @@ -164,7 +169,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // Middle fragment is missing for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) { if (!_rxFragmentBuffer[i].wasReceived) { - Serial.println(F("Middle missing")); + Hoymiles.getMessageOutput()->println(F("Middle missing")); if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { return i + 1; } else { diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 5c9088b18..018e06d22 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "../commands/ActivePowerControlCommand.h" diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 4958b6a70..be781d146 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -1,4 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "AlarmLogParser.h" +#include "../Hoymiles.h" #include void AlarmLogParser::clearBuffer() @@ -10,7 +15,7 @@ void AlarmLogParser::clearBuffer() void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > ALARM_LOG_PAYLOAD_SIZE) { - Serial.printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE); return; } memcpy(&_payloadAlarmLog[offset], payload, len); diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 5b62c61b5..4910abc07 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" #include diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 2b1bd330e..c5b5b79d6 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -1,4 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "DevInfoParser.h" +#include "../Hoymiles.h" #include #define ALL 0xff @@ -32,7 +37,7 @@ void DevInfoParser::clearBufferAll() void DevInfoParser::appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > DEV_INFO_SIZE) { - Serial.printf("FATAL: (%s, %d) dev info all packet too large for buffer\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info all packet too large for buffer\n", __FILE__, __LINE__); return; } memcpy(&_payloadDevInfoAll[offset], payload, len); @@ -48,7 +53,7 @@ void DevInfoParser::clearBufferSimple() void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > DEV_INFO_SIZE) { - Serial.printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\n", __FILE__, __LINE__); return; } memcpy(&_payloadDevInfoSimple[offset], payload, len); @@ -84,7 +89,7 @@ uint16_t DevInfoParser::getFwBuildVersion() time_t DevInfoParser::getFwBuildDateTime() { - struct tm timeinfo = { }; + struct tm timeinfo = {}; timeinfo.tm_year = ((((uint16_t)_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900; timeinfo.tm_mon = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1; @@ -139,10 +144,10 @@ String DevInfoParser::getHwModelName() uint8_t DevInfoParser::getDevIdx() { - uint8_t pos; - // Check for all 4 bytes first + uint8_t pos; + // Check for all 4 bytes first for (pos = 0; pos < sizeof(devInfo) / sizeof(devInfo_t); pos++) { - if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2] + if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2] && devInfo[pos].hwPart[1] == _payloadDevInfoSimple[3] && devInfo[pos].hwPart[2] == _payloadDevInfoSimple[4] && devInfo[pos].hwPart[3] == _payloadDevInfoSimple[5]) { @@ -152,7 +157,7 @@ uint8_t DevInfoParser::getDevIdx() // Then only for 3 bytes for (pos = 0; pos < sizeof(devInfo) / sizeof(devInfo_t); pos++) { - if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2] + if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2] && devInfo[pos].hwPart[1] == _payloadDevInfoSimple[3] && devInfo[pos].hwPart[2] == _payloadDevInfoSimple[4]) { return pos; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index a4ce10e7f..02b5307d0 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" #include diff --git a/lib/Hoymiles/src/parser/Parser.cpp b/lib/Hoymiles/src/parser/Parser.cpp index 57db09c0b..94c694c0b 100644 --- a/lib/Hoymiles/src/parser/Parser.cpp +++ b/lib/Hoymiles/src/parser/Parser.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "Parser.h" uint32_t Parser::getLastUpdate() diff --git a/lib/Hoymiles/src/parser/Parser.h b/lib/Hoymiles/src/parser/Parser.h index 3d29d3005..ccb486bda 100644 --- a/lib/Hoymiles/src/parser/Parser.h +++ b/lib/Hoymiles/src/parser/Parser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.cpp b/lib/Hoymiles/src/parser/PowerCommandParser.cpp index 1808660eb..0ba3a8ae6 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.cpp +++ b/lib/Hoymiles/src/parser/PowerCommandParser.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "PowerCommandParser.h" void PowerCommandParser::setLastPowerCommandSuccess(LastCommandSuccess status) diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.h b/lib/Hoymiles/src/parser/PowerCommandParser.h index c0ba6beca..6aa042f30 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.h +++ b/lib/Hoymiles/src/parser/PowerCommandParser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" #include diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index dbbdf135d..0ac833f1e 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -1,4 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "StatisticsParser.h" +#include "../Hoymiles.h" static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); @@ -38,7 +43,7 @@ void StatisticsParser::clearBuffer() void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > STATISTIC_PACKET_SIZE) { - Serial.printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__); return; } memcpy(&_payloadStatistic[offset], payload, len); @@ -123,20 +128,7 @@ const char* StatisticsParser::getChannelFieldName(uint8_t channel, uint8_t field uint8_t StatisticsParser::getChannelFieldDigits(uint8_t channel, uint8_t fieldId) { uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - const byteAssign_t* b = _byteAssignment; - - switch (b[pos].div) { - case 1: - return 0; - case 10: - return 1; - case 100: - return 2; - case 1000: - return 3; - default: - return 2; - } + return _byteAssignment[pos].digits; } uint8_t StatisticsParser::getChannelCount() diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 968d3e988..e4defc94c 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" #include @@ -62,13 +63,14 @@ enum { }; typedef struct { + uint8_t ch; // channel 0 - 4 uint8_t fieldId; // field id uint8_t unitId; // uint id - uint8_t ch; // channel 0 - 4 uint8_t start; // pos of first byte in buffer uint8_t num; // number of bytes in buffer uint16_t div; // divisor / calc command bool isSigned; // allow negative numbers + uint8_t digits; // number of valid digits after the decimal point } byteAssign_t; class StatisticsParser : public Parser { diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index ed59588fb..b9ac2fc0b 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -1,4 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "SystemConfigParaParser.h" +#include "../Hoymiles.h" #include void SystemConfigParaParser::clearBuffer() @@ -10,7 +15,7 @@ void SystemConfigParaParser::clearBuffer() void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > (SYSTEM_CONFIG_PARA_SIZE)) { - Serial.printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__); + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__); return; } memcpy(&_payload[offset], payload, len); diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.h b/lib/Hoymiles/src/parser/SystemConfigParaParser.h index 32d1e4bb2..5aced3c87 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.h +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" #include diff --git a/lib/Hoymiles/src/types.h b/lib/Hoymiles/src/types.h index a0883a0ba..9a43d2f1c 100644 --- a/lib/Hoymiles/src/types.h +++ b/lib/Hoymiles/src/types.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include diff --git a/lib/MqttSubscribeParser/MqttSubscribeParser.cpp b/lib/MqttSubscribeParser/MqttSubscribeParser.cpp new file mode 100644 index 000000000..c0cc3bb70 --- /dev/null +++ b/lib/MqttSubscribeParser/MqttSubscribeParser.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttSubscribeParser.h" + +void MqttSubscribeParser::register_callback(const std::string& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) +{ + cb_filter_t cbf; + cbf.topic = topic; + cbf.qos = qos; + cbf.cb = cb; + _callbacks.push_back(cbf); +} + +void MqttSubscribeParser::unregister_callback(const std::string& topic) +{ + for (auto it = _callbacks.begin(); it != _callbacks.end();) { + if ((*it).topic == topic) { + it = _callbacks.erase(it); + } else { + ++it; + } + } +} + +void MqttSubscribeParser::handle_message(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + bool result = false; + for (const auto& cb : _callbacks) { + if (mosquitto_topic_matches_sub(cb.topic.c_str(), topic, &result) == MOSQ_ERR_SUCCESS) { + if (result) { + cb.cb(properties, topic, payload, len, index, total); + } + } + } +} + +std::vector MqttSubscribeParser::get_callbacks() +{ + return _callbacks; +} + +/* Does a topic match a subscription? */ +int MqttSubscribeParser::mosquitto_topic_matches_sub(const char* sub, const char* topic, bool* result) +{ + size_t spos; + + if (!result) + return MOSQ_ERR_INVAL; + *result = false; + + if (!sub || !topic || sub[0] == 0 || topic[0] == 0) { + return MOSQ_ERR_INVAL; + } + + if ((sub[0] == '$' && topic[0] != '$') + || (topic[0] == '$' && sub[0] != '$')) { + + return MOSQ_ERR_SUCCESS; + } + + spos = 0; + + while (sub[0] != 0) { + if (topic[0] == '+' || topic[0] == '#') { + return MOSQ_ERR_INVAL; + } + if (sub[0] != topic[0] || topic[0] == 0) { /* Check for wildcard matches */ + if (sub[0] == '+') { + /* Check for bad "+foo" or "a/+foo" subscription */ + if (spos > 0 && sub[-1] != '/') { + return MOSQ_ERR_INVAL; + } + /* Check for bad "foo+" or "foo+/a" subscription */ + if (sub[1] != 0 && sub[1] != '/') { + return MOSQ_ERR_INVAL; + } + spos++; + sub++; + while (topic[0] != 0 && topic[0] != '/') { + if (topic[0] == '+' || topic[0] == '#') { + return MOSQ_ERR_INVAL; + } + topic++; + } + if (topic[0] == 0 && sub[0] == 0) { + *result = true; + return MOSQ_ERR_SUCCESS; + } + } else if (sub[0] == '#') { + /* Check for bad "foo#" subscription */ + if (spos > 0 && sub[-1] != '/') { + return MOSQ_ERR_INVAL; + } + /* Check for # not the final character of the sub, e.g. "#foo" */ + if (sub[1] != 0) { + return MOSQ_ERR_INVAL; + } else { + while (topic[0] != 0) { + if (topic[0] == '+' || topic[0] == '#') { + return MOSQ_ERR_INVAL; + } + topic++; + } + *result = true; + return MOSQ_ERR_SUCCESS; + } + } else { + /* Check for e.g. foo/bar matching foo/+/# */ + if (topic[0] == 0 + && spos > 0 + && sub[-1] == '+' + && sub[0] == '/' + && sub[1] == '#') { + *result = true; + return MOSQ_ERR_SUCCESS; + } + + /* There is no match at this point, but is the sub invalid? */ + while (sub[0] != 0) { + if (sub[0] == '#' && sub[1] != 0) { + return MOSQ_ERR_INVAL; + } + spos++; + sub++; + } + + /* Valid input, but no match */ + return MOSQ_ERR_SUCCESS; + } + } else { + /* sub[spos] == topic[tpos] */ + if (topic[1] == 0) { + /* Check for e.g. foo matching foo/# */ + if (sub[1] == '/' + && sub[2] == '#' + && sub[3] == 0) { + *result = true; + return MOSQ_ERR_SUCCESS; + } + } + spos++; + sub++; + topic++; + if (sub[0] == 0 && topic[0] == 0) { + *result = true; + return MOSQ_ERR_SUCCESS; + } else if (topic[0] == 0 && sub[0] == '+' && sub[1] == 0) { + if (spos > 0 && sub[-1] != '/') { + return MOSQ_ERR_INVAL; + } + spos++; + sub++; + *result = true; + return MOSQ_ERR_SUCCESS; + } + } + } + if ((topic[0] != 0 || sub[0] != 0)) { + *result = false; + } + while (topic[0] != 0) { + if (topic[0] == '+' || topic[0] == '#') { + return MOSQ_ERR_INVAL; + } + topic++; + } + + return MOSQ_ERR_SUCCESS; +} diff --git a/lib/MqttSubscribeParser/MqttSubscribeParser.h b/lib/MqttSubscribeParser/MqttSubscribeParser.h new file mode 100644 index 000000000..68589fdb6 --- /dev/null +++ b/lib/MqttSubscribeParser/MqttSubscribeParser.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include + +struct cb_filter_t { + std::string topic; + uint8_t qos; + espMqttClientTypes::OnMessageCallback cb; +}; + +class MqttSubscribeParser { +public: + void register_callback(const std::string& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); + void unregister_callback(const std::string& topic); + void handle_message(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + std::vector get_callbacks(); + +private: + int mosquitto_topic_matches_sub(const char* sub, const char* topic, bool* result); + + std::vector _callbacks; + + enum mosq_err_t { + MOSQ_ERR_SUCCESS = 0, + MOSQ_ERR_INVAL = 3, + }; +}; \ No newline at end of file diff --git a/lib/ResetReason/src/ResetReason.cpp b/lib/ResetReason/src/ResetReason.cpp index ac7b2cb14..53779f8f6 100644 --- a/lib/ResetReason/src/ResetReason.cpp +++ b/lib/ResetReason/src/ResetReason.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "ResetReason.h" #if ESP_IDF_VERSION_MAJOR > 3 // IDF 4+ diff --git a/lib/ResetReason/src/ResetReason.h b/lib/ResetReason/src/ResetReason.h index 65664ebdc..34427bfaa 100644 --- a/lib/ResetReason/src/ResetReason.h +++ b/lib/ResetReason/src/ResetReason.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include diff --git a/lib/TimeoutHelper/TimeoutHelper.cpp b/lib/TimeoutHelper/TimeoutHelper.cpp index 8a57031e3..bb33737d5 100644 --- a/lib/TimeoutHelper/TimeoutHelper.cpp +++ b/lib/TimeoutHelper/TimeoutHelper.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ #include "TimeoutHelper.h" #include diff --git a/lib/TimeoutHelper/TimeoutHelper.h b/lib/TimeoutHelper/TimeoutHelper.h index d4ecb4ce3..36248dc1f 100644 --- a/lib/TimeoutHelper/TimeoutHelper.h +++ b/lib/TimeoutHelper/TimeoutHelper.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include diff --git a/platformio.ini b/platformio.ini index 495cda6f0..13bd52b67 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,6 +10,8 @@ [platformio] default_envs = generic +extra_configs = + platformio_override.ini [env] framework = arduino @@ -35,9 +37,9 @@ monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 upload_protocol = esptool -; Specify port here. Comment out (add ; in front of line) to use auto detection. -monitor_port = COM4 -upload_port = COM4 +; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection. +; monitor_port = COM4 +; upload_port = COM4 [env:generic] diff --git a/platformio_override.ini b/platformio_override.ini new file mode 100644 index 000000000..e7391dc88 --- /dev/null +++ b/platformio_override.ini @@ -0,0 +1,31 @@ +; *** PlatformIO Project Configuration Override File *** +; *** Changes done here override settings in platformio.ini *** +; +; Place your personal settings like monitor_port and upload_port here +; instead of editing platformio.ini +; to avoid annoying merge conflicts when you pull updates into your +; personal clone of the repository +; +; Please visit documentation for the options and examples +; http://docs.platformio.org/en/stable/projectconf.html + +[env] +; Specify port here. Comment out (add ; in front of line) to use auto detection. +monitor_port = COM4 +upload_port = COM4 + + +; you can define your personal board and/or settings here +; non functional example: + +;[env:my_very_special_board] +;board = esp32dev +;build_flags = ${env.build_flags} +; -DHOYMILES_PIN_MISO=1 +; -DHOYMILES_PIN_MOSI=2 +; -DHOYMILES_PIN_SCLK=3 +; -DHOYMILES_PIN_IRQ=4 +; -DHOYMILES_PIN_CE=5 +; -DHOYMILES_PIN_CS=6 +;monitor_port = /dev/ttyACM0 +;upload_port = /dev/ttyACM0 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 3d8901036..8a473d4e6 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Configuration.h" +#include "MessageOutput.h" #include "defaults.h" #include #include @@ -100,7 +101,7 @@ bool ConfigurationClass::write() // Serialize JSON to file if (serializeJson(doc, f) == 0) { - Serial.println("Failed to write file"); + MessageOutput.println("Failed to write file"); return false; } @@ -116,7 +117,7 @@ bool ConfigurationClass::read() // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); if (error) { - Serial.println(F("Failed to read file, using default configuration")); + MessageOutput.println(F("Failed to read file, using default configuration")); } JsonObject cfg = doc["cfg"]; @@ -232,7 +233,7 @@ void ConfigurationClass::migrate() if (config.Cfg_Version < 0x00011700) { File f = LittleFS.open(CONFIG_FILENAME, "r", false); if (!f) { - Serial.println(F("Failed to open file, cancel migration")); + MessageOutput.println(F("Failed to open file, cancel migration")); return; } @@ -240,7 +241,7 @@ void ConfigurationClass::migrate() // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); if (error) { - Serial.println(F("Failed to read file, cancel migration")); + MessageOutput.println(F("Failed to read file, cancel migration")); return; } diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp new file mode 100644 index 000000000..2a84816b0 --- /dev/null +++ b/src/MessageOutput.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MessageOutput.h" + +#include + +MessageOutputClass MessageOutput; + +#define MSG_LOCK() xSemaphoreTake(_lock, portMAX_DELAY) +#define MSG_UNLOCK() xSemaphoreGive(_lock) + +MessageOutputClass::MessageOutputClass() +{ + _lock = xSemaphoreCreateMutex(); + MSG_UNLOCK(); +} + +void MessageOutputClass::register_ws_output(AsyncWebSocket* output) +{ + _ws = output; +} + +size_t MessageOutputClass::write(uint8_t c) +{ + if (_buff_pos < BUFFER_SIZE) { + MSG_LOCK(); + _buffer[_buff_pos] = c; + _buff_pos++; + MSG_UNLOCK(); + } else { + _forceSend = true; + } + + return Serial.write(c); +} + +void MessageOutputClass::loop() +{ + // Send data via websocket if either time is over or buffer is full + if (_forceSend || (millis() - _lastSend > 1000)) { + MSG_LOCK(); + if (_ws && _buff_pos > 0) { + _ws->textAll(_buffer, _buff_pos); + _buff_pos = 0; + } + if(_forceSend) { + _buff_pos = 0; + } + MSG_UNLOCK(); + _forceSend = false; + } +} \ No newline at end of file diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp new file mode 100644 index 000000000..818a1b409 --- /dev/null +++ b/src/MqttHandleDtu.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHandleDtu.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include + +MqttHandleDtuClass MqttHandleDtu; + +void MqttHandleDtuClass::init() +{ +} + +void MqttHandleDtuClass::loop() +{ + if (!MqttSettings.getConnected() || !Hoymiles.getRadio()->isIdle()) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { + MqttSettings.publish("dtu/uptime", String(millis() / 1000)); + MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); + MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); + if (NetworkSettings.NetworkMode() == network_mode::WiFi) { + MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); + } + + _lastPublish = millis(); + } +} \ No newline at end of file diff --git a/src/MqttHassPublishing.cpp b/src/MqttHandleHass.cpp similarity index 81% rename from src/MqttHassPublishing.cpp rename to src/MqttHandleHass.cpp index 5f77b70e9..9d07fe177 100644 --- a/src/MqttHassPublishing.cpp +++ b/src/MqttHandleHass.cpp @@ -2,18 +2,18 @@ /* * Copyright (C) 2022 Thomas Basler and others */ -#include "MqttHassPublishing.h" -#include "MqttPublishing.h" +#include "MqttHandleHass.h" +#include "MqttHandleInverter.h" #include "MqttSettings.h" #include "NetworkSettings.h" -MqttHassPublishingClass MqttHassPublishing; +MqttHandleHassClass MqttHandleHass; -void MqttHassPublishingClass::init() +void MqttHandleHassClass::init() { } -void MqttHassPublishingClass::loop() +void MqttHandleHassClass::loop() { if (_updateForced) { publishConfig(); @@ -30,12 +30,12 @@ void MqttHassPublishingClass::loop() } } -void MqttHassPublishingClass::forceUpdate() +void MqttHandleHassClass::forceUpdate() { _updateForced = true; } -void MqttHassPublishingClass::publishConfig() +void MqttHandleHassClass::publishConfig() { if (!Configuration.get().Mqtt_Hass_Enabled) { return; @@ -79,7 +79,7 @@ void MqttHassPublishingClass::publishConfig() } } -void MqttHassPublishingClass::publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) +void MqttHandleHassClass::publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) { if (!inv->Statistics()->hasChannelFieldValue(channel, fieldType.fieldId)) { return; @@ -99,7 +99,7 @@ void MqttHassPublishingClass::publishField(std::shared_ptr inv + "/config"; if (!clear) { - String stateTopic = MqttSettings.getPrefix() + MqttPublishing.getTopic(inv, channel, fieldType.fieldId); + String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, channel, fieldType.fieldId); const char* devCls = deviceClasses[fieldType.deviceClsId]; const char* stateCls = stateClasses[fieldType.stateClsId]; @@ -131,13 +131,13 @@ void MqttHassPublishingClass::publishField(std::shared_ptr inv char buffer[512]; serializeJson(root, buffer); - MqttSettings.publishHass(configTopic, buffer); + publish(configTopic, buffer); } else { - MqttSettings.publishHass(configTopic, ""); + publish(configTopic, ""); } } -void MqttHassPublishingClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload) +void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload) { String serial = inv->serialString(); @@ -169,10 +169,10 @@ void MqttHassPublishingClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, int16_t min, int16_t max) @@ -208,10 +208,10 @@ void MqttHassPublishingClass::publishInverterNumber( char buffer[512]; serializeJson(root, buffer); - MqttSettings.publishHass(configTopic, buffer); + publish(configTopic, buffer); } -void MqttHassPublishingClass::publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) +void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) { String serial = inv->serialString(); @@ -237,15 +237,22 @@ void MqttHassPublishingClass::publishInverterBinarySensor(std::shared_ptr inv) +void MqttHandleHassClass::createDeviceInfo(JsonObject& object, std::shared_ptr inv) { object[F("name")] = inv->name(); object[F("ids")] = inv->serialString(); - object[F("cu")] = String(F("http://")) + WiFi.localIP().toString(); + object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); object[F("mf")] = F("OpenDTU"); object[F("mdl")] = inv->typeName(); object[F("sw")] = AUTO_GIT_HASH; +} + +void MqttHandleHassClass::publish(const String& subtopic, const String& payload) +{ + String topic = Configuration.get().Mqtt_Hass_Topic; + topic += subtopic; + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); } \ No newline at end of file diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp new file mode 100644 index 000000000..0c0a4d7d1 --- /dev/null +++ b/src/MqttHandleInverter.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHandleInverter.h" +#include "MessageOutput.h" +#include "MqttSettings.h" +#include + +#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative" +#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute" +#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" +#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" +#define TOPIC_SUB_POWER "power" +#define TOPIC_SUB_RESTART "restart" + +MqttHandleInverterClass MqttHandleInverter; + +void MqttHandleInverterClass::init() +{ + 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 topic = MqttSettings.getPrefix(); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); +} + +void MqttHandleInverterClass::loop() +{ + if (!MqttSettings.getConnected() || !Hoymiles.getRadio()->isIdle()) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String subtopic = inv->serialString(); + + // Name + MqttSettings.publish(subtopic + "/name", inv->name()); + + if (inv->DevInfo()->getLastUpdate() > 0) { + // Bootloader Version + MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); + + // Firmware Version + MqttSettings.publish(subtopic + "/device/fwbuildversion", String(inv->DevInfo()->getFwBuildVersion())); + + // Firmware Build DateTime + char timebuffer[32]; + const time_t t = inv->DevInfo()->getFwBuildDateTime(); + std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t)); + MqttSettings.publish(subtopic + "/device/fwbuilddatetime", String(timebuffer)); + + // Hardware part number + MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); + + // Hardware version + MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); + } + + if (inv->SystemConfigPara()->getLastUpdate() > 0) { + // Limit + MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent())); + + uint16_t maxpower = inv->DevInfo()->getMaxPower(); + if (maxpower > 0) { + MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100)); + } + } + + MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); + MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing())); + + if (inv->Statistics()->getLastUpdate() > 0) { + MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000)); + } else { + MqttSettings.publish(subtopic + "/status/last_update", String(0)); + } + + uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); + if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { + _lastPublishStats[i] = lastUpdate; + + // Loop all channels + for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { + if (c > 0) { + INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg != nullptr) { + MqttSettings.publish(inv->serialString() + "/" + String(c) + "/name", inv_cfg->channel[c - 1].Name); + } + } + for (uint8_t f = 0; f < sizeof(_publishFields); f++) { + publishField(inv, c, _publishFields[f]); + } + } + } + + yield(); + } + + _lastPublish = millis(); + } +} + +void MqttHandleInverterClass::publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) +{ + String topic = getTopic(inv, channel, fieldId); + if (topic == "") { + return; + } + + MqttSettings.publish(topic, String(inv->Statistics()->getChannelFieldValue(channel, fieldId))); +} + +String MqttHandleInverterClass::getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) +{ + if (!inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { + return String(""); + } + + String chanName; + if (channel == 0 && fieldId == FLD_PDC) { + chanName = "powerdc"; + } else { + chanName = inv->Statistics()->getChannelFieldName(channel, fieldId); + chanName.toLowerCase(); + } + + return inv->serialString() + "/" + String(channel) + "/" + chanName; +} + +void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + const CONFIG_T& config = Configuration.get(); + + char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics + strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* + + char* serial_str; + char* subtopic; + char* setting; + char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + + serial_str = strtok_r(rest, "/", &rest); + subtopic = strtok_r(rest, "/", &rest); + setting = strtok_r(rest, "/", &rest); + + if (serial_str == NULL || subtopic == NULL || setting == NULL) { + return; + } + + uint64_t serial; + serial = strtoull(serial_str, 0, 16); + + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv == nullptr) { + MessageOutput.println(F("Inverter not found")); + return; + } + + // check if subtopic is unequal cmd + if (strcmp(subtopic, "cmd")) { + return; + } + + char* strlimit = new char[len + 1]; + memcpy(strlimit, payload, len); + strlimit[len] = '\0'; + uint32_t payload_val = strtol(strlimit, NULL, 10); + delete[] strlimit; + + if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { + // Set inverter limit relative persistent + MessageOutput.printf("Limit Persistent: %d %%\n", payload_val); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent); + + } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { + // Set inverter limit absolute persistent + MessageOutput.printf("Limit Persistent: %d W\n", payload_val); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutPersistent); + + } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { + // Set inverter limit relative non persistent + MessageOutput.printf("Limit Non-Persistent: %d %%\n", payload_val); + if (!properties.retain) { + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent); + } else { + MessageOutput.println("Ignored because retained"); + } + + } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { + // Set inverter limit absolute non persistent + MessageOutput.printf("Limit Non-Persistent: %d W\n", payload_val); + if (!properties.retain) { + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutNonPersistent); + } else { + MessageOutput.println("Ignored because retained"); + } + + } else if (!strcmp(setting, TOPIC_SUB_POWER)) { + // Turn inverter on or off + MessageOutput.printf("Set inverter power to: %d\n", payload_val); + inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0); + + } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { + // Restart inverter + MessageOutput.printf("Restart inverter\n"); + if (!properties.retain && payload_val == 1) { + inv->sendRestartControlRequest(Hoymiles.getRadio()); + } else { + MessageOutput.println("Ignored because retained"); + } + } +} \ No newline at end of file diff --git a/src/MqttVedirectPublishing.cpp b/src/MqttHandleVedirect.cpp similarity index 88% rename from src/MqttVedirectPublishing.cpp rename to src/MqttHandleVedirect.cpp index 9a7c047a4..9d01a44e0 100644 --- a/src/MqttVedirectPublishing.cpp +++ b/src/MqttHandleVedirect.cpp @@ -3,19 +3,19 @@ * Copyright (C) 2022 Helge Erbe and others */ #include "VeDirectFrameHandler.h" -#include "MqttVedirectPublishing.h" +#include "MqttHandleVedirect.h" #include "MqttSettings.h" -MqttVedirectPublishingClass MqttVedirectPublishing; +MqttHandleVedirectClass MqttHandleVedirect; -void MqttVedirectPublishingClass::init() +void MqttHandleVedirectClass::init() { } -void MqttVedirectPublishingClass::loop() +void MqttHandleVedirectClass::loop() { CONFIG_T& config = Configuration.get(); diff --git a/src/MqttPublishing.cpp b/src/MqttPublishing.cpp deleted file mode 100644 index 9bc73b057..000000000 --- a/src/MqttPublishing.cpp +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "MqttPublishing.h" -#include "MqttSettings.h" -#include "NetworkSettings.h" -#include - -MqttPublishingClass MqttPublishing; - -void MqttPublishingClass::init() -{ -} - -void MqttPublishingClass::loop() -{ - if (!MqttSettings.getConnected() || !Hoymiles.getRadio()->isIdle()) { - return; - } - - const CONFIG_T& config = Configuration.get(); - - if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { - MqttSettings.publish("dtu/uptime", String(millis() / 1000)); - MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); - MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); - if (NetworkSettings.NetworkMode() == network_mode::WiFi) { - MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); - } - - // Loop all inverters - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - - String subtopic = inv->serialString(); - - // Name - MqttSettings.publish(subtopic + "/name", inv->name()); - - if (inv->DevInfo()->getLastUpdate() > 0) { - // Bootloader Version - MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); - - // Firmware Version - MqttSettings.publish(subtopic + "/device/fwbuildversion", String(inv->DevInfo()->getFwBuildVersion())); - - // Firmware Build DateTime - char timebuffer[32]; - const time_t t = inv->DevInfo()->getFwBuildDateTime(); - std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t)); - MqttSettings.publish(subtopic + "/device/fwbuilddatetime", String(timebuffer)); - - // Hardware part number - MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); - - // Hardware version - MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); - } - - if (inv->SystemConfigPara()->getLastUpdate() > 0) { - // Limit - MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent())); - - uint16_t maxpower = inv->DevInfo()->getMaxPower(); - if (maxpower > 0) { - MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100)); - } - } - - MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); - MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing())); - - if (inv->Statistics()->getLastUpdate() > 0) { - MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000)); - } else { - MqttSettings.publish(subtopic + "/status/last_update", String(0)); - } - - uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); - if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { - _lastPublishStats[i] = lastUpdate; - - // Loop all channels - for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - if (c > 0) { - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg != nullptr) { - MqttSettings.publish(inv->serialString() + "/" + String(c) + "/name", inv_cfg->channel[c - 1].Name); - } - } - for (uint8_t f = 0; f < sizeof(_publishFields); f++) { - publishField(inv, c, _publishFields[f]); - } - } - } - - yield(); - } - - _lastPublish = millis(); - } -} - -void MqttPublishingClass::publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) -{ - String topic = getTopic(inv, channel, fieldId); - if (topic == "") { - return; - } - - MqttSettings.publish(topic, String(inv->Statistics()->getChannelFieldValue(channel, fieldId))); -} - -String MqttPublishingClass::getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) -{ - if (!inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { - return String(""); - } - - String chanName; - if (channel == 0 && fieldId == FLD_PDC) { - chanName = "powerdc"; - } else { - chanName = inv->Statistics()->getChannelFieldName(channel, fieldId); - chanName.toLowerCase(); - } - - return inv->serialString() + "/" + String(channel) + "/" + chanName; -} \ No newline at end of file diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index 516409bdb..709590535 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -3,19 +3,8 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "MqttSettings.h" +#include "MessageOutput.h" #include "Configuration.h" -#include "NetworkSettings.h" -#include -#include -#include -#include - -#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative" -#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" -#define TOPIC_SUB_POWER "power" -#define TOPIC_SUB_RESTART "restart" MqttSettingsClass::MqttSettingsClass() { @@ -25,11 +14,11 @@ void MqttSettingsClass::NetworkEvent(network_event event) { switch (event) { case network_event::NETWORK_GOT_IP: - Serial.println(F("Network connected")); + MessageOutput.println(F("Network connected")); performConnect(); break; case network_event::NETWORK_DISCONNECTED: - Serial.println(F("Network lost connection")); + MessageOutput.println(F("Network lost connection")); mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi break; default: @@ -39,45 +28,53 @@ void MqttSettingsClass::NetworkEvent(network_event event) void MqttSettingsClass::onMqttConnect(bool sessionPresent) { - Serial.println(F("Connected to MQTT.")); + MessageOutput.println(F("Connected to MQTT.")); const CONFIG_T& config = Configuration.get(); publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online); - String topic = getPrefix(); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE).c_str(), 0); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE).c_str(), 0); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0); - mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0); + for (const auto& cb : _mqttSubscribeParser.get_callbacks()) { + mqttClient->subscribe(cb.topic.c_str(), cb.qos); + } +} + +void MqttSettingsClass::subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) +{ + _mqttSubscribeParser.register_callback(topic.c_str(), qos, cb); + mqttClient->subscribe(topic.c_str(), qos); +} + +void MqttSettingsClass::unsubscribe(const String& topic) +{ + _mqttSubscribeParser.unregister_callback(topic.c_str()); + mqttClient->unsubscribe(topic.c_str()); } void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.println(F("Disconnected from MQTT.")); + MessageOutput.println(F("Disconnected from MQTT.")); - Serial.print(F("Disconnect reason:")); + MessageOutput.print(F("Disconnect reason:")); switch (reason) { case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED: - Serial.println(F("TCP_DISCONNECTED")); + MessageOutput.println(F("TCP_DISCONNECTED")); break; case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - Serial.println(F("MQTT_UNACCEPTABLE_PROTOCOL_VERSION")); + MessageOutput.println(F("MQTT_UNACCEPTABLE_PROTOCOL_VERSION")); break; case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED: - Serial.println(F("MQTT_IDENTIFIER_REJECTED")); + MessageOutput.println(F("MQTT_IDENTIFIER_REJECTED")); break; case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE: - Serial.println(F("MQTT_SERVER_UNAVAILABLE")); + MessageOutput.println(F("MQTT_SERVER_UNAVAILABLE")); break; case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS: - Serial.println(F("MQTT_MALFORMED_CREDENTIALS")); + MessageOutput.println(F("MQTT_MALFORMED_CREDENTIALS")); break; case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED: - Serial.println(F("MQTT_NOT_AUTHORIZED")); + MessageOutput.println(F("MQTT_NOT_AUTHORIZED")); break; default: - Serial.println(F("Unknown")); + MessageOutput.println(F("Unknown")); } mqttReconnectTimer.once( 2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this); @@ -85,90 +82,10 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - const CONFIG_T& config = Configuration.get(); - - Serial.print(F("Received MQTT message on topic: ")); - Serial.println(topic); - - char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics - strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* - - char* serial_str; - char* subtopic; - char* setting; - char* rest = &token_topic[strlen(config.Mqtt_Topic)]; - - serial_str = strtok_r(rest, "/", &rest); - subtopic = strtok_r(rest, "/", &rest); - setting = strtok_r(rest, "/", &rest); - - if (serial_str == NULL || subtopic == NULL || setting == NULL) { - return; - } - - uint64_t serial; - serial = strtoull(serial_str, 0, 16); - - auto inv = Hoymiles.getInverterBySerial(serial); + MessageOutput.print(F("Received MQTT message on topic: ")); + MessageOutput.println(topic); - if (inv == nullptr) { - Serial.println(F("Inverter not found")); - return; - } - - // check if subtopic is unequal cmd - if (strcmp(subtopic, "cmd")) { - return; - } - - char* strlimit = new char[len + 1]; - memcpy(strlimit, payload, len); - strlimit[len] = '\0'; - uint32_t payload_val = strtol(strlimit, NULL, 10); - delete[] strlimit; - - if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { - // Set inverter limit relative persistent - Serial.printf("Limit Persistent: %d %%\n", payload_val); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent); - - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { - // Set inverter limit absolute persistent - Serial.printf("Limit Persistent: %d W\n", payload_val); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutPersistent); - - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { - // Set inverter limit relative non persistent - Serial.printf("Limit Non-Persistent: %d %%\n", payload_val); - if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent); - } else { - Serial.println("Ignored because retained"); - } - - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { - // Set inverter limit absolute non persistent - Serial.printf("Limit Non-Persistent: %d W\n", payload_val); - if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutNonPersistent); - } else { - Serial.println("Ignored because retained"); - } - - } else if (!strcmp(setting, TOPIC_SUB_POWER)) { - // Turn inverter on or off - Serial.printf("Set inverter power to: %d\n", payload_val); - inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0); - - } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { - // Restart inverter - Serial.printf("Restart inverter\n"); - if (!properties.retain && payload_val == 1) { - inv->sendRestartControlRequest(Hoymiles.getRadio()); - } else { - Serial.println("Ignored because retained"); - } - } + _mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total); } void MqttSettingsClass::performConnect() @@ -180,7 +97,7 @@ void MqttSettingsClass::performConnect() using std::placeholders::_4; using std::placeholders::_5; using std::placeholders::_6; - Serial.println(F("Connecting to MQTT...")); + MessageOutput.println(F("Connecting to MQTT...")); const CONFIG_T& config = Configuration.get(); willTopic = getPrefix() + config.Mqtt_LwtTopic; clientId = NetworkSettings.getApName(); @@ -240,11 +157,9 @@ void MqttSettingsClass::publish(const String& subtopic, const String& payload) mqttClient->publish(topic.c_str(), 0, Configuration.get().Mqtt_Retain, payload.c_str()); } -void MqttSettingsClass::publishHass(const String& subtopic, const String& payload) +void MqttSettingsClass::publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos) { - String topic = Configuration.get().Mqtt_Hass_Topic; - topic += subtopic; - mqttClient->publish(topic.c_str(), 0, Configuration.get().Mqtt_Hass_Retain, payload.c_str()); + mqttClient->publish(topic.c_str(), qos, retain, payload.c_str()); } void MqttSettingsClass::init() diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index b975043cd..0e7e84bbc 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -4,9 +4,9 @@ */ #include "NetworkSettings.h" #include "Configuration.h" +#include "MessageOutput.h" #include "Utils.h" #include "defaults.h" -#include #ifdef OPENDTU_ETHERNET #include #endif @@ -31,30 +31,30 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) switch (event) { #ifdef OPENDTU_ETHERNET case ARDUINO_EVENT_ETH_START: - Serial.println(F("ETH start")); + MessageOutput.println(F("ETH start")); if (_networkMode == network_mode::Ethernet) { raiseEvent(network_event::NETWORK_START); } break; case ARDUINO_EVENT_ETH_STOP: - Serial.println(F("ETH stop")); + MessageOutput.println(F("ETH stop")); if (_networkMode == network_mode::Ethernet) { raiseEvent(network_event::NETWORK_STOP); } break; case ARDUINO_EVENT_ETH_CONNECTED: - Serial.println(F("ETH connected")); + MessageOutput.println(F("ETH connected")); _ethConnected = true; raiseEvent(network_event::NETWORK_CONNECTED); break; case ARDUINO_EVENT_ETH_GOT_IP: - Serial.printf("ETH got IP: %s\n", ETH.localIP().toString().c_str()); + MessageOutput.printf("ETH got IP: %s\n", ETH.localIP().toString().c_str()); if (_networkMode == network_mode::Ethernet) { raiseEvent(network_event::NETWORK_GOT_IP); } break; case ARDUINO_EVENT_ETH_DISCONNECTED: - Serial.println(F("ETH disconnected")); + MessageOutput.println(F("ETH disconnected")); _ethConnected = false; if (_networkMode == network_mode::Ethernet) { raiseEvent(network_event::NETWORK_DISCONNECTED); @@ -62,21 +62,21 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) break; #endif case ARDUINO_EVENT_WIFI_STA_CONNECTED: - Serial.println(F("WiFi connected")); + MessageOutput.println(F("WiFi connected")); if (_networkMode == network_mode::WiFi) { raiseEvent(network_event::NETWORK_CONNECTED); } break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println(F("WiFi disconnected")); + MessageOutput.println(F("WiFi disconnected")); if (_networkMode == network_mode::WiFi) { - Serial.println(F("Try reconnecting")); + MessageOutput.println(F("Try reconnecting")); WiFi.reconnect(); raiseEvent(network_event::NETWORK_DISCONNECTED); } break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.printf("WiFi got ip: %s\n", WiFi.localIP().toString().c_str()); + MessageOutput.printf("WiFi got ip: %s\n", WiFi.localIP().toString().c_str()); if (_networkMode == network_mode::WiFi) { raiseEvent(network_event::NETWORK_GOT_IP); } @@ -152,7 +152,7 @@ void NetworkSettingsClass::loop() if (_ethConnected) { if (_networkMode != network_mode::Ethernet) { // Do stuff when switching to Ethernet mode - Serial.println(F("Switch to Ethernet mode")); + MessageOutput.println(F("Switch to Ethernet mode")); _networkMode = network_mode::Ethernet; WiFi.mode(WIFI_MODE_NULL); setStaticIp(); @@ -162,7 +162,7 @@ void NetworkSettingsClass::loop() #endif if (_networkMode != network_mode::WiFi) { // Do stuff when switching to Ethernet mode - Serial.println(F("Switch to WiFi mode")); + MessageOutput.println(F("Switch to WiFi mode")); _networkMode = network_mode::WiFi; enableAdminMode(); applyConfig(); @@ -183,7 +183,7 @@ void NetworkSettingsClass::loop() // seconds, disable the internal Access Point if (adminTimeoutCounter > ADMIN_TIMEOUT) { adminEnabled = false; - Serial.println(F("Admin mode disabled")); + MessageOutput.println(F("Admin mode disabled")); setupMode(); } // It's nearly not possible to use the internal AP if the @@ -194,16 +194,16 @@ void NetworkSettingsClass::loop() connectRedoTimer = 0; } else { if (connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !forceDisconnection) { - Serial.print(F("Disable search for AP... ")); + MessageOutput.print(F("Disable search for AP... ")); WiFi.mode(WIFI_AP); - Serial.println(F("done")); + MessageOutput.println(F("done")); connectRedoTimer = 0; forceDisconnection = true; } if (connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && forceDisconnection) { - Serial.print(F("Enable search for AP... ")); + MessageOutput.print(F("Enable search for AP... ")); WiFi.mode(WIFI_AP_STA); - Serial.println(F("done")); + MessageOutput.println(F("done")); applyConfig(); connectTimeoutTimer = 0; forceDisconnection = false; @@ -221,28 +221,28 @@ void NetworkSettingsClass::applyConfig() if (!strcmp(Configuration.get().WiFi_Ssid, "")) { return; } - Serial.print(F("Configuring WiFi STA using ")); + MessageOutput.print(F("Configuring WiFi STA using ")); if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi_Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi_Password)) { - Serial.print(F("new credentials... ")); + MessageOutput.print(F("new credentials... ")); WiFi.begin( Configuration.get().WiFi_Ssid, Configuration.get().WiFi_Password); } else { - Serial.print(F("existing credentials... ")); + MessageOutput.print(F("existing credentials... ")); WiFi.begin(); } - Serial.println(F("done")); + MessageOutput.println(F("done")); setStaticIp(); } void NetworkSettingsClass::setHostname() { - Serial.print(F("Setting Hostname... ")); + MessageOutput.print(F("Setting Hostname... ")); if (_networkMode == network_mode::WiFi) { if (WiFi.hostname(getHostname())) { - Serial.println(F("done")); + MessageOutput.println(F("done")); } else { - Serial.println(F("failed")); + MessageOutput.println(F("failed")); } // Evil bad hack to get the hostname set up correctly @@ -253,9 +253,9 @@ void NetworkSettingsClass::setHostname() #ifdef OPENDTU_ETHERNET else if (_networkMode == network_mode::Ethernet) { if (ETH.setHostname(getHostname().c_str())) { - Serial.println(F("done")); + MessageOutput.println(F("done")); } else { - Serial.println(F("failed")); + MessageOutput.println(F("failed")); } } #endif @@ -265,35 +265,35 @@ void NetworkSettingsClass::setStaticIp() { if (_networkMode == network_mode::WiFi) { if (Configuration.get().WiFi_Dhcp) { - Serial.print(F("Configuring WiFi STA DHCP IP... ")); + MessageOutput.print(F("Configuring WiFi STA DHCP IP... ")); WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); - Serial.println(F("done")); + MessageOutput.println(F("done")); } else { - Serial.print(F("Configuring WiFi STA static IP... ")); + MessageOutput.print(F("Configuring WiFi STA static IP... ")); WiFi.config( IPAddress(Configuration.get().WiFi_Ip), IPAddress(Configuration.get().WiFi_Gateway), IPAddress(Configuration.get().WiFi_Netmask), IPAddress(Configuration.get().WiFi_Dns1), IPAddress(Configuration.get().WiFi_Dns2)); - Serial.println(F("done")); + MessageOutput.println(F("done")); } } #ifdef OPENDTU_ETHERNET else if (_networkMode == network_mode::Ethernet) { if (Configuration.get().WiFi_Dhcp) { - Serial.print(F("Configuring Ethernet DHCP IP... ")); + MessageOutput.print(F("Configuring Ethernet DHCP IP... ")); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); - Serial.println(F("done")); + MessageOutput.println(F("done")); } else { - Serial.print(F("Configuring Ethernet static IP... ")); + MessageOutput.print(F("Configuring Ethernet static IP... ")); ETH.config( IPAddress(Configuration.get().WiFi_Ip), IPAddress(Configuration.get().WiFi_Gateway), IPAddress(Configuration.get().WiFi_Netmask), IPAddress(Configuration.get().WiFi_Dns1), IPAddress(Configuration.get().WiFi_Dns2)); - Serial.println(F("done")); + MessageOutput.println(F("done")); } } #endif diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 857067adb..96f819f61 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -3,10 +3,9 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "defaults.h" +#include WebApiClass::WebApiClass() : _server(HTTP_PORT) @@ -34,6 +33,7 @@ void WebApiClass::init() _webApiSecurity.init(&_server); _webApiSysstatus.init(&_server); _webApiWebapp.init(&_server); + _webApiWsConsole.init(&_server); _webApiWsLive.init(&_server); _webApiWsVedirectLive.init(&_server); _webApiVedirect.init(&_server); @@ -58,6 +58,7 @@ void WebApiClass::loop() _webApiSecurity.loop(); _webApiSysstatus.loop(); _webApiWebapp.loop(); + _webApiWsConsole.loop(); _webApiWsLive.loop(); _webApiWsVedirectLive.loop(); _webApiVedirect.loop(); diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 84e6af95d..bd0874554 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -3,10 +3,10 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_config.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "WebApi.h" +#include "WebApi_errors.h" +#include #include void WebApiConfigClass::init(AsyncWebServer* server) @@ -52,6 +52,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -61,6 +62,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -71,6 +73,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -78,6 +81,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (!(root.containsKey("delete"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -85,6 +89,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (root[F("delete")].as() == false) { retMsg[F("message")] = F("Not deleted anything!"); + retMsg[F("code")] = WebApiError::ConfigNotDeleted; response->setLength(); request->send(response); return; @@ -92,6 +97,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Configuration resettet. Rebooting now..."); + retMsg[F("code")] = WebApiError::ConfigSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 1a29081ca..d55235db1 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -3,10 +3,9 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_devinfo.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Hoymiles.h" #include "WebApi.h" +#include +#include #include void WebApiDevInfoClass::init(AsyncWebServer* server) diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index bb61b0597..b53b1cf36 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -3,11 +3,11 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_dtu.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" -#include "Hoymiles.h" #include "WebApi.h" +#include "WebApi_errors.h" +#include +#include void WebApiDtuClass::init(AsyncWebServer* server) { @@ -58,6 +58,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -67,6 +68,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -77,6 +79,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -84,6 +87,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (!(root.containsKey("dtu_serial") && root.containsKey("dtu_pollinterval") && root.containsKey("dtu_palevel"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -91,6 +95,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (root[F("dtu_serial")].as() == 0) { retMsg[F("message")] = F("Serial cannot be zero!"); + retMsg[F("code")] = WebApiError::DtuSerialZero; response->setLength(); request->send(response); return; @@ -98,6 +103,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (root[F("dtu_pollinterval")].as() == 0) { retMsg[F("message")] = F("Poll interval must be greater zero!"); + retMsg[F("code")] = WebApiError::DtuPollZero; response->setLength(); request->send(response); return; @@ -105,6 +111,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (root[F("dtu_palevel")].as() > 3) { retMsg[F("message")] = F("Invalid power level setting!"); + retMsg[F("code")] = WebApiError::DtuInvalidPowerLevel; response->setLength(); request->send(response); return; @@ -120,6 +127,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 6c03fb467..c312739fd 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -3,10 +3,9 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_eventlog.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Hoymiles.h" #include "WebApi.h" +#include +#include void WebApiEventlogClass::init(AsyncWebServer* server) { diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 48984f426..798264a06 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -3,12 +3,11 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_firmware.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "Update.h" #include "WebApi.h" #include "helper.h" +#include void WebApiFirmwareClass::init(AsyncWebServer* server) { diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index a99fe7c2f..cda8f338b 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -3,13 +3,13 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_inverter.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" -#include "Hoymiles.h" -#include "MqttHassPublishing.h" +#include "MqttHandleHass.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" +#include +#include void WebApiInverterClass::init(AsyncWebServer* server) { @@ -87,6 +87,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -96,6 +97,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -106,6 +108,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -113,6 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (!(root.containsKey("serial") && root.containsKey("name"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -120,6 +124,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (root[F("serial")].as() == 0) { retMsg[F("message")] = F("Serial must be a number > 0!"); + retMsg[F("code")] = WebApiError::InverterSerialZero; response->setLength(); request->send(response); return; @@ -127,6 +132,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::InverterNameLength; + retMsg[F("param")][F("max")] = INV_MAX_NAME_STRLEN; response->setLength(); request->send(response); return; @@ -136,6 +143,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (!inverter) { retMsg[F("message")] = F("Only " STR(INV_MAX_COUNT) " inverters are supported!"); + retMsg[F("code")] = WebApiError::InverterCount; + retMsg[F("param")][F("max")] = INV_MAX_COUNT; response->setLength(); request->send(response); return; @@ -149,6 +158,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Inverter created!"); + retMsg[F("code")] = WebApiError::InverterAdded; response->setLength(); request->send(response); @@ -161,7 +171,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } } - MqttHassPublishing.forceUpdate(); + MqttHandleHass.forceUpdate(); } void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) @@ -176,6 +186,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -185,6 +196,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -195,6 +207,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -202,6 +215,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -209,6 +223,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (root[F("id")].as() > INV_MAX_COUNT - 1) { retMsg[F("message")] = F("Invalid ID specified!"); + retMsg[F("code")] = WebApiError::InverterInvalidId; response->setLength(); request->send(response); return; @@ -216,6 +231,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (root[F("serial")].as() == 0) { retMsg[F("message")] = F("Serial must be a number > 0!"); + retMsg[F("code")] = WebApiError::InverterSerialZero; response->setLength(); request->send(response); return; @@ -223,6 +239,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::InverterNameLength; + retMsg[F("param")][F("max")] = INV_MAX_NAME_STRLEN; response->setLength(); request->send(response); return; @@ -231,6 +249,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) JsonArray channelArray = root[F("channel")].as(); if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { retMsg[F("message")] = F("Invalid amount of max channel setting given!"); + retMsg[F("code")] = WebApiError::InverterInvalidMaxChannel; response->setLength(); request->send(response); return; @@ -255,6 +274,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) Configuration.write(); retMsg[F("type")] = F("success"); + retMsg[F("code")] = WebApiError::InverterChanged; retMsg[F("message")] = F("Inverter changed!"); response->setLength(); @@ -280,7 +300,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } } - MqttHassPublishing.forceUpdate(); + MqttHandleHass.forceUpdate(); } void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) @@ -295,6 +315,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -304,6 +325,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -314,6 +336,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -321,6 +344,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (!(root.containsKey("id"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -328,6 +352,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (root[F("id")].as() > INV_MAX_COUNT - 1) { retMsg[F("message")] = F("Invalid ID specified!"); + retMsg[F("code")] = WebApiError::InverterInvalidId; response->setLength(); request->send(response); return; @@ -344,9 +369,10 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Inverter deleted!"); + retMsg[F("code")] = WebApiError::InverterDeleted; response->setLength(); request->send(response); - MqttHassPublishing.forceUpdate(); + MqttHandleHass.forceUpdate(); } \ No newline at end of file diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 3195d40eb..9999c71a0 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -3,10 +3,10 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_limit.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Hoymiles.h" #include "WebApi.h" +#include "WebApi_errors.h" +#include +#include void WebApiLimitClass::init(AsyncWebServer* server) { @@ -43,11 +43,9 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) String limitStatus = "Unknown"; if (status == LastCommandSuccess::CMD_OK) { limitStatus = "Ok"; - } - else if (status == LastCommandSuccess::CMD_NOK) { + } else if (status == LastCommandSuccess::CMD_NOK) { limitStatus = "Failure"; - } - else if (status == LastCommandSuccess::CMD_PENDING) { + } else if (status == LastCommandSuccess::CMD_PENDING) { limitStatus = "Pending"; } root[serial]["limit_set_status"] = limitStatus; @@ -69,6 +67,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -78,6 +77,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -88,6 +88,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -97,6 +98,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) && root.containsKey("limit_value") && root.containsKey("limit_type"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -104,6 +106,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (root[F("serial")].as() == 0) { retMsg[F("message")] = F("Serial must be a number > 0!"); + retMsg[F("code")] = WebApiError::LimitSerialZero; response->setLength(); request->send(response); return; @@ -111,6 +114,8 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (root[F("limit_value")].as() == 0 || root[F("limit_value")].as() > 1500) { retMsg[F("message")] = F("Limit must between 1 and 1500!"); + retMsg[F("code")] = WebApiError::LimitInvalidLimit; + retMsg[F("param")][F("max")] = 1500; response->setLength(); request->send(response); return; @@ -122,6 +127,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) || (root[F("limit_type")].as() == PowerLimitControlType::RelativPersistent))) { retMsg[F("message")] = F("Invalid type specified!"); + retMsg[F("code")] = WebApiError::LimitInvalidType; response->setLength(); request->send(response); return; @@ -134,6 +140,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg[F("message")] = F("Invalid inverter specified!"); + retMsg[F("code")] = WebApiError::LimitInvalidInverter; response->setLength(); request->send(response); return; @@ -143,6 +150,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 9a1768223..387fbe7c0 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -4,8 +4,9 @@ */ #include "WebApi_maintenance.h" -#include "AsyncJson.h" #include "WebApi.h" +#include "WebApi_errors.h" +#include void WebApiMaintenanceClass::init(AsyncWebServer* server) { @@ -32,6 +33,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -41,6 +43,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (json.length() > MQTT_JSON_DOC_SIZE) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -51,6 +54,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -58,6 +62,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (!(root.containsKey("reboot"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -66,6 +71,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (root[F("reboot")].as()) { retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Reboot triggered!"); + retMsg[F("code")] = WebApiError::MaintenanceRebootTriggered; response->setLength(); request->send(response); @@ -75,6 +81,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) ESP.restart(); } else { retMsg[F("message")] = F("Reboot cancled!"); + retMsg[F("code")] = WebApiError::MaintenanceRebootCancled; response->setLength(); request->send(response); diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 5431024af..2bcf79c10 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -3,13 +3,13 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_mqtt.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" -#include "MqttHassPublishing.h" +#include "MqttHandleHass.h" #include "MqttSettings.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" +#include void WebApiMqttClass::init(AsyncWebServer* server) { @@ -102,6 +102,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -111,6 +112,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (json.length() > MQTT_JSON_DOC_SIZE) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -121,6 +123,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -144,6 +147,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_hass_topic") && root.containsKey("mqtt_hass_individualpanels"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -152,6 +156,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_enabled")].as()) { if (root[F("mqtt_hostname")].as().length() == 0 || root[F("mqtt_hostname")].as().length() > MQTT_MAX_HOSTNAME_STRLEN) { retMsg[F("message")] = F("MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::MqttHostnameLength; + retMsg[F("param")][F("max")] = MQTT_MAX_HOSTNAME_STRLEN; response->setLength(); request->send(response); return; @@ -159,18 +165,24 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_username")].as().length() > MQTT_MAX_USERNAME_STRLEN) { retMsg[F("message")] = F("Username must not longer then " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttUsernameLength; + retMsg[F("param")][F("max")] = MQTT_MAX_USERNAME_STRLEN; response->setLength(); request->send(response); return; } if (root[F("mqtt_password")].as().length() > MQTT_MAX_PASSWORD_STRLEN) { retMsg[F("message")] = F("Password must not longer then " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttPasswordLength; + retMsg[F("param")][F("max")] = MQTT_MAX_PASSWORD_STRLEN; response->setLength(); request->send(response); return; } if (root[F("mqtt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg[F("message")] = F("Topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttTopicLength; + retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN; response->setLength(); request->send(response); return; @@ -178,6 +190,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_topic")].as().indexOf(' ') != -1) { retMsg[F("message")] = F("Topic must not contain space characters!"); + retMsg[F("code")] = WebApiError::MqttTopicCharacter; response->setLength(); request->send(response); return; @@ -185,6 +198,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (!root[F("mqtt_topic")].as().endsWith("/")) { retMsg[F("message")] = F("Topic must end with slash (/)!"); + retMsg[F("code")] = WebApiError::MqttTopicTrailingSlash; response->setLength(); request->send(response); return; @@ -192,6 +206,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_port")].as() == 0 || root[F("mqtt_port")].as() > 65535) { retMsg[F("message")] = F("Port must be a number between 1 and 65535!"); + retMsg[F("code")] = WebApiError::MqttPort; response->setLength(); request->send(response); return; @@ -199,6 +214,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_root_ca_cert")].as().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN) { retMsg[F("message")] = F("Certificate must not longer then " STR(MQTT_MAX_ROOT_CA_CERT_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttCertificateLength; + retMsg[F("param")][F("max")] = MQTT_MAX_ROOT_CA_CERT_STRLEN; response->setLength(); request->send(response); return; @@ -206,6 +223,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_lwt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg[F("message")] = F("LWT topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttLwtTopicLength; + retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN; response->setLength(); request->send(response); return; @@ -213,6 +232,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_lwt_topic")].as().indexOf(' ') != -1) { retMsg[F("message")] = F("LWT topic must not contain space characters!"); + retMsg[F("code")] = WebApiError::MqttLwtTopicCharacter; response->setLength(); request->send(response); return; @@ -220,6 +240,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_lwt_online")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { retMsg[F("message")] = F("LWT online value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttLwtOnlineLength; + retMsg[F("param")][F("max")] = MQTT_MAX_LWTVALUE_STRLEN; response->setLength(); request->send(response); return; @@ -227,6 +249,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_lwt_offline")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { retMsg[F("message")] = F("LWT offline value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttLwtOfflineLength; + retMsg[F("param")][F("max")] = MQTT_MAX_LWTVALUE_STRLEN; response->setLength(); request->send(response); return; @@ -234,6 +258,9 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_publish_interval")].as() < 5 || root[F("mqtt_publish_interval")].as() > 65535) { retMsg[F("message")] = F("Publish interval must be a number between 5 and 65535!"); + retMsg[F("code")] = WebApiError::MqttPublishInterval; + retMsg[F("param")][F("min")] = 5; + retMsg[F("param")][F("max")] = 65535; response->setLength(); request->send(response); return; @@ -242,6 +269,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_hass_enabled")].as()) { if (root[F("mqtt_hass_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg[F("message")] = F("Hass topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + retMsg[F("code")] = WebApiError::MqttHassTopicLength; + retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN; response->setLength(); request->send(response); return; @@ -249,6 +278,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) if (root[F("mqtt_hass_topic")].as().indexOf(' ') != -1) { retMsg[F("message")] = F("Hass topic must not contain space characters!"); + retMsg[F("code")] = WebApiError::MqttHassTopicCharacter; response->setLength(); request->send(response); return; @@ -279,12 +309,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); MqttSettings.performReconnect(); - MqttHassPublishing.forceUpdate(); + MqttHandleHass.forceUpdate(); } String WebApiMqttClass::getRootCaCertInfo(const char* cert) diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index c5a34846a..9f930eaef 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -3,12 +3,12 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_network.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "NetworkSettings.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" +#include void WebApiNetworkClass::init(AsyncWebServer* server) { @@ -91,6 +91,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -100,6 +101,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -110,6 +112,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -117,6 +120,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (!(root.containsKey("ssid") && root.containsKey("password") && root.containsKey("hostname") && root.containsKey("dhcp") && root.containsKey("ipaddress") && root.containsKey("netmask") && root.containsKey("gateway") && root.containsKey("dns1") && root.containsKey("dns2"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -125,6 +129,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) IPAddress ipaddress; if (!ipaddress.fromString(root[F("ipaddress")].as())) { retMsg[F("message")] = F("IP address is invalid!"); + retMsg[F("code")] = WebApiError::NetworkIpInvalid; response->setLength(); request->send(response); return; @@ -132,6 +137,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) IPAddress netmask; if (!netmask.fromString(root[F("netmask")].as())) { retMsg[F("message")] = F("Netmask is invalid!"); + retMsg[F("code")] = WebApiError::NetworkNetmaskInvalid; response->setLength(); request->send(response); return; @@ -139,6 +145,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) IPAddress gateway; if (!gateway.fromString(root[F("gateway")].as())) { retMsg[F("message")] = F("Gateway is invalid!"); + retMsg[F("code")] = WebApiError::NetworkGatewayInvalid; response->setLength(); request->send(response); return; @@ -146,6 +153,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) IPAddress dns1; if (!dns1.fromString(root[F("dns1")].as())) { retMsg[F("message")] = F("DNS Server IP 1 is invalid!"); + retMsg[F("code")] = WebApiError::NetworkDns1Invalid; response->setLength(); request->send(response); return; @@ -153,6 +161,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) IPAddress dns2; if (!dns2.fromString(root[F("dns2")].as())) { retMsg[F("message")] = F("DNS Server IP 2 is invalid!"); + retMsg[F("code")] = WebApiError::NetworkDns2Invalid; response->setLength(); request->send(response); return; @@ -212,6 +221,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 4c3715473..53a92e87a 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -3,12 +3,12 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_ntp.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "NtpSettings.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" +#include void WebApiNtpClass::init(AsyncWebServer* server) { @@ -85,6 +85,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -94,6 +95,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -104,6 +106,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -111,6 +114,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -118,6 +122,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (root[F("ntp_server")].as().length() == 0 || root[F("ntp_server")].as().length() > NTP_MAX_SERVER_STRLEN) { retMsg[F("message")] = F("NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::NtpServerLength; + retMsg[F("param")][F("max")] = NTP_MAX_SERVER_STRLEN; response->setLength(); request->send(response); return; @@ -125,6 +131,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (root[F("ntp_timezone")].as().length() == 0 || root[F("ntp_timezone")].as().length() > NTP_MAX_TIMEZONE_STRLEN) { retMsg[F("message")] = F("Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::NtpTimezoneLength; + retMsg[F("param")][F("max")] = NTP_MAX_TIMEZONE_STRLEN; response->setLength(); request->send(response); return; @@ -132,6 +140,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) if (root[F("ntp_timezone_descr")].as().length() == 0 || root[F("ntp_timezone_descr")].as().length() > NTP_MAX_TIMEZONEDESCR_STRLEN) { retMsg[F("message")] = F("Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::NtpTimezoneDescriptionLength; + retMsg[F("param")][F("max")] = NTP_MAX_TIMEZONEDESCR_STRLEN; response->setLength(); request->send(response); return; @@ -145,6 +155,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); @@ -192,6 +203,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -201,6 +213,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -211,6 +224,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -223,13 +237,17 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) && root.containsKey("minute") && root.containsKey("second"))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; } if (root[F("year")].as() < 2022 || root[F("year")].as() > 2100) { - retMsg[F("message")] = F("Year must be a number between 1 and 2100!"); + retMsg[F("message")] = F("Year must be a number between 2022 and 2100!"); + retMsg[F("code")] = WebApiError::NtpYearInvalid; + retMsg[F("param")][F("min")] = 2022; + retMsg[F("param")][F("max")] = 2100; response->setLength(); request->send(response); return; @@ -237,6 +255,9 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (root[F("month")].as() < 1 || root[F("month")].as() > 12) { retMsg[F("message")] = F("Month must be a number between 1 and 12!"); + retMsg[F("code")] = WebApiError::NtpMonthInvalid; + retMsg[F("param")][F("min")] = 1; + retMsg[F("param")][F("max")] = 12; response->setLength(); request->send(response); return; @@ -244,6 +265,9 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (root[F("day")].as() < 1 || root[F("day")].as() > 31) { retMsg[F("message")] = F("Day must be a number between 1 and 31!"); + retMsg[F("code")] = WebApiError::NtpDayInvalid; + retMsg[F("param")][F("min")] = 1; + retMsg[F("param")][F("max")] = 31; response->setLength(); request->send(response); return; @@ -251,6 +275,9 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (root[F("hour")].as() > 23) { retMsg[F("message")] = F("Hour must be a number between 0 and 23!"); + retMsg[F("code")] = WebApiError::NtpHourInvalid; + retMsg[F("param")][F("min")] = 0; + retMsg[F("param")][F("max")] = 23; response->setLength(); request->send(response); return; @@ -258,6 +285,9 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (root[F("minute")].as() > 59) { retMsg[F("message")] = F("Minute must be a number between 0 and 59!"); + retMsg[F("code")] = WebApiError::NtpMinuteInvalid; + retMsg[F("param")][F("min")] = 0; + retMsg[F("param")][F("max")] = 59; response->setLength(); request->send(response); return; @@ -265,6 +295,9 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) if (root[F("second")].as() > 59) { retMsg[F("message")] = F("Second must be a number between 0 and 59!"); + retMsg[F("code")] = WebApiError::NtpSecondInvalid; + retMsg[F("param")][F("min")] = 0; + retMsg[F("param")][F("max")] = 59; response->setLength(); request->send(response); return; @@ -285,6 +318,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Time updated!"); + retMsg[F("code")] = WebApiError::NtpTimeUpdated; response->setLength(); request->send(response); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 22591c054..97e4aba64 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -3,10 +3,10 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_power.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Hoymiles.h" #include "WebApi.h" +#include "WebApi_errors.h" +#include +#include void WebApiPowerClass::init(AsyncWebServer* server) { @@ -62,6 +62,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -71,6 +72,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -81,6 +83,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -89,6 +92,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (!(root.containsKey("serial") && (root.containsKey("power") || root.containsKey("restart")))) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -96,6 +100,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (root[F("serial")].as() == 0) { retMsg[F("message")] = F("Serial must be a number > 0!"); + retMsg[F("code")] = WebApiError::PowerSerialZero; response->setLength(); request->send(response); return; @@ -105,6 +110,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg[F("message")] = F("Invalid inverter specified!"); + retMsg[F("code")] = WebApiError::PowerInvalidInverter; response->setLength(); request->send(response); return; @@ -121,6 +127,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index e5346e2d9..45f23ff6a 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -1,8 +1,12 @@ -#include "WebApi_prometheus.h" +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_prometheus.h" #include "Configuration.h" -#include "Hoymiles.h" #include "NetworkSettings.h" +#include void WebApiPrometheusClass::init(AsyncWebServer* server) { @@ -28,7 +32,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print(F("# HELP opendtu_platform Platform info\n")); stream->print(F("# TYPE opendtu_platform gauge\n")); - stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), WiFi.macAddress().c_str()); + stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), NetworkSettings.macAddress().c_str()); stream->print(F("# HELP opendtu_uptime Uptime in seconds\n")); stream->print(F("# TYPE opendtu_uptime counter\n")); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index c345d3859..91f09279a 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -3,11 +3,11 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_security.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" +#include void WebApiSecurityClass::init(AsyncWebServer* server) { @@ -53,6 +53,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -62,6 +63,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -72,6 +74,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -80,6 +83,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) if (!root.containsKey("password") && root.containsKey("allow_readonly")) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; @@ -87,6 +91,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) if (root[F("password")].as().length() < 8 || root[F("password")].as().length() > WIFI_MAX_PASSWORD_STRLEN) { retMsg[F("message")] = F("Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"); + retMsg[F("code")] = WebApiError::SecurityPasswordLength; + retMsg[F("param")][F("max")] = WIFI_MAX_PASSWORD_STRLEN; response->setLength(); request->send(response); return; @@ -99,6 +105,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); @@ -114,6 +121,7 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Authentication successfull!"); + retMsg[F("code")] = WebApiError::SecurityAuthSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index ce7c21a20..ca4cebe36 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -3,11 +3,10 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_sysstatus.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" #include "Configuration.h" #include "NetworkSettings.h" #include "WebApi.h" +#include #include #include #include diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index cb5b2ed5f..5c880f8c5 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -8,6 +8,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "WebApi.h" +#include "WebApi_errors.h" #include "helper.h" void WebApiVedirectClass::init(AsyncWebServer* server) @@ -73,6 +74,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) if (!request->hasParam("data", true)) { retMsg[F("message")] = F("No values found!"); + retMsg[F("code")] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -82,6 +84,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) if (json.length() > 1024) { retMsg[F("message")] = F("Data too large!"); + retMsg[F("code")] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -92,6 +95,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) if (error) { retMsg[F("message")] = F("Failed to parse data!"); + retMsg[F("code")] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -99,13 +103,18 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) if (!(root.containsKey("vedirect_enabled") && root.containsKey("vedirect_pollinterval") && root.containsKey("vedirect_updatesonly")) ) { retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; } if (root[F("vedirect_pollinterval")].as() == 0) { - retMsg[F("message")] = F("Poll interval must be greater zero!"); + retMsg[F("message")] = F("Poll interval must be a number between 5 and 65535!"); + retMsg[F("code")] = WebApiError::MqttPublishInterval; + retMsg[F("param")][F("min")] = 5; + retMsg[F("param")][F("max")] = 65535; + response->setLength(); request->send(response); return; @@ -119,6 +128,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + retMsg[F("code")] = WebApiError::GenericSuccess; response->setLength(); request->send(response); diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp new file mode 100644 index 000000000..2837fc39d --- /dev/null +++ b/src/WebApi_ws_console.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_ws_console.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "defaults.h" + +WebApiWsConsoleClass::WebApiWsConsoleClass() + : _ws("/console") +{ +} + +void WebApiWsConsoleClass::init(AsyncWebServer* server) +{ + _server = server; + _server->addHandler(&_ws); + MessageOutput.register_ws_output(&_ws); +} + +void WebApiWsConsoleClass::loop() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + if (millis() - _lastWsCleanup > 1000) { + _ws.cleanupClients(); + + if (Configuration.get().Security_AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + } + + _lastWsCleanup = millis(); + } +} \ No newline at end of file diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 0027c8f23..ae1ed1881 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -3,10 +3,11 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_ws_live.h" -#include "AsyncJson.h" #include "Configuration.h" +#include "MessageOutput.h" #include "WebApi.h" #include "defaults.h" +#include WebApiWsLiveClass::WebApiWsLiveClass() : _ws("/livedata") @@ -198,11 +199,11 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC 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); } } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index ef3e67cf0..f7ee21ced 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -5,6 +5,7 @@ #include "WebApi_ws_vedirect_live.h" #include "AsyncJson.h" #include "Configuration.h" +#include "MessageOutput.h" #include "WebApi.h" #include "defaults.h" @@ -125,10 +126,12 @@ void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWe 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); } } diff --git a/src/main.cpp b/src/main.cpp index 5e51c6e95..9fbb5647b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,11 +3,12 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Configuration.h" -#include "Hoymiles.h" +#include "MessageOutput.h" #include "VeDirectFrameHandler.h" -#include "MqttHassPublishing.h" -#include "MqttPublishing.h" -#include "MqttVedirectPublishing.h" +#include "MqttHandleDtu.h" +#include "MqttHandleHass.h" +#include "MqttHandleInverter.h" +#include "MqttHandleVedirect.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -15,6 +16,7 @@ #include "WebApi.h" #include "defaults.h" #include +#include #include void setup() @@ -23,98 +25,100 @@ void setup() Serial.begin(SERIAL_BAUDRATE); while (!Serial) yield(); - Serial.println(); - Serial.println(F("Starting OpenDTU")); + MessageOutput.println(); + MessageOutput.println(F("Starting OpenDTU")); // Initialize file system - Serial.print(F("Initialize FS... ")); + MessageOutput.print(F("Initialize FS... ")); if (!LittleFS.begin(false)) { // Do not format if mount failed - Serial.print(F("failed... trying to format...")); + MessageOutput.print(F("failed... trying to format...")); if (!LittleFS.begin(true)) { - Serial.print("success"); + MessageOutput.print("success"); } else { - Serial.print("failed"); + MessageOutput.print("failed"); } } else { - Serial.println(F("done")); + MessageOutput.println(F("done")); } // Read configuration values - Serial.print(F("Reading configuration... ")); + MessageOutput.print(F("Reading configuration... ")); if (!Configuration.read()) { - Serial.print(F("initializing... ")); + MessageOutput.print(F("initializing... ")); Configuration.init(); if (Configuration.write()) { - Serial.print(F("written... ")); + MessageOutput.print(F("written... ")); } else { - Serial.print(F("failed... ")); + MessageOutput.print(F("failed... ")); } } if (Configuration.get().Cfg_Version != CONFIG_VERSION) { - Serial.print(F("migrated... ")); + MessageOutput.print(F("migrated... ")); Configuration.migrate(); } - Serial.println(F("done")); + MessageOutput.println(F("done")); // Initialize WiFi - Serial.print(F("Initialize Network... ")); + MessageOutput.print(F("Initialize Network... ")); NetworkSettings.init(); - Serial.println(F("done")); + MessageOutput.println(F("done")); NetworkSettings.applyConfig(); // Initialize NTP - Serial.print(F("Initialize NTP... ")); + MessageOutput.print(F("Initialize NTP... ")); NtpSettings.init(); - Serial.println(F("done")); + MessageOutput.println(F("done")); // Initialize MqTT - Serial.print(F("Initialize MqTT... ")); + MessageOutput.print(F("Initialize MqTT... ")); MqttSettings.init(); - MqttPublishing.init(); - MqttVedirectPublishing.init(); - MqttHassPublishing.init(); - Serial.println(F("done")); + MqttHandleDtu.init(); + MqttHandleInverter.init(); + MqttHandleVedirect.init(); + MqttHandleHass.init(); + MessageOutput.println(F("done")); // Initialize WebApi - Serial.print(F("Initialize WebApi... ")); + MessageOutput.print(F("Initialize WebApi... ")); WebApi.init(); - Serial.println(F("done")); + MessageOutput.println(F("done")); // Check for default DTU serial - Serial.print(F("Check for default DTU serial... ")); + MessageOutput.print(F("Check for default DTU serial... ")); CONFIG_T& config = Configuration.get(); if (config.Dtu_Serial == DTU_SERIAL) { - Serial.print(F("generate serial based on ESP chip id: ")); + MessageOutput.print(F("generate serial based on ESP chip id: ")); uint64_t dtuId = Utils::generateDtuSerial(); - Serial.printf("%0x%08x... ", + MessageOutput.printf("%0x%08x... ", ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), ((uint32_t)(dtuId & 0xFFFFFFFF))); config.Dtu_Serial = dtuId; Configuration.write(); } - Serial.println(F("done")); + MessageOutput.println(F("done")); // Initialize inverter communication - Serial.print(F("Initialize Hoymiles interface... ")); + MessageOutput.print(F("Initialize Hoymiles interface... ")); SPIClass* spiClass = new SPIClass(HSPI); spiClass->begin(HOYMILES_PIN_SCLK, HOYMILES_PIN_MISO, HOYMILES_PIN_MOSI, HOYMILES_PIN_CS); + Hoymiles.setMessageOutput(&MessageOutput); Hoymiles.init(spiClass, HOYMILES_PIN_CE, HOYMILES_PIN_IRQ); - Serial.println(F(" Setting radio PA level... ")); + MessageOutput.println(F(" Setting radio PA level... ")); Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); - Serial.println(F(" Setting DTU serial... ")); + MessageOutput.println(F(" Setting DTU serial... ")); Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); - Serial.println(F(" Setting poll interval... ")); + MessageOutput.println(F(" Setting poll interval... ")); Hoymiles.setPollInterval(config.Dtu_PollInterval); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { - Serial.print(F(" Adding inverter: ")); - Serial.print(config.Inverter[i].Serial, HEX); - Serial.print(F(" - ")); - Serial.print(config.Inverter[i].Name); + MessageOutput.print(F(" Adding inverter: ")); + MessageOutput.print(config.Inverter[i].Serial, HEX); + MessageOutput.print(F(" - ")); + MessageOutput.print(config.Inverter[i].Name); auto inv = Hoymiles.addInverter( config.Inverter[i].Name, config.Inverter[i].Serial); @@ -124,16 +128,16 @@ void setup() inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); } } - Serial.println(F(" done")); + MessageOutput.println(F(" done")); } } - Serial.println(F("done")); + MessageOutput.println(F("done")); // Initialize ve.direct communication - Serial.print(F("Initialize ve.direct interface... ")); + MessageOutput.println(F("Initialize ve.direct interface... ")); VeDirect.init(); VeDirect.setPollInterval(config.Vedirect_PollInterval); - Serial.println(F("done")); + MessageOutput.println(F("done")); } void loop() @@ -146,14 +150,18 @@ void loop() VeDirect.loop(); yield(); } - MqttPublishing.loop(); + MqttHandleDtu.loop(); + yield(); + MqttHandleInverter.loop(); yield(); if (Configuration.get().Vedirect_Enabled) { - MqttVedirectPublishing.loop(); + MqttHandleVedirect.loop(); yield(); } - MqttHassPublishing.loop(); + MqttHandleHass.loop(); yield(); WebApi.loop(); yield(); + MessageOutput.loop(); + yield(); } \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index 83df423c2..6cdd4582e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,24 +17,25 @@ "mitt": "^3.0.0", "spark-md5": "^3.0.2", "vue": "^3.2.45", + "vue-i18n": "^9.2.2", "vue-router": "^4.1.6" }, "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.6", - "@types/node": "^18.11.9", + "@types/node": "^18.11.17", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^3.2.0", + "@vitejs/plugin-vue": "^4.0.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", - "eslint": "^8.28.0", + "eslint": "^8.30.0", "eslint-plugin-vue": "^9.8.0", "npm-run-all": "^4.1.5", - "sass": "^1.56.1", - "typescript": "^4.9.3", - "vite": "^3.2.4", + "sass": "^1.57.1", + "typescript": "^4.9.4", + "vite": "^4.0.3", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^2.1.1", - "vue-tsc": "^1.0.9" + "vite-plugin-css-injected-by-js": "^2.2.0", + "vue-tsc": "^1.0.18" } } diff --git a/webapp/src/App.vue b/webapp/src/App.vue index f1bb6a241..8ace89a17 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -6,14 +6,15 @@ \ No newline at end of file diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 802def3c7..ba3b1e56e 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -1,71 +1,67 @@