diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ad0a0f93..e955856a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: environments: ${{ steps.envs.outputs.environments }} build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: @@ -93,18 +93,27 @@ jobs: python -m pip install --upgrade pip pip install --upgrade platformio setuptools + - name: Enable Corepack + run: | + cd webapp + corepack enable + - name: Setup Node.js and yarn uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: | + cd webapp + yarn install --frozen-lockfile - name: Build WebApp - run: yarn --cwd webapp build + run: | + cd webapp + yarn build - name: Build firmware run: pio run -e ${{ matrix.environment }} @@ -130,7 +139,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/2') steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get tags run: git fetch --force --tags origin diff --git a/.github/workflows/config/release-notes-config.json b/.github/workflows/config/release-notes-config.json index fa2d2db6f..a6dacfea3 100644 --- a/.github/workflows/config/release-notes-config.json +++ b/.github/workflows/config/release-notes-config.json @@ -18,6 +18,12 @@ "fix" ] }, + { + "title": "## 🌎 Web Application", + "labels": [ + "webapp" + ] + }, { "title": "## 📚 Documentation", "labels": [ diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index 6ada43cf7..6ea139dfb 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -6,8 +6,11 @@ jobs: build: runs-on: ubuntu-latest + # prevent push event from triggering if it's part of a PR + if: github.event_name != 'push' || github.event.pull_request == null + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml deleted file mode 100644 index c4d07e20d..000000000 --- a/.github/workflows/test_build.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: OpenDTU-onBattery Test Build - -on: workflow_dispatch - -jobs: - get_default_envs: - name: Gather Environments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - uses: actions/setup-python@v4 - with: - python-version: "3.9" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install --upgrade platformio - - - name: Get default environments - id: envs - run: | - echo "environments=$(pio project config --json-output | jq -cr '.[1][1][0][1]|split(",")')" >> $GITHUB_OUTPUT - - outputs: - environments: ${{ steps.envs.outputs.environments }} - - build: - name: Build Enviornments - runs-on: ubuntu-latest - needs: get_default_envs - strategy: - matrix: - environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} - steps: - - uses: actions/checkout@v3 - - - name: Get tags - run: git fetch --force --tags origin - - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Cache PlatformIO - uses: actions/cache@v3 - with: - path: ~/.platformio - key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.9" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install --upgrade platformio - - - name: Setup Node.js and yarn - uses: actions/setup-node@v3 - with: - node-version: "18" - cache: "yarn" - cache-dependency-path: "webapp/yarn.lock" - - - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile - - - name: Build WebApp - run: yarn --cwd webapp build - - - name: Build firmware - run: pio run -e ${{ matrix.environment }} - - - name: Rename Firmware - run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin - - - name: Rename Factory Firmware - run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin - - - uses: actions/upload-artifact@v3 - with: - name: opendtu-onbattery-${{ matrix.environment }} - path: | - .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin - .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin - - diff --git a/.github/workflows/yarnlint.yml b/.github/workflows/yarnlint.yml index f1c912c96..165235ff7 100644 --- a/.github/workflows/yarnlint.yml +++ b/.github/workflows/yarnlint.yml @@ -6,17 +6,26 @@ jobs: build: runs-on: ubuntu-latest + # prevent push event from triggering if it's part of a PR + if: github.event_name != 'push' || github.event.pull_request == null + + defaults: + run: + working-directory: webapp + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable - name: Setup Node.js and yarn - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: yarn install --frozen-lockfile - name: Linting - run: yarn --cwd webapp lint \ No newline at end of file + run: yarn lint diff --git a/.github/workflows/yarnprettier.yml b/.github/workflows/yarnprettier.yml new file mode 100644 index 000000000..d288c8b90 --- /dev/null +++ b/.github/workflows/yarnprettier.yml @@ -0,0 +1,31 @@ +name: Yarn Prettier + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + # prevent push event from triggering if it's part of a PR + if: github.event_name != 'push' || github.event.pull_request == null + + defaults: + run: + working-directory: webapp + + steps: + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable + - name: Setup Node.js and yarn + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "yarn" + cache-dependency-path: "webapp/yarn.lock" + + - name: Install WebApp dependencies + run: yarn install --frozen-lockfile + + - name: Check Formatting + run: yarn prettier --check src/ diff --git a/docs/DeviceProfiles/opendtu_fusion.json b/docs/DeviceProfiles/opendtu_fusion.json index cf20772da..b74058a44 100644 --- a/docs/DeviceProfiles/opendtu_fusion.json +++ b/docs/DeviceProfiles/opendtu_fusion.json @@ -197,6 +197,18 @@ "en": 38, "cs": 37 }, + "cmt": { + "clk": -1, + "cs": -1, + "fcs": -1, + "sdio": -1, + "gpio2": -1, + "gpio3": -1 + }, + "led": { + "led0": 17, + "led1": 18 + }, "w5500": { "sclk": 39, "mosi": 40, @@ -208,6 +220,14 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A and W5500 ethernet", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, "cmt": { "clk": 6, "cs": 4, @@ -216,6 +236,10 @@ "gpio2": 3, "gpio3": 8 }, + "led": { + "led0": 17, + "led1": 18 + }, "w5500": { "sclk": 39, "mosi": 40, diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index 0b34315a8..7c0db3589 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -62,7 +62,7 @@ class NetworkSettingsClass { void setStaticIp(); void handleMDNS(); void setupMode(); - void NetworkEvent(const WiFiEvent_t event); + void NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info); Task _loopTask; @@ -86,4 +86,4 @@ class NetworkSettingsClass { bool _spiEth = false; }; -extern NetworkSettingsClass NetworkSettings; \ No newline at end of file +extern NetworkSettingsClass NetworkSettings; diff --git a/include/PinMapping.h b/include/PinMapping.h index 2cf44d3df..0603b4c4d 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -67,6 +67,8 @@ struct PinMapping_t { int8_t powermeter_rx; int8_t powermeter_tx; int8_t powermeter_dere; + int8_t powermeter_rxen; + int8_t powermeter_txen; }; class PinMappingClass { diff --git a/include/RestartHelper.h b/include/RestartHelper.h new file mode 100644 index 000000000..80f5f6758 --- /dev/null +++ b/include/RestartHelper.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class RestartHelperClass { +public: + RestartHelperClass(); + void init(Scheduler& scheduler); + void triggerRestart(); + +private: + void loop(); + + Task _rebootTask; +}; + +extern RestartHelperClass RestartHelper; diff --git a/include/Utils.h b/include/Utils.h index fa09874ef..3058a0d8f 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -10,7 +10,6 @@ class Utils { static uint32_t getChipId(); static uint64_t generateDtuSerial(); static int getTimezoneOffset(); - static void restartDtu(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index fe7e3ee28..e12aad91a 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -4,6 +4,7 @@ */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_1CH.h" #include "inverters/HERF_2CH.h" #include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" @@ -144,6 +145,7 @@ void HoymilesClass::loop() if (inv->getClearEventlogOnMidnight()) { inv->EventLog()->clearBuffer(); } + inv->resetRadioStats(); } lastWeekDay = currentWeekDay; @@ -173,6 +175,8 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, c i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_1CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_2CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_4CH::isValidSerial(serial)) { diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 7534dcbed..7b3d4f32e 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -66,16 +66,25 @@ void HoymilesRadio::handleReceivedPackage() } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); + // Statistics: Count RX Fail No Answer + inv->RadioStats.RxFailNoAnswer++; + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { Hoymiles.getMessageOutput()->println("Retransmit timeout"); + // Statistics: Count RX Fail Partial Answer + inv->RadioStats.RxFailPartialAnswer++; + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { Hoymiles.getMessageOutput()->println("Packet handling error"); + // Statistics: Count RX Fail Corrupt Data + inv->RadioStats.RxFailCorruptData++; + _commandQueue.pop(); _busyFlag = false; @@ -83,17 +92,24 @@ void HoymilesRadio::handleReceivedPackage() // Perform Retransmit Hoymiles.getMessageOutput()->print("Request retransmit: "); Hoymiles.getMessageOutput()->println(verifyResult); + // Statistics: Count TX Re-Request Fragment + inv->RadioStats.TxReRequestFragment++; + sendRetransmitPacket(verifyResult); } else { // Successful received all packages Hoymiles.getMessageOutput()->println("Success"); + // Statistics: Count RX Success + inv->RadioStats.RxSuccess++; + _commandQueue.pop(); _busyFlag = false; } } else { // If inverter was not found, assume the command is invalid Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); + // Statistics: Count RX Fail Unknown Data _commandQueue.pop(); _busyFlag = false; } @@ -105,6 +121,9 @@ void HoymilesRadio::handleReceivedPackage() auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); if (nullptr != inv) { inv->clearRxFragmentBuffer(); + // Statistics: TX Requests + inv->RadioStats.TxRequestData++; + sendEsbPacket(*cmd); } else { Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.cpp b/lib/Hoymiles/src/inverters/HERF_1CH.cpp new file mode 100644 index 000000000..49531d99c --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.cpp @@ -0,0 +1,55 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_1CH.h" + +static const byteAssign_t byteAssignment[] = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 40, 2, 10, false, 1 }, // to be verified + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, // to be verified + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, // to be verified + + { TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 }, + { TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 }, + { TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 }, + { TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 } +}; + +HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_1CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x284100000000 && serial <= 0x2841ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2841; +} + +String HERF_1CH::typeName() const +{ + return "HERF-300-1T"; +} + +const byteAssign_t* HERF_1CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_1CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.h b/lib/Hoymiles/src/inverters/HERF_1CH.h new file mode 100644 index 000000000..8220272e3 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_1CH : public HM_Abstract { +public: + explicit HERF_1CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index 4ad0157f5..4cbc686cd 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; - return preSerial == 0x1144 || preSerial == 0x1143; + return preSerial == 0x1144 || preSerial == 0x1143 || preSerial == 0x1410; } String HMS_2CH::typeName() const diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 68d611836..ab169696b 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -272,3 +272,8 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) return FRAGMENT_OK; } + +void InverterAbstract::resetRadioStats() +{ + RadioStats = {}; +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 2a51079ba..72ad7a8e4 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -65,6 +65,28 @@ class InverterAbstract { void addRxFragment(const uint8_t fragment[], const uint8_t len); uint8_t verifyAllFragments(CommandAbstract& cmd); + void resetRadioStats(); + + struct { + // TX Request Data + uint32_t TxRequestData; + + // TX Re-Request Fragment + uint32_t TxReRequestFragment; + + // RX Success + uint32_t RxSuccess; + + // RX Fail Partial Answer + uint32_t RxFailPartialAnswer; + + // RX Fail No Answer + uint32_t RxFailNoAnswer; + + // RX Fail Corrupt Data + uint32_t RxFailCorruptData; + } RadioStats = {}; + virtual bool sendStatsRequest() = 0; virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendDevInfoRequest() = 0; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index 8d913deb5..b55445328 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -1,15 +1,16 @@ # Class overview -| Class | Models | Serial range | -| --------------| --------------------------- | ------------ | -| HM_1CH | HM-300/350/400-1T | 1121 | -| HM_2CH | HM-600/700/800-2T | 1141 | -| HM_4CH | HM-1000/1200/1500-4T | 1161 | -| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | -| HMS_1CHv2 | HMS-500-1T v2 | 1125 | -| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 | -| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | -| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | -| HMT_6CH | HMT-1800/2250-6T | 1382 | -| HERF_2CH | HERF 800 | 2821 | -| HERF_4CH | HERF 1800 | 2801 | +| Class | Models | Serial range | +| --------------| --------------------------- | ------------- -- | +| HM_1CH | HM-300/350/400-1T | 1121 | +| HM_2CH | HM-600/700/800-2T | 1141 | +| HM_4CH | HM-1000/1200/1500-4T | 1161 | +| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | +| HMS_1CHv2 | HMS-500-1T v2 | 1125 | +| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 | +| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | +| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | +| HMT_6CH | HMT-1800/2250-6T | 1382 | +| HERF_1CH | HERF 300 | 2841 | +| HERF_2CH | HERF 800 | 2821 | +| HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/SdmEnergyMeter/SDM.cpp b/lib/SdmEnergyMeter/SDM.cpp index 4e12d3c6e..8f464824f 100644 --- a/lib/SdmEnergyMeter/SDM.cpp +++ b/lib/SdmEnergyMeter/SDM.cpp @@ -38,6 +38,15 @@ SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_ this->_rx_pin = rx_pin; this->_tx_pin = tx_pin; } +SDM::SDM(SoftwareSerial &serial, long baud, int dere_pin, int re_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) +{ + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_re_pin = re_pin; + this->_config = config; + this->_rx_pin = rx_pin; + this->_tx_pin = tx_pin; +} #else SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin) : sdmSer(serial) { this->_baud = baud; @@ -73,6 +82,9 @@ void SDM::begin(void) { if (_dere_pin != NOT_A_PIN) { pinMode(_dere_pin, OUTPUT); //set output pin mode for DE/RE pin when used (for control MAX485) } + if (_re_pin != NOT_A_PIN) { + pinMode(_re_pin, OUTPUT); // set output pin mode /RE pin when used (for control MAX485) + } dereSet(LOW); //set init state to receive from SDM -> DE Disable, /RE Enable (for control MAX485) } @@ -360,6 +372,8 @@ void SDM::flush(unsigned long _flushtime) { void SDM::dereSet(bool _state) { if (_dere_pin != NOT_A_PIN) digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) + if (_re_pin != NOT_A_PIN) + digitalWrite(_re_pin, _state); //receive from SDM -> /RE Enable (for control MAX485) } bool SDM::validChecksum(const uint8_t* data, size_t messageLength) const { diff --git a/lib/SdmEnergyMeter/SDM.h b/lib/SdmEnergyMeter/SDM.h index dd9c5c1af..005d4d981 100644 --- a/lib/SdmEnergyMeter/SDM.h +++ b/lib/SdmEnergyMeter/SDM.h @@ -23,6 +23,7 @@ #if !defined ( DERE_PIN ) #define DERE_PIN NOT_A_PIN // default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin) + #define RE_PIN NOT_A_PIN // default digital pin for control MAX485 RE line (use DERE_PIN for DE line) #endif #if defined ( USE_HARDWARESERIAL ) @@ -332,6 +333,7 @@ class SDM { #else // software serial #if defined ( ESP8266 ) || defined ( ESP32 ) // on esp8266/esp32 SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN); + SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int re_pin = RE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN); #else // on avr SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN); #endif @@ -390,6 +392,7 @@ class SDM { #endif long _baud = SDM_UART_BAUD; int _dere_pin = DERE_PIN; + int _re_pin = RE_PIN; uint16_t readingerrcode = SDM_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error uint16_t msturnaround = WAITING_TURNAROUND_DELAY; uint16_t mstimeout = RESPONSE_TIMEOUT; diff --git a/pio-scripts/compile_webapp.py b/pio-scripts/compile_webapp.py index 0ae54e769..f28de2969 100644 --- a/pio-scripts/compile_webapp.py +++ b/pio-scripts/compile_webapp.py @@ -33,22 +33,29 @@ def check_files(directories, filepaths, hash_file): print("INFO: compiling webapp (hang on, this can take a while and there might be little output)...") + # we need shell=True as on Windows, path resolution to find the yarn + # "exectuable" (a shell script) is only performed by cmd.exe, not by + # Python itself. as we are calling yarn with fixed arguments, using + # shell=True is fine. + # we need to change the working directory to the webapp directory such + # that corepack installs and uses the expected version of yarn. otherwise, + # corepack installs a copy of yarn into the repository root directory. yarn = "yarn" try: - subprocess.check_output([yarn, "--version"]) + subprocess.check_output(yarn + " --version", cwd="webapp", shell=True) except FileNotFoundError: yarn = "yarnpkg" try: - subprocess.check_output([yarn, "--version"]) + subprocess.check_output(yarn + " --version", cwd="webapp", shell=True) except FileNotFoundError: - raise Exception("it seems neither 'yarn' nor 'yarnpkg' is installed/available on your system") + raise Exception("it seems neither 'yarn' nor 'yarnpkg' is available on your system") # if these commands fail, an exception will prevent us from # persisting the current hashes => commands will be executed again - subprocess.run([yarn, "--cwd", "webapp", "install", "--frozen-lockfile"], - check=True) + subprocess.run(yarn + " install --frozen-lockfile", + cwd="webapp", check=True, shell=True) - subprocess.run([yarn, "--cwd", "webapp", "build"], check=True) + subprocess.run(yarn + " build", cwd="webapp", check=True, shell=True) with open(hash_file, 'wb') as f: pickle.dump(file_hashes, f) @@ -63,7 +70,7 @@ def main(): directories = ["webapp/src/", "webapp/public/"] files = ["webapp/index.html", "webapp/tsconfig.config.json", "webapp/tsconfig.json", "webapp/vite.config.ts", - "webapp/yarn.lock"] + "webapp/yarn.lock", "webapp/package.json"] hash_file = "webapp_dist/.hashes.pkl" check_files(directories, files, hash_file) diff --git a/platformio.ini b/platformio.ini index b7917a10e..f360c6556 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,13 +39,13 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESPAsyncWebServer @ 3.1.2 - bblanchon/ArduinoJson @ 7.1.0 + mathieucarbou/ESPAsyncWebServer @ 3.3.1 + bblanchon/ArduinoJson @ 7.2.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0 nrf24/RF24 @ 1.4.9 - olikraus/U8g2 @ 2.35.19 + olikraus/U8g2 @ 2.35.30 buelowp/sunset @ 1.1.7 - https://github.com/arkhipenko/TaskScheduler#testing + arkhipenko/TaskScheduler @ 3.8.5 https://github.com/coryjfowler/MCP_CAN_lib plerup/EspSoftwareSerial @ ^8.2.0 diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index 6c8c38c8e..df025f12c 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -35,7 +35,6 @@ void MqttHandleDtuClass::loop() MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); - MqttSettings.publish("dtu/temperature", String(CpuTemperature.read())); MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize())); MqttSettings.publish("dtu/heap/free", String(ESP.getFreeHeap())); MqttSettings.publish("dtu/heap/minfree", String(ESP.getMinFreeHeap())); @@ -44,4 +43,9 @@ void MqttHandleDtuClass::loop() MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); } + + float temperature = CpuTemperature.read(); + if (!std::isnan(temperature)) { + MqttSettings.publish("dtu/temperature", String(temperature)); + } } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 258a3cdce..4ed5eb77d 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -64,8 +64,8 @@ void MqttHandleHassClass::publishConfig() publishDtuSensor("Temperature", "temperature", "diagnostic", "mdi:thermometer", "°C", "temperature"); publishDtuSensor("Heap Size", "", "diagnostic", "mdi:memory", "Bytes", "heap/size"); publishDtuSensor("Heap Free", "", "diagnostic", "mdi:memory", "Bytes", "heap/free"); - publishDtuSensor("Largest Free Heap Block", "", "diagnostic", "mdi:memory", "Bytes", "heap/maxalloc`"); - publishDtuSensor("Lifetime Minimum Free Heap", "", "diagnostic", "mdi:memory", "Bytes", "heap/minfree`"); + publishDtuSensor("Largest Free Heap Block", "", "diagnostic", "mdi:memory", "Bytes", "heap/maxalloc"); + publishDtuSensor("Lifetime Minimum Free Heap", "", "diagnostic", "mdi:memory", "Bytes", "heap/minfree"); publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); yield(); diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index d099b4443..b90abdb60 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -50,6 +50,14 @@ void MqttHandleInverterClass::loop() // Name MqttSettings.publish(subtopic + "/name", inv->name()); + // Radio Statistics + MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData)); + MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment)); + MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess)); + MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData)); + if (inv->DevInfo()->getLastUpdate() > 0) { // Bootloader Version MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index cd3b26f34..c104fca2e 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -25,13 +25,14 @@ NetworkSettingsClass::NetworkSettingsClass() void NetworkSettingsClass::init(Scheduler& scheduler) { using std::placeholders::_1; + using std::placeholders::_2; WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.disconnect(true, true); - WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); + WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2)); if (PinMapping.isValidEthConfig()) { PinMapping_t& pin = PinMapping.get(); @@ -54,7 +55,7 @@ void NetworkSettingsClass::init(Scheduler& scheduler) _loopTask.enable(); } -void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) +void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info) { switch (event) { case ARDUINO_EVENT_ETH_START: @@ -94,7 +95,8 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) } break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - MessageOutput.println("WiFi disconnected"); + // Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141 + MessageOutput.printf("WiFi disconnected: %d\r\n", info.wifi_sta_disconnected.reason); if (_networkMode == network_mode::WiFi) { MessageOutput.println("Try reconnecting"); WiFi.disconnect(true, false); diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 840b2daa2..48ddcd117 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -170,6 +170,14 @@ #define POWERMETER_PIN_DERE -1 #endif +#ifndef POWERMETER_PIN_TXEN +#define POWERMETER_PIN_TXEN -1 +#endif + +#ifndef POWERMETER_PIN_RXEN +#define POWERMETER_PIN_RXEN -1 +#endif + #ifndef W5500_SCLK #define W5500_SCLK -1 #endif @@ -267,6 +275,8 @@ PinMappingClass::PinMappingClass() _pinMapping.powermeter_rx = POWERMETER_PIN_RX; _pinMapping.powermeter_tx = POWERMETER_PIN_TX; _pinMapping.powermeter_dere = POWERMETER_PIN_DERE; + _pinMapping.powermeter_rxen = POWERMETER_PIN_RXEN; + _pinMapping.powermeter_txen = POWERMETER_PIN_TXEN; } PinMapping_t& PinMappingClass::get() @@ -359,6 +369,8 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX; _pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX; _pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE; + _pinMapping.powermeter_rxen = doc[i]["powermeter"]["rxen"] | POWERMETER_PIN_RXEN; + _pinMapping.powermeter_txen = doc[i]["powermeter"]["txen"] | POWERMETER_PIN_TXEN; return true; } diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 6589d7fec..c96a5a26f 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -3,7 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ -#include "Utils.h" +#include "RestartHelper.h" #include "Battery.h" #include "PowerMeter.h" #include "PowerLimiter.h" @@ -588,7 +588,7 @@ bool PowerLimiterClass::updateInverter() if (_inverterUpdateTimeouts >= 20) { MessageOutput.println("[DPL::loop] restarting system since inverter is unresponsive"); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } return reset(); diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index 6c0ce82fc..a42c37e7a 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -28,8 +28,8 @@ bool PowerMeterSerialSdm::init() { const PinMapping_t& pin = PinMapping.get(); - MessageOutput.printf("[PowerMeterSerialSdm] rx = %d, tx = %d, dere = %d\r\n", - pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); + MessageOutput.printf("[PowerMeterSerialSdm] rx = %d, tx = %d, dere = %d, rxen = %d, txen = %d \r\n", + pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere, pin.powermeter_rxen, pin.powermeter_txen); if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { MessageOutput.println("[PowerMeterSerialSdm] invalid pin config for SDM " @@ -38,8 +38,17 @@ bool PowerMeterSerialSdm::init() } _upSdmSerial = std::make_unique(); - _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, + + if (pin.powermeter_rxen > -1 && pin.powermeter_txen > -1){ + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_rxen, pin.powermeter_txen, + SWSERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + } + else + { + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, SWSERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + } + _upSdm->begin(); return true; diff --git a/src/RestartHelper.cpp b/src/RestartHelper.cpp new file mode 100644 index 000000000..ab385ef6b --- /dev/null +++ b/src/RestartHelper.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "RestartHelper.h" +#include "Display_Graphic.h" +#include "Led_Single.h" +#include + +RestartHelperClass RestartHelper; + +RestartHelperClass::RestartHelperClass() + : _rebootTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&RestartHelperClass::loop, this)) +{ +} + +void RestartHelperClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_rebootTask); +} + +void RestartHelperClass::triggerRestart() +{ + _rebootTask.enable(); + _rebootTask.restart(); +} + +void RestartHelperClass::loop() +{ + if (_rebootTask.isFirstIteration()) { + LedSingle.turnAllOff(); + Display.setStatus(false); + } else { + ESP.restart(); + } +} diff --git a/src/Utils.cpp b/src/Utils.cpp index e87e3efc1..2dee2ca67 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -4,10 +4,8 @@ */ #include "Utils.h" -#include "Display_Graphic.h" -#include "Led_Single.h" #include "MessageOutput.h" -#include +#include "PinMapping.h" #include uint32_t Utils::getChipId() @@ -59,16 +57,6 @@ int Utils::getTimezoneOffset() return static_cast(difftime(rawtime, gmt)); } -void Utils::restartDtu() -{ - LedSingle.turnAllOff(); - Display.setStatus(false); - yield(); - delay(1000); - yield(); - ESP.restart(); -} - bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) { if (doc.overflowed()) { diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index d2f1d2e10..e836842e7 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -83,7 +83,7 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (root.containsKey("online")) { + if (root["online"].is()) { online = root["online"].as(); if (online) { minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE; @@ -98,7 +98,7 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) return; } - if (root.containsKey("voltage_valid")) { + if (root["voltage_valid"].is()) { if (root["voltage_valid"].as()) { if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 58) { retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !"; @@ -119,7 +119,7 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } } - if (root.containsKey("current_valid")) { + if (root["current_valid"].is()) { if (root["current_valid"].as()) { if (root["current"].as() < 0 || root["current"].as() > 60) { retMsg["message"] = "current must be in range between 0 and 60!"; @@ -186,13 +186,13 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("enabled")) || - !(root.containsKey("can_controller_frequency")) || - !(root.containsKey("auto_power_enabled")) || - !(root.containsKey("emergency_charge_enabled")) || - !(root.containsKey("voltage_limit")) || - !(root.containsKey("lower_power_limit")) || - !(root.containsKey("upper_power_limit"))) { + if (!(root["enabled"].is()) || + !(root["can_controller_frequency"].is()) || + !(root["auto_power_enabled"].is()) || + !(root["emergency_charge_enabled"].is()) || + !(root["voltage_limit"].is()) || + !(root["lower_power_limit"].is()) || + !(root["upper_power_limit"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 5965caeef..bc3081e9a 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -63,7 +63,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!root.containsKey("enabled") || !root.containsKey("provider")) { + if (!root["enabled"].is() || !root["provider"].is()) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 759b6b243..51a7aab14 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_config.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" @@ -61,7 +62,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("delete"))) { + if (!(root["delete"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -82,7 +83,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::removeAllFiles(); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) @@ -124,7 +125,7 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 29603e259..405c1dea1 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -6,7 +6,7 @@ #include "Configuration.h" #include "Display_Graphic.h" #include "PinMapping.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -116,6 +116,13 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) huaweiPinObj["cs"] = pin.huawei_cs; huaweiPinObj["power"] = pin.huawei_power; + auto powermeterPinObj = curPin["powermeter"].to(); + powermeterPinObj["rx"] = pin.powermeter_rx; + powermeterPinObj["tx"] = pin.powermeter_tx; + powermeterPinObj["dere"] = pin.powermeter_dere; + powermeterPinObj["rxen"] = pin.powermeter_rxen; + powermeterPinObj["txen"] = pin.powermeter_txen; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -133,8 +140,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("curPin") - || root.containsKey("display"))) { + if (!(root["curPin"].is() + || root["display"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -179,6 +186,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { - Utils::restartDtu(); + RestartHelper.triggerRestart(); } } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 0c038c227..7fd12271e 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -91,13 +91,13 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("pollinterval") - && root.containsKey("verbose_logging") - && root.containsKey("nrf_palevel") - && root.containsKey("cmt_palevel") - && root.containsKey("cmt_frequency") - && root.containsKey("cmt_country"))) { + if (!(root["serial"].is() + && root["pollinterval"].is() + && root["verbose_logging"].is() + && root["nrf_palevel"].is() + && root["cmt_palevel"].is() + && root["cmt_frequency"].is() + && root["cmt_country"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 6988b7fc9..1511ae590 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_firmware.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Update.h" #include "Utils.h" #include "WebApi.h" @@ -47,7 +48,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 5a8585f70..4e8fc53a3 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -95,8 +95,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("name"))) { + if (!(root["serial"].is() + && root["name"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -165,7 +165,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { + if (!(root["id"].is() + && root["serial"].is() + && root["name"].is() + && root["channel"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -281,7 +284,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id"))) { + if (!(root["id"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -323,7 +326,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("order"))) { + if (!(root["order"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 6a6c90ca4..e3f53ae05 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -64,9 +64,9 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("limit_value") - && root.containsKey("limit_type"))) { + if (!(root["serial"].is() + && root["limit_value"].is() + && root["limit_type"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 1504f9d75..1835138f5 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -4,7 +4,7 @@ */ #include "WebApi_maintenance.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include @@ -30,7 +30,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("reboot"))) { + if (!(root["reboot"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -43,7 +43,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MaintenanceRebootTriggered; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 601cc55c9..9f04d6221 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -117,30 +117,30 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("mqtt_enabled") - && root.containsKey("mqtt_verbose_logging") - && root.containsKey("mqtt_hostname") - && root.containsKey("mqtt_port") - && root.containsKey("mqtt_clientid") - && root.containsKey("mqtt_username") - && root.containsKey("mqtt_password") - && root.containsKey("mqtt_topic") - && root.containsKey("mqtt_retain") - && root.containsKey("mqtt_tls") - && root.containsKey("mqtt_tls_cert_login") - && root.containsKey("mqtt_client_cert") - && root.containsKey("mqtt_client_key") - && root.containsKey("mqtt_lwt_topic") - && root.containsKey("mqtt_lwt_online") - && root.containsKey("mqtt_lwt_offline") - && root.containsKey("mqtt_lwt_qos") - && root.containsKey("mqtt_publish_interval") - && root.containsKey("mqtt_clean_session") - && root.containsKey("mqtt_hass_enabled") - && root.containsKey("mqtt_hass_expire") - && root.containsKey("mqtt_hass_retain") - && root.containsKey("mqtt_hass_topic") - && root.containsKey("mqtt_hass_individualpanels"))) { + if (!(root["mqtt_enabled"].is() + && root["mqtt_verbose_logging"].is() + && root["mqtt_hostname"].is() + && root["mqtt_port"].is() + && root["mqtt_clientid"].is() + && root["mqtt_username"].is() + && root["mqtt_password"].is() + && root["mqtt_topic"].is() + && root["mqtt_retain"].is() + && root["mqtt_tls"].is() + && root["mqtt_tls_cert_login"].is() + && root["mqtt_client_cert"].is() + && root["mqtt_client_key"].is() + && root["mqtt_lwt_topic"].is() + && root["mqtt_lwt_online"].is() + && root["mqtt_lwt_offline"].is() + && root["mqtt_lwt_qos"].is() + && root["mqtt_publish_interval"].is() + && root["mqtt_clean_session"].is() + && root["mqtt_hass_enabled"].is() + && root["mqtt_hass_expire"].is() + && root["mqtt_hass_retain"].is() + && root["mqtt_hass_topic"].is() + && root["mqtt_hass_individualpanels"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 7fec44b2a..75275755f 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -88,16 +88,16 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - 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") - && root.containsKey("aptimeout"))) { + if (!(root["ssid"].is() + && root["password"].is() + && root["hostname"].is() + && root["dhcp"].is() + && root["ipaddress"].is() + && root["netmask"].is() + && root["gateway"].is() + && root["dns1"].is() + && root["dns2"].is() + && root["aptimeout"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index d50e0f02f..5dc874b53 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -100,11 +100,11 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("ntp_server") - && root.containsKey("ntp_timezone") - && root.containsKey("longitude") - && root.containsKey("latitude") - && root.containsKey("sunsettype"))) { + if (!(root["ntp_server"].is() + && root["ntp_timezone"].is() + && root["longitude"].is() + && root["latitude"].is() + && root["sunsettype"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -193,12 +193,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("year") - && root.containsKey("month") - && root.containsKey("day") - && root.containsKey("hour") - && root.containsKey("minute") - && root.containsKey("second"))) { + if (!(root["year"].is() + && root["month"].is() + && root["day"].is() + && root["hour"].is() + && root["minute"].is() + && root["second"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index b2b2ce42e..83e7fac6e 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -57,9 +57,9 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && (root.containsKey("power") - || root.containsKey("restart")))) { + if (!(root["serial"].is() + && (root["power"].is() + || root["restart"].is()))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -84,8 +84,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - if (root.containsKey("power")) { - uint16_t power = root["power"].as(); + if (root["power"].is()) { + bool power = root["power"].as(); inv->sendPowerControlRequest(power); } else { if (root["restart"].as()) { diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index adea6105b..89e671d8c 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -137,7 +137,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) // anyways to always include the keys accessed below. if we wanted to // support a simpler API, like only sending the "enabled" key which only // changes that key, we need to refactor all of the code below. - if (!root.containsKey("enabled")) { + if (!root["enabled"].is()) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 59297a0b3..d27fd65d1 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -82,7 +82,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("enabled") && root.containsKey("source"))) { + if (!(root["enabled"].is() && root["source"].is())) { retMsg["message"] = "Values are missing!"; response->setLength(); request->send(response); @@ -90,7 +90,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } auto checkHttpConfig = [&](JsonObject const& cfg) -> bool { - if (!cfg.containsKey("url") + if (!cfg["url"].is() || (!cfg["url"].as().startsWith("http://") && !cfg["url"].as().startsWith("https://"))) { retMsg["message"] = "URL must either start with http:// or https://!"; @@ -107,7 +107,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return false; } - if (!cfg.containsKey("timeout") + if (!cfg["timeout"].is() || cfg["timeout"].as() <= 0) { retMsg["message"] = "Timeout must be greater than 0 ms!"; response->setLength(); @@ -134,7 +134,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } - if (!valueConfig.containsKey("json_path") + if (!valueConfig["json_path"].is() || valueConfig["json_path"].as().length() == 0) { retMsg["message"] = "Json path must not be empty!"; response->setLength(); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index eb0f27d20..ddd8bb507 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -48,8 +48,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!root.containsKey("password") - && root.containsKey("allow_readonly")) { + if (!root["password"].is() + && root["allow_readonly"].is()) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 2499ebed3..653ab8ca4 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -73,9 +73,9 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!root.containsKey("vedirect_enabled") || - !root.containsKey("verbose_logging") || - !root.containsKey("vedirect_updatesonly") ) { + if (!root["vedirect_enabled"].is() || + !root["verbose_logging"].is() || + !root["vedirect_updatesonly"].is() ) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index cda20e72f..aacb9659f 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -229,6 +229,12 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std } else { root["limit_absolute"] = -1; } + root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData; + root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment; + root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess; + root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer; + root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer; + root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData; } void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) diff --git a/src/main.cpp b/src/main.cpp index 5743f4288..b9f451afd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,6 +27,7 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "RestartHelper.h" #include "Scheduler.h" #include "SunPosition.h" #include "Utils.h" @@ -173,11 +174,11 @@ void setup() Configuration.write(); } MessageOutput.println("done"); - MessageOutput.println("done"); InverterSettings.init(scheduler); Datastore.init(scheduler); + RestartHelper.init(scheduler); VictronMppt.init(scheduler); diff --git a/webapp/README.md b/webapp/README.md index d342f47d7..e038a6b90 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,27 +1,3 @@ -# OpenDTU web frontend +# Moved -You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.user.ts` beforehand (template can be found in `vite.config.ts`), all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser. - -## Project Setup - -```sh -yarn install -``` - -### Compile and Hot-Reload for Development - -```sh -yarn dev -``` - -### Type-Check, Compile and Minify for Production - -```sh -yarn build -``` - -### Lint with [ESLint](https://eslint.org/) - -```sh -yarn lint -``` +Have a look at the [OpenDTU-OnBattery documentation](https://opendtu-onbattery.net/firmware/compile_webapp/). diff --git a/webapp/package.json b/webapp/package.json index a427ce2c7..5d7c59089 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,34 +17,35 @@ "bootstrap": "^5.3.3", "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", - "sortablejs": "^1.15.2", + "sortablejs": "^1.15.3", "spark-md5": "^3.0.2", - "vue": "^3.4.35", - "vue-i18n": "^9.13.1", - "vue-router": "^4.4.2" + "vue": "^3.5.8", + "vue-i18n": "9.13.1", + "vue-router": "^4.4.5" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", - "@tsconfig/node18": "^18.2.4", + "@tsconfig/node22": "^22.0.0", "@types/bootstrap": "^5.2.10", - "@types/node": "^22.1.0", + "@types/node": "^22.5.5", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue": "^5.1.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.8.0", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^9.11.0", + "eslint-plugin-vue": "^9.28.0", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "pulltorefreshjs": "^0.1.22", "sass": "^1.77.6", - "terser": "^5.31.3", - "typescript": "^5.5.4", - "vite": "^5.3.5", + "terser": "^5.33.0", + "typescript": "^5.6.2", + "vite": "^5.4.7", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.1", - "vue-tsc": "^2.0.29" - } + "vue-tsc": "^2.1.6" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/webapp/src/components/EventLog.vue b/webapp/src/components/EventLog.vue index 9011467d0..f3c243a1f 100644 --- a/webapp/src/components/EventLog.vue +++ b/webapp/src/components/EventLog.vue @@ -1,10 +1,12 @@