From d49481097542c3b4d064acbe9283f791617b5c76 Mon Sep 17 00:00:00 2001 From: helgeerbe <59169507+helgeerbe@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:49:57 +0100 Subject: [PATCH] merge V23.12.16 (#556) * Optimize Sun data calculation * Remove not required enum * Split config struct into different sub structs * Feature: Allow configuration of LWT QoS * Made resetreason methods static * Feature: Implement offset cache for "YieldDay" Thanks to @broth-itk for the idea! Fix: #1258 #1397 * Add Esp32-Stick-PoE-A * remove broken LilyGO_T_ETH_POE config, use device profile instead * Feature: High resolution Icon and PWA (Progressive Web App) functionality Fix: #1289 * webapp: Update dependencies * Initialize TaskScheduler * Migrate SunPosition to TaskScheduler * Migrate Datastore to TaskScheduler * Migrate MqttHandleInverterTotal to TaskSchedule * Migrate MqttHandleHass to TaskScheduler * Migrate MqttHandleDtu to TaskScheduler * Migrate MqttHandleInverter to TaskScheduler * Migrate LedSingle to TaskScheduler * Migrate NetworkSettings to TaskScheduler * Migrate InverterSettings to TaskScheduler * Migrate MessageOutput to TaskScheduler * Migrate Display_Graphic to TaskScheduler * Migrate WebApi to TaskScheduler * Split InverterSettings into multiple tasks * Calculate SunPosition only every 5 seconds * Split LedSingle into multiple tasks * Upgrade espMqttClient from 1.4.5 to 1.5.0 * Doc: Correct amount of MPP-Tracker * Added HMT-1600-4T and HMT-1800-4T to DevInfoParser Fix #1524 * Adjusted inverter names for HMS-1600/1800/2000-4T * Add channel count to description of detected inverter type (DevInfoParser) * Adjust device web api endpoint for dynamic led count * Feature: Added ability to change the brightness of the LEDs Based on the idea of @moritzlerch with several modifications like pwmTable and structure * webapp: Update dependencies * Update olikraus/U8g2 from 2.35.7 to 2.35.8 * Remove not required onWebsocketEvent * Remove code nesting * Introduce several const statements * Remove not required AsyncEventSource * Doc: Added byte specification to each command * Feature: Added basic Grid Profile parser which shows the used profile and version Other values are still outstanding. * Optimize AlarmLogParser to save memory * Add libfrozen to project to create constexpr maps * Feature: First version of GridProfile Parser which shows all values contained in the profile. * webapp: Update dependencies * Apply better variable names * Remove not required casts * Add additional compiler flags to prevent errors * Add const statement to several variables * Replace NULL by nullptr * Update bblanchon/ArduinoJson from 6.21.3 to 6.21.4 * Add const keyword to method parameters * Add const keyword to methods * Use references instead of pointers whenver possible * Adjust member variable names in MqttSettings * Adjust member variable names in NetworkSettings * webapp: Update timezone database to latest version * webapp: Beautify and unify form footers * Feature: Allow setting of an inverter limit of 0% and 0W Thanks to @madmartin in #1270 * Feature: Allow links in device profiles These links will be shown on the hardware settings page. * Doc: Added hint regarding HMS-xxxx-xT-NA inverters * Feature: Added DeviceProfile for CASmo-DTU Based on #1565 * Upgrade actions/upload-artifact from v3 to v4 * Upgrade actions/download-artifact from v3 to v4 * webapp: add app.js.gz * Gridprofileparser: Added latest known values Thanks to @stefan123t and @noone2k * webapp: Fix lint errors * Feature: Add DTU to Home Assistant Auto Discovery This is based on PR 1365 from @CFenner with several fixes and optimizations * Fix: Remove debug output as it floods the console * Fix: Gridprofileparser: Add additional error handling if profile is unknown * webapp: add app.js.gz * Fix: Offset cache for "YieldDay" did not work correctly * webapp: update dependencies * webapp: add app.js.gz * Fix: yarn.lock was outdated * Fix: yarn build error * Fix: Reset Yield day correction in combination with Zero Yield Day on Midnight lead to wrong values. * Fix: Allow negative values in GridProfileParser * Correct variable name * Fix #1579: Static IP in Ethernet mode did not work correctly * Feature: Added diagram to display This is based on the idea of @Henrik-Ingenieur and was discussed in #1504 * webapp: update dependencies * webapp: add app.js.gz --------- Co-authored-by: Thomas Basler Co-authored-by: Pierre Kancir --- .github/workflows/build.yml | 4 +- .vscode/settings.json | 58 +- docs/DeviceProfiles/CASmo-DTU.json | 20 + docs/DeviceProfiles/blinkyparts_esp32.json | 48 ++ docs/DeviceProfiles/esp32_stick_poe_a.json | 25 + .../lilygo_ttgo_t-internet_poe.json | 9 + docs/DeviceProfiles/olimex_esp32_evb.json | 3 + docs/DeviceProfiles/olimex_esp32_poe.json | 9 + docs/DeviceProfiles/wt32-eth01.json | 6 + include/Battery.h | 10 +- include/Configuration.h | 285 ++++++---- include/Datastore.h | 10 +- include/Display_Graphic.h | 24 +- include/Display_Graphic_Diagram.h | 39 ++ include/Huawei_can.h | 8 +- include/InverterSettings.h | 10 +- include/Led_Single.h | 26 +- include/MessageOutput.h | 28 +- include/MqttHandleDtu.h | 8 +- include/MqttHandleHass.h | 22 +- include/MqttHandleHuawei.h | 7 +- include/MqttHandleInverter.h | 15 +- include/MqttHandleInverterTotal.h | 9 +- include/MqttHandlePowerLimiter.h | 7 +- include/MqttHandlePylontechHass.h | 7 +- include/MqttHandleVedirect.h | 9 +- include/MqttHandleVedirectHass.h | 7 +- include/MqttSettings.h | 16 +- include/NetworkSettings.h | 59 +- include/PinMapping.h | 6 +- include/PowerLimiter.h | 8 +- include/PowerMeter.h | 7 +- include/Scheduler.h | 6 + include/SunPosition.h | 27 +- include/VictronMppt.h | 8 +- include/WebApi.h | 9 +- include/WebApi_Huawei.h | 2 +- include/WebApi_battery.h | 2 +- include/WebApi_config.h | 2 +- include/WebApi_device.h | 2 +- include/WebApi_devinfo.h | 2 +- include/WebApi_dtu.h | 2 +- include/WebApi_errors.h | 1 + include/WebApi_eventlog.h | 2 +- include/WebApi_firmware.h | 2 +- include/WebApi_gridprofile.h | 3 +- include/WebApi_inverter.h | 2 +- include/WebApi_limit.h | 2 +- include/WebApi_maintenance.h | 2 +- include/WebApi_mqtt.h | 2 +- include/WebApi_network.h | 2 +- include/WebApi_ntp.h | 2 +- include/WebApi_power.h | 2 +- include/WebApi_powerlimiter.h | 2 +- include/WebApi_powermeter.h | 2 +- include/WebApi_prometheus.h | 6 +- include/WebApi_security.h | 2 +- include/WebApi_sysstatus.h | 2 +- include/WebApi_vedirect.h | 2 +- include/WebApi_webapp.h | 2 +- include/WebApi_ws_Huawei.h | 2 +- include/WebApi_ws_battery.h | 2 +- include/WebApi_ws_console.h | 4 +- include/WebApi_ws_live.h | 6 +- include/WebApi_ws_vedirect_live.h | 2 +- include/defaults.h | 6 + lib/Frozen/AUTHORS | 3 + lib/Frozen/LICENSE | 202 +++++++ lib/Frozen/README.rst | 245 +++++++++ lib/Frozen/frozen/CMakeLists.txt | 12 + lib/Frozen/frozen/algorithm.h | 198 +++++++ lib/Frozen/frozen/bits/algorithms.h | 235 ++++++++ lib/Frozen/frozen/bits/basic_types.h | 198 +++++++ lib/Frozen/frozen/bits/constexpr_assert.h | 40 ++ lib/Frozen/frozen/bits/defines.h | 66 +++ lib/Frozen/frozen/bits/elsa.h | 57 ++ lib/Frozen/frozen/bits/elsa_std.h | 41 ++ lib/Frozen/frozen/bits/exceptions.h | 39 ++ lib/Frozen/frozen/bits/hash_string.h | 28 + lib/Frozen/frozen/bits/mpl.h | 56 ++ lib/Frozen/frozen/bits/pmh.h | 254 +++++++++ lib/Frozen/frozen/bits/version.h | 30 + lib/Frozen/frozen/map.h | 357 ++++++++++++ lib/Frozen/frozen/random.h | 97 ++++ lib/Frozen/frozen/set.h | 260 +++++++++ lib/Frozen/frozen/string.h | 152 ++++++ lib/Frozen/frozen/unordered_map.h | 217 ++++++++ lib/Frozen/frozen/unordered_set.h | 181 ++++++ lib/Hoymiles/src/Hoymiles.cpp | 188 +++---- lib/Hoymiles/src/Hoymiles.h | 22 +- lib/Hoymiles/src/HoymilesRadio.cpp | 30 +- lib/Hoymiles/src/HoymilesRadio.h | 20 +- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 32 +- lib/Hoymiles/src/HoymilesRadio_CMT.h | 12 +- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 38 +- lib/Hoymiles/src/HoymilesRadio_NRF.h | 14 +- .../commands/ActivePowerControlCommand.cpp | 50 +- .../src/commands/ActivePowerControlCommand.h | 12 +- .../src/commands/AlarmDataCommand.cpp | 42 +- lib/Hoymiles/src/commands/AlarmDataCommand.h | 8 +- .../src/commands/ChannelChangeCommand.cpp | 24 +- .../src/commands/ChannelChangeCommand.h | 10 +- lib/Hoymiles/src/commands/CommandAbstract.cpp | 56 +- lib/Hoymiles/src/commands/CommandAbstract.h | 34 +- .../src/commands/DevControlCommand.cpp | 27 +- lib/Hoymiles/src/commands/DevControlCommand.h | 6 +- .../src/commands/DevInfoAllCommand.cpp | 34 +- lib/Hoymiles/src/commands/DevInfoAllCommand.h | 6 +- .../src/commands/DevInfoSimpleCommand.cpp | 34 +- .../src/commands/DevInfoSimpleCommand.h | 6 +- .../src/commands/GridOnProFilePara.cpp | 34 +- lib/Hoymiles/src/commands/GridOnProFilePara.h | 6 +- .../src/commands/MultiDataCommand.cpp | 43 +- lib/Hoymiles/src/commands/MultiDataCommand.h | 16 +- lib/Hoymiles/src/commands/ParaSetCommand.cpp | 2 +- lib/Hoymiles/src/commands/ParaSetCommand.h | 2 +- .../src/commands/PowerControlCommand.cpp | 37 +- .../src/commands/PowerControlCommand.h | 10 +- .../src/commands/RealTimeRunDataCommand.cpp | 44 +- .../src/commands/RealTimeRunDataCommand.h | 8 +- .../src/commands/RequestFrameCommand.cpp | 30 +- .../src/commands/RequestFrameCommand.h | 10 +- .../src/commands/SingleDataCommand.cpp | 19 +- lib/Hoymiles/src/commands/SingleDataCommand.h | 2 +- .../src/commands/SystemConfigParaCommand.cpp | 44 +- .../src/commands/SystemConfigParaCommand.h | 8 +- lib/Hoymiles/src/crc.cpp | 6 +- lib/Hoymiles/src/crc.h | 6 +- lib/Hoymiles/src/inverters/HMS_1CH.cpp | 10 +- lib/Hoymiles/src/inverters/HMS_1CH.h | 10 +- lib/Hoymiles/src/inverters/HMS_1CHv2.cpp | 10 +- lib/Hoymiles/src/inverters/HMS_1CHv2.h | 10 +- lib/Hoymiles/src/inverters/HMS_2CH.cpp | 10 +- lib/Hoymiles/src/inverters/HMS_2CH.h | 10 +- lib/Hoymiles/src/inverters/HMS_4CH.cpp | 12 +- lib/Hoymiles/src/inverters/HMS_4CH.h | 10 +- lib/Hoymiles/src/inverters/HMS_Abstract.cpp | 2 +- lib/Hoymiles/src/inverters/HMS_Abstract.h | 2 +- lib/Hoymiles/src/inverters/HMT_4CH.cpp | 10 +- lib/Hoymiles/src/inverters/HMT_4CH.h | 10 +- lib/Hoymiles/src/inverters/HMT_6CH.cpp | 10 +- lib/Hoymiles/src/inverters/HMT_6CH.h | 10 +- lib/Hoymiles/src/inverters/HMT_Abstract.cpp | 2 +- lib/Hoymiles/src/inverters/HMT_Abstract.h | 2 +- lib/Hoymiles/src/inverters/HM_1CH.cpp | 10 +- lib/Hoymiles/src/inverters/HM_1CH.h | 10 +- lib/Hoymiles/src/inverters/HM_2CH.cpp | 10 +- lib/Hoymiles/src/inverters/HM_2CH.h | 10 +- lib/Hoymiles/src/inverters/HM_4CH.cpp | 10 +- lib/Hoymiles/src/inverters/HM_4CH.h | 10 +- lib/Hoymiles/src/inverters/HM_Abstract.cpp | 8 +- lib/Hoymiles/src/inverters/HM_Abstract.h | 8 +- .../src/inverters/InverterAbstract.cpp | 52 +- lib/Hoymiles/src/inverters/InverterAbstract.h | 44 +- lib/Hoymiles/src/parser/AlarmLogParser.cpp | 42 +- lib/Hoymiles/src/parser/AlarmLogParser.h | 20 +- lib/Hoymiles/src/parser/DevInfoParser.cpp | 116 ++-- lib/Hoymiles/src/parser/DevInfoParser.h | 32 +- lib/Hoymiles/src/parser/GridProfileParser.cpp | 404 +++++++++++++- lib/Hoymiles/src/parser/GridProfileParser.h | 41 +- lib/Hoymiles/src/parser/Parser.cpp | 4 +- lib/Hoymiles/src/parser/Parser.h | 4 +- .../src/parser/PowerCommandParser.cpp | 8 +- lib/Hoymiles/src/parser/PowerCommandParser.h | 8 +- lib/Hoymiles/src/parser/StatisticsParser.cpp | 124 +++-- lib/Hoymiles/src/parser/StatisticsParser.h | 51 +- .../src/parser/SystemConfigParaParser.cpp | 26 +- .../src/parser/SystemConfigParaParser.h | 24 +- lib/ResetReason/src/ResetReason.cpp | 8 +- lib/ResetReason/src/ResetReason.h | 10 +- lib/TimeoutHelper/TimeoutHelper.cpp | 6 +- lib/TimeoutHelper/TimeoutHelper.h | 6 +- platformio.ini | 38 +- src/Battery.cpp | 22 +- src/BatteryStats.cpp | 2 +- src/Configuration.cpp | 516 +++++++++--------- src/Datastore.cpp | 142 ++--- src/Display_Graphic.cpp | 128 +++-- src/Display_Graphic_Diagram.cpp | 105 ++++ src/HttpPowerMeter.cpp | 4 +- src/Huawei_can.cpp | 46 +- src/InverterSettings.cpp | 59 +- src/JkBmsController.cpp | 6 +- src/Led_Single.cpp | 121 ++-- src/MessageOutput.cpp | 106 ++-- src/MqttHandlVedirectHass.cpp | 20 +- src/MqttHandleDtu.cpp | 30 +- src/MqttHandleHass.cpp | 215 ++++++-- src/MqttHandleHuawei.cpp | 15 +- src/MqttHandleInverter.cpp | 140 ++--- src/MqttHandleInverterTotal.cpp | 30 +- src/MqttHandlePowerLimiter.cpp | 11 +- src/MqttHandlePylontechHass.cpp | 56 +- src/MqttHandleVedirect.cpp | 24 +- src/MqttSettings.cpp | 120 ++-- src/NetworkSettings.cpp | 147 ++--- src/NtpSettings.cpp | 4 +- src/PinMapping.cpp | 6 +- src/PowerLimiter.cpp | 94 ++-- src/PowerMeter.cpp | 35 +- src/Scheduler.cpp | 7 + src/SunPosition.cpp | 82 ++- src/VictronMppt.cpp | 16 +- src/WebApi.cpp | 72 +-- src/WebApi_Huawei.cpp | 164 +++--- src/WebApi_battery.cpp | 26 +- src/WebApi_config.cpp | 12 +- src/WebApi_device.cpp | 61 ++- src/WebApi_devinfo.cpp | 6 +- src/WebApi_dtu.cpp | 50 +- src/WebApi_eventlog.cpp | 8 +- src/WebApi_firmware.cpp | 6 +- src/WebApi_gridprofile.cpp | 51 +- src/WebApi_inverter.cpp | 25 +- src/WebApi_limit.cpp | 18 +- src/WebApi_maintenance.cpp | 10 +- src/WebApi_mqtt.cpp | 156 +++--- src/WebApi_network.cpp | 86 +-- src/WebApi_ntp.cpp | 44 +- src/WebApi_power.cpp | 10 +- src/WebApi_powerlimiter.cpp | 92 ++-- src/WebApi_powermeter.cpp | 80 +-- src/WebApi_prometheus.cpp | 12 +- src/WebApi_security.cpp | 18 +- src/WebApi_sysstatus.cpp | 12 +- src/WebApi_vedirect.cpp | 24 +- src/WebApi_webapp.cpp | 15 +- src/WebApi_ws_Huawei.cpp | 8 +- src/WebApi_ws_battery.cpp | 8 +- src/WebApi_ws_console.cpp | 10 +- src/WebApi_ws_live.cpp | 24 +- src/WebApi_ws_vedirect_live.cpp | 10 +- src/main.cpp | 119 ++-- webapp/index.html | 1 + webapp/package.json | 32 +- webapp/public/favicon.png | Bin 682 -> 4590 bytes webapp/public/site.webmanifest | 13 + webapp/public/zones.json | 36 +- webapp/src/components/FormFooter.vue | 7 + webapp/src/components/GridProfile.vue | 82 ++- webapp/src/components/PinInfo.vue | 2 +- webapp/src/locales/de.json | 45 +- webapp/src/locales/en.json | 41 +- webapp/src/locales/fr.json | 37 +- webapp/src/types/DeviceConfig.ts | 6 + webapp/src/types/GridProfileRawdata.ts | 3 + webapp/src/types/GridProfileStatus.ts | 15 +- webapp/src/types/MqttConfig.ts | 1 + webapp/src/types/PinMapping.ts | 6 + webapp/src/views/DeviceAdminView.vue | 76 ++- webapp/src/views/DtuAdminView.vue | 4 +- webapp/src/views/HomeView.vue | 18 +- webapp/src/views/InverterAdminView.vue | 6 + webapp/src/views/MqttAdminView.vue | 22 +- webapp/src/views/NetworkAdminView.vue | 4 +- webapp/src/views/NtpAdminView.vue | 4 +- webapp/src/views/SecurityAdminView.vue | 4 +- webapp/tsconfig.config.json | 1 + webapp/yarn.lock | 413 +++++++------- webapp_dist/favicon.png | Bin 682 -> 4590 bytes webapp_dist/index.html.gz | Bin 371 -> 392 bytes webapp_dist/js/app.js.gz | Bin 195042 -> 197784 bytes webapp_dist/site.webmanifest | 13 + webapp_dist/zones.json.gz | Bin 4100 -> 4050 bytes 264 files changed, 7681 insertions(+), 2964 deletions(-) create mode 100644 docs/DeviceProfiles/CASmo-DTU.json create mode 100644 docs/DeviceProfiles/esp32_stick_poe_a.json create mode 100644 include/Display_Graphic_Diagram.h create mode 100644 include/Scheduler.h create mode 100644 lib/Frozen/AUTHORS create mode 100644 lib/Frozen/LICENSE create mode 100644 lib/Frozen/README.rst create mode 100644 lib/Frozen/frozen/CMakeLists.txt create mode 100644 lib/Frozen/frozen/algorithm.h create mode 100644 lib/Frozen/frozen/bits/algorithms.h create mode 100644 lib/Frozen/frozen/bits/basic_types.h create mode 100644 lib/Frozen/frozen/bits/constexpr_assert.h create mode 100644 lib/Frozen/frozen/bits/defines.h create mode 100644 lib/Frozen/frozen/bits/elsa.h create mode 100644 lib/Frozen/frozen/bits/elsa_std.h create mode 100644 lib/Frozen/frozen/bits/exceptions.h create mode 100644 lib/Frozen/frozen/bits/hash_string.h create mode 100644 lib/Frozen/frozen/bits/mpl.h create mode 100644 lib/Frozen/frozen/bits/pmh.h create mode 100644 lib/Frozen/frozen/bits/version.h create mode 100644 lib/Frozen/frozen/map.h create mode 100644 lib/Frozen/frozen/random.h create mode 100644 lib/Frozen/frozen/set.h create mode 100644 lib/Frozen/frozen/string.h create mode 100644 lib/Frozen/frozen/unordered_map.h create mode 100644 lib/Frozen/frozen/unordered_set.h create mode 100644 src/Display_Graphic_Diagram.cpp create mode 100644 src/Scheduler.cpp create mode 100644 webapp/public/site.webmanifest create mode 100644 webapp/src/components/FormFooter.vue create mode 100644 webapp/src/types/GridProfileRawdata.ts create mode 100644 webapp_dist/site.webmanifest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9dc0c0dff..dd88f54c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,7 @@ jobs: - 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 + - uses: actions/upload-artifact@v4 with: name: opendtu-onbattery-${{ matrix.environment }} path: | @@ -149,7 +149,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: artifacts/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 012b4c280..2ce88dd1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,62 @@ "C_Cpp.clang_format_style": "WebKit", "files.associations": { "*.tcc": "cpp", - "algorithm": "cpp" + "algorithm": "cpp", + "array": "cpp", + "atomic": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "condition_variable": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp" } } \ No newline at end of file diff --git a/docs/DeviceProfiles/CASmo-DTU.json b/docs/DeviceProfiles/CASmo-DTU.json new file mode 100644 index 000000000..0de52e1dc --- /dev/null +++ b/docs/DeviceProfiles/CASmo-DTU.json @@ -0,0 +1,20 @@ +[ + { + "name": "CASmo-DTU", + "links": [ + {"name": "Information", "url": "https://casmo.info/product-details/?product=2"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + } + } +] \ No newline at end of file diff --git a/docs/DeviceProfiles/blinkyparts_esp32.json b/docs/DeviceProfiles/blinkyparts_esp32.json index 0ee922bfe..617777f5c 100644 --- a/docs/DeviceProfiles/blinkyparts_esp32.json +++ b/docs/DeviceProfiles/blinkyparts_esp32.json @@ -1,6 +1,12 @@ [ { "name": "NRF, LEDs, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -21,6 +27,12 @@ }, { "name": "CMT, LEDs, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -49,6 +61,12 @@ }, { "name": "NRF, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -65,6 +83,12 @@ }, { "name": "CMT, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -89,6 +113,12 @@ }, { "name": "NRF, LEDs", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -104,6 +134,12 @@ }, { "name": "CMT, LEDs", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -127,6 +163,12 @@ }, { "name": "NRF", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -138,6 +180,12 @@ }, { "name": "CMT", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, diff --git a/docs/DeviceProfiles/esp32_stick_poe_a.json b/docs/DeviceProfiles/esp32_stick_poe_a.json new file mode 100644 index 000000000..aca95d6b9 --- /dev/null +++ b/docs/DeviceProfiles/esp32_stick_poe_a.json @@ -0,0 +1,25 @@ +[ + { + "name": "Esp32-Stick-PoE-A", + "links": [ + {"name": "Information", "url": "https://github.com/allexoK/Esp32-Stick-Boards-Docs"} + ], + "nrf24": { + "miso": 2, + "mosi": 15, + "clk": 14, + "irq": 34, + "en": 12, + "cs": 4 + }, + "eth": { + "enabled": true, + "phy_addr": 1, + "power": -1, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + } + } +] diff --git a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json index 538f3000b..b5bb4ace4 100644 --- a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json +++ b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json @@ -1,6 +1,9 @@ [ { "name": "LILYGO TTGO T-Internet-POE", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 2, "mosi": 15, @@ -21,6 +24,9 @@ }, { "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 12, "mosi": 4, @@ -41,6 +47,9 @@ }, { "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder, SSD1306", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 12, "mosi": 4, diff --git a/docs/DeviceProfiles/olimex_esp32_evb.json b/docs/DeviceProfiles/olimex_esp32_evb.json index 9b66926dd..ea0a8065d 100644 --- a/docs/DeviceProfiles/olimex_esp32_evb.json +++ b/docs/DeviceProfiles/olimex_esp32_evb.json @@ -1,6 +1,9 @@ [ { "name": "Olimex ESP32-EVB", + "links": [ + { "name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware" } + ], "nrf24": { "miso": 15, "mosi": 2, diff --git a/docs/DeviceProfiles/olimex_esp32_poe.json b/docs/DeviceProfiles/olimex_esp32_poe.json index e43dff245..27f8242f9 100644 --- a/docs/DeviceProfiles/olimex_esp32_poe.json +++ b/docs/DeviceProfiles/olimex_esp32_poe.json @@ -1,6 +1,9 @@ [ { "name": "Olimex ESP32-POE", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, @@ -21,6 +24,9 @@ }, { "name": "Olimex ESP32-POE with SSD1306", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, @@ -46,6 +52,9 @@ }, { "name": "Olimex ESP32-POE with SH1106", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index e8aee2312..388db2094 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -1,6 +1,9 @@ [ { "name": "WT32-ETH01", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], "nrf24": { "miso": 4, "mosi": 2, @@ -21,6 +24,9 @@ }, { "name": "WT32-ETH01 with SSD1306", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], "nrf24": { "miso": 4, "mosi": 2, diff --git a/include/Battery.h b/include/Battery.h index 7e9290348..ffb6f47d8 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "BatteryStats.h" @@ -19,12 +20,15 @@ class BatteryProvider { class BatteryClass { public: - void init(); - void loop(); + void init(Scheduler&); + void updateSettings(); std::shared_ptr getStats() const; - private: + void loop(); + + Task _loopTask; + uint32_t _lastMqttPublish = 0; mutable std::mutex _mutex; std::unique_ptr _upProvider = nullptr; diff --git a/include/Configuration.h b/include/Configuration.h index 14fc6a738..38b915bd1 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "PinMapping.h" #include #define CONFIG_FILENAME "/config.json" @@ -57,6 +58,7 @@ struct INVERTER_CONFIG_T { uint8_t ReachableThreshold; bool ZeroRuntimeDataIfUnrechable; bool ZeroYieldDayOnMidnight; + bool YieldDayCorrection; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; @@ -74,128 +76,171 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { }; struct CONFIG_T { - uint32_t Cfg_Version; - uint32_t Cfg_SaveCount; - - char WiFi_Ssid[WIFI_MAX_SSID_STRLEN + 1]; - char WiFi_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; - uint8_t WiFi_Ip[4]; - uint8_t WiFi_Netmask[4]; - uint8_t WiFi_Gateway[4]; - uint8_t WiFi_Dns1[4]; - uint8_t WiFi_Dns2[4]; - bool WiFi_Dhcp; - char WiFi_Hostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; - uint32_t WiFi_ApTimeout; - - bool Mdns_Enabled; - - char Ntp_Server[NTP_MAX_SERVER_STRLEN + 1]; - char Ntp_Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; - char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; - double Ntp_Longitude; - double Ntp_Latitude; - uint8_t Ntp_SunsetType; - - bool Mqtt_Enabled; - char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; - bool Mqtt_VerboseLogging; - uint32_t Mqtt_Port; - char Mqtt_Username[MQTT_MAX_USERNAME_STRLEN + 1]; - char Mqtt_Password[MQTT_MAX_PASSWORD_STRLEN + 1]; - char Mqtt_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - bool Mqtt_Retain; - char Mqtt_LwtTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char Mqtt_LwtValue_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; - char Mqtt_LwtValue_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; - uint32_t Mqtt_PublishInterval; - bool Mqtt_CleanSession; + struct { + uint32_t Version; + uint32_t SaveCount; + } Cfg; + + struct { + char Ssid[WIFI_MAX_SSID_STRLEN + 1]; + char Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + uint8_t Ip[4]; + uint8_t Netmask[4]; + uint8_t Gateway[4]; + uint8_t Dns1[4]; + uint8_t Dns2[4]; + bool Dhcp; + char Hostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; + uint32_t ApTimeout; + } WiFi; + + struct { + bool Enabled; + } Mdns; + + struct { + char Server[NTP_MAX_SERVER_STRLEN + 1]; + char Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; + char TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; + double Longitude; + double Latitude; + uint8_t SunsetType; + } Ntp; + + struct { + bool Enabled; + char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; + bool VerboseLogging; + uint32_t Port; + char Username[MQTT_MAX_USERNAME_STRLEN + 1]; + char Password[MQTT_MAX_PASSWORD_STRLEN + 1]; + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + bool Retain; + uint32_t PublishInterval; + bool CleanSession; + + struct { + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + char Value_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; + char Value_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; + uint8_t Qos; + } Lwt; + + struct { + bool Enabled; + bool Retain; + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + bool IndividualPanels; + bool Expire; + } Hass; + + struct { + bool Enabled; + char RootCaCert[MQTT_MAX_CERT_STRLEN + 1]; + bool CertLogin; + char ClientCert[MQTT_MAX_CERT_STRLEN + 1]; + char ClientKey[MQTT_MAX_CERT_STRLEN + 1]; + } Tls; + } Mqtt; + + struct { + uint64_t Serial; + uint32_t PollInterval; + struct { + uint8_t PaLevel; + } Nrf; + struct { + int8_t PaLevel; + uint32_t Frequency; + } Cmt; + bool VerboseLogging; + } Dtu; + + struct { + char Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + bool AllowReadonly; + } Security; + + struct { + bool PowerSafe; + bool ScreenSaver; + uint8_t Rotation; + uint8_t Contrast; + uint8_t Language; + uint32_t DiagramDuration; + } Display; + + struct { + uint8_t Brightness; + } Led_Single[PINMAPPING_LED_COUNT]; + + struct { + bool Enabled; + bool VerboseLogging; + bool UpdatesOnly; + } Vedirect; + + struct { + bool Enabled; + bool VerboseLogging; + uint32_t Interval; + uint32_t Source; + char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; + uint32_t SdmBaudrate; + uint32_t SdmAddress; + uint32_t HttpInterval; + bool HttpIndividualRequests; + POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; + } PowerMeter; + + struct { + bool Enabled; + bool VerboseLogging; + bool SolarPassThroughEnabled; + uint8_t SolarPassThroughLosses; + uint8_t BatteryDrainStategy; + uint32_t Interval; + bool IsInverterBehindPowerMeter; + uint8_t InverterId; + uint8_t InverterChannelId; + int32_t TargetPowerConsumption; + int32_t TargetPowerConsumptionHysteresis; + int32_t LowerPowerLimit; + int32_t UpperPowerLimit; + uint32_t BatterySocStartThreshold; + uint32_t BatterySocStopThreshold; + float VoltageStartThreshold; + float VoltageStopThreshold; + float VoltageLoadCorrectionFactor; + int8_t RestartHour; + uint32_t FullSolarPassThroughSoc; + float FullSolarPassThroughStartVoltage; + float FullSolarPassThroughStopVoltage; + } PowerLimiter; + + struct { + bool Enabled; + bool VerboseLogging; + uint8_t Provider; + uint8_t JkBmsInterface; + uint8_t JkBmsPollingInterval; + } Battery; + + struct { + bool Enabled; + uint32_t CAN_Controller_Frequency; + bool Auto_Power_Enabled; + float Auto_Power_Voltage_Limit; + float Auto_Power_Enable_Voltage_Limit; + float Auto_Power_Lower_Power_Limit; + float Auto_Power_Upper_Power_Limit; + } Huawei; - INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; - - uint64_t Dtu_Serial; - uint32_t Dtu_PollInterval; - bool Dtu_VerboseLogging; - uint8_t Dtu_NrfPaLevel; - int8_t Dtu_CmtPaLevel; - uint32_t Dtu_CmtFrequency; - - bool Mqtt_Hass_Enabled; - bool Mqtt_Hass_Retain; - char Mqtt_Hass_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - bool Mqtt_Hass_IndividualPanels; - bool Mqtt_Hass_Expire; - - bool Mqtt_Tls; - char Mqtt_RootCaCert[MQTT_MAX_CERT_STRLEN + 1]; - bool Mqtt_TlsCertLogin; - char Mqtt_ClientCert[MQTT_MAX_CERT_STRLEN + 1]; - char Mqtt_ClientKey[MQTT_MAX_CERT_STRLEN +1]; - - bool Vedirect_Enabled; - bool Vedirect_VerboseLogging; - bool Vedirect_UpdatesOnly; - - bool PowerMeter_Enabled; - bool PowerMeter_VerboseLogging; - uint32_t PowerMeter_Interval; - uint32_t PowerMeter_Source; - char PowerMeter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; - char PowerMeter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; - char PowerMeter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; - uint32_t PowerMeter_SdmBaudrate; - uint32_t PowerMeter_SdmAddress; - uint32_t PowerMeter_HttpInterval; - bool PowerMeter_HttpIndividualRequests; - POWERMETER_HTTP_PHASE_CONFIG_T Powermeter_Http_Phase[POWERMETER_MAX_PHASES]; - - bool PowerLimiter_Enabled; - bool PowerLimiter_VerboseLogging; - bool PowerLimiter_SolarPassThroughEnabled; - uint8_t PowerLimiter_SolarPassThroughLosses; - uint8_t PowerLimiter_BatteryDrainStategy; - uint32_t PowerLimiter_Interval; - bool PowerLimiter_IsInverterBehindPowerMeter; - uint8_t PowerLimiter_InverterId; - uint8_t PowerLimiter_InverterChannelId; - int32_t PowerLimiter_TargetPowerConsumption; - int32_t PowerLimiter_TargetPowerConsumptionHysteresis; - int32_t PowerLimiter_LowerPowerLimit; - int32_t PowerLimiter_UpperPowerLimit; - uint32_t PowerLimiter_BatterySocStartThreshold; - uint32_t PowerLimiter_BatterySocStopThreshold; - float PowerLimiter_VoltageStartThreshold; - float PowerLimiter_VoltageStopThreshold; - float PowerLimiter_VoltageLoadCorrectionFactor; - int8_t PowerLimiter_RestartHour; - uint32_t PowerLimiter_FullSolarPassThroughSoc; - float PowerLimiter_FullSolarPassThroughStartVoltage; - float PowerLimiter_FullSolarPassThroughStopVoltage; - - bool Battery_Enabled; - bool Battery_VerboseLogging; - uint8_t Battery_Provider; - uint8_t Battery_JkBmsInterface; - uint8_t Battery_JkBmsPollingInterval; - - bool Huawei_Enabled; - uint32_t Huawei_CAN_Controller_Frequency; - bool Huawei_Auto_Power_Enabled; - float Huawei_Auto_Power_Voltage_Limit; - float Huawei_Auto_Power_Enable_Voltage_Limit; - float Huawei_Auto_Power_Lower_Power_Limit; - float Huawei_Auto_Power_Upper_Power_Limit; - - char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; - bool Security_AllowReadonly; + INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; - - bool Display_PowerSafe; - bool Display_ScreenSaver; - uint8_t Display_Rotation; - uint8_t Display_Contrast; - uint8_t Display_Language; }; class ConfigurationClass { @@ -207,7 +252,7 @@ class ConfigurationClass { CONFIG_T& get(); INVERTER_CONFIG_T* getFreeInverterSlot(); - INVERTER_CONFIG_T* getInverterConfig(uint64_t serial); + INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); }; extern ConfigurationClass Configuration; \ No newline at end of file diff --git a/include/Datastore.h b/include/Datastore.h index 6e4c03964..351a822ee 100644 --- a/include/Datastore.h +++ b/include/Datastore.h @@ -1,13 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include #include class DatastoreClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); // Sum of yield total of all enabled inverters, a inverter which is just disabled at night is also included float getTotalAcYieldTotalEnabled(); @@ -58,7 +57,10 @@ class DatastoreClass { bool getIsAllEnabledReachable(); private: - TimeoutHelper _updateTimeout; + void loop(); + + Task _loopTask; + std::mutex _mutex; float _totalAcYieldTotalEnabled = 0; diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 9fe202c4b..1d620e546 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Display_Graphic_Diagram.h" #include "defaults.h" +#include #include enum DisplayType_t { @@ -16,23 +18,28 @@ class DisplayGraphicClass { DisplayGraphicClass(); ~DisplayGraphicClass(); - void init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset); - void loop(); - void setContrast(uint8_t contrast); - void setStatus(bool turnOn); - void setOrientation(uint8_t rotation = DISPLAY_ROTATION); - void setLanguage(uint8_t language); + void init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset); + void setContrast(const uint8_t contrast); + void setStatus(const bool turnOn); + void setOrientation(const uint8_t rotation = DISPLAY_ROTATION); + void setLanguage(const uint8_t language); void setStartupDisplay(); + DisplayGraphicDiagramClass& Diagram(); + bool enablePowerSafe = true; bool enableScreensaver = true; private: - void printText(const char* text, uint8_t line); + void loop(); + void printText(const char* text, const uint8_t line); void calcLineHeights(); - void setFont(uint8_t line); + void setFont(const uint8_t line); + + Task _loopTask; U8G2* _display; + DisplayGraphicDiagramClass _diagram; bool _displayTurnedOn; @@ -41,7 +48,6 @@ class DisplayGraphicClass { uint8_t _mExtra; uint16_t _period = 1000; uint16_t _interval = 60000; // interval at which to power save (milliseconds) - uint32_t _lastDisplayUpdate = 0; uint32_t _previousMillis = 0; char _fmtText[32]; bool _isLarge = false; diff --git a/include/Display_Graphic_Diagram.h b/include/Display_Graphic_Diagram.h new file mode 100644 index 000000000..0626436cb --- /dev/null +++ b/include/Display_Graphic_Diagram.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +#define CHART_HEIGHT 20 // chart area hight in pixels +#define CHART_WIDTH 47 // chart area width in pixels +#define DIAG_POSX 80 // position were Diag is drawn at +#define DIAG_POSY 0 + +class DisplayGraphicDiagramClass { +public: + DisplayGraphicDiagramClass(); + + void init(Scheduler& scheduler, U8G2* display); + void redraw(); + + void updatePeriod(); + +private: + void averageLoop(); + void dataPointLoop(); + + static uint32_t getSecondsPerDot(); + + Task _averageTask; + Task _dataPointTask; + + U8G2* _display = nullptr; + std::array _graphValues = {}; + uint8_t _graphValuesCount = 0; + + float _iRunningAverage = 0; + uint16_t _iRunningAverageCnt = 0; + + uint8_t _graphPosX = DIAG_POSX; +}; \ No newline at end of file diff --git a/include/Huawei_can.h b/include/Huawei_can.h index 32e2a7766..db97bd012 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -5,6 +5,7 @@ #include "SPI.h" #include #include +#include #ifndef HUAWEI_PIN_MISO #define HUAWEI_PIN_MISO 12 @@ -118,8 +119,8 @@ class HuaweiCanCommClass { class HuaweiCanClass { public: - void init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); - void loop(); + void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); + void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); void setValue(float in, uint8_t parameterType); void setMode(uint8_t mode); @@ -128,9 +129,12 @@ class HuaweiCanClass { bool getAutoPowerStatus(); private: + void loop(); void processReceivedParameters(); void _setValue(float in, uint8_t parameterType); + Task _loopTask; + TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL; bool _initialized = false; uint8_t _huaweiPower; // Power pin diff --git a/include/InverterSettings.h b/include/InverterSettings.h index 6375dfcfa..aad05ed79 100644 --- a/include/InverterSettings.h +++ b/include/InverterSettings.h @@ -1,17 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include #define INVERTER_UPDATE_SETTINGS_INTERVAL 60000l class InverterSettingsClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); private: - uint32_t _lastUpdate = 0; + void settingsLoop(); + void hoyLoop(); + + Task _settingsTask; + Task _hoyTask; }; extern InverterSettingsClass InverterSettings; diff --git a/include/Led_Single.h b/include/Led_Single.h index a5c601bda..9404152be 100644 --- a/include/Led_Single.h +++ b/include/Led_Single.h @@ -2,38 +2,38 @@ #pragma once #include "PinMapping.h" +#include #include #define LEDSINGLE_UPDATE_INTERVAL 2000 -enum eLedFunction { - CONNECTED_NETWORK, - CONNECTED_MQTT, - INV_REACHABLE, - INV_PRODUCING, -}; - class LedSingleClass { public: LedSingleClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); void turnAllOff(); void turnAllOn(); private: + void setLoop(); + void outputLoop(); + + void setLed(const uint8_t ledNo, const bool ledState); + + Task _setTask; + Task _outputTask; + enum class LedState_t { On, Off, Blink, }; - LedState_t _ledState[PINMAPPING_LED_COUNT]; - LedState_t _allState; - TimeoutHelper _updateTimeout; + LedState_t _ledMode[PINMAPPING_LED_COUNT]; + LedState_t _allMode; + bool _ledStateCurrent[PINMAPPING_LED_COUNT]; TimeoutHelper _blinkTimeout; - uint8_t _ledActive = 0; }; extern LedSingleClass LedSingle; \ No newline at end of file diff --git a/include/MessageOutput.h b/include/MessageOutput.h index 7ff745ebd..94f915a5d 100644 --- a/include/MessageOutput.h +++ b/include/MessageOutput.h @@ -2,34 +2,32 @@ #pragma once #include -#include -#include +#include +#include +#include #include -#include -#include -#include + +#define BUFFER_SIZE 500 class MessageOutputClass : public Print { public: - void loop(); + void init(Scheduler& scheduler); size_t write(uint8_t c) override; - size_t write(const uint8_t *buffer, size_t size) override; + size_t write(const uint8_t* buffer, size_t size) override; void register_ws_output(AsyncWebSocket* output); private: - using message_t = std::vector; + void loop(); - // we keep a buffer for every task and only write complete lines to the - // serial output and then move them to be pushed through the websocket. - // this way we prevent mangling of messages from different contexts. - std::unordered_map _task_messages; - std::queue _lines; + Task _loopTask; AsyncWebSocket* _ws = nullptr; + char _buffer[BUFFER_SIZE]; + uint16_t _buff_pos = 0; + uint32_t _lastSend = 0; + bool _forceSend = false; std::mutex _msgLock; - - void serialWrite(message_t const& m); }; extern MessageOutputClass MessageOutput; \ No newline at end of file diff --git a/include/MqttHandleDtu.h b/include/MqttHandleDtu.h index fb5663450..01e179857 100644 --- a/include/MqttHandleDtu.h +++ b/include/MqttHandleDtu.h @@ -1,15 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include class MqttHandleDtuClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); private: - uint32_t _lastPublish = 0; + void loop(); + + Task _loopTask; }; extern MqttHandleDtuClass MqttHandleDtu; \ No newline at end of file diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 5bf7e71eb..41f7bf8c3 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -3,6 +3,7 @@ #include #include +#include // mqtt discovery device classes enum { @@ -50,18 +51,29 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { class MqttHandleHassClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); void publishConfig(); void forceUpdate(); private: + void loop(); void publish(const String& subtopic, const String& payload); - void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); + void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic); + void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = ""); + void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const 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); + void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); - void createDeviceInfo(JsonObject& object, std::shared_ptr inv); + + static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr inv); + static void createDtuInfo(DynamicJsonDocument& doc); + + static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); + + static String getDtuUniqueId(); + static String getDtuUrl(); + + Task _loopTask; bool _wasConnected = false; bool _updateForced = false; diff --git a/include/MqttHandleHuawei.h b/include/MqttHandleHuawei.h index bdb014c0e..e25a82e0f 100644 --- a/include/MqttHandleHuawei.h +++ b/include/MqttHandleHuawei.h @@ -4,15 +4,18 @@ #include "Configuration.h" #include #include +#include class MqttHandleHuaweiClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); private: + void loop(); void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + Task _loopTask; + uint32_t _lastPublishStats; uint32_t _lastPublish; diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index 0194bacf4..3925935f6 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -3,22 +3,23 @@ #include "Configuration.h" #include -#include +#include #include class MqttHandleInverterClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); - static String getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + static String getTopic(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); private: - void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + void loop(); + void publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); + + Task _loopTask; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; - uint32_t _lastPublish = 0; FieldId_t _publishFields[14] = { FLD_UDC, diff --git a/include/MqttHandleInverterTotal.h b/include/MqttHandleInverterTotal.h index fa4ce4b63..193178180 100644 --- a/include/MqttHandleInverterTotal.h +++ b/include/MqttHandleInverterTotal.h @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include class MqttHandleInverterTotalClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); private: - TimeoutHelper _lastPublish; + void loop(); + + Task _loopTask; }; extern MqttHandleInverterTotalClass MqttHandleInverterTotal; \ No newline at end of file diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index d52d202d6..b81917db6 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -3,15 +3,18 @@ #include "Configuration.h" #include +#include class MqttHandlePowerLimiterClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); private: + void loop(); void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + Task _loopTask; + uint32_t _lastPublishStats; uint32_t _lastPublish; diff --git a/include/MqttHandlePylontechHass.h b/include/MqttHandlePylontechHass.h index 318b970bf..64f5a841f 100644 --- a/include/MqttHandlePylontechHass.h +++ b/include/MqttHandlePylontechHass.h @@ -2,20 +2,23 @@ #pragma once #include +#include class MqttHandlePylontechHassClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); void publishConfig(); void forceUpdate(); private: + void loop(); void publish(const String& subtopic, const String& payload); void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off); void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); void createDeviceInfo(JsonObject& object); + Task _loopTask; + bool _wasConnected = false; bool _updateForced = false; String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 79c04fb28..571ee1e6a 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -4,6 +4,7 @@ #include "VeDirectMpptController.h" #include "Configuration.h" #include +#include #ifndef VICTRON_PIN_RX #define VICTRON_PIN_RX 22 @@ -15,12 +16,14 @@ class MqttHandleVedirectClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); + void forceUpdate(); private: - + void loop(); VeDirectMpptController::veMpptStruct _kvFrame{}; + Task _loopTask; + // point of time in millis() when updated values will be published uint32_t _nextPublishUpdatesOnly = 0; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index ca22a73f9..577f08d6a 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -3,20 +3,23 @@ #include #include "VeDirectMpptController.h" +#include class MqttHandleVedirectHassClass { public: - void init(); - void loop(); + void init(Scheduler& scheduler); void publishConfig(); void forceUpdate(); private: + void loop(); void publish(const String& subtopic, const String& payload); void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off); void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); void createDeviceInfo(JsonObject& object); + Task _loopTask; + bool _wasConnected = false; bool _updateForced = false; }; diff --git a/include/MqttSettings.h b/include/MqttSettings.h index af48ea5ef..875385684 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -15,29 +15,27 @@ class MqttSettingsClass { void performReconnect(); bool getConnected(); void publish(const String& subtopic, const String& payload); - void publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos = 0); + void publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos = 0); - void subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); + void subscribe(const String& topic, const uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); void unsubscribe(const String& topic); - String getPrefix(); + String getPrefix() const; private: void NetworkEvent(network_event event); void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason); - void onMqttConnect(bool sessionPresent); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + void onMqttConnect(const bool sessionPresent); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); void performConnect(); void performDisconnect(); void createMqttClientObject(); - MqttClient* mqttClient = nullptr; - String clientId; - String willTopic; - Ticker mqttReconnectTimer; + MqttClient* _mqttClient = nullptr; + Ticker _mqttReconnectTimer; MqttSubscribeParser _mqttSubscribeParser; std::mutex _clientLock; bool _verboseLogging = true; diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index fa94c8b3a..40ddc914d 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include @@ -29,7 +30,7 @@ typedef struct NetworkEventCbList { network_event event; NetworkEventCbList() - : cb(NULL) + : cb(nullptr) , event(network_event::NETWORK_UNKNOWN) { } @@ -38,46 +39,50 @@ typedef struct NetworkEventCbList { class NetworkSettingsClass { public: NetworkSettingsClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); void applyConfig(); void enableAdminMode(); - String getApName(); + String getApName() const; - IPAddress localIP(); - IPAddress subnetMask(); - IPAddress gatewayIP(); - IPAddress dnsIP(uint8_t dns_no = 0); - String macAddress(); + IPAddress localIP() const; + IPAddress subnetMask() const; + IPAddress gatewayIP() const; + IPAddress dnsIP(const uint8_t dns_no = 0) const; + String macAddress() const; static String getHostname(); - bool isConnected(); - network_mode NetworkMode(); + bool isConnected() const; + network_mode NetworkMode() const; - bool onEvent(NetworkEventCb cbEvent, network_event event = network_event::NETWORK_EVENT_MAX); - void raiseEvent(network_event event); + bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); + void raiseEvent(const network_event event); private: + void loop(); void setHostname(); void setStaticIp(); void handleMDNS(); void setupMode(); - void NetworkEvent(WiFiEvent_t event); - bool adminEnabled = true; - bool forceDisconnection = false; - uint32_t adminTimeoutCounter = 0; - uint32_t adminTimeoutCounterMax = 0; - uint32_t connectTimeoutTimer = 0; - uint32_t connectRedoTimer = 0; - uint32_t lastTimerCall = 0; - const byte DNS_PORT = 53; - IPAddress apIp; - IPAddress apNetmask; - std::unique_ptr dnsServer; - bool dnsServerStatus = false; + void NetworkEvent(const WiFiEvent_t event); + + Task _loopTask; + + static constexpr byte DNS_PORT = 53; + + bool _adminEnabled = true; + bool _forceDisconnection = false; + uint32_t _adminTimeoutCounter = 0; + uint32_t _adminTimeoutCounterMax = 0; + uint32_t _connectTimeoutTimer = 0; + uint32_t _connectRedoTimer = 0; + uint32_t _lastTimerCall = 0; + IPAddress _apIp; + IPAddress _apNetmask; + std::unique_ptr _dnsServer; + bool _dnsServerStatus = false; network_mode _networkMode = network_mode::Undefined; bool _ethConnected = false; std::vector _cbEventList; - bool lastMdnsEnabled = false; + bool _lastMdnsEnabled = false; }; extern NetworkSettingsClass NetworkSettings; \ No newline at end of file diff --git a/include/PinMapping.h b/include/PinMapping.h index c9541c4ba..30c564024 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -59,9 +59,9 @@ class PinMappingClass { bool init(const String& deviceMapping); PinMapping_t& get(); - bool isValidNrf24Config(); - bool isValidCmt2300Config(); - bool isValidEthConfig(); + bool isValidNrf24Config() const; + bool isValidCmt2300Config() const; + bool isValidEthConfig() const; bool isValidHuaweiConfig(); private: diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index c2b92d460..40d8aa892 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -7,6 +7,7 @@ #include #include #include +#include #define PL_UI_STATE_INACTIVE 0 #define PL_UI_STATE_CHARGING 1 @@ -47,8 +48,7 @@ class PowerLimiterClass { Stable, }; - void init(); - void loop(); + void init(Scheduler& scheduler); uint8_t getPowerLimiterState(); int32_t getLastRequestedPowerLimit(); @@ -63,6 +63,10 @@ class PowerLimiterClass { void calcNextInverterRestart(); private: + void loop(); + + Task _loopTask; + int32_t _lastRequestedPowerLimit = 0; uint32_t _lastPowerLimitMillis = 0; uint32_t _shutdownTimeout = 0; diff --git a/include/PowerMeter.h b/include/PowerMeter.h index fce5c8322..f6d29f2f0 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -8,6 +8,7 @@ #include #include "SDM.h" #include "sml.h" +#include #ifndef SDM_RX_PIN #define SDM_RX_PIN 13 @@ -36,17 +37,19 @@ class PowerMeterClass { SOURCE_HTTP = 3, SOURCE_SML = 4 }; - void init(); - void loop(); + void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); uint32_t getLastPowerMeterUpdate(); private: + void loop(); void mqtt(); void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + Task _loopTask; + bool _verboseLogging = true; uint32_t _lastPowerMeterCheck; // Used in Power limiter for safety check diff --git a/include/Scheduler.h b/include/Scheduler.h new file mode 100644 index 000000000..44f1b51d7 --- /dev/null +++ b/include/Scheduler.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +extern Scheduler scheduler; \ No newline at end of file diff --git a/include/SunPosition.h b/include/SunPosition.h index 26caab7b7..49c9be4f4 100644 --- a/include/SunPosition.h +++ b/include/SunPosition.h @@ -1,34 +1,35 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include +#include #include class SunPositionClass { public: SunPositionClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); - bool isDayPeriod(); - bool isSunsetAvailable(); - bool sunsetTime(struct tm* info); - bool sunriseTime(struct tm* info); - void setDoRecalc(bool doRecalc); + bool isDayPeriod() const; + bool isSunsetAvailable() const; + bool sunsetTime(struct tm* info) const; + bool sunriseTime(struct tm* info) const; + void setDoRecalc(const bool doRecalc); private: + void loop(); void updateSunData(); - bool checkRecalcDayChanged(); - bool getDoRecalc(); + bool checkRecalcDayChanged() const; + bool getSunTime(struct tm* info, const uint32_t offset) const; + + Task _loopTask; - SunSet _sun; bool _isSunsetAvailable = true; uint32_t _sunriseMinutes = 0; uint32_t _sunsetMinutes = 0; bool _isValidInfo = false; - bool _doRecalc = true; - std::mutex _recalcLock; + std::atomic_bool _doRecalc = true; uint32_t _lastSunPositionCalculatedYMD = 0; }; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index d64fbb4cf..091ddb005 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -5,14 +5,15 @@ #include #include "VeDirectMpptController.h" +#include class VictronMpptClass { public: VictronMpptClass() = default; ~VictronMpptClass() = default; - void init(); - void loop(); + void init(Scheduler& scheduler); + void updateSettings(); bool isDataValid() const; @@ -35,11 +36,14 @@ class VictronMpptClass { double getYieldDay() const; private: + void loop(); VictronMpptClass(VictronMpptClass const& other) = delete; VictronMpptClass(VictronMpptClass&& other) = delete; VictronMpptClass& operator=(VictronMpptClass const& other) = delete; VictronMpptClass& operator=(VictronMpptClass&& other) = delete; + Task _loopTask; + mutable std::mutex _mutex; using controller_t = std::unique_ptr; std::vector _controllers; diff --git a/include/WebApi.h b/include/WebApi.h index 40e66e339..b41fd6b92 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -30,12 +30,12 @@ #include "WebApi_Huawei.h" #include "WebApi_ws_battery.h" #include +#include class WebApiClass { public: WebApiClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); @@ -43,8 +43,11 @@ class WebApiClass { static void sendTooManyRequests(AsyncWebServerRequest* request); private: + void loop(); + + Task _loopTask; + AsyncWebServer _server; - AsyncEventSource _events; WebApiBatteryClass _webApiBattery; WebApiConfigClass _webApiConfig; diff --git a/include/WebApi_Huawei.h b/include/WebApi_Huawei.h index 1eed61391..7484a7fff 100644 --- a/include/WebApi_Huawei.h +++ b/include/WebApi_Huawei.h @@ -6,7 +6,7 @@ class WebApiHuaweiClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); void getJsonData(JsonObject& root); private: diff --git a/include/WebApi_battery.h b/include/WebApi_battery.h index bd43bb924..2d85d5a13 100644 --- a/include/WebApi_battery.h +++ b/include/WebApi_battery.h @@ -6,7 +6,7 @@ class WebApiBatteryClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_config.h b/include/WebApi_config.h index e022af654..edc8b2917 100644 --- a/include/WebApi_config.h +++ b/include/WebApi_config.h @@ -5,7 +5,7 @@ class WebApiConfigClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_device.h b/include/WebApi_device.h index ae76edd03..9fca20fd4 100644 --- a/include/WebApi_device.h +++ b/include/WebApi_device.h @@ -5,7 +5,7 @@ class WebApiDeviceClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_devinfo.h b/include/WebApi_devinfo.h index 0b8471e77..6e5602bac 100644 --- a/include/WebApi_devinfo.h +++ b/include/WebApi_devinfo.h @@ -5,7 +5,7 @@ class WebApiDevInfoClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_dtu.h b/include/WebApi_dtu.h index 4dcc235ec..45f58d32e 100644 --- a/include/WebApi_dtu.h +++ b/include/WebApi_dtu.h @@ -5,7 +5,7 @@ class WebApiDtuClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index ac91941ef..31ddae036 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -56,6 +56,7 @@ enum WebApiError { MqttPublishInterval, MqttHassTopicLength, MqttHassTopicCharacter, + MqttLwtQos, NetworkBase = 8000, NetworkIpInvalid, diff --git a/include/WebApi_eventlog.h b/include/WebApi_eventlog.h index 311b52894..ccc1658ca 100644 --- a/include/WebApi_eventlog.h +++ b/include/WebApi_eventlog.h @@ -5,7 +5,7 @@ class WebApiEventlogClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h index f99b248dc..1b7e92366 100644 --- a/include/WebApi_firmware.h +++ b/include/WebApi_firmware.h @@ -5,7 +5,7 @@ class WebApiFirmwareClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_gridprofile.h b/include/WebApi_gridprofile.h index cf78cf647..878165993 100644 --- a/include/WebApi_gridprofile.h +++ b/include/WebApi_gridprofile.h @@ -5,11 +5,12 @@ class WebApiGridProfileClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: void onGridProfileStatus(AsyncWebServerRequest* request); + void onGridProfileRawdata(AsyncWebServerRequest* request); AsyncWebServer* _server; }; \ No newline at end of file diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index 9f2b06731..a8605153f 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -5,7 +5,7 @@ class WebApiInverterClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_limit.h b/include/WebApi_limit.h index 026f7ef88..c2d1a7d75 100644 --- a/include/WebApi_limit.h +++ b/include/WebApi_limit.h @@ -5,7 +5,7 @@ class WebApiLimitClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_maintenance.h b/include/WebApi_maintenance.h index dd7915375..db6dcf196 100644 --- a/include/WebApi_maintenance.h +++ b/include/WebApi_maintenance.h @@ -5,7 +5,7 @@ class WebApiMaintenanceClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index 91f736798..00a2b0b70 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -7,7 +7,7 @@ class WebApiMqttClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_network.h b/include/WebApi_network.h index 693bf583f..47ef8d9a1 100644 --- a/include/WebApi_network.h +++ b/include/WebApi_network.h @@ -5,7 +5,7 @@ class WebApiNetworkClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_ntp.h b/include/WebApi_ntp.h index fae87811f..153aeeec5 100644 --- a/include/WebApi_ntp.h +++ b/include/WebApi_ntp.h @@ -5,7 +5,7 @@ class WebApiNtpClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_power.h b/include/WebApi_power.h index f8912c0fb..faed5c4e2 100644 --- a/include/WebApi_power.h +++ b/include/WebApi_power.h @@ -5,7 +5,7 @@ class WebApiPowerClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_powerlimiter.h b/include/WebApi_powerlimiter.h index d4fb392a2..a983a51b3 100644 --- a/include/WebApi_powerlimiter.h +++ b/include/WebApi_powerlimiter.h @@ -6,7 +6,7 @@ class WebApiPowerLimiterClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 4651dfbbd..01b0d4ae3 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -6,7 +6,7 @@ class WebApiPowerMeterClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index b03f81786..9fbf6e220 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -7,15 +7,15 @@ class WebApiPrometheusClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: void onPrometheusMetricsGet(AsyncWebServerRequest* request); - void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName = NULL); + void addField(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const char* metricName, const char* channelName = nullptr); - void addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel); + void addPanelInfo(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel); AsyncWebServer* _server; diff --git a/include/WebApi_security.h b/include/WebApi_security.h index 37c56fae8..66e921018 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -5,7 +5,7 @@ class WebApiSecurityClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_sysstatus.h b/include/WebApi_sysstatus.h index 9b22a835a..f63edd2ce 100644 --- a/include/WebApi_sysstatus.h +++ b/include/WebApi_sysstatus.h @@ -5,7 +5,7 @@ class WebApiSysstatusClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_vedirect.h b/include/WebApi_vedirect.h index 1dd3a3fa1..ed194117f 100644 --- a/include/WebApi_vedirect.h +++ b/include/WebApi_vedirect.h @@ -6,7 +6,7 @@ class WebApiVedirectClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_webapp.h b/include/WebApi_webapp.h index ad1614382..da50d9635 100644 --- a/include/WebApi_webapp.h +++ b/include/WebApi_webapp.h @@ -5,7 +5,7 @@ class WebApiWebappClass { public: - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h index 9ace7e883..8e61b7f7a 100644 --- a/include/WebApi_ws_Huawei.h +++ b/include/WebApi_ws_Huawei.h @@ -8,7 +8,7 @@ class WebApiWsHuaweiLiveClass { public: WebApiWsHuaweiLiveClass(); - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_ws_battery.h b/include/WebApi_ws_battery.h index 5fe5b228b..3454d71b6 100644 --- a/include/WebApi_ws_battery.h +++ b/include/WebApi_ws_battery.h @@ -7,7 +7,7 @@ class WebApiWsBatteryLiveClass { public: WebApiWsBatteryLiveClass(); - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index 81df81e90..4eea2c6a2 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -6,12 +6,10 @@ class WebApiWsConsoleClass { public: WebApiWsConsoleClass(); - void init(AsyncWebServer* server); + 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; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 1e6649200..37c7fbde1 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -8,13 +8,13 @@ class WebApiWsLiveClass { public: WebApiWsLiveClass(); - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: void generateJsonResponse(JsonVariant& root); - void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic = ""); - void addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits); + void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); + void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index d084e74ee..3797b9b0b 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -8,7 +8,7 @@ class WebApiWsVedirectLiveClass { public: WebApiWsVedirectLiveClass(); - void init(AsyncWebServer* server); + void init(AsyncWebServer& server); void loop(); private: diff --git a/include/defaults.h b/include/defaults.h index 40cd9b9bb..9ec384065 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -75,6 +75,7 @@ #define MQTT_LWT_TOPIC "dtu/status" #define MQTT_LWT_ONLINE "online" #define MQTT_LWT_OFFLINE "offline" +#define MQTT_LWT_QOS 2U #define MQTT_PUBLISH_INTERVAL 5U #define MQTT_CLEAN_SESSION true @@ -97,6 +98,7 @@ #define DISPLAY_ROTATION 2U #define DISPLAY_CONTRAST 60U #define DISPLAY_LANGUAGE 0U +#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) #define REACHABLE_THRESHOLD 2U @@ -146,3 +148,7 @@ #define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000 #define VERBOSE_LOGGING true + +#define LED_BRIGHTNESS 100U + +#define MAX_INVERTER_LIMIT 2250 \ No newline at end of file diff --git a/lib/Frozen/AUTHORS b/lib/Frozen/AUTHORS new file mode 100644 index 000000000..d83d0f86e --- /dev/null +++ b/lib/Frozen/AUTHORS @@ -0,0 +1,3 @@ +serge-sans-paille +Jérôme Dumesnil +Chris Beck diff --git a/lib/Frozen/LICENSE b/lib/Frozen/LICENSE new file mode 100644 index 000000000..5b4b9bdc6 --- /dev/null +++ b/lib/Frozen/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Quarkslab + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/Frozen/README.rst b/lib/Frozen/README.rst new file mode 100644 index 000000000..5a1ceb9f2 --- /dev/null +++ b/lib/Frozen/README.rst @@ -0,0 +1,245 @@ +Frozen +###### + +.. image:: https://travis-ci.org/serge-sans-paille/frozen.svg?branch=master + :target: https://travis-ci.org/serge-sans-paille/frozen + +Header-only library that provides 0 cost initialization for immutable containers, fixed-size containers, and various algorithms. + +Frozen provides: + +- immutable (a.k.a. frozen), ``constexpr``-compatible versions of ``std::set``, + ``std::unordered_set``, ``std::map`` and ``std::unordered_map``. + +- fixed-capacity, ``constinit``-compatible versions of ``std::map`` and + ``std::unordered_map`` with immutable, compile-time selected keys mapped + to mutable values. + +- 0-cost initialization version of ``std::search`` for frozen needles using + Boyer-Moore or Knuth-Morris-Pratt algorithms. + + +The ``unordered_*`` containers are guaranteed *perfect* (a.k.a. no hash +collision) and the extra storage is linear with respect to the number of keys. + +Once initialized, the container keys cannot be updated, and in exchange, lookups +are faster. And initialization is free when ``constexpr`` or ``constinit`` is +used :-). + + +Installation +------------ + +Just copy the ``include/frozen`` directory somewhere and points to it using the ``-I`` flag. Alternatively, using CMake: + +.. code:: sh + + > mkdir build + > cd build + > cmake -D CMAKE_BUILD_TYPE=Release .. + > make install + + +Installation via CMake populates configuration files into the ``/usr/local/share`` +directory which can be consumed by CMake's ``find_package`` instrinsic function. + +Requirements +------------ + +A C++ compiler that supports C++14. Clang version 5 is a good pick, GCC version +6 lags behind in terms of ``constexpr`` compilation time (At least on my +setup), but compiles correctly. Visual Studio 2017 also works correctly! + +Note that gcc 5 isn't supported. (Here's an `old compat branch`_ where a small amount of stuff was ported.) + +.. _old compat branch: https://github.com/cbeck88/frozen/tree/gcc5-support + +Usage +----- + +Compiled with ``-std=c++14`` flag: + +.. code:: C++ + + #include + + constexpr frozen::set some_ints = {1,2,3,5}; + + constexpr bool letitgo = some_ints.count(8); + + extern int n; + bool letitgoooooo = some_ints.count(n); + + +As the constructor and some methods are ``constexpr``, it's also possible to write weird stuff like: + +.. code:: C++ + + #include + + template + std::enable_if_t< frozen::set{{1,11,111}}.count(N), int> foo(); + +String support is built-in: + +.. code:: C++ + + #include + #include + + constexpr frozen::unordered_map olaf = { + {"19", 19}, + {"31", 31}, + }; + constexpr auto val = olaf.at("19"); + +The associative containers have different functionality with and without ``constexpr``. +With ``constexpr``, frozen maps have immutable keys and values. Without ``constexpr``, the +values can be updated in runtime (the keys, however, remain immutable): + +.. code:: C++ + + + #include + #include + + static constinit frozen::unordered_map voice = { + {"Anna", "???"}, + {"Elsa", "???"} + }; + + int main() { + voice.at("Anna") = "Kristen"; + voice.at("Elsa") = "Idina"; + } + +You may also prefer a slightly more DRY initialization syntax: + +.. code:: C++ + + #include + + constexpr auto some_ints = frozen::make_set({1,2,3,5}); + +There are similar ``make_X`` functions for all frozen containers. + +Exception Handling +------------------ + +For compatibility with STL's API, Frozen may eventually throw exceptions, as in +``frozen::map::at``. If you build your code without exception support, or +define the ``FROZEN_NO_EXCEPTIONS`` macro variable, they will be turned into an +``std::abort``. + +Extending +--------- + +Just like the regular C++14 container, you can specialize the hash function, +the key equality comparator for ``unordered_*`` containers, and the comparison +functions for the ordered version. + +It's also possible to specialize the ``frozen::elsa`` structure used for +hashing. Note that unlike `std::hash`, the hasher also takes a seed in addition +to the value being hashed. + +.. code:: C++ + + template struct elsa { + // in case of collisions, different seeds are tried + constexpr std::size_t operator()(T const &value, std::size_t seed) const; + }; + +Ideally, the hash function should have nice statistical properties like *pairwise-independence*: + +If ``x`` and ``y`` are different values, the chance that ``elsa{}(x, seed) == elsa{}(y, seed)`` +should be very low for a random value of ``seed``. + +Note that frozen always ultimately produces a perfect hash function, and you will always have ``O(1)`` +lookup with frozen. It's just that if the input hasher performs poorly, the search will take longer and +your project will take longer to compile. + +Troubleshooting +--------------- + +If you hit a message like this: + +.. code:: none + + [...] + note: constexpr evaluation hit maximum step limit; possible infinite loop? + +Then either you've got a very big container and you should increase Clang's +thresholds, using ``-fconstexpr-steps=1000000000`` for instance, or the hash +functions used by frozen do not suit your data, and you should change them, as +in the following: + +.. code:: c++ + + struct olaf { + constexpr std::size_t operator()(frozen::string const &value, std::size_t seed) const { return seed ^ value[0];} + }; + + constexpr frozen::unordered_set hans = { "a", "b" }; + +Tests and Benchmarks +-------------------- + +Using hand-written Makefiles crafted with love and care: + +.. code:: sh + + > # running tests + > make -C tests check + > # running benchmarks + > make -C benchmarks GOOGLE_BENCHMARK_PREFIX= + +Using CMake to generate a static configuration build system: + +.. code:: sh + + > mkdir build + > cd build + > cmake -D CMAKE_BUILD_TYPE=Release \ + -D frozen.benchmark=ON \ + -G <"Unix Makefiles" or "Ninja"> .. + > # building the tests and benchmarks... + > make # ... with make + > ninja # ... with ninja + > cmake --build . # ... with cmake + > # running the tests... + > make test # ... with make + > ninja test # ... with ninja + > cmake --build . --target test # ... with cmake + > ctest # ... with ctest + > # running the benchmarks... + > make benchmark # ... with make + > ninja benchmark # ... with ninja + > cmake --build . --target benchmark # ... with cmake + +Using CMake to generate an IDE build system with test and benchmark targets + +.. code:: sh + + > mkdir build + > cd build + > cmake -D frozen.benchmark=ON -G <"Xcode" or "Visual Studio 15 2017"> .. + > # using cmake to drive the IDE build, test, and benchmark + > cmake --build . --config Release + > cmake --build . --target test + > cmake --build . --target benchmark + + +Credits +------- + +The perfect hashing is strongly inspired by the blog post `Throw away the keys: +Easy, Minimal Perfect Hashing `_. + +Thanks a lot to Jérôme Dumesnil for his high-quality reviews, and to Chris Beck +for his contributions on perfect hashing. + +Contact +------- + +Serge sans Paille ```` + diff --git a/lib/Frozen/frozen/CMakeLists.txt b/lib/Frozen/frozen/CMakeLists.txt new file mode 100644 index 000000000..185378d5c --- /dev/null +++ b/lib/Frozen/frozen/CMakeLists.txt @@ -0,0 +1,12 @@ +target_sources(frozen-headers INTERFACE + "${prefix}/frozen/algorithm.h" + "${prefix}/frozen/map.h" + "${prefix}/frozen/random.h" + "${prefix}/frozen/set.h" + "${prefix}/frozen/string.h" + "${prefix}/frozen/unordered_map.h" + "${prefix}/frozen/unordered_set.h" + "${prefix}/frozen/bits/algorithms.h" + "${prefix}/frozen/bits/basic_types.h" + "${prefix}/frozen/bits/elsa.h" + "${prefix}/frozen/bits/pmh.h") diff --git a/lib/Frozen/frozen/algorithm.h b/lib/Frozen/frozen/algorithm.h new file mode 100644 index 000000000..3abd529b6 --- /dev/null +++ b/lib/Frozen/frozen/algorithm.h @@ -0,0 +1,198 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_ALGORITHM_H +#define FROZEN_LETITGO_ALGORITHM_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/version.h" +#include "frozen/string.h" + +namespace frozen { + +// 'search' implementation if C++17 is not available +// https://en.cppreference.com/w/cpp/algorithm/search +template +ForwardIterator search(ForwardIterator first, ForwardIterator last, const Searcher & searcher) +{ + return searcher(first, last).first; +} + +// text book implementation from +// https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + +template class knuth_morris_pratt_searcher { + bits::carray step_; + bits::carray needle_; + + static constexpr bits::carray + build_kmp_cache(char const (&needle)[size + 1]) { + std::ptrdiff_t cnd = 0; + bits::carray cache(-1); + for (std::size_t pos = 1; pos < size; ++pos) { + if (needle[pos] == needle[cnd]) { + cache[pos] = cache[cnd]; + cnd += 1; + } else { + cache[pos] = cnd; + cnd = cache[cnd]; + while (cnd >= 0 && needle[pos] != needle[cnd]) + cnd = cache[cnd]; + cnd += 1; + } + } + return cache; + } + +public: + constexpr knuth_morris_pratt_searcher(char const (&needle)[size + 1]) + : step_{build_kmp_cache(needle)}, needle_(needle) {} + + template + constexpr std::pair operator()(ForwardIterator first, ForwardIterator last) const { + std::size_t i = 0; + ForwardIterator iter = first; + while (iter != last) { + if (needle_[i] == *iter) { + if (i == (size - 1)) + return { iter - i, iter - i + size }; + ++i; + ++iter; + } else { + if (step_[i] > -1) { + i = step_[i]; + } else { + ++iter; + i = 0; + } + } + } + return { last, last }; + } +}; + +template +constexpr knuth_morris_pratt_searcher make_knuth_morris_pratt_searcher(char const (&needle)[N]) { + return {needle}; +} + +// text book implementation from +// https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm + +template class boyer_moore_searcher { + using skip_table_type = bits::carray; + using suffix_table_type = bits::carray; + + skip_table_type skip_table_; + suffix_table_type suffix_table_; + bits::carray needle_; + + constexpr auto build_skip_table(char const (&needle)[size + 1]) { + skip_table_type skip_table(size); + for (std::size_t i = 0; i < size - 1; ++i) + skip_table[needle[i]] -= i + 1; + return skip_table; + } + + constexpr bool is_prefix(char const (&needle)[size + 1], std::size_t pos) { + std::size_t suffixlen = size - pos; + + for (std::size_t i = 0; i < suffixlen; i++) { + if (needle[i] != needle[pos + i]) + return false; + } + return true; + } + + constexpr std::size_t suffix_length(char const (&needle)[size + 1], + std::size_t pos) { + // increment suffix length slen to the first mismatch or beginning + // of the word + for (std::size_t slen = 0; slen < pos ; slen++) + if (needle[pos - slen] != needle[size - 1 - slen]) + return slen; + + return pos; + } + + constexpr auto build_suffix_table(char const (&needle)[size + 1]) { + suffix_table_type suffix; + std::ptrdiff_t last_prefix_index = size - 1; + + // first loop + for (std::ptrdiff_t p = size - 1; p >= 0; p--) { + if (is_prefix(needle, p + 1)) + last_prefix_index = p + 1; + + suffix[p] = last_prefix_index + (size - 1 - p); + } + + // second loop + for (std::size_t p = 0; p < size - 1; p++) { + auto slen = suffix_length(needle, p); + if (needle[p - slen] != needle[size - 1 - slen]) + suffix[size - 1 - slen] = size - 1 - p + slen; + + } + return suffix; + } + +public: + constexpr boyer_moore_searcher(char const (&needle)[size + 1]) + : skip_table_{build_skip_table(needle)}, + suffix_table_{build_suffix_table(needle)}, + needle_(needle) {} + + template + constexpr std::pair operator()(RandomAccessIterator first, RandomAccessIterator last) const { + if (size == 0) + return { first, first }; + + if (size > size_t(last - first)) + return { last, last }; + + RandomAccessIterator iter = first + size - 1; + while (true) { + std::ptrdiff_t j = size - 1; + while (j > 0 && (*iter == needle_[j])) { + --iter; + --j; + } + if (j == 0 && *iter == needle_[0]) + return { iter, iter + size}; + + std::ptrdiff_t jump = std::max(skip_table_[*iter], suffix_table_[j]); + if (jump >= last - iter) + return { last, last }; + iter += jump; + } + } +}; + +template +constexpr boyer_moore_searcher make_boyer_moore_searcher(char const (&needle)[N]) { + return {needle}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/algorithms.h b/lib/Frozen/frozen/bits/algorithms.h new file mode 100644 index 000000000..4efa61b21 --- /dev/null +++ b/lib/Frozen/frozen/bits/algorithms.h @@ -0,0 +1,235 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BITS_ALGORITHMS_H +#define FROZEN_LETITGO_BITS_ALGORITHMS_H + +#include "frozen/bits/basic_types.h" + +#include +#include + +namespace frozen { + +namespace bits { + +auto constexpr next_highest_power_of_two(std::size_t v) { + // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + constexpr auto trip_count = std::numeric_limits::digits; + v--; + for(std::size_t i = 1; i < trip_count; i <<= 1) + v |= v >> i; + v++; + return v; +} + +template +auto constexpr log(T v) { + std::size_t n = 0; + while (v > 1) { + n += 1; + v >>= 1; + } + return n; +} + +constexpr std::size_t bit_weight(std::size_t n) { + return (n <= 8*sizeof(unsigned int)) + + (n <= 8*sizeof(unsigned long)) + + (n <= 8*sizeof(unsigned long long)) + + (n <= 128); +} + +unsigned int select_uint_least(std::integral_constant); +unsigned long select_uint_least(std::integral_constant); +unsigned long long select_uint_least(std::integral_constant); +template +unsigned long long select_uint_least(std::integral_constant) { + static_assert(N < 2, "unsupported type size"); + return {}; +} + + +template +using select_uint_least_t = decltype(select_uint_least(std::integral_constant())); + +template +constexpr auto min_element(Iter begin, const Iter end, + Compare const &compare) { + auto result = begin; + while (begin != end) { + if (compare(*begin, *result)) { + result = begin; + } + ++begin; + } + return result; +} + +template +constexpr void cswap(T &a, T &b) { + auto tmp = a; + a = b; + b = tmp; +} + +template +constexpr void cswap(std::pair & a, std::pair & b) { + cswap(a.first, b.first); + cswap(a.second, b.second); +} + +template +constexpr void cswap(std::tuple &a, std::tuple &b, std::index_sequence) { + using swallow = int[]; + (void) swallow{(cswap(std::get(a), std::get(b)), 0)...}; +} + +template +constexpr void cswap(std::tuple &a, std::tuple &b) { + cswap(a, b, std::make_index_sequence()); +} + +template +constexpr void iter_swap(Iter a, Iter b) { + cswap(*a, *b); +} + +template +constexpr Iterator partition(Iterator left, Iterator right, Compare const &compare) { + auto pivot = left + (right - left) / 2; + iter_swap(right, pivot); + pivot = right; + for (auto it = left; 0 < right - it; ++it) { + if (compare(*it, *pivot)) { + iter_swap(it, left); + left++; + } + } + iter_swap(pivot, left); + pivot = left; + return pivot; +} + +template +constexpr void quicksort(Iterator left, Iterator right, Compare const &compare) { + while (0 < right - left) { + auto new_pivot = bits::partition(left, right, compare); + quicksort(left, new_pivot, compare); + left = new_pivot + 1; + } +} + +template +constexpr Container quicksort(Container const &array, + Compare const &compare) { + Container res = array; + quicksort(res.begin(), res.end() - 1, compare); + return res; +} + +template struct LowerBound { + T const &value_; + Compare const &compare_; + constexpr LowerBound(T const &value, Compare const &compare) + : value_(value), compare_(compare) {} + + template + inline constexpr ForwardIt doit_fast(ForwardIt first, + std::integral_constant) { + return first; + } + + template + inline constexpr ForwardIt doit_fast(ForwardIt first, + std::integral_constant) { + auto constexpr step = N / 2; + static_assert(N/2 == N - N / 2 - 1, "power of two minus 1"); + auto it = first + step; + auto next_it = compare_(*it, value_) ? it + 1 : first; + return doit_fast(next_it, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + return doit_fast(first, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + auto constexpr next_power = next_highest_power_of_two(N); + auto constexpr next_start = next_power / 2 - 1; + auto it = first + next_start; + if (compare_(*it, value_)) { + auto constexpr next = N - next_start - 1; + return doitfirst(it + 1, std::integral_constant{}, std::integral_constant{}); + } + else + return doit_fast(first, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + return doit_fast(first, std::integral_constant{}); + } +}; + +template +constexpr ForwardIt lower_bound(ForwardIt first, const T &value, Compare const &compare) { + return LowerBound{value, compare}.doitfirst(first, std::integral_constant{}, std::integral_constant{}); +} + +template +constexpr bool binary_search(ForwardIt first, const T &value, + Compare const &compare) { + ForwardIt where = lower_bound(first, value, compare); + return (!(where == first + N) && !(compare(value, *where))); +} + + +template +constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2) +{ + for (; first1 != last1; ++first1, ++first2) { + if (!(*first1 == *first2)) { + return false; + } + } + return true; +} + +template +constexpr bool lexicographical_compare(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2) +{ + for (; (first1 != last1) && (first2 != last2); ++first1, ++first2) { + if (*first1 < *first2) + return true; + if (*first2 < *first1) + return false; + } + return (first1 == last1) && (first2 != last2); +} + +} // namespace bits +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/basic_types.h b/lib/Frozen/frozen/bits/basic_types.h new file mode 100644 index 000000000..239270afc --- /dev/null +++ b/lib/Frozen/frozen/bits/basic_types.h @@ -0,0 +1,198 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BASIC_TYPES_H +#define FROZEN_LETITGO_BASIC_TYPES_H + +#include "frozen/bits/exceptions.h" + +#include +#include +#include +#include + +namespace frozen { + +namespace bits { + +// used as a fake argument for frozen::make_set and frozen::make_map in the case of N=0 +struct ignored_arg {}; + +template +class cvector { + T data [N] = {}; // zero-initialization for scalar type T, default-initialized otherwise + std::size_t dsize = 0; + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr cvector(void) = default; + constexpr cvector(size_type count, const T& value) : dsize(count) { + for (std::size_t i = 0; i < N; ++i) + data[i] = value; + } + + // Iterators + constexpr iterator begin() noexcept { return data; } + constexpr iterator end() noexcept { return data + dsize; } + constexpr const_iterator begin() const noexcept { return data; } + constexpr const_iterator end() const noexcept { return data + dsize; } + + // Capacity + constexpr size_type size() const { return dsize; } + + // Element access + constexpr reference operator[](std::size_t index) { return data[index]; } + constexpr const_reference operator[](std::size_t index) const { return data[index]; } + + constexpr reference back() { return data[dsize - 1]; } + constexpr const_reference back() const { return data[dsize - 1]; } + + // Modifiers + constexpr void push_back(const T & a) { data[dsize++] = a; } + constexpr void push_back(T && a) { data[dsize++] = std::move(a); } + constexpr void pop_back() { --dsize; } + + constexpr void clear() { dsize = 0; } +}; + +template +class carray { + T data_ [N] = {}; // zero-initialization for scalar type T, default-initialized otherwise + + template + constexpr carray(Iter iter, std::index_sequence) + : data_{((void)I, *iter++)...} {} + template + constexpr carray(const T& value, std::index_sequence) + : data_{((void)I, value)...} {} + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr carray() = default; + constexpr carray(const value_type& val) + : carray(val, std::make_index_sequence()) {} + template ::value, std::size_t> M> + constexpr carray(U const (&init)[M]) + : carray(init, std::make_index_sequence()) + { + static_assert(M >= N, "Cannot initialize a carray with an smaller array"); + } + template ::value, std::size_t> M> + constexpr carray(std::array const &init) + : carray(init.begin(), std::make_index_sequence()) + { + static_assert(M >= N, "Cannot initialize a carray with an smaller array"); + } + template ::value>* = nullptr> + constexpr carray(std::initializer_list init) + : carray(init.begin(), std::make_index_sequence()) + { + // clang & gcc doesn't recognize init.size() as a constexpr + // static_assert(init.size() >= N, "Cannot initialize a carray with an smaller initializer list"); + } + template ::value>* = nullptr> + constexpr carray(const carray& rhs) + : carray(rhs.begin(), std::make_index_sequence()) + { + } + + // Iterators + constexpr iterator begin() noexcept { return data_; } + constexpr const_iterator begin() const noexcept { return data_; } + constexpr iterator end() noexcept { return data_ + N; } + constexpr const_iterator end() const noexcept { return data_ + N; } + + // Capacity + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + // Element access + constexpr reference operator[](std::size_t index) { return data_[index]; } + constexpr const_reference operator[](std::size_t index) const { return data_[index]; } + + constexpr reference at(std::size_t index) { + if (index > N) + FROZEN_THROW_OR_ABORT(std::out_of_range("Index (" + std::to_string(index) + ") out of bound (" + std::to_string(N) + ')')); + return data_[index]; + } + constexpr const_reference at(std::size_t index) const { + if (index > N) + FROZEN_THROW_OR_ABORT(std::out_of_range("Index (" + std::to_string(index) + ") out of bound (" + std::to_string(N) + ')')); + return data_[index]; + } + + constexpr reference front() { return data_[0]; } + constexpr const_reference front() const { return data_[0]; } + + constexpr reference back() { return data_[N - 1]; } + constexpr const_reference back() const { return data_[N - 1]; } + + constexpr value_type* data() noexcept { return data_; } + constexpr const value_type* data() const noexcept { return data_; } +}; +template +class carray { + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr carray(void) = default; + +}; + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/constexpr_assert.h b/lib/Frozen/frozen/bits/constexpr_assert.h new file mode 100644 index 000000000..912210dc2 --- /dev/null +++ b/lib/Frozen/frozen/bits/constexpr_assert.h @@ -0,0 +1,40 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_CONSTEXPR_ASSERT_H +#define FROZEN_LETITGO_CONSTEXPR_ASSERT_H + +#include + +#ifdef _MSC_VER + +// FIXME: find a way to implement that correctly for msvc +#define constexpr_assert(cond, msg) + +#else + +#define constexpr_assert(cond, msg)\ + assert(cond && msg); +#endif + +#endif + diff --git a/lib/Frozen/frozen/bits/defines.h b/lib/Frozen/frozen/bits/defines.h new file mode 100644 index 000000000..e20f6d0ce --- /dev/null +++ b/lib/Frozen/frozen/bits/defines.h @@ -0,0 +1,66 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_DEFINES_H +#define FROZEN_LETITGO_DEFINES_H + +#if defined(_MSVC_LANG) && !(defined(__EDG__) && defined(__clang__)) // TRANSITION, VSO#273681 + #define FROZEN_LETITGO_IS_MSVC +#endif + +// Code taken from https://stackoverflow.com/questions/43639122/which-values-can-msvc-lang-have +#if defined(FROZEN_LETITGO_IS_MSVC) + #if _MSVC_LANG > 201402 + #define FROZEN_LETITGO_HAS_CXX17 1 + #else /* _MSVC_LANG > 201402 */ + #define FROZEN_LETITGO_HAS_CXX17 0 + #endif /* _MSVC_LANG > 201402 */ +#else /* _MSVC_LANG etc. */ + #if __cplusplus > 201402 + #define FROZEN_LETITGO_HAS_CXX17 1 + #else /* __cplusplus > 201402 */ + #define FROZEN_LETITGO_HAS_CXX17 0 + #endif /* __cplusplus > 201402 */ +#endif /* _MSVC_LANG etc. */ +// End if taken code + +#if FROZEN_LETITGO_HAS_CXX17 == 1 && defined(FROZEN_LETITGO_IS_MSVC) + #define FROZEN_LETITGO_HAS_STRING_VIEW // We assume Visual Studio always has string_view in C++17 +#else + #if FROZEN_LETITGO_HAS_CXX17 == 1 && __has_include() + #define FROZEN_LETITGO_HAS_STRING_VIEW + #endif +#endif + +#ifdef __cpp_char8_t + #define FROZEN_LETITGO_HAS_CHAR8T +#endif + +#if __cpp_deduction_guides >= 201703L + #define FROZEN_LETITGO_HAS_DEDUCTION_GUIDES +#endif + +#if __cpp_lib_constexpr_string >= 201907L + #define FROZEN_LETITGO_HAS_CONSTEXPR_STRING +#endif + +#endif // FROZEN_LETITGO_DEFINES_H diff --git a/lib/Frozen/frozen/bits/elsa.h b/lib/Frozen/frozen/bits/elsa.h new file mode 100644 index 000000000..6c9ecb78f --- /dev/null +++ b/lib/Frozen/frozen/bits/elsa.h @@ -0,0 +1,57 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_ELSA_H +#define FROZEN_LETITGO_ELSA_H + +#include + +namespace frozen { + +template struct elsa { + static_assert(std::is_integral::value || std::is_enum::value, + "only supports integral types, specialize for other types"); + + constexpr std::size_t operator()(T const &value, std::size_t seed) const { + std::size_t key = seed ^ static_cast(value); + key = (~key) + (key << 21); // key = (key << 21) - key - 1; + key = key ^ (key >> 24); + key = (key + (key << 3)) + (key << 8); // key * 265 + key = key ^ (key >> 14); + key = (key + (key << 2)) + (key << 4); // key * 21 + key = key ^ (key >> 28); + key = key + (key << 31); + return key; + } +}; + +template <> struct elsa { + template + constexpr std::size_t operator()(T const &value, std::size_t seed) const { + return elsa{}(value, seed); + } +}; + +template using anna = elsa; +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/elsa_std.h b/lib/Frozen/frozen/bits/elsa_std.h new file mode 100644 index 000000000..df1a9cfc3 --- /dev/null +++ b/lib/Frozen/frozen/bits/elsa_std.h @@ -0,0 +1,41 @@ +#ifndef FROZEN_LETITGO_BITS_ELSA_STD_H +#define FROZEN_LETITGO_BITS_ELSA_STD_H + +#include "defines.h" +#include "elsa.h" +#include "hash_string.h" + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW +#include +#endif +#include + +namespace frozen { + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW + +template struct elsa> +{ + constexpr std::size_t operator()(const std::basic_string_view& value) const { + return hash_string(value); + } + constexpr std::size_t operator()(const std::basic_string_view& value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +#endif + +template struct elsa> +{ + constexpr std::size_t operator()(const std::basic_string& value) const { + return hash_string(value); + } + constexpr std::size_t operator()(const std::basic_string& value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +} // namespace frozen + +#endif // FROZEN_LETITGO_BITS_ELSA_STD_H diff --git a/lib/Frozen/frozen/bits/exceptions.h b/lib/Frozen/frozen/bits/exceptions.h new file mode 100644 index 000000000..b43e3e6b9 --- /dev/null +++ b/lib/Frozen/frozen/bits/exceptions.h @@ -0,0 +1,39 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_EXCEPTIONS_H +#define FROZEN_LETITGO_EXCEPTIONS_H + +#if defined(FROZEN_NO_EXCEPTIONS) || (defined(_MSC_VER) && !defined(_CPPUNWIND)) || (!defined(_MSC_VER) && !defined(__cpp_exceptions)) + +#include +#define FROZEN_THROW_OR_ABORT(_) std::abort() + +#else + +#include +#define FROZEN_THROW_OR_ABORT(err) throw err + + +#endif + +#endif diff --git a/lib/Frozen/frozen/bits/hash_string.h b/lib/Frozen/frozen/bits/hash_string.h new file mode 100644 index 000000000..b2f7e90e6 --- /dev/null +++ b/lib/Frozen/frozen/bits/hash_string.h @@ -0,0 +1,28 @@ +#ifndef FROZEN_LETITGO_BITS_HASH_STRING_H +#define FROZEN_LETITGO_BITS_HASH_STRING_H + +#include + +namespace frozen { + +template +constexpr std::size_t hash_string(const String& value) { + std::size_t d = 5381; + for (const auto& c : value) + d = d * 33 + static_cast(c); + return d; +} + +// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function +// With the lowest bits removed, based on experimental setup. +template +constexpr std::size_t hash_string(const String& value, std::size_t seed) { + std::size_t d = (0x811c9dc5 ^ seed) * static_cast(0x01000193); + for (const auto& c : value) + d = (d ^ static_cast(c)) * static_cast(0x01000193); + return d >> 8 ; +} + +} // namespace frozen + +#endif // FROZEN_LETITGO_BITS_HASH_STRING_H \ No newline at end of file diff --git a/lib/Frozen/frozen/bits/mpl.h b/lib/Frozen/frozen/bits/mpl.h new file mode 100644 index 000000000..8f87f99c8 --- /dev/null +++ b/lib/Frozen/frozen/bits/mpl.h @@ -0,0 +1,56 @@ +/* + * Frozen + * Copyright 2022 Giel van Schijndel + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BITS_MPL_H +#define FROZEN_LETITGO_BITS_MPL_H + +#include + +namespace frozen { + +namespace bits { + +// Forward declarations +template +class carray; + +template +struct remove_cv : std::remove_cv {}; + +template +struct remove_cv> { + using type = std::pair::type...>; +}; + +template +struct remove_cv> { + using type = carray::type, N>; +}; + +template +using remove_cv_t = typename remove_cv::type; + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/pmh.h b/lib/Frozen/frozen/bits/pmh.h new file mode 100644 index 000000000..1bb402163 --- /dev/null +++ b/lib/Frozen/frozen/bits/pmh.h @@ -0,0 +1,254 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// inspired from http://stevehanov.ca/blog/index.php?id=119 +#ifndef FROZEN_LETITGO_PMH_H +#define FROZEN_LETITGO_PMH_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" + +#include +#include +#include +#include + +namespace frozen { + +namespace bits { + +// Function object for sorting buckets in decreasing order of size +struct bucket_size_compare { + template + bool constexpr operator()(B const &b0, + B const &b1) const { + return b0.size() > b1.size(); + } +}; + +// Step One in pmh routine is to take all items and hash them into buckets, +// with some collisions. Then process those buckets further to build a perfect +// hash function. +// pmh_buckets represents the initial placement into buckets. + +template +struct pmh_buckets { + // Step 0: Bucket max is 2 * sqrt M + // TODO: Come up with justification for this, should it not be O(log M)? + static constexpr auto bucket_max = 2 * (1u << (log(M) / 2)); + + using bucket_t = cvector; + carray buckets; + std::uint64_t seed; + + // Represents a reference to a bucket. This is used because the buckets + // have to be sorted, but buckets are big, making it slower than sorting refs + struct bucket_ref { + unsigned hash; + const bucket_t * ptr; + + // Forward some interface of bucket + using value_type = typename bucket_t::value_type; + using const_iterator = typename bucket_t::const_iterator; + + constexpr auto size() const { return ptr->size(); } + constexpr const auto & operator[](std::size_t idx) const { return (*ptr)[idx]; } + constexpr auto begin() const { return ptr->begin(); } + constexpr auto end() const { return ptr->end(); } + }; + + // Make a bucket_ref for each bucket + template + carray constexpr make_bucket_refs(std::index_sequence) const { + return {{ bucket_ref{Is, &buckets[Is]}... }}; + } + + // Makes a bucket_ref for each bucket and sorts them by size + carray constexpr get_sorted_buckets() const { + carray result{this->make_bucket_refs(std::make_index_sequence())}; + bits::quicksort(result.begin(), result.end() - 1, bucket_size_compare{}); + return result; + } +}; + +template +pmh_buckets constexpr make_pmh_buckets(const carray & items, + Hash const & hash, + Key const & key, + PRG & prg) { + using result_t = pmh_buckets; + // Continue until all items are placed without exceeding bucket_max + while (1) { + result_t result{}; + result.seed = prg(); + bool rejected = false; + for (std::size_t i = 0; i < items.size(); ++i) { + auto & bucket = result.buckets[hash(key(items[i]), static_cast(result.seed)) % M]; + if (bucket.size() >= result_t::bucket_max) { + rejected = true; + break; + } + bucket.push_back(i); + } + if (!rejected) { return result; } + } +} + +// Check if an item appears in a cvector +template +constexpr bool all_different_from(cvector & data, T & a) { + for (std::size_t i = 0; i < data.size(); ++i) + if (data[i] == a) + return false; + + return true; +} + +// Represents either an index to a data item array, or a seed to be used with +// a hasher. Seed must have high bit of 1, value has high bit of zero. +struct seed_or_index { + using value_type = std::uint64_t; + +private: + static constexpr value_type MINUS_ONE = std::numeric_limits::max(); + static constexpr value_type HIGH_BIT = ~(MINUS_ONE >> 1); + + value_type value_ = 0; + +public: + constexpr value_type value() const { return value_; } + constexpr bool is_seed() const { return value_ & HIGH_BIT; } + + constexpr seed_or_index(bool is_seed, value_type value) + : value_(is_seed ? (value | HIGH_BIT) : (value & ~HIGH_BIT)) {} + + constexpr seed_or_index() = default; + constexpr seed_or_index(const seed_or_index &) = default; + constexpr seed_or_index & operator =(const seed_or_index &) = default; +}; + +// Represents the perfect hash function created by pmh algorithm +template +struct pmh_tables : private Hasher { + std::uint64_t first_seed_; + carray first_table_; + carray second_table_; + + constexpr pmh_tables( + std::uint64_t first_seed, + carray first_table, + carray second_table, + Hasher hash) noexcept + : Hasher(hash) + , first_seed_(first_seed) + , first_table_(first_table) + , second_table_(second_table) + {} + + constexpr Hasher const& hash_function() const noexcept { + return static_cast(*this); + } + + template + constexpr std::size_t lookup(const KeyType & key) const { + return lookup(key, hash_function()); + } + + // Looks up a given key, to find its expected index in carray + // Always returns a valid index, must use KeyEqual test after to confirm. + template + constexpr std::size_t lookup(const KeyType & key, const HasherType& hasher) const { + auto const d = first_table_[hasher(key, static_cast(first_seed_)) % M]; + if (!d.is_seed()) { return static_cast(d.value()); } // this is narrowing std::uint64 -> std::size_t but should be fine + else { return second_table_[hasher(key, static_cast(d.value())) % M]; } + } +}; + +// Make pmh tables for given items, hash function, prg, etc. +template +pmh_tables constexpr make_pmh_tables(const carray & + items, + Hash const &hash, + Key const &key, + PRG prg) { + // Step 1: Place all of the keys into buckets + auto step_one = make_pmh_buckets(items, hash, key, prg); + + // Step 2: Sort the buckets to process the ones with the most items first. + auto buckets = step_one.get_sorted_buckets(); + + // Special value for unused slots. This is purposefully the index + // one-past-the-end of 'items' to function as a sentinel value. Both to avoid + // the need to apply the KeyEqual predicate and to be easily convertible to + // end(). + // Unused entries in both hash tables (G and H) have to contain this value. + const auto UNUSED = items.size(); + + // G becomes the first hash table in the resulting pmh function + carray G({false, UNUSED}); + + // H becomes the second hash table in the resulting pmh function + carray H(UNUSED); + + // Step 3: Map the items in buckets into hash tables. + for (const auto & bucket : buckets) { + auto const bsize = bucket.size(); + + if (bsize == 1) { + // Store index to the (single) item in G + // assert(bucket.hash == hash(key(items[bucket[0]]), step_one.seed) % M); + G[bucket.hash] = {false, static_cast(bucket[0])}; + } else if (bsize > 1) { + + // Repeatedly try different H of d until we find a hash function + // that places all items in the bucket into free slots + seed_or_index d{true, prg()}; + cvector bucket_slots; + + while (bucket_slots.size() < bsize) { + auto slot = hash(key(items[bucket[bucket_slots.size()]]), static_cast(d.value())) % M; + + if (H[slot] != UNUSED || !all_different_from(bucket_slots, slot)) { + bucket_slots.clear(); + d = {true, prg()}; + continue; + } + + bucket_slots.push_back(slot); + } + + // Put successful seed in G, and put indices to items in their slots + // assert(bucket.hash == hash(key(items[bucket[0]]), step_one.seed) % M); + G[bucket.hash] = d; + for (std::size_t i = 0; i < bsize; ++i) + H[bucket_slots[i]] = bucket[i]; + } + } + + return {step_one.seed, G, H, hash}; +} + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/version.h b/lib/Frozen/frozen/bits/version.h new file mode 100644 index 000000000..7e57d707e --- /dev/null +++ b/lib/Frozen/frozen/bits/version.h @@ -0,0 +1,30 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_VERSION_H +#define FROZEN_LETITGO_VERSION_H + +#define FROZEN_MAJOR_VERSION 1 +#define FROZEN_MINOR_VERSION 1 +#define FROZEN_PATCH_VERSION 1 + +#endif diff --git a/lib/Frozen/frozen/map.h b/lib/Frozen/frozen/map.h new file mode 100644 index 000000000..d54128a6c --- /dev/null +++ b/lib/Frozen/frozen/map.h @@ -0,0 +1,357 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_MAP_H +#define FROZEN_LETITGO_MAP_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/exceptions.h" +#include "frozen/bits/mpl.h" +#include "frozen/bits/version.h" + +#include +#include + +namespace frozen { + +namespace impl { + +template class CompareKey : private Comparator { +public: + constexpr Comparator const& key_comp() const noexcept { + return static_cast(*this); + } + + constexpr CompareKey(Comparator const &comparator) + : Comparator(comparator) {} + + template + constexpr int operator()(std::pair const &self, + std::pair const &other) const { + return key_comp()(std::get<0>(self), std::get<0>(other)); + } + + template + constexpr int operator()(Key1 const &self_key, + std::pair const &other) const { + return key_comp()(self_key, std::get<0>(other)); + } + + template + constexpr int operator()(std::pair const &self, + Key2 const &other_key) const { + return key_comp()(std::get<0>(self), other_key); + } + + template + constexpr int operator()(Key1 const &self_key, Key2 const &other_key) const { + return key_comp()(self_key, other_key); + } +}; + +} // namespace impl + +template > +class map : private impl::CompareKey { + using container_type = bits::carray, N>; + container_type items_; + +public: + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using key_compare = Compare; + using value_compare = impl::CompareKey; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = typename container_type::iterator; + using const_iterator = typename container_type::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + +public: + /* constructors */ + constexpr map(container_type items, Compare const &compare) + : impl::CompareKey{compare} + , items_{bits::quicksort(bits::remove_cv_t(items), value_comp())} {} + + explicit constexpr map(container_type items) + : map{items, Compare{}} {} + + constexpr map(std::initializer_list items, Compare const &compare) + : map{container_type {items}, compare} { + constexpr_assert(items.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr map(std::initializer_list items) + : map{items, Compare{}} {} + + /* element access */ + constexpr Value const& at(Key const &key) const { + return at_impl(*this, key); + } + constexpr Value& at(Key const &key) { + return at_impl(*this, key); + } + + /* iterators */ + constexpr iterator begin() { return items_.begin(); } + constexpr const_iterator begin() const { return items_.begin(); } + constexpr const_iterator cbegin() const { return items_.begin(); } + constexpr iterator end() { return items_.end(); } + constexpr const_iterator end() const { return items_.end(); } + constexpr const_iterator cend() const { return items_.end(); } + + constexpr reverse_iterator rbegin() { return reverse_iterator{items_.end()}; } + constexpr const_reverse_iterator rbegin() const { return const_reverse_iterator{items_.end()}; } + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{items_.end()}; } + constexpr reverse_iterator rend() { return reverse_iterator{items_.begin()}; } + constexpr const_reverse_iterator rend() const { return const_reverse_iterator{items_.begin()}; } + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{items_.begin()}; } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + + template + constexpr std::size_t count(KeyType const &key) const { + return bits::binary_search(items_.begin(), key, value_comp()); + } + + template + constexpr const_iterator find(KeyType const &key) const { + return map::find_impl(*this, key); + } + template + constexpr iterator find(KeyType const &key) { + return map::find_impl(*this, key); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != this->end(); + } + + template + constexpr std::pair + equal_range(KeyType const &key) const { + return equal_range_impl(*this, key); + } + template + constexpr std::pair equal_range(KeyType const &key) { + return equal_range_impl(*this, key); + } + + template + constexpr const_iterator lower_bound(KeyType const &key) const { + return lower_bound_impl(*this, key); + } + template + constexpr iterator lower_bound(KeyType const &key) { + return lower_bound_impl(*this, key); + } + + template + constexpr const_iterator upper_bound(KeyType const &key) const { + return upper_bound_impl(*this, key); + } + template + constexpr iterator upper_bound(KeyType const &key) { + return upper_bound_impl(*this, key); + } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp().key_comp(); } + constexpr const value_compare& value_comp() const { return static_cast const&>(*this); } + + private: + template + static inline constexpr auto& at_impl(This&& self, KeyType const &key) { + auto where = self.find(key); + if (where != self.end()) + return where->second; + else + FROZEN_THROW_OR_ABORT(std::out_of_range("unknown key")); + } + + template + static inline constexpr auto find_impl(This&& self, KeyType const &key) { + auto where = self.lower_bound(key); + if (where != self.end() && !self.value_comp()(key, *where)) + return where; + else + return self.end(); + } + + template + static inline constexpr auto equal_range_impl(This&& self, KeyType const &key) { + auto lower = self.lower_bound(key); + using lower_t = decltype(lower); + if (lower != self.end() && !self.value_comp()(key, *lower)) + return std::pair{lower, lower + 1}; + else + return std::pair{lower, lower}; + } + + template + static inline constexpr auto lower_bound_impl(This&& self, KeyType const &key) -> decltype(self.end()) { + return bits::lower_bound(self.items_.begin(), key, self.value_comp()); + } + + template + static inline constexpr auto upper_bound_impl(This&& self, KeyType const &key) { + auto lower = self.lower_bound(key); + if (lower != self.end() && !self.value_comp()(key, *lower)) + return lower + 1; + else + return lower; + } +}; + +template +class map : private impl::CompareKey { + using container_type = bits::carray, 0>; + +public: + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using key_compare = Compare; + using value_compare = impl::CompareKey; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = pointer; + using const_reverse_iterator = const_pointer; + +public: + /* constructors */ + constexpr map(const map &other) = default; + constexpr map(std::initializer_list, Compare const &compare) + : impl::CompareKey{compare} {} + constexpr map(std::initializer_list items) + : map{items, Compare{}} {} + + /* element access */ + template + constexpr mapped_type at(KeyType const &) const { + FROZEN_THROW_OR_ABORT(std::out_of_range("invalid key")); + } + template + constexpr mapped_type at(KeyType const &) { + FROZEN_THROW_OR_ABORT(std::out_of_range("invalid key")); + } + + /* iterators */ + constexpr iterator begin() { return nullptr; } + constexpr const_iterator begin() const { return nullptr; } + constexpr const_iterator cbegin() const { return nullptr; } + constexpr iterator end() { return nullptr; } + constexpr const_iterator end() const { return nullptr; } + constexpr const_iterator cend() const { return nullptr; } + + constexpr reverse_iterator rbegin() { return nullptr; } + constexpr const_reverse_iterator rbegin() const { return nullptr; } + constexpr const_reverse_iterator crbegin() const { return nullptr; } + constexpr reverse_iterator rend() { return nullptr; } + constexpr const_reverse_iterator rend() const { return nullptr; } + constexpr const_reverse_iterator crend() const { return nullptr; } + + /* capacity */ + constexpr bool empty() const { return true; } + constexpr size_type size() const { return 0; } + constexpr size_type max_size() const { return 0; } + + /* lookup */ + + template + constexpr std::size_t count(KeyType const &) const { return 0; } + + template + constexpr const_iterator find(KeyType const &) const { return end(); } + template + constexpr iterator find(KeyType const &) { return end(); } + + template + constexpr std::pair + equal_range(KeyType const &) const { return {end(), end()}; } + template + constexpr std::pair + equal_range(KeyType const &) { return {end(), end()}; } + + template + constexpr const_iterator lower_bound(KeyType const &) const { return end(); } + template + constexpr iterator lower_bound(KeyType const &) { return end(); } + + template + constexpr const_iterator upper_bound(KeyType const &) const { return end(); } + template + constexpr iterator upper_bound(KeyType const &) { return end(); } + +/* observers */ + constexpr key_compare const& key_comp() const { return value_comp().key_comp(); } + constexpr value_compare const& value_comp() const { return static_cast const&>(*this); } +}; + +template > +constexpr auto make_map(bits::ignored_arg = {}/* for consistency with the initializer below for N = 0*/) { + return map{}; +} + +template +constexpr auto make_map(std::pair const (&items)[N]) { + return map{items}; +} + +template +constexpr auto make_map(std::array, N> const &items) { + return map{items}; +} + +template +constexpr auto make_map(std::pair const (&items)[N], Compare const& compare = Compare{}) { + return map{items, compare}; +} + +template +constexpr auto make_map(std::array, N> const &items, Compare const& compare = Compare{}) { + return map{items, compare}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/random.h b/lib/Frozen/frozen/random.h new file mode 100644 index 000000000..727133bb1 --- /dev/null +++ b/lib/Frozen/frozen/random.h @@ -0,0 +1,97 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_RANDOM_H +#define FROZEN_LETITGO_RANDOM_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/version.h" + +#include +#include + +namespace frozen { +template +class linear_congruential_engine { + + static_assert(std::is_unsigned::value, + "UIntType must be an unsigned integral type"); + + template + static constexpr UIntType modulo(T val, std::integral_constant) { + return static_cast(val); + } + + template + static constexpr UIntType modulo(T val, std::integral_constant) { + // the static cast below may end up doing a truncation + return static_cast(val % M); + } + +public: + using result_type = UIntType; + static constexpr result_type multiplier = a; + static constexpr result_type increment = c; + static constexpr result_type modulus = m; + static constexpr result_type default_seed = 1u; + + linear_congruential_engine() = default; + constexpr linear_congruential_engine(result_type s) { seed(s); } + + void seed(result_type s = default_seed) { state_ = s; } + constexpr result_type operator()() { + using uint_least_t = bits::select_uint_least_t; + uint_least_t tmp = static_cast(multiplier) * state_ + increment; + + state_ = modulo(tmp, std::integral_constant()); + return state_; + } + constexpr void discard(unsigned long long n) { + while (n--) + operator()(); + } + static constexpr result_type min() { return increment == 0u ? 1u : 0u; } + static constexpr result_type max() { return modulus - 1u; } + friend constexpr bool operator==(linear_congruential_engine const &self, + linear_congruential_engine const &other) { + return self.state_ == other.state_; + } + friend constexpr bool operator!=(linear_congruential_engine const &self, + linear_congruential_engine const &other) { + return !(self == other); + } + +private: + result_type state_ = default_seed; +}; + +using minstd_rand0 = + linear_congruential_engine; +using minstd_rand = + linear_congruential_engine; + +// This generator is used by default in unordered frozen containers +using default_prg_t = minstd_rand; + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/set.h b/lib/Frozen/frozen/set.h new file mode 100644 index 000000000..430d4a54c --- /dev/null +++ b/lib/Frozen/frozen/set.h @@ -0,0 +1,260 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_SET_H +#define FROZEN_SET_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/version.h" +#include "frozen/bits/defines.h" + +#include +#include + +namespace frozen { + +template > class set : private Compare { + using container_type = bits::carray; + container_type keys_; + +public: + /* container typedefs*/ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::size_type; + using key_compare = Compare; + using value_compare = Compare; + using reference = typename container_type::const_reference; + using const_reference = reference; + using pointer = typename container_type::const_pointer; + using const_pointer = pointer; + using iterator = typename container_type::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_iterator = iterator; + using const_reverse_iterator = std::reverse_iterator; + +public: + /* constructors */ + constexpr set(const set &other) = default; + + constexpr set(container_type keys, Compare const & comp) + : Compare{comp} + , keys_(bits::quicksort(keys, value_comp())) { + } + + explicit constexpr set(container_type keys) + : set{keys, Compare{}} {} + + constexpr set(std::initializer_list keys, Compare const & comp) + : set{container_type{keys}, comp} { + constexpr_assert(keys.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr set(std::initializer_list keys) + : set{keys, Compare{}} {} + + constexpr set& operator=(const set &other) = default; + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return bits::binary_search(keys_.begin(), key, value_comp()); + } + + template + constexpr const_iterator find(KeyType const &key) const { + const_iterator where = lower_bound(key); + if ((where != end()) && !value_comp()(key, *where)) + return where; + else + return end(); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != keys_.end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + auto const lower = lower_bound(key); + if (lower == end()) + return {lower, lower}; + else + return {lower, lower + 1}; + } + + template + constexpr const_iterator lower_bound(KeyType const &key) const { + auto const where = bits::lower_bound(keys_.begin(), key, value_comp()); + if ((where != end()) && !value_comp()(key, *where)) + return where; + else + return end(); + } + + template + constexpr const_iterator upper_bound(KeyType const &key) const { + auto const where = bits::lower_bound(keys_.begin(), key, value_comp()); + if ((where != end()) && !value_comp()(key, *where)) + return where + 1; + else + return end(); + } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp(); } + constexpr const key_compare& value_comp() const { return static_cast(*this); } + + /* iterators */ + constexpr const_iterator begin() const { return keys_.begin(); } + constexpr const_iterator cbegin() const { return keys_.begin(); } + constexpr const_iterator end() const { return keys_.end(); } + constexpr const_iterator cend() const { return keys_.end(); } + + constexpr const_reverse_iterator rbegin() const { return const_reverse_iterator{keys_.end()}; } + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{keys_.end()}; } + constexpr const_reverse_iterator rend() const { return const_reverse_iterator{keys_.begin()}; } + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{keys_.begin()}; } + + /* comparison */ + constexpr bool operator==(set const& rhs) const { return bits::equal(begin(), end(), rhs.begin()); } + constexpr bool operator!=(set const& rhs) const { return !(*this == rhs); } + constexpr bool operator<(set const& rhs) const { return bits::lexicographical_compare(begin(), end(), rhs.begin(), rhs.end()); } + constexpr bool operator<=(set const& rhs) const { return (*this < rhs) || (*this == rhs); } + constexpr bool operator>(set const& rhs) const { return bits::lexicographical_compare(rhs.begin(), rhs.end(), begin(), end()); } + constexpr bool operator>=(set const& rhs) const { return (*this > rhs) || (*this == rhs); } +}; + +template class set : private Compare { + using container_type = bits::carray; // just for the type definitions + +public: + /* container typedefs*/ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::size_type; + using key_compare = Compare; + using value_compare = Compare; + using reference = typename container_type::const_reference; + using const_reference = reference; + using pointer = typename container_type::const_pointer; + using const_pointer = pointer; + using iterator = pointer; + using reverse_iterator = pointer; + using const_iterator = const_pointer; + using const_reverse_iterator = const_pointer; + +public: + /* constructors */ + constexpr set(const set &other) = default; + constexpr set(bits::carray, Compare const &) {} + explicit constexpr set(bits::carray) {} + + constexpr set(std::initializer_list, Compare const &comp) + : Compare{comp} {} + constexpr set(std::initializer_list keys) : set{keys, Compare{}} {} + + constexpr set& operator=(const set &other) = default; + + /* capacity */ + constexpr bool empty() const { return true; } + constexpr size_type size() const { return 0; } + constexpr size_type max_size() const { return 0; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &) const { return 0; } + + template + constexpr const_iterator find(KeyType const &) const { return end(); } + + template + constexpr std::pair + equal_range(KeyType const &) const { return {end(), end()}; } + + template + constexpr const_iterator lower_bound(KeyType const &) const { return end(); } + + template + constexpr const_iterator upper_bound(KeyType const &) const { return end(); } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp(); } + constexpr const key_compare& value_comp() const { return static_cast(*this); } + + /* iterators */ + constexpr const_iterator begin() const { return nullptr; } + constexpr const_iterator cbegin() const { return nullptr; } + constexpr const_iterator end() const { return nullptr; } + constexpr const_iterator cend() const { return nullptr; } + + constexpr const_reverse_iterator rbegin() const { return nullptr; } + constexpr const_reverse_iterator crbegin() const { return nullptr; } + constexpr const_reverse_iterator rend() const { return nullptr; } + constexpr const_reverse_iterator crend() const { return nullptr; } +}; + +template +constexpr auto make_set(bits::ignored_arg = {}/* for consistency with the initializer below for N = 0*/) { + return set{}; +} + +template +constexpr auto make_set(const T (&args)[N]) { + return set(args); +} + +template +constexpr auto make_set(std::array const &args) { + return set(args); +} + +template +constexpr auto make_set(const T (&args)[N], Compare const& compare = Compare{}) { + return set(args, compare); +} + +template +constexpr auto make_set(std::array const &args, Compare const& compare = Compare{}) { + return set(args, compare); +} + +#ifdef FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +template +set(T, Args...) -> set; + +#endif // FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/string.h b/lib/Frozen/frozen/string.h new file mode 100644 index 000000000..354ed9c15 --- /dev/null +++ b/lib/Frozen/frozen/string.h @@ -0,0 +1,152 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_STRING_H +#define FROZEN_LETITGO_STRING_H + +#include "frozen/bits/elsa.h" +#include "frozen/bits/hash_string.h" +#include "frozen/bits/version.h" +#include "frozen/bits/defines.h" + +#include +#include + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW +#include +#endif + +namespace frozen { + +template +class basic_string { + using chr_t = _CharT; + + chr_t const *data_; + std::size_t size_; + +public: + template + constexpr basic_string(chr_t const (&data)[N]) + : data_(data), size_(N - 1) {} + constexpr basic_string(chr_t const *data, std::size_t size) + : data_(data), size_(size) {} + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW + constexpr basic_string(std::basic_string_view data) + : data_(data.data()), size_(data.size()) {} +#endif + + constexpr basic_string(const basic_string &) noexcept = default; + constexpr basic_string &operator=(const basic_string &) noexcept = default; + + constexpr std::size_t size() const { return size_; } + + constexpr chr_t operator[](std::size_t i) const { return data_[i]; } + + constexpr bool operator==(basic_string other) const { + if (size_ != other.size_) + return false; + for (std::size_t i = 0; i < size_; ++i) + if (data_[i] != other.data_[i]) + return false; + return true; + } + + constexpr bool operator<(const basic_string &other) const { + unsigned i = 0; + while (i < size() && i < other.size()) { + if ((*this)[i] < other[i]) { + return true; + } + if ((*this)[i] > other[i]) { + return false; + } + ++i; + } + return size() < other.size(); + } + + friend constexpr bool operator>(const basic_string& lhs, const basic_string& rhs) { + return rhs < lhs; + } + + constexpr const chr_t *data() const { return data_; } + constexpr const chr_t *begin() const { return data(); } + constexpr const chr_t *end() const { return data() + size(); } +}; + +template struct elsa> { + constexpr std::size_t operator()(basic_string<_CharT> value) const { + return hash_string(value); + } + constexpr std::size_t operator()(basic_string<_CharT> value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +using string = basic_string; +using wstring = basic_string; +using u16string = basic_string; +using u32string = basic_string; + +#ifdef FROZEN_LETITGO_HAS_CHAR8T +using u8string = basic_string; +#endif + +namespace string_literals { + +constexpr string operator"" _s(const char *data, std::size_t size) { + return {data, size}; +} + +constexpr wstring operator"" _s(const wchar_t *data, std::size_t size) { + return {data, size}; +} + +constexpr u16string operator"" _s(const char16_t *data, std::size_t size) { + return {data, size}; +} + +constexpr u32string operator"" _s(const char32_t *data, std::size_t size) { + return {data, size}; +} + +#ifdef FROZEN_LETITGO_HAS_CHAR8T +constexpr u8string operator"" _s(const char8_t *data, std::size_t size) { + return {data, size}; +} +#endif + +} // namespace string_literals + +} // namespace frozen + +namespace std { +template struct hash> { + std::size_t operator()(frozen::basic_string<_CharT> s) const { + return frozen::elsa>{}(s); + } +}; +} // namespace std + +#endif diff --git a/lib/Frozen/frozen/unordered_map.h b/lib/Frozen/frozen/unordered_map.h new file mode 100644 index 000000000..6f7b4a009 --- /dev/null +++ b/lib/Frozen/frozen/unordered_map.h @@ -0,0 +1,217 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_UNORDERED_MAP_H +#define FROZEN_LETITGO_UNORDERED_MAP_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/elsa.h" +#include "frozen/bits/exceptions.h" +#include "frozen/bits/pmh.h" +#include "frozen/bits/version.h" +#include "frozen/random.h" + +#include +#include +#include + +namespace frozen { + +namespace bits { + +struct GetKey { + template constexpr auto const &operator()(KV const &kv) const { + return kv.first; + } +}; + +} // namespace bits + +template , + class KeyEqual = std::equal_to> +class unordered_map : private KeyEqual { + static constexpr std::size_t storage_size = + bits::next_highest_power_of_two(N) * (N < 32 ? 2 : 1); // size adjustment to prevent high collision rate for small sets + using container_type = bits::carray, N>; + using tables_type = bits::pmh_tables; + + container_type items_; + tables_type tables_; + +public: + /* typedefs */ + using Self = unordered_map; + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using hasher = Hash; + using key_equal = KeyEqual; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = typename container_type::iterator; + using const_iterator = typename container_type::const_iterator; + +public: + /* constructors */ + unordered_map(unordered_map const &) = default; + constexpr unordered_map(container_type items, + Hash const &hash, KeyEqual const &equal) + : KeyEqual{equal} + , items_{items} + , tables_{ + bits::make_pmh_tables( + items_, hash, bits::GetKey{}, default_prg_t{})} {} + explicit constexpr unordered_map(container_type items) + : unordered_map{items, Hash{}, KeyEqual{}} {} + + constexpr unordered_map(std::initializer_list items, + Hash const & hash, KeyEqual const & equal) + : unordered_map{container_type{items}, hash, equal} { + constexpr_assert(items.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr unordered_map(std::initializer_list items) + : unordered_map{items, Hash{}, KeyEqual{}} {} + + /* iterators */ + constexpr iterator begin() { return items_.begin(); } + constexpr iterator end() { return items_.end(); } + constexpr const_iterator begin() const { return items_.begin(); } + constexpr const_iterator end() const { return items_.end(); } + constexpr const_iterator cbegin() const { return items_.begin(); } + constexpr const_iterator cend() const { return items_.end(); } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return find(key) != end(); + } + + template + constexpr Value const &at(KeyType const &key) const { + return at_impl(*this, key); + } + template + constexpr Value &at(KeyType const &key) { + return at_impl(*this, key); + } + + template + constexpr const_iterator find(KeyType const &key) const { + return find_impl(*this, key, hash_function(), key_eq()); + } + template + constexpr iterator find(KeyType const &key) { + return find_impl(*this, key, hash_function(), key_eq()); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != this->end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + return equal_range_impl(*this, key); + } + template + constexpr std::pair equal_range(KeyType const &key) { + return equal_range_impl(*this, key); + } + + /* bucket interface */ + constexpr std::size_t bucket_count() const { return storage_size; } + constexpr std::size_t max_bucket_count() const { return storage_size; } + + /* observers*/ + constexpr const hasher& hash_function() const { return tables_.hash_function(); } + constexpr const key_equal& key_eq() const { return static_cast(*this); } + +private: + template + static inline constexpr auto& at_impl(This&& self, KeyType const &key) { + auto it = self.find(key); + if (it != self.end()) + return it->second; + else + FROZEN_THROW_OR_ABORT(std::out_of_range("unknown key")); + } + + template + static inline constexpr auto find_impl(This&& self, KeyType const &key, Hasher const &hash, Equal const &equal) { + auto const pos = self.tables_.lookup(key, hash); + auto it = self.items_.begin() + pos; + if (it != self.items_.end() && equal(it->first, key)) + return it; + else + return self.items_.end(); + } + + template + static inline constexpr auto equal_range_impl(This&& self, KeyType const &key) { + auto const it = self.find(key); + if (it != self.end()) + return std::make_pair(it, it + 1); + else + return std::make_pair(self.end(), self.end()); + } +}; + +template +constexpr auto make_unordered_map(std::pair const (&items)[N]) { + return unordered_map{items}; +} + +template +constexpr auto make_unordered_map( + std::pair const (&items)[N], + Hasher const &hash = elsa{}, + Equal const &equal = std::equal_to{}) { + return unordered_map{items, hash, equal}; +} + +template +constexpr auto make_unordered_map(std::array, N> const &items) { + return unordered_map{items}; +} + +template +constexpr auto make_unordered_map( + std::array, N> const &items, + Hasher const &hash = elsa{}, + Equal const &equal = std::equal_to{}) { + return unordered_map{items, hash, equal}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/unordered_set.h b/lib/Frozen/frozen/unordered_set.h new file mode 100644 index 000000000..81bca6c5f --- /dev/null +++ b/lib/Frozen/frozen/unordered_set.h @@ -0,0 +1,181 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_UNORDERED_SET_H +#define FROZEN_LETITGO_UNORDERED_SET_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/elsa.h" +#include "frozen/bits/pmh.h" +#include "frozen/bits/version.h" +#include "frozen/random.h" + +#include + +namespace frozen { + +namespace bits { + +struct Get { + template constexpr T const &operator()(T const &key) const { + return key; + } +}; + +} // namespace bits + +template , + class KeyEqual = std::equal_to> +class unordered_set : private KeyEqual { + static constexpr std::size_t storage_size = + bits::next_highest_power_of_two(N) * (N < 32 ? 2 : 1); // size adjustment to prevent high collision rate for small sets + using container_type = bits::carray; + using tables_type = bits::pmh_tables; + + container_type keys_; + tables_type tables_; + +public: + /* typedefs */ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using hasher = Hash; + using key_equal = KeyEqual; + using const_reference = typename container_type::const_reference; + using reference = const_reference; + using const_pointer = typename container_type::const_pointer; + using pointer = const_pointer; + using const_iterator = typename container_type::const_iterator; + using iterator = const_iterator; + +public: + /* constructors */ + unordered_set(unordered_set const &) = default; + constexpr unordered_set(container_type keys, Hash const &hash, + KeyEqual const &equal) + : KeyEqual{equal} + , keys_{keys} + , tables_{bits::make_pmh_tables( + keys_, hash, bits::Get{}, default_prg_t{})} {} + explicit constexpr unordered_set(container_type keys) + : unordered_set{keys, Hash{}, KeyEqual{}} {} + + constexpr unordered_set(std::initializer_list keys) + : unordered_set{keys, Hash{}, KeyEqual{}} {} + + constexpr unordered_set(std::initializer_list keys, Hash const & hash, KeyEqual const & equal) + : unordered_set{container_type{keys}, hash, equal} { + constexpr_assert(keys.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + /* iterators */ + constexpr const_iterator begin() const { return keys_.begin(); } + constexpr const_iterator end() const { return keys_.end(); } + constexpr const_iterator cbegin() const { return keys_.begin(); } + constexpr const_iterator cend() const { return keys_.end(); } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return find(key, hash_function(), key_eq()) != end(); + } + + template + constexpr const_iterator find(KeyType const &key, Hasher const &hash, Equal const &equal) const { + auto const pos = tables_.lookup(key, hash); + auto it = keys_.begin() + pos; + if (it != keys_.end() && equal(*it, key)) + return it; + else + return keys_.end(); + } + template + constexpr const_iterator find(KeyType const &key) const { + auto const pos = tables_.lookup(key, hash_function()); + auto it = keys_.begin() + pos; + if (it != keys_.end() && key_eq()(*it, key)) + return it; + else + return keys_.end(); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != keys_.end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + auto const it = find(key); + if (it != end()) + return {it, it + 1}; + else + return {keys_.end(), keys_.end()}; + } + + /* bucket interface */ + constexpr std::size_t bucket_count() const { return storage_size; } + constexpr std::size_t max_bucket_count() const { return storage_size; } + + /* observers*/ + constexpr const hasher& hash_function() const { return tables_.hash_function(); } + constexpr const key_equal& key_eq() const { return static_cast(*this); } +}; + +template +constexpr auto make_unordered_set(T const (&keys)[N]) { + return unordered_set{keys}; +} + +template +constexpr auto make_unordered_set(T const (&keys)[N], Hasher const& hash, Equal const& equal) { + return unordered_set{keys, hash, equal}; +} + +template +constexpr auto make_unordered_set(std::array const &keys) { + return unordered_set{keys}; +} + +template +constexpr auto make_unordered_set(std::array const &keys, Hasher const& hash, Equal const& equal) { + return unordered_set{keys, hash, equal}; +} + +#ifdef FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +template +unordered_set(T, Args...) -> unordered_set; + +#endif + +} // namespace frozen + +#endif diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index c43c968bf..38791f584 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "Hoymiles.h" #include "Utils.h" @@ -24,12 +24,12 @@ void HoymilesClass::init() _radioCmt.reset(new HoymilesRadio_CMT()); } -void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ) { _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); } -void HoymilesClass::initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3) +void HoymilesClass::initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { _radioCmt->init(pin_sdio, pin_clk, pin_cs, pin_fcs, pin_gpio2, pin_gpio3); } @@ -40,110 +40,114 @@ void HoymilesClass::loop() _radioNrf->loop(); _radioCmt->loop(); - if (getNumInverters() > 0) { - if (millis() - _lastPoll > (_pollInterval * 1000)) { - static uint8_t inverterPos = 0; + if (getNumInverters() == 0) { + return; + } - std::shared_ptr iv = getInverterByPos(inverterPos); - if ((iv == nullptr) || ((iv != nullptr) && (!iv->getRadio()->isInitialized()))) { - if (++inverterPos >= getNumInverters()) { - inverterPos = 0; - } + if (millis() - _lastPoll > (_pollInterval * 1000)) { + static uint8_t inverterPos = 0; + + std::shared_ptr iv = getInverterByPos(inverterPos); + if ((iv == nullptr) || ((iv != nullptr) && (!iv->getRadio()->isInitialized()))) { + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; } + } - if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { + if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { - if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { - Hoymiles.getMessageOutput()->println("Set runtime data to zero"); - iv->Statistics()->zeroRuntimeData(); - } + if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { + iv->Statistics()->zeroRuntimeData(); + } - if (iv->getEnablePolling() || iv->getEnableCommands()) { - _messageOutput->print("Fetch inverter: "); - _messageOutput->println(iv->serial(), HEX); + if (iv->getEnablePolling() || iv->getEnableCommands()) { + _messageOutput->print("Fetch inverter: "); + _messageOutput->println(iv->serial(), HEX); - if (!iv->isReachable()) { - iv->sendChangeChannelRequest(); - } + if (!iv->isReachable()) { + iv->sendChangeChannelRequest(); + } - iv->sendStatsRequest(); + iv->sendStatsRequest(); - // Fetch event log - bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(force); + // Fetch event log + const bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; + iv->sendAlarmLogRequest(force); - // Fetch limit - if (((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) - && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - _messageOutput->println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(); - } + // Fetch limit + if (((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) + && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { + _messageOutput->println("Request SystemConfigPara"); + iv->sendSystemConfigParaRequest(); + } - // Set limit if required - if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend ActivePowerControl"); - iv->resendActivePowerControlRequest(); - } + // Set limit if required + if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend ActivePowerControl"); + iv->resendActivePowerControlRequest(); + } - // Set power status if required - if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend PowerCommand"); - iv->resendPowerControlRequest(); - } + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend PowerCommand"); + iv->resendPowerControlRequest(); + } - // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0) { - bool invalidDevInfo = !iv->DevInfo()->containsValidData() - && iv->DevInfo()->getLastUpdateAll() > 0 - && iv->DevInfo()->getLastUpdateSimple() > 0; - - if (invalidDevInfo) { - _messageOutput->println("DevInfo: No Valid Data"); - } - - if ((iv->DevInfo()->getLastUpdateAll() == 0) - || (iv->DevInfo()->getLastUpdateSimple() == 0) - || invalidDevInfo) { - _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(); - } - } + // Fetch dev info (but first fetch stats) + if (iv->Statistics()->getLastUpdate() > 0) { + const bool invalidDevInfo = !iv->DevInfo()->containsValidData() + && iv->DevInfo()->getLastUpdateAll() > 0 + && iv->DevInfo()->getLastUpdateSimple() > 0; - // Fetch grid profile - if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { - iv->sendGridOnProFileParaRequest(); + if (invalidDevInfo) { + _messageOutput->println("DevInfo: No Valid Data"); } - _lastPoll = millis(); + if ((iv->DevInfo()->getLastUpdateAll() == 0) + || (iv->DevInfo()->getLastUpdateSimple() == 0) + || invalidDevInfo) { + _messageOutput->println("Request device info"); + iv->sendDevInfoRequest(); + } } - if (++inverterPos >= getNumInverters()) { - inverterPos = 0; + // Fetch grid profile + if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { + iv->sendGridOnProFileParaRequest(); } + + _lastPoll = millis(); } - // Perform housekeeping of all inverters on day change - int8_t currentWeekDay = Utils::getWeekDay(); - static int8_t lastWeekDay = -1; - if (lastWeekDay == -1) { - lastWeekDay = currentWeekDay; - } else { - if (currentWeekDay != lastWeekDay) { + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; + } + } - for (auto& inv : _inverters) { - if (inv->getZeroYieldDayOnMidnight()) { - inv->Statistics()->zeroDailyData(); - } + // Perform housekeeping of all inverters on day change + const int8_t currentWeekDay = Utils::getWeekDay(); + static int8_t lastWeekDay = -1; + if (lastWeekDay == -1) { + lastWeekDay = currentWeekDay; + } else { + if (currentWeekDay != lastWeekDay) { + + for (auto& inv : _inverters) { + // Have to reset the offets first, otherwise it will + // Substract the offset from zero which leads to a high value + inv->Statistics()->resetYieldDayCorrection(); + if (inv->getZeroYieldDayOnMidnight()) { + inv->Statistics()->zeroDailyData(); } - - lastWeekDay = currentWeekDay; } + + lastWeekDay = currentWeekDay; } } } } -std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) +std::shared_ptr HoymilesClass::addInverter(const char* name, const uint64_t serial) { std::shared_ptr i = nullptr; if (HMT_4CH::isValidSerial(serial)) { @@ -176,7 +180,7 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u return nullptr; } -std::shared_ptr HoymilesClass::getInverterByPos(uint8_t pos) +std::shared_ptr HoymilesClass::getInverterByPos(const uint8_t pos) { if (pos >= _inverters.size()) { return nullptr; @@ -185,7 +189,7 @@ std::shared_ptr HoymilesClass::getInverterByPos(uint8_t pos) } } -std::shared_ptr HoymilesClass::getInverterBySerial(uint64_t serial) +std::shared_ptr HoymilesClass::getInverterBySerial(const uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { @@ -195,9 +199,9 @@ std::shared_ptr HoymilesClass::getInverterBySerial(uint64_t se return nullptr; } -std::shared_ptr HoymilesClass::getInverterByFragment(fragment_t* fragment) +std::shared_ptr HoymilesClass::getInverterByFragment(const fragment_t& fragment) { - if (fragment->len <= 4) { + if (fragment.len <= 4) { return nullptr; } @@ -207,10 +211,10 @@ std::shared_ptr HoymilesClass::getInverterByFragment(fragment_ serial_u p; p.u64 = inv->serial(); - if ((p.b[3] == fragment->fragment[1]) - && (p.b[2] == fragment->fragment[2]) - && (p.b[1] == fragment->fragment[3]) - && (p.b[0] == fragment->fragment[4])) { + if ((p.b[3] == fragment.fragment[1]) + && (p.b[2] == fragment.fragment[2]) + && (p.b[1] == fragment.fragment[3]) + && (p.b[0] == fragment.fragment[4])) { return inv; } @@ -218,7 +222,7 @@ std::shared_ptr HoymilesClass::getInverterByFragment(fragment_ return nullptr; } -void HoymilesClass::removeInverterBySerial(uint64_t serial) +void HoymilesClass::removeInverterBySerial(const uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { @@ -229,7 +233,7 @@ void HoymilesClass::removeInverterBySerial(uint64_t serial) } } -size_t HoymilesClass::getNumInverters() +size_t HoymilesClass::getNumInverters() const { return _inverters.size(); } @@ -244,17 +248,17 @@ HoymilesRadio_CMT* HoymilesClass::getRadioCmt() return _radioCmt.get(); } -bool HoymilesClass::isAllRadioIdle() +bool HoymilesClass::isAllRadioIdle() const { return _radioNrf.get()->isIdle() && _radioCmt.get()->isIdle(); } -uint32_t HoymilesClass::PollInterval() +uint32_t HoymilesClass::PollInterval() const { return _pollInterval; } -void HoymilesClass::setPollInterval(uint32_t interval) +void HoymilesClass::setPollInterval(const uint32_t interval) { _pollInterval = interval; } diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 7975f1ba1..86a7d6ca6 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -16,29 +16,29 @@ class HoymilesClass { public: void init(); - void initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); - void initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3); + void initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ); + void initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); void setMessageOutput(Print* output); Print* getMessageOutput(); Print* getVerboseMessageOutput(); - std::shared_ptr addInverter(const char* name, uint64_t serial); - std::shared_ptr getInverterByPos(uint8_t pos); - std::shared_ptr getInverterBySerial(uint64_t serial); - std::shared_ptr getInverterByFragment(fragment_t* fragment); - void removeInverterBySerial(uint64_t serial); - size_t getNumInverters(); + std::shared_ptr addInverter(const char* name, const uint64_t serial); + std::shared_ptr getInverterByPos(const uint8_t pos); + std::shared_ptr getInverterBySerial(const uint64_t serial); + std::shared_ptr getInverterByFragment(const fragment_t& fragment); + void removeInverterBySerial(const uint64_t serial); + size_t getNumInverters() const; HoymilesRadio_NRF* getRadioNrf(); HoymilesRadio_CMT* getRadioCmt(); - uint32_t PollInterval(); - void setPollInterval(uint32_t interval); + uint32_t PollInterval() const; + void setPollInterval(const uint32_t interval); void setVerboseLogging(bool verboseLogging); - bool isAllRadioIdle(); + bool isAllRadioIdle() const; private: std::vector> _inverters; diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 77fa609bd..7534dcbed 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -6,17 +6,17 @@ #include "Hoymiles.h" #include "crc.h" -serial_u HoymilesRadio::DtuSerial() +serial_u HoymilesRadio::DtuSerial() const { return _dtuSerial; } -void HoymilesRadio::setDtuSerial(uint64_t serial) +void HoymilesRadio::setDtuSerial(const uint64_t serial) { _dtuSerial.u64 = serial; } -serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) +serial_u HoymilesRadio::convertSerialToRadioId(const serial_u serial) { serial_u radioId; radioId.u64 = 0; @@ -28,27 +28,27 @@ serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) return radioId; } -bool HoymilesRadio::checkFragmentCrc(fragment_t* fragment) +bool HoymilesRadio::checkFragmentCrc(const fragment_t& fragment) const { - uint8_t crc = crc8(fragment->fragment, fragment->len - 1); - return (crc == fragment->fragment[fragment->len - 1]); + const uint8_t crc = crc8(fragment.fragment, fragment.len - 1); + return (crc == fragment.fragment[fragment.len - 1]); } -void HoymilesRadio::sendRetransmitPacket(uint8_t fragment_id) +void HoymilesRadio::sendRetransmitPacket(const uint8_t fragment_id) { CommandAbstract* cmd = _commandQueue.front().get(); CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); if (requestCmd != nullptr) { - sendEsbPacket(requestCmd); + sendEsbPacket(*requestCmd); } } void HoymilesRadio::sendLastPacketAgain() { CommandAbstract* cmd = _commandQueue.front().get(); - sendEsbPacket(cmd); + sendEsbPacket(*cmd); } void HoymilesRadio::handleReceivedPackage() @@ -59,7 +59,7 @@ void HoymilesRadio::handleReceivedPackage() if (nullptr != inv) { CommandAbstract* cmd = _commandQueue.front().get(); - uint8_t verifyResult = inv->verifyAllFragments(cmd); + uint8_t verifyResult = inv->verifyAllFragments(*cmd); if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { Hoymiles.getMessageOutput()->println("Nothing received, resend whole request"); sendLastPacketAgain(); @@ -105,7 +105,7 @@ void HoymilesRadio::handleReceivedPackage() auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); if (nullptr != inv) { inv->clearRxFragmentBuffer(); - sendEsbPacket(cmd); + sendEsbPacket(*cmd); } else { Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); _commandQueue.pop(); @@ -114,7 +114,7 @@ void HoymilesRadio::handleReceivedPackage() } } -void HoymilesRadio::dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline) +void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline) { for (uint8_t i = 0; i < len; i++) { Hoymiles.getVerboseMessageOutput()->printf("%02X ", buf[i]); @@ -124,17 +124,17 @@ void HoymilesRadio::dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline } } -bool HoymilesRadio::isInitialized() +bool HoymilesRadio::isInitialized() const { return _isInitialized; } -bool HoymilesRadio::isIdle() +bool HoymilesRadio::isIdle() const { return !_busyFlag; } -bool HoymilesRadio::isQueueEmpty() +bool HoymilesRadio::isQueueEmpty() const { return _commandQueue.size() == 0; } diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index fa2f6945d..33b8c613b 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -9,12 +9,12 @@ class HoymilesRadio { public: - serial_u DtuSerial(); - virtual void setDtuSerial(uint64_t serial); + serial_u DtuSerial() const; + virtual void setDtuSerial(const uint64_t serial); - bool isIdle(); - bool isQueueEmpty(); - bool isInitialized(); + bool isIdle() const; + bool isQueueEmpty() const; + bool isInitialized() const; void enqueCommand(std::shared_ptr cmd) { @@ -28,12 +28,12 @@ class HoymilesRadio { } protected: - static serial_u convertSerialToRadioId(serial_u serial); - void dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline = true); + static serial_u convertSerialToRadioId(const serial_u serial); + static void dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline = true); - bool checkFragmentCrc(fragment_t* fragment); - virtual void sendEsbPacket(CommandAbstract* cmd) = 0; - void sendRetransmitPacket(uint8_t fragment_id); + bool checkFragmentCrc(const fragment_t& fragment) const; + virtual void sendEsbPacket(CommandAbstract& cmd) = 0; + void sendRetransmitPacket(const uint8_t fragment_id); void sendLastPacketAgain(); void handleReceivedPackage(); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 9e69feec7..5c61c3b6a 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -53,7 +53,7 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) return true; } -void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3) +void HoymilesRadio_CMT::init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { _dtuSerial.u64 = 0; @@ -122,15 +122,15 @@ void HoymilesRadio_CMT::loop() // Perform package parsing only if no packages are received if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); - if (checkFragmentCrc(&f)) { + if (checkFragmentCrc(f)) { - serial_u dtuId = convertSerialToRadioId(_dtuSerial); + const serial_u dtuId = convertSerialToRadioId(_dtuSerial); // The CMT RF module does not filter foreign packages by itself. // Has to be done manually here. if (memcmp(&f.fragment[5], &dtuId.b[1], 4) == 0) { - std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + std::shared_ptr inv = Hoymiles.getInverterByFragment(f); if (nullptr != inv) { // Save packet in inverter rx buffer @@ -156,7 +156,7 @@ void HoymilesRadio_CMT::loop() handleReceivedPackage(); } -void HoymilesRadio_CMT::setPALevel(int8_t paLevel) +void HoymilesRadio_CMT::setPALevel(const int8_t paLevel) { if (!_isInitialized) { return; @@ -169,7 +169,7 @@ void HoymilesRadio_CMT::setPALevel(int8_t paLevel) } } -void HoymilesRadio_CMT::setInverterTargetFrequency(uint32_t frequency) +void HoymilesRadio_CMT::setInverterTargetFrequency(const uint32_t frequency) { _inverterTargetFrequency = frequency; if (!_isInitialized) { @@ -178,12 +178,12 @@ void HoymilesRadio_CMT::setInverterTargetFrequency(uint32_t frequency) cmtSwitchDtuFreq(_inverterTargetFrequency); } -uint32_t HoymilesRadio_CMT::getInverterTargetFrequency() +uint32_t HoymilesRadio_CMT::getInverterTargetFrequency() const { return _inverterTargetFrequency; } -bool HoymilesRadio_CMT::isConnected() +bool HoymilesRadio_CMT::isConnected() const { if (!_isInitialized) { return false; @@ -211,27 +211,27 @@ void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt2() _packetReceived = true; } -void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) +void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd) { - cmd->incrementSendCount(); + cmd.incrementSendCount(); - cmd->setRouterAddress(DtuSerial().u64); + cmd.setRouterAddress(DtuSerial().u64); _radio->stopListening(); - if (cmd->getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command + if (cmd.getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command cmtSwitchDtuFreq(HOY_BOOT_FREQ / 1000); } Hoymiles.getVerboseMessageOutput()->printf("TX %s %.2f MHz --> ", - cmd->getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); - cmd->dumpDataPayload(Hoymiles.getVerboseMessageOutput()); + cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); + cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput()); - if (!_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { + if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) { Hoymiles.getMessageOutput()->println("TX SPI Timeout"); } cmtSwitchDtuFreq(_inverterTargetFrequency); _radio->startListening(); _busyFlag = true; - _rxTimeout.set(cmd->getTimeout()); + _rxTimeout.set(cmd.getTimeout()); } diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 66314b3d4..ee566c3e9 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -18,13 +18,13 @@ class HoymilesRadio_CMT : public HoymilesRadio { public: - void init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3); + void init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); - void setPALevel(int8_t paLevel); - void setInverterTargetFrequency(uint32_t frequency); - uint32_t getInverterTargetFrequency(); + void setPALevel(const int8_t paLevel); + void setInverterTargetFrequency(const uint32_t frequency); + uint32_t getInverterTargetFrequency() const; - bool isConnected(); + bool isConnected() const; static uint32_t getMinFrequency(); static uint32_t getMaxFrequency(); @@ -36,7 +36,7 @@ class HoymilesRadio_CMT : public HoymilesRadio { void ARDUINO_ISR_ATTR handleInt1(); void ARDUINO_ISR_ATTR handleInt2(); - void sendEsbPacket(CommandAbstract* cmd); + void sendEsbPacket(CommandAbstract& cmd); std::unique_ptr _radio; diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 8406fde41..635014d65 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -8,7 +8,7 @@ #include #include -void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ) { _dtuSerial.u64 = 0; @@ -71,8 +71,8 @@ void HoymilesRadio_NRF::loop() // Perform package parsing only if no packages are received if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); - if (checkFragmentCrc(&f)) { - std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + if (checkFragmentCrc(f)) { + std::shared_ptr inv = Hoymiles.getInverterByFragment(f); if (nullptr != inv) { // Save packet in inverter rx buffer @@ -97,7 +97,7 @@ void HoymilesRadio_NRF::loop() handleReceivedPackage(); } -void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) +void HoymilesRadio_NRF::setPALevel(const rf24_pa_dbm_e paLevel) { if (!_isInitialized) { return; @@ -105,7 +105,7 @@ void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) _radio->setPALevel(paLevel); } -void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) +void HoymilesRadio_NRF::setDtuSerial(const uint64_t serial) { HoymilesRadio::setDtuSerial(serial); @@ -115,7 +115,7 @@ void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) openReadingPipe(); } -bool HoymilesRadio_NRF::isConnected() +bool HoymilesRadio_NRF::isConnected() const { if (!_isInitialized) { return false; @@ -123,7 +123,7 @@ bool HoymilesRadio_NRF::isConnected() return _radio->isChipConnected(); } -bool HoymilesRadio_NRF::isPVariant() +bool HoymilesRadio_NRF::isPVariant() const { if (!_isInitialized) { return false; @@ -133,15 +133,13 @@ bool HoymilesRadio_NRF::isPVariant() void HoymilesRadio_NRF::openReadingPipe() { - serial_u s; - s = convertSerialToRadioId(_dtuSerial); + const serial_u s = convertSerialToRadioId(_dtuSerial); _radio->openReadingPipe(1, s.u64); } -void HoymilesRadio_NRF::openWritingPipe(serial_u serial) +void HoymilesRadio_NRF::openWritingPipe(const serial_u serial) { - serial_u s; - s = convertSerialToRadioId(serial); + const serial_u s = convertSerialToRadioId(serial); _radio->openWritingPipe(s.u64); } @@ -171,29 +169,29 @@ void HoymilesRadio_NRF::switchRxCh() _radio->startListening(); } -void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) +void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd) { - cmd->incrementSendCount(); + cmd.incrementSendCount(); - cmd->setRouterAddress(DtuSerial().u64); + cmd.setRouterAddress(DtuSerial().u64); _radio->stopListening(); _radio->setChannel(getTxNxtChannel()); serial_u s; - s.u64 = cmd->getTargetAddress(); + s.u64 = cmd.getTargetAddress(); openWritingPipe(s); _radio->setRetries(3, 15); Hoymiles.getVerboseMessageOutput()->printf("TX %s Channel: %d --> ", - cmd->getCommandName().c_str(), _radio->getChannel()); - cmd->dumpDataPayload(Hoymiles.getVerboseMessageOutput()); - _radio->write(cmd->getDataPayload(), cmd->getDataSize()); + cmd.getCommandName().c_str(), _radio->getChannel()); + cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput()); + _radio->write(cmd.getDataPayload(), cmd.getDataSize()); _radio->setRetries(0, 0); openReadingPipe(); _radio->setChannel(getRxNxtChannel()); _radio->startListening(); _busyFlag = true; - _rxTimeout.set(cmd->getTimeout()); + _rxTimeout.set(cmd.getTimeout()); } diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.h b/lib/Hoymiles/src/HoymilesRadio_NRF.h index 8530a0e34..a6777ce52 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.h +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.h @@ -13,14 +13,14 @@ class HoymilesRadio_NRF : public HoymilesRadio { public: - void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); + void init(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ); void loop(); - void setPALevel(rf24_pa_dbm_e paLevel); + void setPALevel(const rf24_pa_dbm_e paLevel); - virtual void setDtuSerial(uint64_t serial); + virtual void setDtuSerial(const uint64_t serial); - bool isConnected(); - bool isPVariant(); + bool isConnected() const; + bool isPVariant() const; private: void ARDUINO_ISR_ATTR handleIntr(); @@ -28,9 +28,9 @@ class HoymilesRadio_NRF : public HoymilesRadio { uint8_t getTxNxtChannel(); void switchRxCh(); void openReadingPipe(); - void openWritingPipe(serial_u serial); + void openWritingPipe(const serial_u serial); - void sendEsbPacket(CommandAbstract* cmd); + void sendEsbPacket(CommandAbstract& cmd); std::unique_ptr _spiPtr; std::unique_ptr _radio; diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 78bcd55eb..95af23cfc 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -1,13 +1,31 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to send a limit to the inverter. + +Derives from DevControlCommand. + +Command structure: +SCmd: Sub-Command ID. Is always 0x0b +Limit: limit to be set in the inverter +Type: absolute / relative and persistant/non-persistant + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +------------------------------------------------------------------------------------------------------------------- + |<------ CRC16 ------>| +51 71 60 35 46 80 12 23 04 81 0b 00 00 00 00 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Cmd SCmd ? Limit Type CRC16 CRC8 +*/ #include "ActivePowerControlCommand.h" #include "inverters/InverterAbstract.h" #define CRC_SIZE 6 -ActivePowerControlCommand::ActivePowerControlCommand(uint64_t target_address, uint64_t router_address) +ActivePowerControlCommand::ActivePowerControlCommand(const uint64_t target_address, const uint64_t router_address) : DevControlCommand(target_address, router_address) { _payload[10] = 0x0b; @@ -17,21 +35,21 @@ ActivePowerControlCommand::ActivePowerControlCommand(uint64_t target_address, ui _payload[14] = 0x00; _payload[15] = 0x00; - udpateCRC(CRC_SIZE); // 2 byte crc + udpateCRC(CRC_SIZE); // 6 byte crc _payload_size = 18; setTimeout(2000); } -String ActivePowerControlCommand::getCommandName() +String ActivePowerControlCommand::getCommandName() const { return "ActivePowerControl"; } -void ActivePowerControlCommand::setActivePowerLimit(float limit, PowerLimitControlType type) +void ActivePowerControlCommand::setActivePowerLimit(const float limit, const PowerLimitControlType type) { - uint16_t l = limit * 10; + const uint16_t l = limit * 10; // limit _payload[12] = (l >> 8) & 0xff; @@ -44,30 +62,30 @@ void ActivePowerControlCommand::setActivePowerLimit(float limit, PowerLimitContr udpateCRC(CRC_SIZE); } -bool ActivePowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool ActivePowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { return false; } if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) { - inverter->SystemConfigPara()->setLimitPercent(getLimit()); + inverter.SystemConfigPara()->setLimitPercent(getLimit()); } else { - uint16_t max_power = inverter->DevInfo()->getMaxPower(); + const uint16_t max_power = inverter.DevInfo()->getMaxPower(); if (max_power > 0) { - inverter->SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); + inverter.SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); } else { // TODO(tbnobody): Not implemented yet because we only can publish the percentage value } } - inverter->SystemConfigPara()->setLastUpdateCommand(millis()); - inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); + inverter.SystemConfigPara()->setLastUpdateCommand(millis()); + inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); return true; } -float ActivePowerControlCommand::getLimit() +float ActivePowerControlCommand::getLimit() const { - uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); + const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); return l / 10; } @@ -76,7 +94,7 @@ PowerLimitControlType ActivePowerControlCommand::getType() return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]); } -void ActivePowerControlCommand::gotTimeout(InverterAbstract* inverter) +void ActivePowerControlCommand::gotTimeout(InverterAbstract& inverter) { - inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); + inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h index f3a359ead..b7831fb86 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h @@ -12,14 +12,14 @@ typedef enum { // ToDo: to be verified by field tests class ActivePowerControlCommand : public DevControlCommand { public: - explicit ActivePowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit ActivePowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract& inverter); - void setActivePowerLimit(float limit, PowerLimitControlType type = RelativNonPersistent); - float getLimit(); + void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent); + float getLimit() const; PowerLimitControlType getType(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index 574e0be2b..143a6cd56 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -1,11 +1,29 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch the eventlog from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x11 +* AlarmId: The last event id received from the inverter or zero in case that no events + has been received yet. --> Not Implemented yet + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 11 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap AlarmId Password CRC16 CRC8 +*/ #include "AlarmDataCommand.h" #include "inverters/InverterAbstract.h" -AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_address, time_t time) +AlarmDataCommand::AlarmDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -13,12 +31,12 @@ AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_addr setTimeout(750); } -String AlarmDataCommand::getCommandName() +String AlarmDataCommand::getCommandName() const { return "AlarmData"; } -bool AlarmDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool AlarmDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -27,19 +45,19 @@ bool AlarmDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fra // Move all fragments into target buffer uint8_t offs = 0; - inverter->EventLog()->beginAppendFragment(); - inverter->EventLog()->clearBuffer(); + inverter.EventLog()->beginAppendFragment(); + inverter.EventLog()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + inverter.EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->EventLog()->endAppendFragment(); - inverter->EventLog()->setLastAlarmRequestSuccess(CMD_OK); - inverter->EventLog()->setLastUpdate(millis()); + inverter.EventLog()->endAppendFragment(); + inverter.EventLog()->setLastAlarmRequestSuccess(CMD_OK); + inverter.EventLog()->setLastUpdate(millis()); return true; } -void AlarmDataCommand::gotTimeout(InverterAbstract* inverter) +void AlarmDataCommand::gotTimeout(InverterAbstract& inverter) { - inverter->EventLog()->setLastAlarmRequestSuccess(CMD_NOK); + inverter.EventLog()->setLastAlarmRequestSuccess(CMD_NOK); } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.h b/lib/Hoymiles/src/commands/AlarmDataCommand.h index 1c34a826c..abdfc5f83 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.h +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.h @@ -5,10 +5,10 @@ class AlarmDataCommand : public MultiDataCommand { public: - explicit AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit AlarmDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract& inverter); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp index 139bbea34..1001790d2 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp @@ -2,9 +2,23 @@ /* * Copyright (C) 2023 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Special command to set frequency channel on HMS/HMT inverters. + +Command structure: +* ID: fixed identifier and everytime 0x56 +* CH: Channel to which the inverter will be switched to + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------- +56 71 60 35 46 80 12 23 04 02 15 21 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^ +ID Target Addr Source Addr ? ? ? CH ? CRC8 +*/ #include "ChannelChangeCommand.h" -ChannelChangeCommand::ChannelChangeCommand(uint64_t target_address, uint64_t router_address, uint8_t channel) +ChannelChangeCommand::ChannelChangeCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t channel) : CommandAbstract(target_address, router_address) { _payload[0] = 0x56; @@ -18,22 +32,22 @@ ChannelChangeCommand::ChannelChangeCommand(uint64_t target_address, uint64_t rou setTimeout(10); } -String ChannelChangeCommand::getCommandName() +String ChannelChangeCommand::getCommandName() const { return "ChannelChangeCommand"; } -void ChannelChangeCommand::setChannel(uint8_t channel) +void ChannelChangeCommand::setChannel(const uint8_t channel) { _payload[12] = channel; } -uint8_t ChannelChangeCommand::getChannel() +uint8_t ChannelChangeCommand::getChannel() const { return _payload[12]; } -bool ChannelChangeCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool ChannelChangeCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { return true; } diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.h b/lib/Hoymiles/src/commands/ChannelChangeCommand.h index b646217c2..a0b38cab1 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.h +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.h @@ -5,14 +5,14 @@ class ChannelChangeCommand : public CommandAbstract { public: - explicit ChannelChangeCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t channel = 0); + explicit ChannelChangeCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t channel = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - void setChannel(uint8_t channel); - uint8_t getChannel(); + void setChannel(const uint8_t channel); + uint8_t getChannel() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); virtual uint8_t getMaxResendCount(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index 78d8d07d7..dafe2b175 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -1,12 +1,36 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +Command structure: +* Each package has a maximum of 32 bytes +* Target Address: the address of the inverter. Has to be read as hex value +* Source Address the address of the dtu itself. Has to be read as hex value +* CRC8: a crc8 checksum added to the end of the payload containing all valid data. + Each sub-commmand has to set it's own payload size. + +Conversion of Target Addr: +Inverter Serial Number: (0x)116171603546 +Target Address: 71 60 35 46 + +Conversion of Source Addr: +DTU Serial Number: (0x)199980122304 +Source Address: 80 12 23 04 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------- +|<------------- CRC8 ------------>| +00 71 60 35 46 80 12 23 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ + Target Addr Source Addr CRC8 +*/ #include "CommandAbstract.h" #include "crc.h" #include -CommandAbstract::CommandAbstract(uint64_t target_address, uint64_t router_address) +CommandAbstract::CommandAbstract(const uint64_t target_address, const uint64_t router_address) { memset(_payload, 0, RF_LEN); _payload_size = 0; @@ -32,48 +56,48 @@ void CommandAbstract::dumpDataPayload(Print* stream) stream->println(""); } -uint8_t CommandAbstract::getDataSize() +uint8_t CommandAbstract::getDataSize() const { return _payload_size + 1; // Original payload plus crc8 } -void CommandAbstract::setTargetAddress(uint64_t address) +void CommandAbstract::setTargetAddress(const uint64_t address) { convertSerialToPacketId(&_payload[1], address); _targetAddress = address; } -uint64_t CommandAbstract::getTargetAddress() +uint64_t CommandAbstract::getTargetAddress() const { return _targetAddress; } -void CommandAbstract::setRouterAddress(uint64_t address) +void CommandAbstract::setRouterAddress(const uint64_t address) { convertSerialToPacketId(&_payload[5], address); _routerAddress = address; } -uint64_t CommandAbstract::getRouterAddress() +uint64_t CommandAbstract::getRouterAddress() const { return _routerAddress; } -void CommandAbstract::setTimeout(uint32_t timeout) +void CommandAbstract::setTimeout(const uint32_t timeout) { _timeout = timeout; } -uint32_t CommandAbstract::getTimeout() +uint32_t CommandAbstract::getTimeout() const { return _timeout; } -void CommandAbstract::setSendCount(uint8_t count) +void CommandAbstract::setSendCount(const uint8_t count) { _sendCount = count; } -uint8_t CommandAbstract::getSendCount() +uint8_t CommandAbstract::getSendCount() const { return _sendCount; } @@ -83,12 +107,12 @@ uint8_t CommandAbstract::incrementSendCount() return _sendCount++; } -CommandAbstract* CommandAbstract::getRequestFrameCommand(uint8_t frame_no) +CommandAbstract* CommandAbstract::getRequestFrameCommand(const uint8_t frame_no) { return nullptr; } -void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial) +void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], const uint64_t serial) { serial_u s; s.u64 = serial; @@ -98,16 +122,16 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial) buffer[0] = s.b[3]; } -void CommandAbstract::gotTimeout(InverterAbstract* inverter) +void CommandAbstract::gotTimeout(InverterAbstract& inverter) { } -uint8_t CommandAbstract::getMaxResendCount() +uint8_t CommandAbstract::getMaxResendCount() const { return MAX_RESEND_COUNT; } -uint8_t CommandAbstract::getMaxRetransmitCount() +uint8_t CommandAbstract::getMaxRetransmitCount() const { return MAX_RETRANSMIT_COUNT; } diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index e6abc686c..677fc0d12 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -13,39 +13,39 @@ class InverterAbstract; class CommandAbstract { public: - explicit CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0); + explicit CommandAbstract(const uint64_t target_address = 0, const uint64_t router_address = 0); virtual ~CommandAbstract() {}; const uint8_t* getDataPayload(); void dumpDataPayload(Print* stream); - uint8_t getDataSize(); + uint8_t getDataSize() const; - void setTargetAddress(uint64_t address); - uint64_t getTargetAddress(); + void setTargetAddress(const uint64_t address); + uint64_t getTargetAddress() const; - void setRouterAddress(uint64_t address); - uint64_t getRouterAddress(); + void setRouterAddress(const uint64_t address); + uint64_t getRouterAddress() const; - void setTimeout(uint32_t timeout); - uint32_t getTimeout(); + void setTimeout(const uint32_t timeout); + uint32_t getTimeout() const; - virtual String getCommandName() = 0; + virtual String getCommandName() const = 0; - void setSendCount(uint8_t count); - uint8_t getSendCount(); + void setSendCount(const uint8_t count); + uint8_t getSendCount() const; uint8_t incrementSendCount(); - virtual CommandAbstract* getRequestFrameCommand(uint8_t frame_no); + virtual CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) = 0; - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) = 0; + virtual void gotTimeout(InverterAbstract& inverter); // Sets the amount how often the specific command is resent if all fragments where missing - virtual uint8_t getMaxResendCount(); + virtual uint8_t getMaxResendCount() const; // Sets the amount how often a missing fragment is re-requested if it was not available - virtual uint8_t getMaxRetransmitCount(); + virtual uint8_t getMaxRetransmitCount() const; protected: uint8_t _payload[RF_LEN]; @@ -57,5 +57,5 @@ class CommandAbstract { uint64_t _routerAddress; private: - static void convertSerialToPacketId(uint8_t buffer[], uint64_t serial); + static void convertSerialToPacketId(uint8_t buffer[], const uint64_t serial); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/DevControlCommand.cpp b/lib/Hoymiles/src/commands/DevControlCommand.cpp index fce935b82..a5e7d2b60 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.cpp +++ b/lib/Hoymiles/src/commands/DevControlCommand.cpp @@ -1,12 +1,29 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Has a variable length. + +Command structure: +* ID: fixed identifier and everytime 0x51 +* Cmd: Fixed at 0x81 for these types of commands +* Payload: dynamic amount of bytes +* CRC16: calcuclated over the highlighted amount of bytes + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +------------------------------------------------------------------------------------------------------------- + |<->| CRC16 +51 71 60 35 46 80 12 23 04 81 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^ ^^ +ID Target Addr Source Addr Cmd Payload CRC16 CRC8 +*/ #include "DevControlCommand.h" #include "crc.h" -DevControlCommand::DevControlCommand(uint64_t target_address, uint64_t router_address) +DevControlCommand::DevControlCommand(const uint64_t target_address, const uint64_t router_address) : CommandAbstract(target_address, router_address) { _payload[0] = 0x51; @@ -15,14 +32,14 @@ DevControlCommand::DevControlCommand(uint64_t target_address, uint64_t router_ad setTimeout(1000); } -void DevControlCommand::udpateCRC(uint8_t len) +void DevControlCommand::udpateCRC(const uint8_t len) { - uint16_t crc = crc16(&_payload[10], len); + const uint16_t crc = crc16(&_payload[10], len); _payload[10 + len] = (uint8_t)(crc >> 8); _payload[10 + len + 1] = (uint8_t)(crc); } -bool DevControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { for (uint8_t i = 0; i < max_fragment_id; i++) { if (fragment[i].mainCmd != (_payload[0] | 0x80)) { diff --git a/lib/Hoymiles/src/commands/DevControlCommand.h b/lib/Hoymiles/src/commands/DevControlCommand.h index f4d0a0495..c24bc60b2 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.h +++ b/lib/Hoymiles/src/commands/DevControlCommand.h @@ -5,10 +5,10 @@ class DevControlCommand : public CommandAbstract { public: - explicit DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit DevControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); protected: - void udpateCRC(uint8_t len); + void udpateCRC(const uint8_t len); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp index b175822e5..c7bd80272 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp @@ -1,11 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch firmware information from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x01 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 01 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "DevInfoAllCommand.h" #include "inverters/InverterAbstract.h" -DevInfoAllCommand::DevInfoAllCommand(uint64_t target_address, uint64_t router_address, time_t time) +DevInfoAllCommand::DevInfoAllCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -13,12 +29,12 @@ DevInfoAllCommand::DevInfoAllCommand(uint64_t target_address, uint64_t router_ad setTimeout(200); } -String DevInfoAllCommand::getCommandName() +String DevInfoAllCommand::getCommandName() const { return "DevInfoAll"; } -bool DevInfoAllCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevInfoAllCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -27,13 +43,13 @@ bool DevInfoAllCommand::handleResponse(InverterAbstract* inverter, fragment_t fr // Move all fragments into target buffer uint8_t offs = 0; - inverter->DevInfo()->beginAppendFragment(); - inverter->DevInfo()->clearBufferAll(); + inverter.DevInfo()->beginAppendFragment(); + inverter.DevInfo()->clearBufferAll(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); + inverter.DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->DevInfo()->endAppendFragment(); - inverter->DevInfo()->setLastUpdateAll(millis()); + inverter.DevInfo()->endAppendFragment(); + inverter.DevInfo()->setLastUpdateAll(millis()); return true; } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.h b/lib/Hoymiles/src/commands/DevInfoAllCommand.h index 165563846..3facffa7c 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.h @@ -5,9 +5,9 @@ class DevInfoAllCommand : public MultiDataCommand { public: - explicit DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit DevInfoAllCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp index 09d5a4675..2afaae4bd 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp @@ -1,11 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch hardware information from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x00 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 00 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "DevInfoSimpleCommand.h" #include "inverters/InverterAbstract.h" -DevInfoSimpleCommand::DevInfoSimpleCommand(uint64_t target_address, uint64_t router_address, time_t time) +DevInfoSimpleCommand::DevInfoSimpleCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -13,12 +29,12 @@ DevInfoSimpleCommand::DevInfoSimpleCommand(uint64_t target_address, uint64_t rou setTimeout(200); } -String DevInfoSimpleCommand::getCommandName() +String DevInfoSimpleCommand::getCommandName() const { return "DevInfoSimple"; } -bool DevInfoSimpleCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevInfoSimpleCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -27,13 +43,13 @@ bool DevInfoSimpleCommand::handleResponse(InverterAbstract* inverter, fragment_t // Move all fragments into target buffer uint8_t offs = 0; - inverter->DevInfo()->beginAppendFragment(); - inverter->DevInfo()->clearBufferSimple(); + inverter.DevInfo()->beginAppendFragment(); + inverter.DevInfo()->clearBufferSimple(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); + inverter.DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->DevInfo()->endAppendFragment(); - inverter->DevInfo()->setLastUpdateSimple(millis()); + inverter.DevInfo()->endAppendFragment(); + inverter.DevInfo()->setLastUpdateSimple(millis()); return true; } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h index 99b7f503b..66a7301a9 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h @@ -5,9 +5,9 @@ class DevInfoSimpleCommand : public MultiDataCommand { public: - explicit DevInfoSimpleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit DevInfoSimpleCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp index e9171672d..c98c7e5a5 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp @@ -1,12 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch the grid profile from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x02 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 02 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "GridOnProFilePara.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -GridOnProFilePara::GridOnProFilePara(uint64_t target_address, uint64_t router_address, time_t time) +GridOnProFilePara::GridOnProFilePara(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -14,12 +30,12 @@ GridOnProFilePara::GridOnProFilePara(uint64_t target_address, uint64_t router_ad setTimeout(500); } -String GridOnProFilePara::getCommandName() +String GridOnProFilePara::getCommandName() const { return "GridOnProFilePara"; } -bool GridOnProFilePara::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool GridOnProFilePara::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -28,13 +44,13 @@ bool GridOnProFilePara::handleResponse(InverterAbstract* inverter, fragment_t fr // Move all fragments into target buffer uint8_t offs = 0; - inverter->GridProfile()->beginAppendFragment(); - inverter->GridProfile()->clearBuffer(); + inverter.GridProfile()->beginAppendFragment(); + inverter.GridProfile()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + inverter.GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->GridProfile()->endAppendFragment(); - inverter->GridProfile()->setLastUpdate(millis()); + inverter.GridProfile()->endAppendFragment(); + inverter.GridProfile()->setLastUpdate(millis()); return true; } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.h b/lib/Hoymiles/src/commands/GridOnProFilePara.h index 41ee57ece..382ebcbb1 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.h +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.h @@ -5,9 +5,9 @@ class GridOnProFilePara : public MultiDataCommand { public: - explicit GridOnProFilePara(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit GridOnProFilePara(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index 39a0d4c64..bbd320916 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -1,11 +1,34 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Has a fixed length of 26 bytes. + +Command structure: +* ID: fixed identifier and everytime 0x15 +* Idx: the counter of sequencial packages to send. Currently it's only 0x80 + because all request requests only consist of one package. +* DT: repressents the data type and specifies which sub-command to be fetched +* Time: represents the current unix timestamp as hex format. The time on the inverter is synced to the sent time. + Can be calculated e.g. using the following command + echo "obase=16; $(date --date='2023-12-07 18:54:00' +%s)" | bc +* Gap: always 0x0 +* Password: currently always 0x0 +* CRC16: calcuclated over the highlighted amount of bytes + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 00 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "MultiDataCommand.h" #include "crc.h" -MultiDataCommand::MultiDataCommand(uint64_t target_address, uint64_t router_address, uint8_t data_type, time_t time) +MultiDataCommand::MultiDataCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t data_type, const time_t time) : CommandAbstract(target_address, router_address) { _payload[0] = 0x15; @@ -27,17 +50,17 @@ MultiDataCommand::MultiDataCommand(uint64_t target_address, uint64_t router_addr _payload_size = 26; } -void MultiDataCommand::setDataType(uint8_t data_type) +void MultiDataCommand::setDataType(const uint8_t data_type) { _payload[10] = data_type; udpateCRC(); } -uint8_t MultiDataCommand::getDataType() +uint8_t MultiDataCommand::getDataType() const { return _payload[10]; } -void MultiDataCommand::setTime(time_t time) +void MultiDataCommand::setTime(const time_t time) { _payload[12] = (uint8_t)(time >> 24); _payload[13] = (uint8_t)(time >> 16); @@ -46,7 +69,7 @@ void MultiDataCommand::setTime(time_t time) udpateCRC(); } -time_t MultiDataCommand::getTime() +time_t MultiDataCommand::getTime() const { return (time_t)(_payload[12] << 24) | (time_t)(_payload[13] << 16) @@ -54,7 +77,7 @@ time_t MultiDataCommand::getTime() | (time_t)(_payload[15]); } -CommandAbstract* MultiDataCommand::getRequestFrameCommand(uint8_t frame_no) +CommandAbstract* MultiDataCommand::getRequestFrameCommand(const uint8_t frame_no) { _cmdRequestFrame.setTargetAddress(getTargetAddress()); _cmdRequestFrame.setFrameNo(frame_no); @@ -62,7 +85,7 @@ CommandAbstract* MultiDataCommand::getRequestFrameCommand(uint8_t frame_no) return &_cmdRequestFrame; } -bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool MultiDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // All fragments are available --> Check CRC uint16_t crc = 0xffff, crcRcv = 0; @@ -88,12 +111,12 @@ bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fra void MultiDataCommand::udpateCRC() { - uint16_t crc = crc16(&_payload[10], 14); // From data_type till password + const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password _payload[24] = (uint8_t)(crc >> 8); _payload[25] = (uint8_t)(crc); } -uint8_t MultiDataCommand::getTotalFragmentSize(fragment_t fragment[], uint8_t max_fragment_id) +uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id) { uint8_t fragmentSize = 0; for (uint8_t i = 0; i < max_fragment_id; i++) { diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.h b/lib/Hoymiles/src/commands/MultiDataCommand.h index 4d2adfde4..821074745 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.h +++ b/lib/Hoymiles/src/commands/MultiDataCommand.h @@ -7,20 +7,20 @@ class MultiDataCommand : public CommandAbstract { public: - explicit MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0); + explicit MultiDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0); - void setTime(time_t time); - time_t getTime(); + void setTime(const time_t time); + time_t getTime() const; - CommandAbstract* getRequestFrameCommand(uint8_t frame_no); + CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); protected: - void setDataType(uint8_t data_type); - uint8_t getDataType(); + void setDataType(const uint8_t data_type); + uint8_t getDataType() const; void udpateCRC(); - static uint8_t getTotalFragmentSize(fragment_t fragment[], uint8_t max_fragment_id); + static uint8_t getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id); RequestFrameCommand _cmdRequestFrame; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.cpp b/lib/Hoymiles/src/commands/ParaSetCommand.cpp index 4a48cbea9..a33749450 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.cpp +++ b/lib/Hoymiles/src/commands/ParaSetCommand.cpp @@ -4,7 +4,7 @@ */ #include "ParaSetCommand.h" -ParaSetCommand::ParaSetCommand(uint64_t target_address, uint64_t router_address) +ParaSetCommand::ParaSetCommand(const uint64_t target_address, const uint64_t router_address) : CommandAbstract(target_address, router_address) { _payload[0] = 0x52; diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.h b/lib/Hoymiles/src/commands/ParaSetCommand.h index 9ca4e8a97..424d0e373 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.h +++ b/lib/Hoymiles/src/commands/ParaSetCommand.h @@ -5,5 +5,5 @@ class ParaSetCommand : public CommandAbstract { public: - explicit ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit ParaSetCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.cpp b/lib/Hoymiles/src/commands/PowerControlCommand.cpp index 522ad5f2d..fbf12db80 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/PowerControlCommand.cpp @@ -1,13 +1,32 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to power cycle the inverter. + +Derives from DevControlCommand. + +Command structure: +SCmd: Sub-Command ID + 00 --> Turn On + 01 --> Turn Off + 02 --> Restart + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------------- + |<--->| CRC16 +51 71 60 35 46 80 12 23 04 81 00 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^ ^^ +ID Target Addr Source Addr Cmd SCmd ? CRC16 CRC8 +*/ #include "PowerControlCommand.h" #include "inverters/InverterAbstract.h" #define CRC_SIZE 2 -PowerControlCommand::PowerControlCommand(uint64_t target_address, uint64_t router_address) +PowerControlCommand::PowerControlCommand(const uint64_t target_address, const uint64_t router_address) : DevControlCommand(target_address, router_address) { _payload[10] = 0x00; // TurnOn @@ -20,28 +39,28 @@ PowerControlCommand::PowerControlCommand(uint64_t target_address, uint64_t route setTimeout(2000); } -String PowerControlCommand::getCommandName() +String PowerControlCommand::getCommandName() const { return "PowerControl"; } -bool PowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool PowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { return false; } - inverter->PowerCommand()->setLastUpdateCommand(millis()); - inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_OK); + inverter.PowerCommand()->setLastUpdateCommand(millis()); + inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_OK); return true; } -void PowerControlCommand::gotTimeout(InverterAbstract* inverter) +void PowerControlCommand::gotTimeout(InverterAbstract& inverter) { - inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); + inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); } -void PowerControlCommand::setPowerOn(bool state) +void PowerControlCommand::setPowerOn(const bool state) { if (state) { _payload[10] = 0x00; // TurnOn diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h index 376d201e9..8b9f11ac4 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.h +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -5,13 +5,13 @@ class PowerControlCommand : public DevControlCommand { public: - explicit PowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit PowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract& inverter); - void setPowerOn(bool state); + void setPowerOn(const bool state); void setRestart(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 3f0aed36b..5f04c948b 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -1,12 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch live run time data from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x0b + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 0b 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "RealTimeRunDataCommand.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -RealTimeRunDataCommand::RealTimeRunDataCommand(uint64_t target_address, uint64_t router_address, time_t time) +RealTimeRunDataCommand::RealTimeRunDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -14,12 +30,12 @@ RealTimeRunDataCommand::RealTimeRunDataCommand(uint64_t target_address, uint64_t setTimeout(500); } -String RealTimeRunDataCommand::getCommandName() +String RealTimeRunDataCommand::getCommandName() const { return "RealTimeRunData"; } -bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -29,8 +45,8 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment // Check if at least all required bytes are received // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. - uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - uint8_t expectedSize = inverter->Statistics()->getExpectedByteCount(); + const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); + const uint8_t expectedSize = inverter.Statistics()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -40,19 +56,19 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment // Move all fragments into target buffer uint8_t offs = 0; - inverter->Statistics()->beginAppendFragment(); - inverter->Statistics()->clearBuffer(); + inverter.Statistics()->beginAppendFragment(); + inverter.Statistics()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + inverter.Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->Statistics()->endAppendFragment(); - inverter->Statistics()->resetRxFailureCount(); - inverter->Statistics()->setLastUpdate(millis()); + inverter.Statistics()->endAppendFragment(); + inverter.Statistics()->resetRxFailureCount(); + inverter.Statistics()->setLastUpdate(millis()); return true; } -void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter) +void RealTimeRunDataCommand::gotTimeout(InverterAbstract& inverter) { - inverter->Statistics()->incrementRxFailureCount(); + inverter.Statistics()->incrementRxFailureCount(); } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h index 8cb5be39b..7a0eeec14 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h @@ -5,10 +5,10 @@ class RealTimeRunDataCommand : public MultiDataCommand { public: - explicit RealTimeRunDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit RealTimeRunDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract& inverter); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp index e2bfb7668..68c4977f7 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp @@ -1,10 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to re-request a specific fragment returned by a MultiDataCommand from the inverter. + +Derives from SingleDataCommand. Has a fixed length of 10 bytes. + +Command structure: +* ID: fixed identifier and everytime 0x15 +* Idx: the counter of sequencial packages to send. Currently it's only 0x80 + because all request requests only consist of one package. +* Frm: is set to the fragment id to re-request. "Or" operation with 0x80 is applied to the frame. + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------- +15 71 60 35 46 80 12 23 04 85 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ +ID Target Addr Source Addr Frm CRC8 +*/ #include "RequestFrameCommand.h" -RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t router_address, uint8_t frame_no) +RequestFrameCommand::RequestFrameCommand(const uint64_t target_address, const uint64_t router_address, uint8_t frame_no) : SingleDataCommand(target_address, router_address) { if (frame_no > 127) { @@ -14,22 +32,22 @@ RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t route _payload_size = 10; } -String RequestFrameCommand::getCommandName() +String RequestFrameCommand::getCommandName() const { return "RequestFrame"; } -void RequestFrameCommand::setFrameNo(uint8_t frame_no) +void RequestFrameCommand::setFrameNo(const uint8_t frame_no) { _payload[9] = frame_no | 0x80; } -uint8_t RequestFrameCommand::getFrameNo() +uint8_t RequestFrameCommand::getFrameNo() const { return _payload[9] & (~0x80); } -bool RequestFrameCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool RequestFrameCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { return true; } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.h b/lib/Hoymiles/src/commands/RequestFrameCommand.h index 5d5e9da15..92663b708 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.h +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.h @@ -5,12 +5,12 @@ class RequestFrameCommand : public SingleDataCommand { public: - explicit RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0); + explicit RequestFrameCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, uint8_t frame_no = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - void setFrameNo(uint8_t frame_no); - uint8_t getFrameNo(); + void setFrameNo(const uint8_t frame_no); + uint8_t getFrameNo() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.cpp b/lib/Hoymiles/src/commands/SingleDataCommand.cpp index 636ee87ac..4f775146d 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.cpp +++ b/lib/Hoymiles/src/commands/SingleDataCommand.cpp @@ -1,10 +1,25 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to send simple commands, containing only one payload, to the inverter. + +Derives from CommandAbstract. + +Command structure: +* ID: fixed identifier and everytime 0x15 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------- +15 71 60 35 46 80 12 23 04 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ +ID Target Addr Source Addr CRC8 +*/ #include "SingleDataCommand.h" -SingleDataCommand::SingleDataCommand(uint64_t target_address, uint64_t router_address) +SingleDataCommand::SingleDataCommand(const uint64_t target_address, const uint64_t router_address) : CommandAbstract(target_address, router_address) { _payload[0] = 0x15; diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.h b/lib/Hoymiles/src/commands/SingleDataCommand.h index c891bda96..d05151691 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.h +++ b/lib/Hoymiles/src/commands/SingleDataCommand.h @@ -5,5 +5,5 @@ class SingleDataCommand : public CommandAbstract { public: - explicit SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit SingleDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 5e238a59b..0c8e7ded7 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -1,12 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ + +/* +This command is used to fetch current set limits from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x05 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 05 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "SystemConfigParaCommand.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64_t router_address, time_t time) +SystemConfigParaCommand::SystemConfigParaCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) : MultiDataCommand(target_address, router_address) { setTime(time); @@ -14,12 +30,12 @@ SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64 setTimeout(200); } -String SystemConfigParaCommand::getCommandName() +String SystemConfigParaCommand::getCommandName() const { return "SystemConfigPara"; } -bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { @@ -29,8 +45,8 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragmen // Check if at least all required bytes are received // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. - uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - uint8_t expectedSize = inverter->SystemConfigPara()->getExpectedByteCount(); + const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); + const uint8_t expectedSize = inverter.SystemConfigPara()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -40,19 +56,19 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragmen // Move all fragments into target buffer uint8_t offs = 0; - inverter->SystemConfigPara()->beginAppendFragment(); - inverter->SystemConfigPara()->clearBuffer(); + inverter.SystemConfigPara()->beginAppendFragment(); + inverter.SystemConfigPara()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + inverter.SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->SystemConfigPara()->endAppendFragment(); - inverter->SystemConfigPara()->setLastUpdateRequest(millis()); - inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); + inverter.SystemConfigPara()->endAppendFragment(); + inverter.SystemConfigPara()->setLastUpdateRequest(millis()); + inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); return true; } -void SystemConfigParaCommand::gotTimeout(InverterAbstract* inverter) +void SystemConfigParaCommand::gotTimeout(InverterAbstract& inverter) { - inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); + inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h index ef266fffd..e2480a973 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h @@ -5,10 +5,10 @@ class SystemConfigParaCommand : public MultiDataCommand { public: - explicit SystemConfigParaCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit SystemConfigParaCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract& inverter); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/crc.cpp b/lib/Hoymiles/src/crc.cpp index 531971328..582f4ed60 100644 --- a/lib/Hoymiles/src/crc.cpp +++ b/lib/Hoymiles/src/crc.cpp @@ -4,7 +4,7 @@ */ #include "crc.h" -uint8_t crc8(const uint8_t buf[], uint8_t len) +uint8_t crc8(const uint8_t buf[], const uint8_t len) { uint8_t crc = CRC8_INIT; for (uint8_t i = 0; i < len; i++) { @@ -16,7 +16,7 @@ uint8_t crc8(const uint8_t buf[], uint8_t len) return crc; } -uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start) +uint16_t crc16(const uint8_t buf[], const uint8_t len, const uint16_t start) { uint16_t crc = start; uint8_t shift = 0; @@ -33,7 +33,7 @@ uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start) return crc; } -uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit, uint16_t crcIn) +uint16_t crc16nrf24(const uint8_t buf[], const uint16_t lenBits, const uint16_t startBit, const uint16_t crcIn) { uint16_t crc = crcIn; uint8_t idx, val = buf[(startBit >> 3)]; diff --git a/lib/Hoymiles/src/crc.h b/lib/Hoymiles/src/crc.h index a1b01febf..e0fad8890 100644 --- a/lib/Hoymiles/src/crc.h +++ b/lib/Hoymiles/src/crc.h @@ -9,6 +9,6 @@ #define CRC16_MODBUS_POLYNOM 0xA001 #define CRC16_NRF24_POLYNOM 0x1021 -uint8_t crc8(const uint8_t buf[], uint8_t len); -uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start = 0xffff); -uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff); +uint8_t crc8(const uint8_t buf[], const uint8_t len); +uint16_t crc16(const uint8_t buf[], const uint8_t len, const uint16_t start = 0xffff); +uint16_t crc16nrf24(const uint8_t buf[], const uint16_t lenBits, const uint16_t startBit = 0, const uint16_t crcIn = 0xffff); diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index c659794c6..312cf6302 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -28,27 +28,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMS_1CH::HMS_1CH(HoymilesRadio* radio, uint64_t serial) +HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_1CH::isValidSerial(uint64_t serial) +bool HMS_1CH::isValidSerial(const uint64_t serial) { // serial >= 0x112400000000 && serial <= 0x112499999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1124; } -String HMS_1CH::typeName() +String HMS_1CH::typeName() const { return "HMS-300/350/400/450/500-1T"; } -const byteAssign_t* HMS_1CH::getByteAssignment() +const byteAssign_t* HMS_1CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_1CH::getByteAssignmentSize() +uint8_t HMS_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.h b/lib/Hoymiles/src/inverters/HMS_1CH.h index 437f3d332..a5a64c177 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.h +++ b/lib/Hoymiles/src/inverters/HMS_1CH.h @@ -6,9 +6,9 @@ class HMS_1CH : public HMS_Abstract { public: - explicit HMS_1CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_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; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp index 08de0a354..b6a9d93e0 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp @@ -28,27 +28,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, uint64_t serial) +HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_1CHv2::isValidSerial(uint64_t serial) +bool HMS_1CHv2::isValidSerial(const uint64_t serial) { // serial >= 0x112500000000 && serial <= 0x112599999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1125; } -String HMS_1CHv2::typeName() +String HMS_1CHv2::typeName() const { return "HMS-500-1T v2"; } -const byteAssign_t* HMS_1CHv2::getByteAssignment() +const byteAssign_t* HMS_1CHv2::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_1CHv2::getByteAssignmentSize() +uint8_t HMS_1CHv2::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.h b/lib/Hoymiles/src/inverters/HMS_1CHv2.h index 5f4981185..c831d1204 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.h +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.h @@ -6,9 +6,9 @@ class HMS_1CHv2 : public HMS_Abstract { public: - explicit HMS_1CHv2(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_1CHv2(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; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index e33de9944..d038e7727 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -35,27 +35,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMS_2CH::HMS_2CH(HoymilesRadio* radio, uint64_t serial) +HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_2CH::isValidSerial(uint64_t serial) +bool HMS_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114400000000 && serial <= 0x114499999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1144; } -String HMS_2CH::typeName() +String HMS_2CH::typeName() const { return "HMS-600/700/800/900/1000-2T"; } -const byteAssign_t* HMS_2CH::getByteAssignment() +const byteAssign_t* HMS_2CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_2CH::getByteAssignmentSize() +uint8_t HMS_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.h b/lib/Hoymiles/src/inverters/HMS_2CH.h index dff704ec2..9f1ed91f6 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.h +++ b/lib/Hoymiles/src/inverters/HMS_2CH.h @@ -6,9 +6,9 @@ class HMS_2CH : public HMS_Abstract { public: - explicit HMS_2CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index ffdc20559..eff44abc5 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -49,27 +49,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMS_4CH::HMS_4CH(HoymilesRadio* radio, uint64_t serial) +HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_4CH::isValidSerial(uint64_t serial) +bool HMS_4CH::isValidSerial(const uint64_t serial) { // serial >= 0x116400000000 && serial <= 0x116499999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1164; } -String HMS_4CH::typeName() +String HMS_4CH::typeName() const { - return "HMS-1600/1800/2000"; + return "HMS-1600/1800/2000-4T"; } -const byteAssign_t* HMS_4CH::getByteAssignment() +const byteAssign_t* HMS_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_4CH::getByteAssignmentSize() +uint8_t HMS_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.h b/lib/Hoymiles/src/inverters/HMS_4CH.h index 6a2e2b144..9d49de07a 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.h +++ b/lib/Hoymiles/src/inverters/HMS_4CH.h @@ -5,9 +5,9 @@ class HMS_4CH : public HMS_Abstract { public: - explicit HMS_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_4CH(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; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp index f67ff11bb..235a9ba77 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -7,7 +7,7 @@ #include "HoymilesRadio_CMT.h" #include "commands/ChannelChangeCommand.h" -HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, uint64_t serial) +HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) { } diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.h b/lib/Hoymiles/src/inverters/HMS_Abstract.h index 6d363f6ec..c4026a536 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.h @@ -5,7 +5,7 @@ class HMS_Abstract : public HM_Abstract { public: - explicit HMS_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HMS_Abstract(HoymilesRadio* radio, const uint64_t serial); virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index d30a404ba..717099b77 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -58,27 +58,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMT_4CH::HMT_4CH(HoymilesRadio* radio, uint64_t serial) +HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) : HMT_Abstract(radio, serial) {}; -bool HMT_4CH::isValidSerial(uint64_t serial) +bool HMT_4CH::isValidSerial(const uint64_t serial) { // serial >= 0x136100000000 && serial <= 0x136199999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1361; } -String HMT_4CH::typeName() +String HMT_4CH::typeName() const { return F("HMT-1600/1800/2000-4T"); } -const byteAssign_t* HMT_4CH::getByteAssignment() +const byteAssign_t* HMT_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMT_4CH::getByteAssignmentSize() +uint8_t HMT_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.h b/lib/Hoymiles/src/inverters/HMT_4CH.h index 7358dd45d..01d328938 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.h +++ b/lib/Hoymiles/src/inverters/HMT_4CH.h @@ -5,9 +5,9 @@ class HMT_4CH : public HMT_Abstract { public: - explicit HMT_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMT_4CH(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; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 69b3a60bb..6cbd20971 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -72,27 +72,27 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HMT_6CH::HMT_6CH(HoymilesRadio* radio, uint64_t serial) +HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) : HMT_Abstract(radio, serial) {}; -bool HMT_6CH::isValidSerial(uint64_t serial) +bool HMT_6CH::isValidSerial(const uint64_t serial) { // serial >= 0x138200000000 && serial <= 0x138299999999 uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1382; } -String HMT_6CH::typeName() +String HMT_6CH::typeName() const { return F("HMT-1800/2250-6T"); } -const byteAssign_t* HMT_6CH::getByteAssignment() +const byteAssign_t* HMT_6CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMT_6CH::getByteAssignmentSize() +uint8_t HMT_6CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.h b/lib/Hoymiles/src/inverters/HMT_6CH.h index ea4be7153..6b7280068 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.h +++ b/lib/Hoymiles/src/inverters/HMT_6CH.h @@ -5,9 +5,9 @@ class HMT_6CH : public HMT_Abstract { public: - explicit HMT_6CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMT_6CH(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; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp index c345be977..578233ee1 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -8,7 +8,7 @@ #include "commands/ChannelChangeCommand.h" #include "parser/AlarmLogParser.h" -HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, uint64_t serial) +HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) { EventLog()->setMessageType(AlarmMessageType_t::HMT); diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.h b/lib/Hoymiles/src/inverters/HMT_Abstract.h index 9e10a2c39..c913683c5 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.h @@ -5,7 +5,7 @@ class HMT_Abstract : public HM_Abstract { public: - explicit HMT_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HMT_Abstract(HoymilesRadio* radio, const uint64_t serial); virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index a7c39f4c0..7b23207da 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -28,10 +28,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HM_1CH::HM_1CH(HoymilesRadio* radio, uint64_t serial) +HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_1CH::isValidSerial(uint64_t serial) +bool HM_1CH::isValidSerial(const uint64_t serial) { // serial >= 0x112100000000 && serial <= 0x112199999999 @@ -51,17 +51,17 @@ bool HM_1CH::isValidSerial(uint64_t serial) return false; } -String HM_1CH::typeName() +String HM_1CH::typeName() const { return "HM-300/350/400-1T"; } -const byteAssign_t* HM_1CH::getByteAssignment() +const byteAssign_t* HM_1CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_1CH::getByteAssignmentSize() +uint8_t HM_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index cb18dcf51..a35b4e568 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -6,9 +6,9 @@ class HM_1CH : public HM_Abstract { public: - explicit HM_1CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_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; }; \ 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 2dc674b26..2f56ec3e6 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -36,10 +36,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HM_2CH::HM_2CH(HoymilesRadio* radio, uint64_t serial) +HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_2CH::isValidSerial(uint64_t serial) +bool HM_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114100000000 && serial <= 0x114199999999 @@ -59,17 +59,17 @@ bool HM_2CH::isValidSerial(uint64_t serial) return false; } -String HM_2CH::typeName() +String HM_2CH::typeName() const { return "HM-600/700/800-2T"; } -const byteAssign_t* HM_2CH::getByteAssignment() +const byteAssign_t* HM_2CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_2CH::getByteAssignmentSize() +uint8_t HM_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index 06ac509da..1fd54496a 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -5,9 +5,9 @@ class HM_2CH : public HM_Abstract { public: - explicit HM_2CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ 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 f5920491a..bcad2536f 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -49,10 +49,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; -HM_4CH::HM_4CH(HoymilesRadio* radio, uint64_t serial) +HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_4CH::isValidSerial(uint64_t serial) +bool HM_4CH::isValidSerial(const uint64_t serial) { // serial >= 0x116100000000 && serial <= 0x116199999999 @@ -72,17 +72,17 @@ bool HM_4CH::isValidSerial(uint64_t serial) return false; } -String HM_4CH::typeName() +String HM_4CH::typeName() const { return "HM-1000/1200/1500-4T"; } -const byteAssign_t* HM_4CH::getByteAssignment() +const byteAssign_t* HM_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_4CH::getByteAssignmentSize() +uint8_t HM_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index 44d341aeb..e54f33234 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -5,9 +5,9 @@ class HM_4CH : public HM_Abstract { public: - explicit HM_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_4CH(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; }; \ 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 10d936791..38515ab83 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -13,7 +13,7 @@ #include "commands/RealTimeRunDataCommand.h" #include "commands/SystemConfigParaCommand.h" -HM_Abstract::HM_Abstract(HoymilesRadio* radio, uint64_t serial) +HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial) : InverterAbstract(radio, serial) {}; bool HM_Abstract::sendStatsRequest() @@ -38,7 +38,7 @@ bool HM_Abstract::sendStatsRequest() return true; } -bool HM_Abstract::sendAlarmLogRequest(bool force) +bool HM_Abstract::sendAlarmLogRequest(const bool force) { if (!getEnablePolling()) { return false; @@ -121,7 +121,7 @@ bool HM_Abstract::sendSystemConfigParaRequest() return true; } -bool HM_Abstract::sendActivePowerControlRequest(float limit, PowerLimitControlType type) +bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitControlType type) { if (!getEnableCommands()) { return false; @@ -152,7 +152,7 @@ bool HM_Abstract::resendActivePowerControlRequest() return sendActivePowerControlRequest(_activePowerControlLimit, _activePowerControlType); } -bool HM_Abstract::sendPowerControlRequest(bool turnOn) +bool HM_Abstract::sendPowerControlRequest(const bool turnOn) { if (!getEnableCommands()) { return false; diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index 3a5cc637b..491149dc2 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -5,14 +5,14 @@ class HM_Abstract : public InverterAbstract { public: - explicit HM_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HM_Abstract(HoymilesRadio* radio, const uint64_t serial); bool sendStatsRequest(); - bool sendAlarmLogRequest(bool force = false); + bool sendAlarmLogRequest(const bool force = false); bool sendDevInfoRequest(); bool sendSystemConfigParaRequest(); - bool sendActivePowerControlRequest(float limit, PowerLimitControlType type); + bool sendActivePowerControlRequest(float limit, const PowerLimitControlType type); bool resendActivePowerControlRequest(); - bool sendPowerControlRequest(bool turnOn); + bool sendPowerControlRequest(const bool turnOn); bool sendRestartControlRequest(); bool resendPowerControlRequest(); bool sendGridOnProFileParaRequest(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 4c5aa422d..d80d0e531 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -7,7 +7,7 @@ #include "crc.h" #include -InverterAbstract::InverterAbstract(HoymilesRadio* radio, uint64_t serial) +InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial) { _serial.u64 = serial; _radio = radio; @@ -35,12 +35,12 @@ void InverterAbstract::init() _statisticsParser.get()->setByteAssignment(getByteAssignment(), getByteAssignmentSize()); } -uint64_t InverterAbstract::serial() +uint64_t InverterAbstract::serial() const { return _serial.u64; } -const String& InverterAbstract::serialString() +const String& InverterAbstract::serialString() const { return _serialString; } @@ -55,7 +55,7 @@ void InverterAbstract::setName(const char* name) _name[len] = '\0'; } -const char* InverterAbstract::name() +const char* InverterAbstract::name() const { return _name; } @@ -77,52 +77,52 @@ bool InverterAbstract::isReachable() return _enablePolling && Statistics()->getRxFailureCount() <= _reachableThreshold; } -void InverterAbstract::setEnablePolling(bool enabled) +void InverterAbstract::setEnablePolling(const bool enabled) { _enablePolling = enabled; } -bool InverterAbstract::getEnablePolling() +bool InverterAbstract::getEnablePolling() const { return _enablePolling; } -void InverterAbstract::setEnableCommands(bool enabled) +void InverterAbstract::setEnableCommands(const bool enabled) { _enableCommands = enabled; } -bool InverterAbstract::getEnableCommands() +bool InverterAbstract::getEnableCommands() const { return _enableCommands; } -void InverterAbstract::setReachableThreshold(uint8_t threshold) +void InverterAbstract::setReachableThreshold(const uint8_t threshold) { _reachableThreshold = threshold; } -uint8_t InverterAbstract::getReachableThreshold() +uint8_t InverterAbstract::getReachableThreshold() const { return _reachableThreshold; } -void InverterAbstract::setZeroValuesIfUnreachable(bool enabled) +void InverterAbstract::setZeroValuesIfUnreachable(const bool enabled) { _zeroValuesIfUnreachable = enabled; } -bool InverterAbstract::getZeroValuesIfUnreachable() +bool InverterAbstract::getZeroValuesIfUnreachable() const { return _zeroValuesIfUnreachable; } -void InverterAbstract::setZeroYieldDayOnMidnight(bool enabled) +void InverterAbstract::setZeroYieldDayOnMidnight(const bool enabled) { _zeroYieldDayOnMidnight = enabled; } -bool InverterAbstract::getZeroYieldDayOnMidnight() +bool InverterAbstract::getZeroYieldDayOnMidnight() const { return _zeroYieldDayOnMidnight; } @@ -175,7 +175,7 @@ void InverterAbstract::clearRxFragmentBuffer() _rxFragmentRetransmitCnt = 0; } -void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) +void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) { if (len < 11) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); @@ -187,10 +187,10 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) return; } - uint8_t fragmentCount = fragment[9]; + const uint8_t fragmentCount = fragment[9]; // Packets with 0x81 will be seen as 1 - uint8_t fragmentId = fragmentCount & 0b01111111; // fragmentId is 1 based + const uint8_t fragmentId = fragmentCount & 0b01111111; // fragmentId is 1 based if (fragmentId == 0) { Hoymiles.getMessageOutput()->println("ERROR: fragment id zero received and ignored"); @@ -218,15 +218,15 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) } // Returns Zero on Success or the Fragment ID for retransmit or error code -uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) +uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) { // All missing if (_rxFragmentLastPacketId == 0) { Hoymiles.getMessageOutput()->println("All missing"); - if (cmd->getSendCount() <= cmd->getMaxResendCount()) { + if (cmd.getSendCount() <= cmd.getMaxResendCount()) { return FRAGMENT_ALL_MISSING_RESEND; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(*this); return FRAGMENT_ALL_MISSING_TIMEOUT; } } @@ -234,10 +234,10 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // Last fragment is missing (the one with 0x80) if (_rxFragmentMaxPacketId == 0) { Hoymiles.getMessageOutput()->println("Last missing"); - if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { + if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return _rxFragmentLastPacketId + 1; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(*this); return FRAGMENT_RETRANSMIT_TIMEOUT; } } @@ -246,17 +246,17 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) { if (!_rxFragmentBuffer[i].wasReceived) { Hoymiles.getMessageOutput()->println("Middle missing"); - if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { + if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return i + 1; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(*this); return FRAGMENT_RETRANSMIT_TIMEOUT; } } } - if (!cmd->handleResponse(this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) { - cmd->gotTimeout(this); + if (!cmd.handleResponse(*this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) { + cmd.gotTimeout(*this); return FRAGMENT_HANDLE_ERROR; } diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index e6f70f070..3d9929d7b 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -30,45 +30,45 @@ class CommandAbstract; class InverterAbstract { public: - explicit InverterAbstract(HoymilesRadio* radio, uint64_t serial); + explicit InverterAbstract(HoymilesRadio* radio, const uint64_t serial); void init(); - uint64_t serial(); - const String& serialString(); + uint64_t serial() const; + const String& serialString() const; void setName(const char* name); - const char* name(); - virtual String typeName() = 0; - virtual const byteAssign_t* getByteAssignment() = 0; - virtual uint8_t getByteAssignmentSize() = 0; + const char* name() const; + virtual String typeName() const = 0; + virtual const byteAssign_t* getByteAssignment() const = 0; + virtual uint8_t getByteAssignmentSize() const = 0; bool isProducing(); bool isReachable(); - void setEnablePolling(bool enabled); - bool getEnablePolling(); + void setEnablePolling(const bool enabled); + bool getEnablePolling() const; - void setEnableCommands(bool enabled); - bool getEnableCommands(); + void setEnableCommands(const bool enabled); + bool getEnableCommands() const; - void setReachableThreshold(uint8_t threshold); - uint8_t getReachableThreshold(); + void setReachableThreshold(const uint8_t threshold); + uint8_t getReachableThreshold() const; - void setZeroValuesIfUnreachable(bool enabled); - bool getZeroValuesIfUnreachable(); + void setZeroValuesIfUnreachable(const bool enabled); + bool getZeroValuesIfUnreachable() const; - void setZeroYieldDayOnMidnight(bool enabled); - bool getZeroYieldDayOnMidnight(); + void setZeroYieldDayOnMidnight(const bool enabled); + bool getZeroYieldDayOnMidnight() const; void clearRxFragmentBuffer(); - void addRxFragment(uint8_t fragment[], uint8_t len); - uint8_t verifyAllFragments(CommandAbstract* cmd); + void addRxFragment(const uint8_t fragment[], const uint8_t len); + uint8_t verifyAllFragments(CommandAbstract& cmd); virtual bool sendStatsRequest() = 0; - virtual bool sendAlarmLogRequest(bool force = false) = 0; + virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendDevInfoRequest() = 0; virtual bool sendSystemConfigParaRequest() = 0; - virtual bool sendActivePowerControlRequest(float limit, PowerLimitControlType type) = 0; + virtual bool sendActivePowerControlRequest(float limit, const PowerLimitControlType type) = 0; virtual bool resendActivePowerControlRequest() = 0; - virtual bool sendPowerControlRequest(bool turnOn) = 0; + virtual bool sendPowerControlRequest(const bool turnOn) = 0; virtual bool sendRestartControlRequest() = 0; virtual bool resendPowerControlRequest() = 0; virtual bool sendChangeChannelRequest(); diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index fe2d2bab4..4086f8e38 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -181,7 +181,7 @@ void AlarmLogParser::clearBuffer() _alarmLogLength = 0; } -void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void AlarmLogParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > ALARM_LOG_PAYLOAD_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\r\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE); @@ -191,7 +191,7 @@ void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t le _alarmLogLength += len; } -uint8_t AlarmLogParser::getEntryCount() +uint8_t AlarmLogParser::getEntryCount() const { if (_alarmLogLength < 2) { return 0; @@ -199,30 +199,30 @@ uint8_t AlarmLogParser::getEntryCount() return (_alarmLogLength - 2) / ALARM_LOG_ENTRY_SIZE; } -void AlarmLogParser::setLastAlarmRequestSuccess(LastCommandSuccess status) +void AlarmLogParser::setLastAlarmRequestSuccess(const LastCommandSuccess status) { _lastAlarmRequestSuccess = status; } -LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess() +LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess() const { return _lastAlarmRequestSuccess; } -void AlarmLogParser::setMessageType(AlarmMessageType_t type) +void AlarmLogParser::setMessageType(const AlarmMessageType_t type) { _messageType = type; } -void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmMessageLocale_t locale) +void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, const AlarmMessageLocale_t locale) { - uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE; + const uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE; - int timezoneOffset = getTimezoneOffset(); + const int timezoneOffset = getTimezoneOffset(); HOY_SEMAPHORE_TAKE(); - uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; + const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; uint32_t startTimeOffset = 0; if (((wcode >> 13) & 0x01) == 1) { startTimeOffset = 12 * 60 * 60; @@ -233,40 +233,40 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmM endTimeOffset = 12 * 60 * 60; } - entry->MessageId = _payloadAlarmLog[entryStartOffset + 1]; - entry->StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; - entry->EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); + entry.MessageId = _payloadAlarmLog[entryStartOffset + 1]; + entry.StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; + entry.EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); HOY_SEMAPHORE_GIVE(); - if (entry->EndTime > 0) { - entry->EndTime += (endTimeOffset + timezoneOffset); + if (entry.EndTime > 0) { + entry.EndTime += (endTimeOffset + timezoneOffset); } switch (locale) { case AlarmMessageLocale_t::DE: - entry->Message = "Unbekannt"; + entry.Message = "Unbekannt"; break; case AlarmMessageLocale_t::FR: - entry->Message = "Inconnu"; + entry.Message = "Inconnu"; break; default: - entry->Message = "Unknown"; + entry.Message = "Unknown"; } for (auto& msg : _alarmMessages) { - if (msg.MessageId == entry->MessageId) { + if (msg.MessageId == entry.MessageId) { if (msg.InverterType == _messageType) { - entry->Message = getLocaleMessage(&msg, locale); + entry.Message = getLocaleMessage(&msg, locale); break; } else if (msg.InverterType == AlarmMessageType_t::ALL) { - entry->Message = getLocaleMessage(&msg, locale); + entry.Message = getLocaleMessage(&msg, locale); } } } } -String AlarmLogParser::getLocaleMessage(const AlarmMessage_t* msg, AlarmMessageLocale_t locale) +String AlarmLogParser::getLocaleMessage(const AlarmMessage_t* msg, const AlarmMessageLocale_t locale) const { if (locale == AlarmMessageLocale_t::DE) { return msg->Message_de[0] != '\0' ? msg->Message_de : msg->Message_en; diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 9189d175e..a6f0c10c6 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -31,28 +31,28 @@ enum class AlarmMessageLocale_t { typedef struct { AlarmMessageType_t InverterType; uint16_t MessageId; - char Message_en[62]; - char Message_de[63]; - char Message_fr[64]; + const char* Message_en; + const char* Message_de; + const char* Message_fr; } AlarmMessage_t; class AlarmLogParser : public Parser { public: AlarmLogParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); - uint8_t getEntryCount(); - void getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN); + uint8_t getEntryCount() const; + void getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, const AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN); - void setLastAlarmRequestSuccess(LastCommandSuccess status); - LastCommandSuccess getLastAlarmRequestSuccess(); + void setLastAlarmRequestSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastAlarmRequestSuccess() const; - void setMessageType(AlarmMessageType_t type); + void setMessageType(const AlarmMessageType_t type); private: static int getTimezoneOffset(); - String getLocaleMessage(const AlarmMessage_t *msg, AlarmMessageLocale_t locale); + String getLocaleMessage(const AlarmMessage_t* msg, const AlarmMessageLocale_t locale) const; uint8_t _payloadAlarmLog[ALARM_LOG_PAYLOAD_SIZE]; uint8_t _alarmLogLength = 0; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index bc28ce39d..d4f599003 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -15,38 +15,41 @@ typedef struct { } devInfo_t; const devInfo_t devInfo[] = { - { { 0x10, 0x10, 0x10, ALL }, 300, "HM-300" }, - { { 0x10, 0x10, 0x20, ALL }, 350, "HM-350" }, - { { 0x10, 0x10, 0x30, ALL }, 400, "HM-400" }, - { { 0x10, 0x10, 0x40, ALL }, 400, "HM-400" }, - { { 0x10, 0x11, 0x10, ALL }, 600, "HM-600" }, - { { 0x10, 0x11, 0x20, ALL }, 700, "HM-700" }, - { { 0x10, 0x11, 0x30, ALL }, 800, "HM-800" }, - { { 0x10, 0x11, 0x40, ALL }, 800, "HM-800" }, - { { 0x10, 0x12, 0x10, ALL }, 1200, "HM-1200" }, - { { 0x10, 0x02, 0x30, ALL }, 1500, "MI-1500 Gen3" }, - { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500" }, - { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300" }, // HM-300 factory limitted to 70% - - { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350" }, // 00 - { { 0x10, 0x20, 0x41, ALL }, 400, "HMS-400" }, // 00 - { { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450" }, // 01 - { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500" }, // 02 - { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500 v2" }, // 02 - { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600" }, // 01 - { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800" }, // 00 - { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900" }, // 01 - { { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900" }, // 03 - { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000" }, // 05 - { { 0x10, 0x11, 0x71, ALL }, 1000, "HMS-1000" }, // 01 - { { 0x10, 0x22, 0x41, ALL }, 1600, "HMS-1600" }, // 4 - { { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800" }, // 01 - { { 0x10, 0x22, 0x51, ALL }, 1800, "HMS-1800" }, // 16 - { { 0x10, 0x12, 0x71, ALL }, 2000, "HMS-2000" }, // 01 - { { 0x10, 0x22, 0x71, ALL }, 2000, "HMS-2000" }, // 10 - - { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800" }, // 01 - { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250" } // 01 + { { 0x10, 0x10, 0x10, ALL }, 300, "HM-300-1T" }, + { { 0x10, 0x10, 0x20, ALL }, 350, "HM-350-1T" }, + { { 0x10, 0x10, 0x30, ALL }, 400, "HM-400-1T" }, + { { 0x10, 0x10, 0x40, ALL }, 400, "HM-400-1T" }, + { { 0x10, 0x11, 0x10, ALL }, 600, "HM-600-2T" }, + { { 0x10, 0x11, 0x20, ALL }, 700, "HM-700-2T" }, + { { 0x10, 0x11, 0x30, ALL }, 800, "HM-800-2T" }, + { { 0x10, 0x11, 0x40, ALL }, 800, "HM-800-2T" }, + { { 0x10, 0x12, 0x10, ALL }, 1200, "HM-1200-4T" }, + { { 0x10, 0x02, 0x30, ALL }, 1500, "MI-1500-4T Gen3" }, + { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500-4T" }, + { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300-1T" }, // HM-300 factory limitted to 70% + + { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350-1T" }, // 00 + { { 0x10, 0x20, 0x41, ALL }, 400, "HMS-400-1T" }, // 00 + { { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450-1T" }, // 01 + { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02 + { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 + { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 + { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 + { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 + { { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900-2T" }, // 03 + { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 05 + { { 0x10, 0x11, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 01 + { { 0x10, 0x22, 0x41, ALL }, 1600, "HMS-1600-4T" }, // 4 + { { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800-4T" }, // 01 + { { 0x10, 0x22, 0x51, ALL }, 1800, "HMS-1800-4T" }, // 16 + { { 0x10, 0x12, 0x71, ALL }, 2000, "HMS-2000-4T" }, // 01 + { { 0x10, 0x22, 0x71, ALL }, 2000, "HMS-2000-4T" }, // 10 + + { { 0x10, 0x32, 0x41, ALL }, 1600, "HMT-1600-4T" }, // 00 + { { 0x10, 0x32, 0x51, ALL }, 1800, "HMT-1800-4T" }, // 00 + + { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 + { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01 }; DevInfoParser::DevInfoParser() @@ -62,7 +65,7 @@ void DevInfoParser::clearBufferAll() _devInfoAllLength = 0; } -void DevInfoParser::appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len) +void DevInfoParser::appendFragmentAll(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > DEV_INFO_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info all packet too large for buffer\r\n", __FILE__, __LINE__); @@ -78,7 +81,7 @@ void DevInfoParser::clearBufferSimple() _devInfoSimpleLength = 0; } -void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len) +void DevInfoParser::appendFragmentSimple(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > DEV_INFO_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\r\n", __FILE__, __LINE__); @@ -88,37 +91,37 @@ void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8 _devInfoSimpleLength += len; } -uint32_t DevInfoParser::getLastUpdateAll() +uint32_t DevInfoParser::getLastUpdateAll() const { return _lastUpdateAll; } -void DevInfoParser::setLastUpdateAll(uint32_t lastUpdate) +void DevInfoParser::setLastUpdateAll(const uint32_t lastUpdate) { _lastUpdateAll = lastUpdate; setLastUpdate(lastUpdate); } -uint32_t DevInfoParser::getLastUpdateSimple() +uint32_t DevInfoParser::getLastUpdateSimple() const { return _lastUpdateSimple; } -void DevInfoParser::setLastUpdateSimple(uint32_t lastUpdate) +void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate) { _lastUpdateSimple = lastUpdate; setLastUpdate(lastUpdate); } -uint16_t DevInfoParser::getFwBuildVersion() +uint16_t DevInfoParser::getFwBuildVersion() const { HOY_SEMAPHORE_TAKE(); - uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; + const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; HOY_SEMAPHORE_GIVE(); return ret; } -time_t DevInfoParser::getFwBuildDateTime() +time_t DevInfoParser::getFwBuildDateTime() const { struct tm timeinfo = {}; HOY_SEMAPHORE_TAKE(); @@ -134,28 +137,25 @@ time_t DevInfoParser::getFwBuildDateTime() return timegm(&timeinfo); } -uint16_t DevInfoParser::getFwBootloaderVersion() +uint16_t DevInfoParser::getFwBootloaderVersion() const { HOY_SEMAPHORE_TAKE(); - uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; + const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; HOY_SEMAPHORE_GIVE(); return ret; } -uint32_t DevInfoParser::getHwPartNumber() +uint32_t DevInfoParser::getHwPartNumber() const { - uint16_t hwpn_h; - uint16_t hwpn_l; - HOY_SEMAPHORE_TAKE(); - hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; - hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; + const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; + const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; HOY_SEMAPHORE_GIVE(); return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); } -String DevInfoParser::getHwVersion() +String DevInfoParser::getHwVersion() const { char buf[8]; HOY_SEMAPHORE_TAKE(); @@ -164,27 +164,27 @@ String DevInfoParser::getHwVersion() return buf; } -uint16_t DevInfoParser::getMaxPower() +uint16_t DevInfoParser::getMaxPower() const { - uint8_t idx = getDevIdx(); + const uint8_t idx = getDevIdx(); if (idx == 0xff) { return 0; } return devInfo[idx].maxPower; } -String DevInfoParser::getHwModelName() +String DevInfoParser::getHwModelName() const { - uint8_t idx = getDevIdx(); + const uint8_t idx = getDevIdx(); if (idx == 0xff) { return ""; } return devInfo[idx].modelName; } -bool DevInfoParser::containsValidData() +bool DevInfoParser::containsValidData() const { - time_t t = getFwBuildDateTime(); + const time_t t = getFwBuildDateTime(); struct tm info; localtime_r(&t, &info); @@ -192,7 +192,7 @@ bool DevInfoParser::containsValidData() return info.tm_year > (2016 - 1900); } -uint8_t DevInfoParser::getDevIdx() +uint8_t DevInfoParser::getDevIdx() const { uint8_t ret = 0xff; uint8_t pos; @@ -228,7 +228,7 @@ uint8_t DevInfoParser::getDevIdx() } /* struct tm to seconds since Unix epoch */ -time_t DevInfoParser::timegm(struct tm* t) +time_t DevInfoParser::timegm(const struct tm* t) { uint32_t year; time_t result; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index 838ba1102..89c40f862 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -8,32 +8,32 @@ class DevInfoParser : public Parser { public: DevInfoParser(); void clearBufferAll(); - void appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragmentAll(const uint8_t offset, const uint8_t* payload, const uint8_t len); void clearBufferSimple(); - void appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragmentSimple(const uint8_t offset, const uint8_t* payload, const uint8_t len); - uint32_t getLastUpdateAll(); - void setLastUpdateAll(uint32_t lastUpdate); + uint32_t getLastUpdateAll() const; + void setLastUpdateAll(const uint32_t lastUpdate); - uint32_t getLastUpdateSimple(); - void setLastUpdateSimple(uint32_t lastUpdate); + uint32_t getLastUpdateSimple() const; + void setLastUpdateSimple(const uint32_t lastUpdate); - uint16_t getFwBuildVersion(); - time_t getFwBuildDateTime(); - uint16_t getFwBootloaderVersion(); + uint16_t getFwBuildVersion() const; + time_t getFwBuildDateTime() const; + uint16_t getFwBootloaderVersion() const; - uint32_t getHwPartNumber(); - String getHwVersion(); + uint32_t getHwPartNumber() const; + String getHwVersion() const; - uint16_t getMaxPower(); - String getHwModelName(); + uint16_t getMaxPower() const; + String getHwModelName() const; - bool containsValidData(); + bool containsValidData() const; private: - time_t timegm(struct tm* tm); - uint8_t getDevIdx(); + static time_t timegm(const struct tm* tm); + uint8_t getDevIdx() const; uint32_t _lastUpdateAll = 0; uint32_t _lastUpdateSimple = 0; diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 35f7689d5..da1c80604 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -5,6 +5,315 @@ #include "GridProfileParser.h" #include "../Hoymiles.h" #include +#include +#include + +const std::array GridProfileParser::_profileTypes = { { + { 0x02, 0x00, "no data (yet)" }, + { 0x03, 0x00, "Germany - DE_VDE4105_2018" }, + { 0x0a, 0x00, "European - EN 50549-1:2019" }, + { 0x0c, 0x00, "AT Tor - EU_EN50438" }, + { 0x0d, 0x04, "France" }, + { 0x12, 0x00, "Poland - EU_EN50438" }, + { 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" }, +} }; + +constexpr frozen::map profileSection = { + { 0x00, "Voltage (H/LVRT)" }, + { 0x10, "Frequency (H/LFRT)" }, + { 0x20, "Island Detection (ID)" }, + { 0x30, "Reconnection (RT)" }, + { 0x40, "Ramp Rates (RR)" }, + { 0x50, "Frequency Watt (FW)" }, + { 0x60, "Volt Watt (VW)" }, + { 0x70, "Active Power Control (APC)" }, + { 0x80, "Volt Var (VV)" }, + { 0x90, "Specified Power Factor (SPF)" }, + { 0xA0, "Reactive Power Control (RPC)" }, + { 0xB0, "Watt Power Factor (WPF)" }, +}; + +struct GridProfileItemDefinition_t { + frozen::string Name; + frozen::string Unit; + uint8_t Divider; +}; + +constexpr GridProfileItemDefinition_t make_value(frozen::string Name, frozen::string Unit, uint8_t divisor) +{ + GridProfileItemDefinition_t v = { Name, Unit, divisor }; + return v; +} + +constexpr frozen::map itemDefinitions = { + { 0x01, make_value("Nominale Voltage (NV)", "V", 10) }, + { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, + { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x04, make_value("High Voltage 1 (HV1)", "V", 10) }, + { 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x06, make_value("Low Voltage 2 (LV2)", "V", 10) }, + { 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x08, make_value("High Voltage 2 (HV2)", "V", 10) }, + { 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) }, + { 0x0b, make_value("High Voltage 3 (HV3)", "V", 10) }, + { 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 10) }, + { 0x0d, make_value("Nominal Frequency", "Hz", 100) }, + { 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) }, + { 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x10, make_value("High Frequency 1 (HF1)", "Hz", 100) }, + { 0x11, make_value("HF1 Maximum Trip time (MTT)", "s", 10) }, + { 0x12, make_value("Low Frequency 2 (LF2)", "Hz", 100) }, + { 0x13, make_value("LF2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x14, make_value("High Frequency 2 (HF2)", "Hz", 100) }, + { 0x15, make_value("HF2 Maximum Trip time (MTT)", "s", 10) }, + { 0x16, make_value("ID Function Activated", "bool", 1) }, + { 0x17, make_value("Reconnect Time (RT)", "s", 10) }, + { 0x18, make_value("Reconnect High Voltage (RHV)", "V", 10) }, + { 0x19, make_value("Reconnect Low Voltage (RLV)", "V", 10) }, + { 0x1a, make_value("Reconnect High Frequency (RHF)", "Hz", 100) }, + { 0x1b, make_value("Reconnect Low Frequency (RLF)", "Hz", 100) }, + { 0x1c, make_value("Normal Ramp up Rate(RUR_NM)", "Rated%/s", 100) }, + { 0x1d, make_value("Soft Start Ramp up Rate (RUR_SS)", "Rated%/s", 100) }, + { 0x1e, make_value("FW Function Activated", "bool", 1) }, + { 0x1f, make_value("Start of Frequency Watt Droop (Fstart)", "Hz", 100) }, + { 0x20, make_value("FW Droop Slope (Kpower_Freq)", "Pn%/Hz", 10) }, + { 0x21, make_value("Recovery Ramp Rate (RRR)", "Pn%/s", 100) }, + { 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 100) }, + { 0x23, make_value("Recovery Low Frequency (RVLF)", "Hz", 100) }, + { 0x24, make_value("VW Function Activated", "bool", 1) }, + { 0x25, make_value("Start of Voltage Watt Droop (Vstart)", "V", 10) }, + { 0x26, make_value("End of Voltage Watt Droop (Vend)", "V", 10) }, + { 0x27, make_value("Droop Slope (Kpower_Volt)", "Pn%/V", 100) }, + { 0x28, make_value("APC Function Activated", "bool", 1) }, + { 0x29, make_value("Power Ramp Rate (PRR)", "Pn%/s", 100) }, + { 0x2a, make_value("VV Function Activated", "bool", 1) }, + { 0x2b, make_value("Voltage Set Point V1", "V", 10) }, + { 0x2c, make_value("Reactive Set Point Q1", "%Pn", 10) }, + { 0x2d, make_value("Voltage Set Point V2", "V", 10) }, + { 0x2e, make_value("Voltage Set Point V3", "V", 10) }, + { 0x2f, make_value("Voltage Set Point V4", "V", 10) }, + { 0x30, make_value("Reactive Set Point Q4", "%Pn", 10) }, + { 0x31, make_value("Setting Time (Tr)", "s", 10) }, + { 0x32, make_value("SPF Function Activated", "bool", 1) }, + { 0x33, make_value("Power Factor (PF)", "", 100) }, + { 0x34, make_value("RPC Function Activated", "bool", 1) }, + { 0x35, make_value("Reactive Power (VAR)", "%Sn", 1) }, + { 0x36, make_value("WPF Function Activated", "bool", 1) }, + { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, + { 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, + { 0xff, make_value("Unkown Value", "", 1) }, +}; + +const std::array GridProfileParser::_profileValues = { { + // Voltage (H/LVRT) + // Version 0x00 + { 0x00, 0x00, 0x01 }, + { 0x00, 0x00, 0x02 }, + { 0x00, 0x00, 0x03 }, + { 0x00, 0x00, 0x04 }, + { 0x00, 0x00, 0x05 }, + + // Version 0x03 + { 0x00, 0x03, 0x01 }, + { 0x00, 0x03, 0x02 }, + { 0x00, 0x03, 0x03 }, + { 0x00, 0x03, 0x05 }, + { 0x00, 0x03, 0x06 }, + { 0x00, 0x03, 0x07 }, + { 0x00, 0x03, 0x08 }, + { 0x00, 0x03, 0x09 }, + + // Version 0x08 + { 0x00, 0x08, 0x01 }, + { 0x00, 0x08, 0x02 }, + { 0x00, 0x08, 0x03 }, + { 0x00, 0x08, 0x04 }, + { 0x00, 0x08, 0x05 }, + { 0x00, 0x08, 0xff }, + + // Version 0x0a + { 0x00, 0x0a, 0x01 }, + { 0x00, 0x0a, 0x02 }, + { 0x00, 0x0a, 0x03 }, + { 0x00, 0x0a, 0x04 }, + { 0x00, 0x0a, 0x05 }, + { 0x00, 0x0a, 0x06 }, + { 0x00, 0x0a, 0x07 }, + { 0x00, 0x0a, 0x0a }, + + // Version 0x0b + { 0x00, 0x0b, 0x01 }, + { 0x00, 0x0b, 0x02 }, + { 0x00, 0x0b, 0x03 }, + { 0x00, 0x0b, 0x04 }, + { 0x00, 0x0b, 0x05 }, + { 0x00, 0x0b, 0x06 }, + { 0x00, 0x0b, 0x07 }, + { 0x00, 0x0b, 0x08 }, + { 0x00, 0x0b, 0x09 }, + { 0x00, 0x0b, 0x0a }, + + // Version 0x0c + { 0x00, 0x0c, 0x01 }, + { 0x00, 0x0c, 0x02 }, + { 0x00, 0x0c, 0x03 }, + { 0x00, 0x0c, 0x04 }, + { 0x00, 0x0c, 0x05 }, + { 0x00, 0x0c, 0x06 }, + { 0x00, 0x0c, 0x07 }, + { 0x00, 0x0c, 0x08 }, + { 0x00, 0x0c, 0x09 }, + { 0x00, 0x0c, 0x0b }, + { 0x00, 0x0c, 0x0c }, + { 0x00, 0x0c, 0x0a }, + + // Version 0x35 + { 0x00, 0x35, 0x01 }, + { 0x00, 0x35, 0x02 }, + { 0x00, 0x35, 0x03 }, + { 0x00, 0x35, 0x04 }, + { 0x00, 0x35, 0x05 }, + { 0x00, 0x35, 0x06 }, + { 0x00, 0x35, 0x07 }, + { 0x00, 0x35, 0x08 }, + { 0x00, 0x35, 0x09 }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + + // Frequency (H/LFRT) + // Version 0x00 + { 0x10, 0x00, 0x0d }, + { 0x10, 0x00, 0x0e }, + { 0x10, 0x00, 0x0f }, + { 0x10, 0x00, 0x10 }, + { 0x10, 0x00, 0x11 }, + + // Version 0x03 + { 0x10, 0x03, 0x0d }, + { 0x10, 0x03, 0x0e }, + { 0x10, 0x03, 0x0f }, + { 0x10, 0x03, 0x10 }, + { 0x10, 0x03, 0x11 }, + { 0x10, 0x03, 0x12 }, + { 0x10, 0x03, 0x13 }, + { 0x10, 0x03, 0x14 }, + { 0x10, 0x03, 0x15 }, + + // Island Detection (ID) + // Version 0x00 + { 0x20, 0x00, 0x16 }, + + // Reconnection (RT) + // Version 0x03 + { 0x30, 0x03, 0x17 }, + { 0x30, 0x03, 0x18 }, + { 0x30, 0x03, 0x19 }, + { 0x30, 0x03, 0x1a }, + { 0x30, 0x03, 0x1b }, + + // Version 0x07 + { 0x30, 0x07, 0x17 }, + { 0x30, 0x07, 0x18 }, + { 0x30, 0x07, 0x19 }, + { 0x30, 0x07, 0x1a }, + { 0x30, 0x07, 0x1b }, + { 0x30, 0x07, 0xff }, + { 0x30, 0x07, 0xff }, + + // Ramp Rates (RR) + // Version 0x00 + { 0x40, 0x00, 0x1c }, + { 0x40, 0x00, 0x1d }, + + // Frequency Watt (FW) + // Version 0x00 + { 0x50, 0x00, 0x1e }, + { 0x50, 0x00, 0x1f }, + { 0x50, 0x00, 0x20 }, + { 0x50, 0x00, 0x21 }, + + // Version 0x01 + { 0x50, 0x01, 0x1e }, + { 0x50, 0x01, 0x1f }, + { 0x50, 0x01, 0x20 }, + { 0x50, 0x01, 0x21 }, + { 0x50, 0x01, 0x22 }, + + // Version 0x08 + { 0x50, 0x08, 0x1e }, + { 0x50, 0x08, 0x1f }, + { 0x50, 0x08, 0x20 }, + { 0x50, 0x08, 0x21 }, + { 0x50, 0x08, 0x22 }, + { 0x50, 0x08, 0x23 }, + + // Version 0x11 + { 0x50, 0x11, 0x1e }, + { 0x50, 0x11, 0x1f }, + { 0x50, 0x11, 0x20 }, + { 0x50, 0x11, 0x21 }, + { 0x50, 0x11, 0x22 }, + + // Volt Watt (VW) + // Version 0x00 + { 0x60, 0x00, 0x24 }, + { 0x60, 0x00, 0x25 }, + { 0x60, 0x00, 0x26 }, + { 0x60, 0x00, 0x27 }, + + // Version 0x04 + { 0x60, 0x04, 0x24 }, + { 0x60, 0x04, 0x25 }, + { 0x60, 0x04, 0x26 }, + { 0x60, 0x04, 0x27 }, + + // Active Power Control (APC) + // Version 0x00 + { 0x70, 0x00, 0x28 }, + + // Version 0x02 + { 0x70, 0x02, 0x28 }, + { 0x70, 0x02, 0x29 }, + + // Volt Var (VV) + // Version 0x00 + { 0x80, 0x00, 0x2a }, + { 0x80, 0x00, 0x2b }, + { 0x80, 0x00, 0x2c }, + { 0x80, 0x00, 0x2d }, + { 0x80, 0x00, 0x2e }, + { 0x80, 0x00, 0x2f }, + { 0x80, 0x00, 0x30 }, + + // Version 0x01 + { 0x80, 0x01, 0x2a }, + { 0x80, 0x01, 0x2b }, + { 0x80, 0x01, 0x2c }, + { 0x80, 0x01, 0x2d }, + { 0x80, 0x01, 0x2e }, + { 0x80, 0x01, 0x2f }, + { 0x80, 0x01, 0x30 }, + { 0x80, 0x01, 0x31 }, + + // Specified Power Factor (SPF) + // Version 0x00 + { 0x90, 0x00, 0x32 }, + { 0x90, 0x00, 0x33 }, + + // Reactive Power Control (RPC) + // Version 0x02 + { 0xa0, 0x02, 0x34 }, + { 0xa0, 0x02, 0x35 }, + + // Watt Power Factor (WPF) + // Version 0x00 + { 0xb0, 0x00, 0x36 }, + { 0xb0, 0x00, 0x37 }, + { 0xb0, 0x00, 0x38 }, +} }; GridProfileParser::GridProfileParser() : Parser() @@ -18,7 +327,7 @@ void GridProfileParser::clearBuffer() _gridProfileLength = 0; } -void GridProfileParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void GridProfileParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > GRID_PROFILE_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) grid profile packet too large for buffer\r\n", __FILE__, __LINE__); @@ -28,7 +337,26 @@ void GridProfileParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _gridProfileLength += len; } -std::vector GridProfileParser::getRawData() +String GridProfileParser::getProfileName() const +{ + for (auto& ptype : _profileTypes) { + if (ptype.lIdx == _payloadGridProfile[0] && ptype.hIdx == _payloadGridProfile[1]) { + return ptype.Name; + } + } + return "Unknown"; +} + +String GridProfileParser::getProfileVersion() const +{ + char buffer[10]; + HOY_SEMAPHORE_TAKE(); + snprintf(buffer, sizeof(buffer), "%d.%d.%d", (_payloadGridProfile[2] >> 4) & 0x0f, _payloadGridProfile[2] & 0x0f, _payloadGridProfile[3]); + HOY_SEMAPHORE_GIVE(); + return buffer; +} + +std::vector GridProfileParser::getRawData() const { std::vector ret; HOY_SEMAPHORE_TAKE(); @@ -38,3 +366,75 @@ std::vector GridProfileParser::getRawData() HOY_SEMAPHORE_GIVE(); return ret; } + +std::list GridProfileParser::getProfile() const +{ + std::list l; + + if (_gridProfileLength > 4) { + uint16_t pos = 4; + do { + const uint8_t section_id = _payloadGridProfile[pos]; + const uint8_t section_version = _payloadGridProfile[pos + 1]; + const int16_t section_start = getSectionStart(section_id, section_version); + const uint8_t section_size = getSectionSize(section_id, section_version); + pos += 2; + + GridProfileSection_t section; + try { + section.SectionName = profileSection.at(section_id).data(); + } catch (const std::out_of_range&) { + section.SectionName = "Unknown"; + break; + } + + if (section_start == -1) { + section.SectionName = "Unknown"; + break; + } + + for (uint8_t val_id = 0; val_id < section_size; val_id++) { + auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition); + + float value = (int16_t)((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]); + value /= itemDefinition.Divider; + + GridProfileItem_t v; + v.Name = itemDefinition.Name.data(); + v.Unit = itemDefinition.Unit.data(); + v.Value = value; + section.items.push_back(v); + + pos += 2; + } + + l.push_back(section); + + } while (pos < _gridProfileLength); + } + + return l; +} + +uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version) +{ + uint8_t count = 0; + for (auto& values : _profileValues) { + if (values.Section == section_id && values.Version == section_version) { + count++; + } + } + return count; +} + +int16_t GridProfileParser::getSectionStart(const uint8_t section_id, const uint8_t section_version) +{ + int16_t count = -1; + for (auto& values : _profileValues) { + count++; + if (values.Section == section_id && values.Version == section_version) { + break; + } + } + return count; +} diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index c2af52f87..031891f3f 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -1,18 +1,55 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" +#include #define GRID_PROFILE_SIZE 141 +#define PROFILE_TYPE_COUNT 7 +#define SECTION_VALUE_COUNT 144 + +typedef struct { + uint8_t lIdx; + uint8_t hIdx; + const char* Name; +} ProfileType_t; + +struct GridProfileValue_t { + uint8_t Section; + uint8_t Version; + uint8_t ItemDefinition; +}; + +struct GridProfileItem_t { + String Name; + String Unit; + float Value; +}; + +struct GridProfileSection_t { + String SectionName; + std::list items; +}; class GridProfileParser : public Parser { public: GridProfileParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); - std::vector getRawData(); + String getProfileName() const; + String getProfileVersion() const; + + std::vector getRawData() const; + + std::list getProfile() const; private: + static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); + static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version); + uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; uint8_t _gridProfileLength = 0; + + static const std::array _profileTypes; + static const std::array _profileValues; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/Parser.cpp b/lib/Hoymiles/src/parser/Parser.cpp index b8e5e0e56..96681ce2a 100644 --- a/lib/Hoymiles/src/parser/Parser.cpp +++ b/lib/Hoymiles/src/parser/Parser.cpp @@ -10,12 +10,12 @@ Parser::Parser() HOY_SEMAPHORE_GIVE(); // release before first use } -uint32_t Parser::getLastUpdate() +uint32_t Parser::getLastUpdate() const { return _lastUpdate; } -void Parser::setLastUpdate(uint32_t lastUpdate) +void Parser::setLastUpdate(const uint32_t lastUpdate) { _lastUpdate = lastUpdate; } diff --git a/lib/Hoymiles/src/parser/Parser.h b/lib/Hoymiles/src/parser/Parser.h index 5d6df75df..dda0ef8a8 100644 --- a/lib/Hoymiles/src/parser/Parser.h +++ b/lib/Hoymiles/src/parser/Parser.h @@ -17,8 +17,8 @@ typedef enum { class Parser { public: Parser(); - uint32_t getLastUpdate(); - void setLastUpdate(uint32_t lastUpdate); + uint32_t getLastUpdate() const; + void setLastUpdate(const uint32_t lastUpdate); void beginAppendFragment(); void endAppendFragment(); diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.cpp b/lib/Hoymiles/src/parser/PowerCommandParser.cpp index d698dad8d..dc8dc7978 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.cpp +++ b/lib/Hoymiles/src/parser/PowerCommandParser.cpp @@ -4,22 +4,22 @@ */ #include "PowerCommandParser.h" -void PowerCommandParser::setLastPowerCommandSuccess(LastCommandSuccess status) +void PowerCommandParser::setLastPowerCommandSuccess(const LastCommandSuccess status) { _lastLimitCommandSuccess = status; } -LastCommandSuccess PowerCommandParser::getLastPowerCommandSuccess() +LastCommandSuccess PowerCommandParser::getLastPowerCommandSuccess() const { return _lastLimitCommandSuccess; } -uint32_t PowerCommandParser::getLastUpdateCommand() +uint32_t PowerCommandParser::getLastUpdateCommand() const { return _lastUpdateCommand; } -void PowerCommandParser::setLastUpdateCommand(uint32_t lastUpdate) +void PowerCommandParser::setLastUpdateCommand(const uint32_t lastUpdate) { _lastUpdateCommand = lastUpdate; setLastUpdate(lastUpdate); diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.h b/lib/Hoymiles/src/parser/PowerCommandParser.h index e005812e6..b448692e6 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.h +++ b/lib/Hoymiles/src/parser/PowerCommandParser.h @@ -4,10 +4,10 @@ class PowerCommandParser : public Parser { public: - void setLastPowerCommandSuccess(LastCommandSuccess status); - LastCommandSuccess getLastPowerCommandSuccess(); - uint32_t getLastUpdateCommand(); - void setLastUpdateCommand(uint32_t lastUpdate); + void setLastPowerCommandSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastPowerCommandSuccess() const; + uint32_t getLastUpdateCommand() const; + void setLastUpdateCommand(const uint32_t lastUpdate); private: LastCommandSuccess _lastLimitCommandSuccess = CMD_OK; // Set to OK because we have to assume nothing is done at startup diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 71c1ebbd2..831c1ad1f 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -60,7 +60,7 @@ StatisticsParser::StatisticsParser() clearBuffer(); } -void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, uint8_t size) +void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t size) { _byteAssignment = byteAssignment; _byteAssignmentSize = size; @@ -84,7 +84,7 @@ void StatisticsParser::clearBuffer() _statisticLength = 0; } -void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void StatisticsParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > STATISTIC_PACKET_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__); @@ -94,38 +94,60 @@ void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _statisticLength += len; } -const byteAssign_t* StatisticsParser::getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +void StatisticsParser::endAppendFragment() +{ + Parser::endAppendFragment(); + + if (!_enableYieldDayCorrection) { + resetYieldDayCorrection(); + return; + } + + for (auto& c : getChannelsByType(TYPE_DC)) { + // check if current yield day is smaller then last cached yield day + if (getChannelFieldValue(TYPE_DC, c, FLD_YD) < _lastYieldDay[static_cast(c)]) { + // currently all values are zero --> Add last known values to offset + Hoymiles.getMessageOutput()->printf("Yield Day reset detected!\r\n"); + + setChannelFieldOffset(TYPE_DC, c, FLD_YD, _lastYieldDay[static_cast(c)]); + + _lastYieldDay[static_cast(c)] = 0; + } else { + _lastYieldDay[static_cast(c)] = getChannelFieldValue(TYPE_DC, c, FLD_YD); + } + } +} + +const byteAssign_t* StatisticsParser::getAssignmentByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { for (uint8_t i = 0; i < _byteAssignmentSize; i++) { if (_byteAssignment[i].type == type && _byteAssignment[i].ch == channel && _byteAssignment[i].fieldId == fieldId) { return &_byteAssignment[i]; } } - return NULL; + return nullptr; } -fieldSettings_t* StatisticsParser::getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +fieldSettings_t* StatisticsParser::getSettingByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { for (auto& i : _fieldSettings) { if (i.type == type && i.ch == channel && i.fieldId == fieldId) { return &i; } } - return NULL; + return nullptr; } -float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +float StatisticsParser::getChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - - if (pos == NULL) { + if (pos == nullptr) { return 0; } uint8_t ptr = pos->start; - uint8_t end = ptr + pos->num; - uint16_t div = pos->div; + const uint8_t end = ptr + pos->num; + const uint16_t div = pos->div; if (CMD_CALC != div) { // Value is a static value @@ -147,7 +169,9 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch } result /= static_cast(div); - if (setting != NULL && _statisticLength > 0) { + + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr && _statisticLength > 0) { result += setting->offset; } return result; @@ -159,24 +183,23 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } -bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value) +bool StatisticsParser::setChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, float value) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - - if (pos == NULL) { + if (pos == nullptr) { return false; } uint8_t ptr = pos->start + pos->num - 1; - uint8_t end = pos->start; - uint16_t div = pos->div; + const uint8_t end = pos->start; + const uint16_t div = pos->div; if (CMD_CALC == div) { return false; } - if (setting != NULL) { + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr) { value -= setting->offset; } value *= static_cast(div); @@ -200,57 +223,57 @@ bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t cha return true; } -String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +String StatisticsParser::getChannelFieldValueString(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { return String( getChannelFieldValue(type, channel, fieldId), static_cast(getChannelFieldDigits(type, channel, fieldId))); } -bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +bool StatisticsParser::hasChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - return pos != NULL; + return pos != nullptr; } -const char* StatisticsParser::getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +const char* StatisticsParser::getChannelFieldUnit(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return units[pos->unitId]; } -const char* StatisticsParser::getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +const char* StatisticsParser::getChannelFieldName(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return fields[pos->fieldId]; } -uint8_t StatisticsParser::getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +uint8_t StatisticsParser::getChannelFieldDigits(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return pos->digits; } -float StatisticsParser::getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +float StatisticsParser::getChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - if (setting != NULL) { + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr) { return setting->offset; } return 0; } -void StatisticsParser::setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset) +void StatisticsParser::setChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const float offset) { fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - if (setting != NULL) { + if (setting != nullptr) { setting->offset = offset; } else { _fieldSettings.push_back({ type, channel, fieldId, offset }); } } -std::list StatisticsParser::getChannelTypes() +std::list StatisticsParser::getChannelTypes() const { return { TYPE_AC, @@ -259,12 +282,12 @@ std::list StatisticsParser::getChannelTypes() }; } -const char* StatisticsParser::getChannelTypeName(ChannelType_t type) +const char* StatisticsParser::getChannelTypeName(const ChannelType_t type) const { return channelsTypes[type]; } -std::list StatisticsParser::getChannelsByType(ChannelType_t type) +std::list StatisticsParser::getChannelsByType(const ChannelType_t type) const { std::list l; for (uint8_t i = 0; i < _byteAssignmentSize; i++) { @@ -276,12 +299,12 @@ std::list StatisticsParser::getChannelsByType(ChannelType_t type) return l; } -uint16_t StatisticsParser::getStringMaxPower(uint8_t channel) +uint16_t StatisticsParser::getStringMaxPower(const uint8_t channel) const { return _stringMaxPower[channel]; } -void StatisticsParser::setStringMaxPower(uint8_t channel, uint16_t power) +void StatisticsParser::setStringMaxPower(const uint8_t channel, const uint16_t power) { if (channel < sizeof(_stringMaxPower) / sizeof(_stringMaxPower[0])) { _stringMaxPower[channel] = power; @@ -298,7 +321,7 @@ void StatisticsParser::incrementRxFailureCount() _rxFailureCount++; } -uint32_t StatisticsParser::getRxFailureCount() +uint32_t StatisticsParser::getRxFailureCount() const { return _rxFailureCount; } @@ -313,22 +336,32 @@ void StatisticsParser::zeroDailyData() zeroFields(dailyProductionFields); } -void StatisticsParser::setLastUpdate(uint32_t lastUpdate) +void StatisticsParser::setLastUpdate(const uint32_t lastUpdate) { Parser::setLastUpdate(lastUpdate); setLastUpdateFromInternal(lastUpdate); } -uint32_t StatisticsParser::getLastUpdateFromInternal() +uint32_t StatisticsParser::getLastUpdateFromInternal() const { return _lastUpdateFromInternal; } -void StatisticsParser::setLastUpdateFromInternal(uint32_t lastUpdate) +void StatisticsParser::setLastUpdateFromInternal(const uint32_t lastUpdate) { _lastUpdateFromInternal = lastUpdate; } +bool StatisticsParser::getYieldDayCorrection() const +{ + return _enableYieldDayCorrection; +} + +void StatisticsParser::setYieldDayCorrection(const bool enabled) +{ + _enableYieldDayCorrection = enabled; +} + void StatisticsParser::zeroFields(const FieldId_t* fields) { // Loop all channels @@ -344,6 +377,15 @@ void StatisticsParser::zeroFields(const FieldId_t* fields) setLastUpdateFromInternal(millis()); } +void StatisticsParser::resetYieldDayCorrection() +{ + // new day detected, reset counters + for (auto& c : getChannelsByType(TYPE_DC)) { + setChannelFieldOffset(TYPE_DC, c, FLD_YD, 0); + _lastYieldDay[static_cast(c)] = 0; + } +} + static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) { float yield = 0; @@ -399,7 +441,7 @@ static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) // arg0 = channel static float calcIrradiation(StatisticsParser* iv, uint8_t arg0) { - if (NULL != iv) { + if (nullptr != iv) { if (iv->getStringMaxPower(arg0) > 0) return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_PDC) / iv->getStringMaxPower(arg0) * 100.0f; } diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index da291004f..10f06e04b 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -105,49 +105,53 @@ class StatisticsParser : public Parser { public: StatisticsParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); + void endAppendFragment(); - void setByteAssignment(const byteAssign_t* byteAssignment, uint8_t size); + void setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t size); // Returns 1 based amount of expected bytes of statistic data uint8_t getExpectedByteCount(); - const byteAssign_t* getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + const byteAssign_t* getAssignmentByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + fieldSettings_t* getSettingByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); - float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - String getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + float getChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + String getChannelFieldValueString(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + bool hasChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + const char* getChannelFieldUnit(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + const char* getChannelFieldName(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + uint8_t getChannelFieldDigits(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; - bool setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value); + bool setChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, float value); - float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); + float getChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + void setChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const float offset); - std::list getChannelTypes(); - const char* getChannelTypeName(ChannelType_t type); - std::list getChannelsByType(ChannelType_t type); + std::list getChannelTypes() const; + const char* getChannelTypeName(const ChannelType_t type) const; + std::list getChannelsByType(const ChannelType_t type) const; - uint16_t getStringMaxPower(uint8_t channel); - void setStringMaxPower(uint8_t channel, uint16_t power); + uint16_t getStringMaxPower(const uint8_t channel) const; + void setStringMaxPower(const uint8_t channel, const uint16_t power); void resetRxFailureCount(); void incrementRxFailureCount(); - uint32_t getRxFailureCount(); + uint32_t getRxFailureCount() const; void zeroRuntimeData(); void zeroDailyData(); + void resetYieldDayCorrection(); // Update time when new data from the inverter is received - void setLastUpdate(uint32_t lastUpdate); + void setLastUpdate(const uint32_t lastUpdate); // Update time when internal data structure changes (from inverter and by internal manipulation) - uint32_t getLastUpdateFromInternal(); - void setLastUpdateFromInternal(uint32_t lastUpdate); + uint32_t getLastUpdateFromInternal() const; + void setLastUpdateFromInternal(const uint32_t lastUpdate); + bool getYieldDayCorrection() const; + void setYieldDayCorrection(const bool enabled); private: void zeroFields(const FieldId_t* fields); @@ -162,4 +166,7 @@ class StatisticsParser : public Parser { uint32_t _rxFailureCount = 0; uint32_t _lastUpdateFromInternal = 0; + + bool _enableYieldDayCorrection = false; + float _lastYieldDay[CH_CNT] = {}; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index d1ed30b63..e866e8749 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -18,7 +18,7 @@ void SystemConfigParaParser::clearBuffer() _payloadLength = 0; } -void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > (SYSTEM_CONFIG_PARA_SIZE)) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__); @@ -28,15 +28,15 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui _payloadLength += len; } -float SystemConfigParaParser::getLimitPercent() +float SystemConfigParaParser::getLimitPercent() const { HOY_SEMAPHORE_TAKE(); - float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; + const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; HOY_SEMAPHORE_GIVE(); return ret; } -void SystemConfigParaParser::setLimitPercent(float value) +void SystemConfigParaParser::setLimitPercent(const float value) { HOY_SEMAPHORE_TAKE(); _payload[2] = ((uint16_t)(value * 10)) >> 8; @@ -44,49 +44,49 @@ void SystemConfigParaParser::setLimitPercent(float value) HOY_SEMAPHORE_GIVE(); } -void SystemConfigParaParser::setLastLimitCommandSuccess(LastCommandSuccess status) +void SystemConfigParaParser::setLastLimitCommandSuccess(const LastCommandSuccess status) { _lastLimitCommandSuccess = status; } -LastCommandSuccess SystemConfigParaParser::getLastLimitCommandSuccess() +LastCommandSuccess SystemConfigParaParser::getLastLimitCommandSuccess() const { return _lastLimitCommandSuccess; } -uint32_t SystemConfigParaParser::getLastUpdateCommand() +uint32_t SystemConfigParaParser::getLastUpdateCommand() const { return _lastUpdateCommand; } -void SystemConfigParaParser::setLastUpdateCommand(uint32_t lastUpdate) +void SystemConfigParaParser::setLastUpdateCommand(const uint32_t lastUpdate) { _lastUpdateCommand = lastUpdate; setLastUpdate(lastUpdate); } -void SystemConfigParaParser::setLastLimitRequestSuccess(LastCommandSuccess status) +void SystemConfigParaParser::setLastLimitRequestSuccess(const LastCommandSuccess status) { _lastLimitRequestSuccess = status; } -LastCommandSuccess SystemConfigParaParser::getLastLimitRequestSuccess() +LastCommandSuccess SystemConfigParaParser::getLastLimitRequestSuccess() const { return _lastLimitRequestSuccess; } -uint32_t SystemConfigParaParser::getLastUpdateRequest() +uint32_t SystemConfigParaParser::getLastUpdateRequest() const { return _lastUpdateRequest; } -void SystemConfigParaParser::setLastUpdateRequest(uint32_t lastUpdate) +void SystemConfigParaParser::setLastUpdateRequest(const uint32_t lastUpdate) { _lastUpdateRequest = lastUpdate; setLastUpdate(lastUpdate); } -uint8_t SystemConfigParaParser::getExpectedByteCount() +uint8_t SystemConfigParaParser::getExpectedByteCount() const { return SYSTEM_CONFIG_PARA_SIZE; } diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.h b/lib/Hoymiles/src/parser/SystemConfigParaParser.h index 300a81822..847a5d1d0 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.h +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.h @@ -8,23 +8,23 @@ class SystemConfigParaParser : public Parser { public: SystemConfigParaParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); - float getLimitPercent(); - void setLimitPercent(float value); + float getLimitPercent() const; + void setLimitPercent(const float value); - void setLastLimitCommandSuccess(LastCommandSuccess status); - LastCommandSuccess getLastLimitCommandSuccess(); - uint32_t getLastUpdateCommand(); - void setLastUpdateCommand(uint32_t lastUpdate); + void setLastLimitCommandSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastLimitCommandSuccess() const; + uint32_t getLastUpdateCommand() const; + void setLastUpdateCommand(const uint32_t lastUpdate); - void setLastLimitRequestSuccess(LastCommandSuccess status); - LastCommandSuccess getLastLimitRequestSuccess(); - uint32_t getLastUpdateRequest(); - void setLastUpdateRequest(uint32_t lastUpdate); + void setLastLimitRequestSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastLimitRequestSuccess() const; + uint32_t getLastUpdateRequest() const; + void setLastUpdateRequest(const uint32_t lastUpdate); // Returns 1 based amount of expected bytes of data - uint8_t getExpectedByteCount(); + uint8_t getExpectedByteCount() const; private: uint8_t _payload[SYSTEM_CONFIG_PARA_SIZE]; diff --git a/lib/ResetReason/src/ResetReason.cpp b/lib/ResetReason/src/ResetReason.cpp index b00dab79c..52367d4a5 100644 --- a/lib/ResetReason/src/ResetReason.cpp +++ b/lib/ResetReason/src/ResetReason.cpp @@ -20,7 +20,7 @@ #include "rom/rtc.h" #endif -String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) +String ResetReason::get_reset_reason_verbose(const uint8_t cpu_id) { RESET_REASON reason; reason = rtc_get_reset_reason(cpu_id); @@ -86,7 +86,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) return reason_str; } -String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) +String ResetReason::get_reset_reason_short(const uint8_t cpu_id) { RESET_REASON reason; reason = rtc_get_reset_reason(cpu_id); @@ -150,6 +150,4 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) } return reason_str; -} - -ResetReasonClass ResetReason; +} \ No newline at end of file diff --git a/lib/ResetReason/src/ResetReason.h b/lib/ResetReason/src/ResetReason.h index 34427bfaa..0238cab2b 100644 --- a/lib/ResetReason/src/ResetReason.h +++ b/lib/ResetReason/src/ResetReason.h @@ -3,10 +3,8 @@ #include -class ResetReasonClass { +class ResetReason { public: - String get_reset_reason_verbose(uint8_t cpu_id); - String get_reset_reason_short(uint8_t cpu_id); -}; - -extern ResetReasonClass ResetReason; \ No newline at end of file + static String get_reset_reason_verbose(const uint8_t cpu_id); + static String get_reset_reason_short(const uint8_t cpu_id); +}; \ No newline at end of file diff --git a/lib/TimeoutHelper/TimeoutHelper.cpp b/lib/TimeoutHelper/TimeoutHelper.cpp index 975a9bbab..3f00c2bc4 100644 --- a/lib/TimeoutHelper/TimeoutHelper.cpp +++ b/lib/TimeoutHelper/TimeoutHelper.cpp @@ -11,13 +11,13 @@ TimeoutHelper::TimeoutHelper() startMillis = 0; } -void TimeoutHelper::set(uint32_t ms) +void TimeoutHelper::set(const uint32_t ms) { timeout = ms; startMillis = millis(); } -void TimeoutHelper::extend(uint32_t ms) +void TimeoutHelper::extend(const uint32_t ms) { timeout += ms; } @@ -27,7 +27,7 @@ void TimeoutHelper::reset() startMillis = millis(); } -bool TimeoutHelper::occured() +bool TimeoutHelper::occured() const { return millis() > (startMillis + timeout); } \ No newline at end of file diff --git a/lib/TimeoutHelper/TimeoutHelper.h b/lib/TimeoutHelper/TimeoutHelper.h index 369749e6e..058de09dc 100644 --- a/lib/TimeoutHelper/TimeoutHelper.h +++ b/lib/TimeoutHelper/TimeoutHelper.h @@ -6,10 +6,10 @@ class TimeoutHelper { public: TimeoutHelper(); - void set(uint32_t ms); - void extend(uint32_t ms); + void set(const uint32_t ms); + void extend(const uint32_t ms); void reset(); - bool occured(); + bool occured() const; private: uint32_t startMillis; diff --git a/platformio.ini b/platformio.ini index 2b48ede13..4a5478d91 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,9 +22,9 @@ framework = arduino platform = espressif32@6.3.2 build_flags = - -DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/favicon.png:webapp_dist/js/app.js.gz -DPIOENV=\"$PIOENV\" - -Wall -Wextra -Werror + -D_TASK_STD_FUNCTION=1 + -Wall -Wextra -Werror -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -std=c++17 -std=gnu++17 build_unflags = @@ -32,11 +32,12 @@ build_unflags = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/bertmelis/espMqttClient.git#v1.4.5 + bblanchon/ArduinoJson @ ^6.21.4 + https://github.com/bertmelis/espMqttClient.git#v1.5.0 nrf24/RF24 @ ^1.4.8 - olikraus/U8g2 @ ^2.35.7 + olikraus/U8g2 @ ^2.35.8 buelowp/sunset @ ^1.1.7 + https://github.com/arkhipenko/TaskScheduler#testing https://github.com/coryjfowler/MCP_CAN_lib plerup/EspSoftwareSerial@^8.0.1 mobizt/FirebaseJson @ ^3.0.6 @@ -49,6 +50,14 @@ extra_scripts = board_build.partitions = partitions_custom.csv board_build.filesystem = littlefs +board_build.embed_files = + webapp_dist/index.html.gz + webapp_dist/zones.json.gz + webapp_dist/favicon.ico + webapp_dist/favicon.png + webapp_dist/js/app.js.gz + webapp_dist/site.webmanifest + monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 upload_protocol = esptool @@ -161,25 +170,6 @@ build_flags = ${env.build_flags} -DOPENDTU_ETHERNET -[env:LilyGO_T_ETH_POE] -; http://www.lilygo.cn/claprod_view.aspx?TypeId=21&Id=1344&FId=t28:21:28 -board = esp32dev -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=2 - -DHOYMILES_PIN_MOSI=15 - -DHOYMILES_PIN_SCLK=14 - -DHOYMILES_PIN_IRQ=34 - -DHOYMILES_PIN_CE=12 - -DHOYMILES_PIN_CS=4 - -DOPENDTU_ETHERNET - -DETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT - -DETH_POWER_PIN=-1 - -DETH_TYPE=ETH_PHY_LAN8720 - -DETH_ADDR=0 - -DETH_MDC_PIN=23 - -DETH_MDIO_PIN=18 - - [env:esp_s3_12k_kit] ; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit board = esp32-s3-devkitc-1 diff --git a/src/Battery.cpp b/src/Battery.cpp index e3af69c56..d68db08e0 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -20,21 +20,30 @@ std::shared_ptr BatteryClass::getStats() const return _upProvider->getStats(); } -void BatteryClass::init() +void BatteryClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&BatteryClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); std::lock_guard lock(_mutex); + this->updateSettings(); +} + +void BatteryClass::updateSettings() +{ if (_upProvider) { _upProvider->deinit(); _upProvider = nullptr; } CONFIG_T& config = Configuration.get(); - if (!config.Battery_Enabled) { return; } + if (!config.Battery.Enabled) { return; } - bool verboseLogging = config.Battery_VerboseLogging; + bool verboseLogging = config.Battery.VerboseLogging; - switch (config.Battery_Provider) { + switch (config.Battery.Provider) { case 0: _upProvider = std::make_unique(); if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } @@ -48,11 +57,12 @@ void BatteryClass::init() if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; default: - MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider); + MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider); break; } } + void BatteryClass::loop() { std::lock_guard lock(_mutex); @@ -64,7 +74,7 @@ void BatteryClass::loop() CONFIG_T& config = Configuration.get(); if (!MqttSettings.getConnected() - || (millis() - _lastMqttPublish) < (config.Mqtt_PublishInterval * 1000)) { + || (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) { return; } diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 4024bf2c7..34e2589ba 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -232,7 +232,7 @@ void JkBmsBatteryStats::mqttPublish() const // publish all topics every minute, unless the retain flag is enabled bool fullPublish = _lastFullMqttPublish + 60 * 1000 < millis(); - fullPublish &= !config.Mqtt_Retain; + fullPublish &= !config.Mqtt.Retain; for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) { // skip data points that did not change since last published diff --git a/src/Configuration.cpp b/src/Configuration.cpp index da07d97ce..485d03519 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -21,89 +21,97 @@ bool ConfigurationClass::write() if (!f) { return false; } - config.Cfg_SaveCount++; + config.Cfg.SaveCount++; DynamicJsonDocument doc(JSON_BUFFER_SIZE); JsonObject cfg = doc.createNestedObject("cfg"); - cfg["version"] = config.Cfg_Version; - cfg["save_count"] = config.Cfg_SaveCount; + cfg["version"] = config.Cfg.Version; + cfg["save_count"] = config.Cfg.SaveCount; JsonObject wifi = doc.createNestedObject("wifi"); - wifi["ssid"] = config.WiFi_Ssid; - wifi["password"] = config.WiFi_Password; - wifi["ip"] = IPAddress(config.WiFi_Ip).toString(); - wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString(); - wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString(); - wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString(); - wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString(); - wifi["dhcp"] = config.WiFi_Dhcp; - wifi["hostname"] = config.WiFi_Hostname; - wifi["aptimeout"] = config.WiFi_ApTimeout; + wifi["ssid"] = config.WiFi.Ssid; + wifi["password"] = config.WiFi.Password; + wifi["ip"] = IPAddress(config.WiFi.Ip).toString(); + wifi["netmask"] = IPAddress(config.WiFi.Netmask).toString(); + wifi["gateway"] = IPAddress(config.WiFi.Gateway).toString(); + wifi["dns1"] = IPAddress(config.WiFi.Dns1).toString(); + wifi["dns2"] = IPAddress(config.WiFi.Dns2).toString(); + wifi["dhcp"] = config.WiFi.Dhcp; + wifi["hostname"] = config.WiFi.Hostname; + wifi["aptimeout"] = config.WiFi.ApTimeout; JsonObject mdns = doc.createNestedObject("mdns"); - mdns["enabled"] = config.Mdns_Enabled; + mdns["enabled"] = config.Mdns.Enabled; JsonObject ntp = doc.createNestedObject("ntp"); - ntp["server"] = config.Ntp_Server; - ntp["timezone"] = config.Ntp_Timezone; - ntp["timezone_descr"] = config.Ntp_TimezoneDescr; - ntp["latitude"] = config.Ntp_Latitude; - ntp["longitude"] = config.Ntp_Longitude; - ntp["sunsettype"] = config.Ntp_SunsetType; + ntp["server"] = config.Ntp.Server; + ntp["timezone"] = config.Ntp.Timezone; + ntp["timezone_descr"] = config.Ntp.TimezoneDescr; + ntp["latitude"] = config.Ntp.Latitude; + ntp["longitude"] = config.Ntp.Longitude; + ntp["sunsettype"] = config.Ntp.SunsetType; JsonObject mqtt = doc.createNestedObject("mqtt"); - mqtt["enabled"] = config.Mqtt_Enabled; - mqtt["verbose_logging"] = config.Mqtt_VerboseLogging; - mqtt["hostname"] = config.Mqtt_Hostname; - mqtt["port"] = config.Mqtt_Port; - mqtt["username"] = config.Mqtt_Username; - mqtt["password"] = config.Mqtt_Password; - mqtt["topic"] = config.Mqtt_Topic; - mqtt["retain"] = config.Mqtt_Retain; - mqtt["publish_interval"] = config.Mqtt_PublishInterval; - mqtt["clean_session"] = config.Mqtt_CleanSession; + mqtt["enabled"] = config.Mqtt.Enabled; + mqtt["verbose_logging"] = config.Mqtt.VerboseLogging; + mqtt["hostname"] = config.Mqtt.Hostname; + mqtt["port"] = config.Mqtt.Port; + mqtt["username"] = config.Mqtt.Username; + mqtt["password"] = config.Mqtt.Password; + mqtt["topic"] = config.Mqtt.Topic; + mqtt["retain"] = config.Mqtt.Retain; + mqtt["publish_interval"] = config.Mqtt.PublishInterval; + mqtt["clean_session"] = config.Mqtt.CleanSession; JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); - mqtt_lwt["topic"] = config.Mqtt_LwtTopic; - mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; - mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; + mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic; + mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online; + mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline; + mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos; JsonObject mqtt_tls = mqtt.createNestedObject("tls"); - mqtt_tls["enabled"] = config.Mqtt_Tls; - mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; - mqtt_tls["certlogin"] = config.Mqtt_TlsCertLogin; - mqtt_tls["client_cert"] = config.Mqtt_ClientCert; - mqtt_tls["client_key"] = config.Mqtt_ClientKey; + mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled; + mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert; + mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin; + mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert; + mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey; JsonObject mqtt_hass = mqtt.createNestedObject("hass"); - mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; - mqtt_hass["retain"] = config.Mqtt_Hass_Retain; - mqtt_hass["topic"] = config.Mqtt_Hass_Topic; - mqtt_hass["individual_panels"] = config.Mqtt_Hass_IndividualPanels; - mqtt_hass["expire"] = config.Mqtt_Hass_Expire; + mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled; + mqtt_hass["retain"] = config.Mqtt.Hass.Retain; + mqtt_hass["topic"] = config.Mqtt.Hass.Topic; + mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels; + mqtt_hass["expire"] = config.Mqtt.Hass.Expire; JsonObject dtu = doc.createNestedObject("dtu"); - dtu["serial"] = config.Dtu_Serial; - dtu["poll_interval"] = config.Dtu_PollInterval; - dtu["verbose_logging"] = config.Dtu_VerboseLogging; - dtu["nrf_pa_level"] = config.Dtu_NrfPaLevel; - dtu["cmt_pa_level"] = config.Dtu_CmtPaLevel; - dtu["cmt_frequency"] = config.Dtu_CmtFrequency; + dtu["serial"] = config.Dtu.Serial; + dtu["poll_interval"] = config.Dtu.PollInterval; + dtu["verbose_logging"] = config.Dtu.VerboseLogging; + dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel; + dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel; + dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency; JsonObject security = doc.createNestedObject("security"); - security["password"] = config.Security_Password; - security["allow_readonly"] = config.Security_AllowReadonly; + security["password"] = config.Security.Password; + security["allow_readonly"] = config.Security.AllowReadonly; JsonObject device = doc.createNestedObject("device"); device["pinmapping"] = config.Dev_PinMapping; JsonObject display = device.createNestedObject("display"); - display["powersafe"] = config.Display_PowerSafe; - display["screensaver"] = config.Display_ScreenSaver; - display["rotation"] = config.Display_Rotation; - display["contrast"] = config.Display_Contrast; - display["language"] = config.Display_Language; + display["powersafe"] = config.Display.PowerSafe; + display["screensaver"] = config.Display.ScreenSaver; + display["rotation"] = config.Display.Rotation; + display["contrast"] = config.Display.Contrast; + display["language"] = config.Display.Language; + display["diagram_duration"] = config.Display.DiagramDuration; + + JsonArray leds = device.createNestedArray("led"); + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + JsonObject led = leds.createNestedObject(); + led["brightness"] = config.Led_Single[i].Brightness; + } JsonArray inverters = doc.createNestedArray("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -118,6 +126,7 @@ bool ConfigurationClass::write() inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -129,76 +138,76 @@ bool ConfigurationClass::write() } JsonObject vedirect = doc.createNestedObject("vedirect"); - vedirect["enabled"] = config.Vedirect_Enabled; - vedirect["verbose_logging"] = config.Vedirect_VerboseLogging; - vedirect["updates_only"] = config.Vedirect_UpdatesOnly; + vedirect["enabled"] = config.Vedirect.Enabled; + vedirect["verbose_logging"] = config.Vedirect.VerboseLogging; + vedirect["updates_only"] = config.Vedirect.UpdatesOnly; JsonObject powermeter = doc.createNestedObject("powermeter"); - powermeter["enabled"] = config.PowerMeter_Enabled; - powermeter["verbose_logging"] = config.PowerMeter_VerboseLogging; - powermeter["interval"] = config.PowerMeter_Interval; - powermeter["source"] = config.PowerMeter_Source; - powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter_MqttTopicPowerMeter1; - powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter_MqttTopicPowerMeter2; - powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3; - powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate; - powermeter["sdmaddress"] = config.PowerMeter_SdmAddress; - powermeter["http_individual_requests"] = config.PowerMeter_HttpIndividualRequests; + powermeter["enabled"] = config.PowerMeter.Enabled; + powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; + powermeter["interval"] = config.PowerMeter.Interval; + powermeter["source"] = config.PowerMeter.Source; + powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; + powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; + powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; + powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate; + powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; + powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases"); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { JsonObject powermeter_phase = powermeter_http_phases.createNestedObject(); - powermeter_phase["enabled"] = config.Powermeter_Http_Phase[i].Enabled; - powermeter_phase["url"] = config.Powermeter_Http_Phase[i].Url; - powermeter_phase["auth_type"] = config.Powermeter_Http_Phase[i].AuthType; - powermeter_phase["username"] = config.Powermeter_Http_Phase[i].Username; - powermeter_phase["password"] = config.Powermeter_Http_Phase[i].Password; - powermeter_phase["header_key"] = config.Powermeter_Http_Phase[i].HeaderKey; - powermeter_phase["header_value"] = config.Powermeter_Http_Phase[i].HeaderValue; - powermeter_phase["timeout"] = config.Powermeter_Http_Phase[i].Timeout; - powermeter_phase["json_path"] = config.Powermeter_Http_Phase[i].JsonPath; + powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; + powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url; + powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType; + powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username; + powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password; + powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey; + powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue; + powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; + powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath; } JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); - powerlimiter["enabled"] = config.PowerLimiter_Enabled; - powerlimiter["verbose_logging"] = config.PowerLimiter_VerboseLogging; - powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassThroughEnabled; - powerlimiter["solar_passtrough_losses"] = config.PowerLimiter_SolarPassThroughLosses; - powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; - powerlimiter["interval"] = config.PowerLimiter_Interval; - powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; - powerlimiter["inverter_id"] = config.PowerLimiter_InverterId; - powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId; - powerlimiter["target_power_consumption"] = config.PowerLimiter_TargetPowerConsumption; - powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter_TargetPowerConsumptionHysteresis; - powerlimiter["lower_power_limit"] = config.PowerLimiter_LowerPowerLimit; - powerlimiter["upper_power_limit"] = config.PowerLimiter_UpperPowerLimit; - powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter_BatterySocStartThreshold; - powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter_BatterySocStopThreshold; - powerlimiter["voltage_start_threshold"] = config.PowerLimiter_VoltageStartThreshold; - powerlimiter["voltage_stop_threshold"] = config.PowerLimiter_VoltageStopThreshold; - powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter_VoltageLoadCorrectionFactor; - powerlimiter["inverter_restart_hour"] = config.PowerLimiter_RestartHour; - powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter_FullSolarPassThroughSoc; - powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter_FullSolarPassThroughStartVoltage; - powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter_FullSolarPassThroughStopVoltage; + powerlimiter["enabled"] = config.PowerLimiter.Enabled; + powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging; + powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled; + powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses; + powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy; + powerlimiter["interval"] = config.PowerLimiter.Interval; + powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; + powerlimiter["inverter_id"] = config.PowerLimiter.InverterId; + powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; + powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; + powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis; + powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit; + powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit; + powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold; + powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold; + powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold; + powerlimiter["voltage_stop_threshold"] = config.PowerLimiter.VoltageStopThreshold; + powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor; + powerlimiter["inverter_restart_hour"] = config.PowerLimiter.RestartHour; + powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc; + powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage; + powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage; JsonObject battery = doc.createNestedObject("battery"); - battery["enabled"] = config.Battery_Enabled; - battery["verbose_logging"] = config.Battery_VerboseLogging; - battery["provider"] = config.Battery_Provider; - battery["jkbms_interface"] = config.Battery_JkBmsInterface; - battery["jkbms_polling_interval"] = config.Battery_JkBmsPollingInterval; + battery["enabled"] = config.Battery.Enabled; + battery["verbose_logging"] = config.Battery.VerboseLogging; + battery["provider"] = config.Battery.Provider; + battery["jkbms_interface"] = config.Battery.JkBmsInterface; + battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; JsonObject huawei = doc.createNestedObject("huawei"); - huawei["enabled"] = config.Huawei_Enabled; - huawei["can_controller_frequency"] = config.Huawei_CAN_Controller_Frequency; - huawei["auto_power_enabled"] = config.Huawei_Auto_Power_Enabled; - huawei["voltage_limit"] = config.Huawei_Auto_Power_Voltage_Limit; - huawei["enable_voltage_limit"] = config.Huawei_Auto_Power_Enable_Voltage_Limit; - huawei["lower_power_limit"] = config.Huawei_Auto_Power_Lower_Power_Limit; - huawei["upper_power_limit"] = config.Huawei_Auto_Power_Upper_Power_Limit; + huawei["enabled"] = config.Huawei.Enabled; + huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; + huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit; + huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit; + huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; + huawei["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; // Serialize JSON to file if (serializeJson(doc, f) == 0) { @@ -216,121 +225,129 @@ bool ConfigurationClass::read() DynamicJsonDocument doc(JSON_BUFFER_SIZE); // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); + const DeserializationError error = deserializeJson(doc, f); if (error) { MessageOutput.println("Failed to read file, using default configuration"); } JsonObject cfg = doc["cfg"]; - config.Cfg_Version = cfg["version"] | CONFIG_VERSION; - config.Cfg_SaveCount = cfg["save_count"] | 0; + config.Cfg.Version = cfg["version"] | CONFIG_VERSION; + config.Cfg.SaveCount = cfg["save_count"] | 0; JsonObject wifi = doc["wifi"]; - strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid)); - strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password)); - strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname)); + strlcpy(config.WiFi.Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi.Ssid)); + strlcpy(config.WiFi.Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi.Password)); + strlcpy(config.WiFi.Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi.Hostname)); IPAddress wifi_ip; wifi_ip.fromString(wifi["ip"] | ""); - config.WiFi_Ip[0] = wifi_ip[0]; - config.WiFi_Ip[1] = wifi_ip[1]; - config.WiFi_Ip[2] = wifi_ip[2]; - config.WiFi_Ip[3] = wifi_ip[3]; + config.WiFi.Ip[0] = wifi_ip[0]; + config.WiFi.Ip[1] = wifi_ip[1]; + config.WiFi.Ip[2] = wifi_ip[2]; + config.WiFi.Ip[3] = wifi_ip[3]; IPAddress wifi_netmask; wifi_netmask.fromString(wifi["netmask"] | ""); - config.WiFi_Netmask[0] = wifi_netmask[0]; - config.WiFi_Netmask[1] = wifi_netmask[1]; - config.WiFi_Netmask[2] = wifi_netmask[2]; - config.WiFi_Netmask[3] = wifi_netmask[3]; + config.WiFi.Netmask[0] = wifi_netmask[0]; + config.WiFi.Netmask[1] = wifi_netmask[1]; + config.WiFi.Netmask[2] = wifi_netmask[2]; + config.WiFi.Netmask[3] = wifi_netmask[3]; IPAddress wifi_gateway; wifi_gateway.fromString(wifi["gateway"] | ""); - config.WiFi_Gateway[0] = wifi_gateway[0]; - config.WiFi_Gateway[1] = wifi_gateway[1]; - config.WiFi_Gateway[2] = wifi_gateway[2]; - config.WiFi_Gateway[3] = wifi_gateway[3]; + config.WiFi.Gateway[0] = wifi_gateway[0]; + config.WiFi.Gateway[1] = wifi_gateway[1]; + config.WiFi.Gateway[2] = wifi_gateway[2]; + config.WiFi.Gateway[3] = wifi_gateway[3]; IPAddress wifi_dns1; wifi_dns1.fromString(wifi["dns1"] | ""); - config.WiFi_Dns1[0] = wifi_dns1[0]; - config.WiFi_Dns1[1] = wifi_dns1[1]; - config.WiFi_Dns1[2] = wifi_dns1[2]; - config.WiFi_Dns1[3] = wifi_dns1[3]; + config.WiFi.Dns1[0] = wifi_dns1[0]; + config.WiFi.Dns1[1] = wifi_dns1[1]; + config.WiFi.Dns1[2] = wifi_dns1[2]; + config.WiFi.Dns1[3] = wifi_dns1[3]; IPAddress wifi_dns2; wifi_dns2.fromString(wifi["dns2"] | ""); - config.WiFi_Dns2[0] = wifi_dns2[0]; - config.WiFi_Dns2[1] = wifi_dns2[1]; - config.WiFi_Dns2[2] = wifi_dns2[2]; - config.WiFi_Dns2[3] = wifi_dns2[3]; + config.WiFi.Dns2[0] = wifi_dns2[0]; + config.WiFi.Dns2[1] = wifi_dns2[1]; + config.WiFi.Dns2[2] = wifi_dns2[2]; + config.WiFi.Dns2[3] = wifi_dns2[3]; - config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP; - config.WiFi_ApTimeout = wifi["aptimeout"] | ACCESS_POINT_TIMEOUT; + config.WiFi.Dhcp = wifi["dhcp"] | WIFI_DHCP; + config.WiFi.ApTimeout = wifi["aptimeout"] | ACCESS_POINT_TIMEOUT; JsonObject mdns = doc["mdns"]; - config.Mdns_Enabled = mdns["enabled"] | MDNS_ENABLED; + config.Mdns.Enabled = mdns["enabled"] | MDNS_ENABLED; JsonObject ntp = doc["ntp"]; - strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); - config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; - config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; - config.Ntp_SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE; + strlcpy(config.Ntp.Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp.Server)); + strlcpy(config.Ntp.Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp.Timezone)); + strlcpy(config.Ntp.TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp.TimezoneDescr)); + config.Ntp.Latitude = ntp["latitude"] | NTP_LATITUDE; + config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE; + config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE; JsonObject mqtt = doc["mqtt"]; - config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; - config.Mqtt_VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING; - strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname)); - config.Mqtt_Port = mqtt["port"] | MQTT_PORT; - strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); - config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; - config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; - config.Mqtt_CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION; + config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED; + config.Mqtt.VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING; + strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname)); + config.Mqtt.Port = mqtt["port"] | MQTT_PORT; + strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username)); + strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password)); + strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic)); + config.Mqtt.Retain = mqtt["retain"] | MQTT_RETAIN; + config.Mqtt.PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; + config.Mqtt.CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION; JsonObject mqtt_lwt = mqtt["lwt"]; - strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + strlcpy(config.Mqtt.Lwt.Topic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt.Lwt.Topic)); + strlcpy(config.Mqtt.Lwt.Value_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt.Lwt.Value_Online)); + strlcpy(config.Mqtt.Lwt.Value_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt.Lwt.Value_Offline)); + config.Mqtt.Lwt.Qos = mqtt_lwt["qos"] | MQTT_LWT_QOS; JsonObject mqtt_tls = mqtt["tls"]; - config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; - strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); - config.Mqtt_TlsCertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN; - strlcpy(config.Mqtt_ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt_ClientCert)); - strlcpy(config.Mqtt_ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt_ClientKey)); + config.Mqtt.Tls.Enabled = mqtt_tls["enabled"] | MQTT_TLS; + strlcpy(config.Mqtt.Tls.RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt.Tls.RootCaCert)); + config.Mqtt.Tls.CertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN; + strlcpy(config.Mqtt.Tls.ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt.Tls.ClientCert)); + strlcpy(config.Mqtt.Tls.ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt.Tls.ClientKey)); JsonObject mqtt_hass = mqtt["hass"]; - config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; - config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; - config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; - config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; - strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt.Hass.Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; + config.Mqtt.Hass.Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; + config.Mqtt.Hass.Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; + config.Mqtt.Hass.IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; + strlcpy(config.Mqtt.Hass.Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt.Hass.Topic)); JsonObject dtu = doc["dtu"]; - config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; - config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; - config.Dtu_VerboseLogging = dtu["verbose_logging"] | VERBOSE_LOGGING; - config.Dtu_NrfPaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; - config.Dtu_CmtPaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; - config.Dtu_CmtFrequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY; + config.Dtu.Serial = dtu["serial"] | DTU_SERIAL; + config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; + config.Dtu.VerboseLogging = dtu["verbose_logging"] | VERBOSE_LOGGING; + config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; + config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; + config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY; JsonObject security = doc["security"]; - strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); - config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; + strlcpy(config.Security.Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security.Password)); + config.Security.AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; JsonObject device = doc["device"]; strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping)); JsonObject display = device["display"]; - config.Display_PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; - config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; - config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; - config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; - config.Display_Language = display["language"] | DISPLAY_LANGUAGE; + config.Display.PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; + config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; + config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION; + config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; + config.Display.Language = display["language"] | DISPLAY_LANGUAGE; + config.Display.DiagramDuration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; + + JsonArray leds = device["led"]; + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + JsonObject led = leds[i].as(); + config.Led_Single[i].Brightness = led["brightness"] | LED_BRIGHTNESS; + } JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -346,6 +363,7 @@ bool ConfigurationClass::read() config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD; config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false; config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false; + config.Inverter[i].YieldDayCorrection = inv["yieldday_correction"] | false; JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -356,76 +374,76 @@ bool ConfigurationClass::read() } JsonObject vedirect = doc["vedirect"]; - config.Vedirect_Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; - config.Vedirect_VerboseLogging = vedirect["verbose_logging"] | VEDIRECT_VERBOSE_LOGGING; - config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; + config.Vedirect.Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; + config.Vedirect.VerboseLogging = vedirect["verbose_logging"] | VEDIRECT_VERBOSE_LOGGING; + config.Vedirect.UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; JsonObject powermeter = doc["powermeter"]; - config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED; - config.PowerMeter_VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING; - config.PowerMeter_Interval = powermeter["interval"] | POWERMETER_INTERVAL; - config.PowerMeter_Source = powermeter["source"] | POWERMETER_SOURCE; - strlcpy(config.PowerMeter_MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3)); - config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; - config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; - config.PowerMeter_HttpIndividualRequests = powermeter["http_individual_requests"] | false; + config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; + config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING; + config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL; + config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE; + strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1)); + strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2)); + strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3)); + config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; + config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; + config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; JsonArray powermeter_http_phases = powermeter["http_phases"]; for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { JsonObject powermeter_phase = powermeter_http_phases[i].as(); - config.Powermeter_Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); - strlcpy(config.Powermeter_Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.Powermeter_Http_Phase[i].Url)); - config.Powermeter_Http_Phase[i].AuthType = powermeter_phase["auth_type"] | Auth::none; - strlcpy(config.Powermeter_Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.Powermeter_Http_Phase[i].Username)); - strlcpy(config.Powermeter_Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.Powermeter_Http_Phase[i].Password)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderKey)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderValue)); - config.Powermeter_Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; - strlcpy(config.Powermeter_Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.Powermeter_Http_Phase[i].JsonPath)); + config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); + strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url)); + config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | Auth::none; + strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username)); + strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password)); + strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); + strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); + config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; + strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); } JsonObject powerlimiter = doc["powerlimiter"]; - config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; - config.PowerLimiter_VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING; - config.PowerLimiter_SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED; - config.PowerLimiter_SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; - config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; - config.PowerLimiter_Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; - config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; - config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; - config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; - config.PowerLimiter_TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; - config.PowerLimiter_TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; - config.PowerLimiter_LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; - config.PowerLimiter_UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; - config.PowerLimiter_BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; - config.PowerLimiter_BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; - config.PowerLimiter_VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; - config.PowerLimiter_VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD; - config.PowerLimiter_VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR; - config.PowerLimiter_RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR; - config.PowerLimiter_FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC; - config.PowerLimiter_FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE; - config.PowerLimiter_FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE; + config.PowerLimiter.Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; + config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING; + config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED; + config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; + config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; + config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; + config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; + config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; + config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; + config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; + config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; + config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; + config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; + config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; + config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; + config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; + config.PowerLimiter.VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD; + config.PowerLimiter.VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR; + config.PowerLimiter.RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR; + config.PowerLimiter.FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC; + config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE; + config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE; JsonObject battery = doc["battery"]; - config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED; - config.Battery_VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING; - config.Battery_Provider = battery["provider"] | BATTERY_PROVIDER; - config.Battery_JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; - config.Battery_JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; + config.Battery.Enabled = battery["enabled"] | BATTERY_ENABLED; + config.Battery.VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING; + config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER; + config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; + config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; JsonObject huawei = doc["huawei"]; - config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED; - config.Huawei_CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; - config.Huawei_Auto_Power_Enabled = huawei["auto_power_enabled"] | false; - config.Huawei_Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; - config.Huawei_Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; - config.Huawei_Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT; - config.Huawei_Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT; + config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; + config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; + config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false; + config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; + config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; + config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT; + config.Huawei.Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT; f.close(); return true; @@ -441,13 +459,13 @@ void ConfigurationClass::migrate() DynamicJsonDocument doc(JSON_BUFFER_SIZE); // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); + const DeserializationError error = deserializeJson(doc, f); if (error) { MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); return; } - if (config.Cfg_Version < 0x00011700) { + if (config.Cfg.Version < 0x00011700) { JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); @@ -459,19 +477,19 @@ void ConfigurationClass::migrate() } } - if (config.Cfg_Version < 0x00011800) { + if (config.Cfg.Version < 0x00011800) { JsonObject mqtt = doc["mqtt"]; - config.Mqtt_PublishInterval = mqtt["publish_invterval"]; + config.Mqtt.PublishInterval = mqtt["publish_invterval"]; } - if (config.Cfg_Version < 0x00011900) { + if (config.Cfg.Version < 0x00011900) { JsonObject dtu = doc["dtu"]; - config.Dtu_NrfPaLevel = dtu["pa_level"]; + config.Dtu.Nrf.PaLevel = dtu["pa_level"]; } f.close(); - config.Cfg_Version = CONFIG_VERSION; + config.Cfg.Version = CONFIG_VERSION; write(); read(); } @@ -489,10 +507,10 @@ INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() } } - return NULL; + return nullptr; } -INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) +INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial == serial) { @@ -500,7 +518,7 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) } } - return NULL; + return nullptr; } ConfigurationClass Configuration; diff --git a/src/Datastore.cpp b/src/Datastore.cpp index 4ff67b803..1d4c2eb05 100644 --- a/src/Datastore.cpp +++ b/src/Datastore.cpp @@ -8,105 +8,109 @@ DatastoreClass Datastore; -void DatastoreClass::init() +void DatastoreClass::init(Scheduler& scheduler) { - _updateTimeout.set(1000); + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&DatastoreClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(1 * TASK_SECOND); + _loopTask.enable(); } void DatastoreClass::loop() { - if (Hoymiles.isAllRadioIdle() && _updateTimeout.occured()) { + if (!Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); + return; + } - uint8_t isProducing = 0; - uint8_t isReachable = 0; - uint8_t pollEnabledCount = 0; + uint8_t isProducing = 0; + uint8_t isReachable = 0; + uint8_t pollEnabledCount = 0; - std::lock_guard lock(_mutex); + std::lock_guard lock(_mutex); - _totalAcYieldTotalEnabled = 0; - _totalAcYieldTotalDigits = 0; + _totalAcYieldTotalEnabled = 0; + _totalAcYieldTotalDigits = 0; - _totalAcYieldDayEnabled = 0; - _totalAcYieldDayDigits = 0; + _totalAcYieldDayEnabled = 0; + _totalAcYieldDayDigits = 0; - _totalAcPowerEnabled = 0; - _totalAcPowerDigits = 0; + _totalAcPowerEnabled = 0; + _totalAcPowerDigits = 0; - _totalDcPowerEnabled = 0; - _totalDcPowerDigits = 0; + _totalDcPowerEnabled = 0; + _totalDcPowerDigits = 0; - _totalDcPowerIrradiation = 0; - _totalDcIrradiationInstalled = 0; + _totalDcPowerIrradiation = 0; + _totalDcIrradiationInstalled = 0; - _isAllEnabledProducing = true; - _isAllEnabledReachable = true; + _isAllEnabledProducing = true; + _isAllEnabledReachable = true; - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - if (inv == nullptr) { - continue; - } + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } - auto cfg = Configuration.getInverterConfig(inv->serial()); - if (cfg == nullptr) { - continue; - } + auto cfg = Configuration.getInverterConfig(inv->serial()); + if (cfg == nullptr) { + continue; + } - if (inv->getEnablePolling()) { - pollEnabledCount++; - } + if (inv->getEnablePolling()) { + pollEnabledCount++; + } - if (inv->isProducing()) { - isProducing++; - } else { - if (inv->getEnablePolling()) { - _isAllEnabledProducing = false; - } + if (inv->isProducing()) { + isProducing++; + } else { + if (inv->getEnablePolling()) { + _isAllEnabledProducing = false; } + } - if (inv->isReachable()) { - isReachable++; - } else { - if (inv->getEnablePolling()) { - _isAllEnabledReachable = false; - } + if (inv->isReachable()) { + isReachable++; + } else { + if (inv->getEnablePolling()) { + _isAllEnabledReachable = false; } + } - for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { - if (cfg->Poll_Enable) { - _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); - _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { + if (cfg->Poll_Enable) { + _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); + _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); - _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); - _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); - } - if (inv->getEnablePolling()) { - _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); - _totalAcPowerDigits = max(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); - } + _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); + _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); + } + if (inv->getEnablePolling()) { + _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + _totalAcPowerDigits = max(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); } + } - for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) { - if (inv->getEnablePolling()) { - _totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); - _totalDcPowerDigits = max(_totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC)); + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) { + if (inv->getEnablePolling()) { + _totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); + _totalDcPowerDigits = max(_totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC)); - if (inv->Statistics()->getStringMaxPower(c) > 0) { - _totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); - _totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c); - } + if (inv->Statistics()->getStringMaxPower(c) > 0) { + _totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); + _totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c); } } } + } - _isAtLeastOneProducing = isProducing > 0; - _isAtLeastOneReachable = isReachable > 0; - _isAtLeastOnePollEnabled = pollEnabledCount > 0; - - _totalDcIrradiation = _totalDcIrradiationInstalled > 0 ? _totalDcPowerIrradiation / _totalDcIrradiationInstalled * 100.0f : 0; + _isAtLeastOneProducing = isProducing > 0; + _isAtLeastOneReachable = isReachable > 0; + _isAtLeastOnePollEnabled = pollEnabledCount > 0; - _updateTimeout.reset(); - } + _totalDcIrradiation = _totalDcIrradiationInstalled > 0 ? _totalDcPowerIrradiation / _totalDcIrradiationInstalled * 100.0f : 0; } float DatastoreClass::getTotalAcYieldTotalEnabled() diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 26991cb5d..5a06452f0 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -1,4 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ #include "Display_Graphic.h" #include "Datastore.h" #include @@ -39,7 +42,7 @@ DisplayGraphicClass::~DisplayGraphicClass() delete _display; } -void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset) +void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset) { _display_type = type; if (_display_type > DisplayType_t::None) { @@ -48,7 +51,14 @@ void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, ui _display->begin(); setContrast(DISPLAY_CONTRAST); setStatus(true); + _diagram.init(scheduler, _display); } + + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(_period); + _loopTask.enable(); } void DisplayGraphicClass::calcLineHeights() @@ -61,7 +71,7 @@ void DisplayGraphicClass::calcLineHeights() } } -void DisplayGraphicClass::setFont(uint8_t line) +void DisplayGraphicClass::setFont(const uint8_t line) { switch (line) { case 0: @@ -76,13 +86,13 @@ void DisplayGraphicClass::setFont(uint8_t line) } } -void DisplayGraphicClass::printText(const char* text, uint8_t line) +void DisplayGraphicClass::printText(const char* text, const uint8_t line) { uint8_t dispX; if (!_isLarge) { dispX = (line == 0) ? 5 : 0; } else { - dispX = (line == 0) ? 20 : 5; + dispX = (line == 0) ? 10 : 5; } setFont(line); @@ -90,7 +100,7 @@ void DisplayGraphicClass::printText(const char* text, uint8_t line) _display->drawStr(dispX, _lineOffsets[line], text); } -void DisplayGraphicClass::setOrientation(uint8_t rotation) +void DisplayGraphicClass::setOrientation(const uint8_t rotation) { if (_display_type == DisplayType_t::None) { return; @@ -115,7 +125,7 @@ void DisplayGraphicClass::setOrientation(uint8_t rotation) calcLineHeights(); } -void DisplayGraphicClass::setLanguage(uint8_t language) +void DisplayGraphicClass::setLanguage(const uint8_t language) { _display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; } @@ -131,71 +141,77 @@ void DisplayGraphicClass::setStartupDisplay() _display->sendBuffer(); } +DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram() +{ + return _diagram; +} + void DisplayGraphicClass::loop() { if (_display_type == DisplayType_t::None) { return; } - if ((millis() - _lastDisplayUpdate) > _period) { - - _display->clearBuffer(); - bool displayPowerSave = false; - - //=====> Actual Production ========== - if (Datastore.getIsAtLeastOneReachable()) { - displayPowerSave = false; - if (Datastore.getTotalAcPowerEnabled() > 999) { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); - } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.getTotalAcPowerEnabled()); - } - printText(_fmtText, 0); - _previousMillis = millis(); - } - //<======================= - - //=====> Offline =========== - else { - printText(i18n_offline[_display_language], 0); - // check if it's time to enter power saving mode - if (millis() - _previousMillis >= (_interval * 2)) { - displayPowerSave = enablePowerSafe; - } - } - //<======================= - - //=====> Today & Total Production ======= - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); - printText(_fmtText, 1); + _loopTask.setInterval(_period); - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled()); - printText(_fmtText, 2); - //<======================= + _display->clearBuffer(); + bool displayPowerSave = false; - //=====> IP or Date-Time ======== - if (!(_mExtra % 10) && NetworkSettings.localIP()) { - printText(NetworkSettings.localIP().toString().c_str(), 3); + //=====> Actual Production ========== + if (Datastore.getIsAtLeastOneReachable()) { + displayPowerSave = false; + if (_isLarge) { + _diagram.redraw(); + } + if (Datastore.getTotalAcPowerEnabled() > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); } else { - // Get current time - time_t now = time(nullptr); - strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); - printText(_fmtText, 3); + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.getTotalAcPowerEnabled()); } - _display->sendBuffer(); + printText(_fmtText, 0); + _previousMillis = millis(); + } + //<======================= + + //=====> Offline =========== + else { + printText(i18n_offline[_display_language], 0); + // check if it's time to enter power saving mode + if (millis() - _previousMillis >= (_interval * 2)) { + displayPowerSave = enablePowerSafe; + } + } + //<======================= - _mExtra++; - _lastDisplayUpdate = millis(); + //=====> Today & Total Production ======= + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); + printText(_fmtText, 1); - if (!_displayTurnedOn) { - displayPowerSave = true; - } + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled()); + printText(_fmtText, 2); + //<======================= - _display->setPowerSave(displayPowerSave); + //=====> IP or Date-Time ======== + if (!(_mExtra % 10) && NetworkSettings.localIP()) { + printText(NetworkSettings.localIP().toString().c_str(), 3); + } else { + // Get current time + time_t now = time(nullptr); + strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); + printText(_fmtText, 3); } + _display->sendBuffer(); + + _mExtra++; + + if (!_displayTurnedOn) { + displayPowerSave = true; + } + + _display->setPowerSave(displayPowerSave); } -void DisplayGraphicClass::setContrast(uint8_t contrast) +void DisplayGraphicClass::setContrast(const uint8_t contrast) { if (_display_type == DisplayType_t::None) { return; @@ -203,7 +219,7 @@ void DisplayGraphicClass::setContrast(uint8_t contrast) _display->setContrast(contrast * 2.55f); } -void DisplayGraphicClass::setStatus(bool turnOn) +void DisplayGraphicClass::setStatus(const bool turnOn) { _displayTurnedOn = turnOn; } diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp new file mode 100644 index 000000000..4a98c1691 --- /dev/null +++ b/src/Display_Graphic_Diagram.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Display_Graphic_Diagram.h" +#include "Configuration.h" +#include "Datastore.h" +#include + +DisplayGraphicDiagramClass::DisplayGraphicDiagramClass() +{ +} + +void DisplayGraphicDiagramClass::init(Scheduler& scheduler, U8G2* display) +{ + _display = display; + + scheduler.addTask(_averageTask); + _averageTask.setCallback(std::bind(&DisplayGraphicDiagramClass::averageLoop, this)); + _averageTask.setIterations(TASK_FOREVER); + _averageTask.setInterval(1 * TASK_SECOND); + _averageTask.enable(); + + scheduler.addTask(_dataPointTask); + _dataPointTask.setCallback(std::bind(&DisplayGraphicDiagramClass::dataPointLoop, this)); + _dataPointTask.setIterations(TASK_FOREVER); + updatePeriod(); + _dataPointTask.enable(); +} + +void DisplayGraphicDiagramClass::averageLoop() +{ + const float currentWatts = Datastore.getTotalAcPowerEnabled(); // get the current AC production + _iRunningAverage += currentWatts; + _iRunningAverageCnt++; +} + +void DisplayGraphicDiagramClass::dataPointLoop() +{ + if (_graphValuesCount >= CHART_WIDTH) { + for (uint8_t i = 0; i < CHART_WIDTH - 1; i++) { + _graphValues[i] = _graphValues[i + 1]; + } + _graphValuesCount = CHART_WIDTH - 1; + } + if (_iRunningAverageCnt != 0) { + _graphValues[_graphValuesCount++] = _iRunningAverage / _iRunningAverageCnt; + _iRunningAverage = 0; + _iRunningAverageCnt = 0; + } + + if (Configuration.get().Display.ScreenSaver) { + _graphPosX = DIAG_POSX - (_graphValuesCount % 2); + } +} + +uint32_t DisplayGraphicDiagramClass::getSecondsPerDot() +{ + return Configuration.get().Display.DiagramDuration / CHART_WIDTH; +} + +void DisplayGraphicDiagramClass::updatePeriod() +{ + _dataPointTask.setInterval(getSecondsPerDot() * TASK_SECOND); +} + +void DisplayGraphicDiagramClass::redraw() +{ + uint8_t graphPosY = DIAG_POSY; + + // draw diagram axis + _display->drawVLine(_graphPosX, graphPosY, CHART_HEIGHT); + _display->drawHLine(_graphPosX, graphPosY + CHART_HEIGHT - 1, CHART_WIDTH); + + _display->drawLine(_graphPosX + 1, graphPosY + 1, _graphPosX + 2, graphPosY + 2); // UP-arrow + _display->drawLine(_graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT - 3, _graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT - 2); // LEFT-arrow + _display->drawLine(_graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT + 1, _graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT); // LEFT-arrow + + // draw AC value + _display->setFont(u8g2_font_tom_thumb_4x6_mr); + char fmtText[7]; + const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end()); + snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); + const uint8_t textLength = strlen(fmtText); + _display->drawStr(_graphPosX - (textLength * 4), graphPosY + 5, fmtText); + + // draw chart + const float scaleFactor = maxWatts / CHART_HEIGHT; + uint8_t axisTick = 1; + for (int i = 0; i < _graphValuesCount; i++) { + if (scaleFactor > 0) { + if (i == 0) { + _display->drawPixel(_graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); // + 0.5 to round mathematical + } else { + _display->drawLine(_graphPosX + i, graphPosY + CHART_HEIGHT - ((_graphValues[i - 1] / scaleFactor) + 0.5), _graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); + } + } + + // draw one tick per hour to the x-axis + if (i * getSecondsPerDot() > (3600u * axisTick)) { + _display->drawPixel(_graphPosX + 1 + i, graphPosY + CHART_HEIGHT); + axisTick++; + } + } +} diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index 3c0e358d9..09da47a53 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -26,14 +26,14 @@ bool HttpPowerMeterClass::updateValues() errorMessage[256]; for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.Powermeter_Http_Phase[i]; + POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { power[i] = 0.0; continue; } - if (i == 0 || config.PowerMeter_HttpIndividualRequests) { + if (i == 0 || config.PowerMeter.HttpIndividualRequests) { if (httpRequest(phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout, response, sizeof(response), errorMessage, sizeof(errorMessage))) { if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) { diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index f42c7b018..87b141aad 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -193,7 +193,17 @@ void HuaweiCanCommClass::sendRequest() // Huawei CAN Controller // ******************************************************* -void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) +void HuaweiCanClass::init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power); +} + +void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) { if (_initialized) { return; @@ -201,11 +211,11 @@ void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huaw const CONFIG_T& config = Configuration.get(); - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { return; } - if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei_CAN_Controller_Frequency)) { + if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) { MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); return; }; @@ -214,7 +224,7 @@ void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huaw digitalWrite(huawei_power, HIGH); _huaweiPower = huawei_power; - if (config.Huawei_Auto_Power_Enabled) { + if (config.Huawei.Auto_Power_Enabled) { _mode = HUAWEI_MODE_AUTO_INT; } @@ -258,17 +268,17 @@ void HuaweiCanClass::loop() { const CONFIG_T& config = Configuration.get(); - if (!config.Huawei_Enabled || !_initialized) { + if (!config.Huawei.Enabled || !_initialized) { return; } processReceivedParameters(); uint8_t com_error = HuaweiCanComm.getErrorCode(true); - if (com_error && HUAWEI_ERROR_CODE_RX) { + if (com_error & HUAWEI_ERROR_CODE_RX) { MessageOutput.println("[HuaweiCanClass::loop] Data request error"); } - if (com_error && HUAWEI_ERROR_CODE_TX) { + if (com_error & HUAWEI_ERROR_CODE_TX) { MessageOutput.println("[HuaweiCanClass::loop] Data set error"); } @@ -296,8 +306,8 @@ void HuaweiCanClass::loop() // Set voltage limit in periodic intervals if ( _nextAutoModePeriodicIntMillis < millis()) { - MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei_Auto_Power_Voltage_Limit); - _setValue(config.Huawei_Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); + MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit); + _setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); _nextAutoModePeriodicIntMillis = millis() + 60000; } @@ -308,14 +318,14 @@ void HuaweiCanClass::loop() } // Re-enable automatic power control if the output voltage has dropped below threshold - if(_rp.output_voltage < config.Huawei_Auto_Power_Enable_Voltage_Limit ) { + if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) { _autoPowerEnabledCounter = 10; } // Check if inverter used by the power limiter is active std::shared_ptr inverter = - Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); + Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); if (inverter != nullptr) { if(inverter->isProducing()) { @@ -339,12 +349,12 @@ void HuaweiCanClass::loop() newPowerLimit += _rp.output_power; MessageOutput.printf("[HuaweiCanClass::loop] PL: %f, OP: %f \r\n", newPowerLimit, _rp.output_power); - if (newPowerLimit > config.Huawei_Auto_Power_Lower_Power_Limit) { + if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) { // Check if the output power has dropped below the lower limit (i.e. the battery is full) // and if the PSU should be turned off. Also we use a simple counter mechanism here to be able // to ramp up from zero output power when starting up - if (_rp.output_power < config.Huawei_Auto_Power_Lower_Power_Limit) { + if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) { MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n"); _autoPowerEnabledCounter--; if (_autoPowerEnabledCounter == 0) { @@ -357,8 +367,8 @@ void HuaweiCanClass::loop() } // Limit power to maximum - if (newPowerLimit > config.Huawei_Auto_Power_Upper_Power_Limit) { - newPowerLimit = config.Huawei_Auto_Power_Upper_Power_Limit; + if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) { + newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; } // Set the actual output limit @@ -391,7 +401,7 @@ void HuaweiCanClass::_setValue(float in, uint8_t parameterType) const CONFIG_T& config = Configuration.get(); - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { return; } @@ -422,7 +432,7 @@ void HuaweiCanClass::_setValue(float in, uint8_t parameterType) void HuaweiCanClass::setMode(uint8_t mode) { const CONFIG_T& config = Configuration.get(); - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { return; } @@ -435,7 +445,7 @@ void HuaweiCanClass::setMode(uint8_t mode) { _mode = HUAWEI_MODE_ON; } - if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei_Auto_Power_Enabled ) { + if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) { MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command"); return; } diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 0188cff45..a81527bf4 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -25,7 +25,7 @@ InverterSettingsClass InverterSettings; -void InverterSettingsClass::init() +void InverterSettingsClass::init(Scheduler& scheduler) { const CONFIG_T& config = Configuration.get(); const PinMapping_t& pin = PinMapping.get(); @@ -46,22 +46,22 @@ void InverterSettingsClass::init() if (PinMapping.isValidCmt2300Config()) { Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); MessageOutput.println(F(" Setting CMT target frequency... ")); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); } MessageOutput.println(" Setting radio PA level... "); - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); - Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel); + Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel); MessageOutput.println(" Setting DTU serial... "); - Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial); MessageOutput.println(" Setting poll interval... "); - Hoymiles.setPollInterval(config.Dtu_PollInterval); + Hoymiles.setPollInterval(config.Dtu.PollInterval); MessageOutput.println(" Setting verbosity... "); - Hoymiles.setVerboseLogging(config.Dtu_VerboseLogging); + Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { @@ -77,6 +77,7 @@ void InverterSettingsClass::init() inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight); + inv->Statistics()->setYieldDayCorrection(config.Inverter[i].YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); @@ -89,27 +90,39 @@ void InverterSettingsClass::init() } else { MessageOutput.println("Invalid pin config"); } + + scheduler.addTask(_hoyTask); + _hoyTask.setCallback(std::bind(&InverterSettingsClass::hoyLoop, this)); + _hoyTask.setIterations(TASK_FOREVER); + _hoyTask.enable(); + + scheduler.addTask(_settingsTask); + _settingsTask.setCallback(std::bind(&InverterSettingsClass::settingsLoop, this)); + _settingsTask.setIterations(TASK_FOREVER); + _settingsTask.setInterval(INVERTER_UPDATE_SETTINGS_INTERVAL); + _settingsTask.enable(); } -void InverterSettingsClass::loop() +void InverterSettingsClass::settingsLoop() { - if (millis() - _lastUpdate > INVERTER_UPDATE_SETTINGS_INTERVAL) { - const CONFIG_T& config = Configuration.get(); - - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - auto const& inv_cfg = config.Inverter[i]; - if (inv_cfg.Serial == 0) { - continue; - } - auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); - if (inv == nullptr) { - continue; - } + const CONFIG_T& config = Configuration.get(); - inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night)); - inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night)); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + auto const& inv_cfg = config.Inverter[i]; + if (inv_cfg.Serial == 0) { + continue; + } + auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); + if (inv == nullptr) { + continue; } + + inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night)); + inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night)); } + } +void InverterSettingsClass::hoyLoop() +{ Hoymiles.loop(); } diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 7e5ea0ac3..2422d6d28 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -250,8 +250,8 @@ void Controller::deinit() Controller::Interface Controller::getInterface() const { CONFIG_T& config = Configuration.get(); - if (0x00 == config.Battery_JkBmsInterface) { return Interface::Uart; } - if (0x01 == config.Battery_JkBmsInterface) { return Interface::Transceiver; } + if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; } + if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; } return Interface::Invalid; } @@ -323,7 +323,7 @@ void Controller::sendRequest(uint8_t pollInterval) void Controller::loop() { CONFIG_T& config = Configuration.get(); - uint8_t pollInterval = config.Battery_JkBmsPollingInterval; + uint8_t pollInterval = config.Battery.JkBmsPollingInterval; while (HwSerial.available()) { rxData(HwSerial.read()); diff --git a/src/Led_Single.cpp b/src/Led_Single.cpp index 09658c85c..744c7b78d 100644 --- a/src/Led_Single.cpp +++ b/src/Led_Single.cpp @@ -12,85 +12,116 @@ LedSingleClass LedSingle; +/* + The table is calculated using the following formula + (See https://www.mikrocontroller.net/articles/LED-Fading) + a = Step count: 101 --> 0 - 100 + b = PWM resolution: 256: 0 - 255 + y = Calculated value of index x: + y = 0 if x = 0 + y = pow(2, log2(b-1) * (x+1) / a) if x > 0 +*/ +const uint8_t pwmTable[] = { + 0, + 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, + 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 7, 7, 8, 8, 8, 9, 9, + 10, 11, 11, 12, 12, 13, 14, 15, 16, 16, + 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, + 30, 32, 33, 35, 37, 39, 42, 44, 47, 49, + 52, 55, 58, 61, 65, 68, 72, 76, 81, 85, + 90, 95, 100, 106, 112, 118, 125, 132, 139, 147, + 156, 164, 174, 183, 194, 205, 216, 228, 241, 255 +}; + +#define LED_OFF 0 + LedSingleClass::LedSingleClass() { } -void LedSingleClass::init() +void LedSingleClass::init(Scheduler& scheduler) { + bool ledActive = false; + _blinkTimeout.set(500); - _updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL); turnAllOn(); - auto& pin = PinMapping.get(); + const auto& pin = PinMapping.get(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { if (pin.led[i] >= 0) { pinMode(pin.led[i], OUTPUT); - digitalWrite(pin.led[i], LOW); - _ledActive++; + setLed(i, false); + ledActive = true; } - _ledState[i] = LedState_t::Off; + _ledMode[i] = LedState_t::Off; } -} -void LedSingleClass::loop() -{ - if (_ledActive == 0) { - return; + if (ledActive) { + scheduler.addTask(_outputTask); + _outputTask.setCallback(std::bind(&LedSingleClass::outputLoop, this)); + _outputTask.setIterations(TASK_FOREVER); + _outputTask.enable(); + + scheduler.addTask(_setTask); + _setTask.setCallback(std::bind(&LedSingleClass::setLoop, this)); + _setTask.setInterval(LEDSINGLE_UPDATE_INTERVAL * TASK_MILLISECOND); + _setTask.setIterations(TASK_FOREVER); + _setTask.enable(); } +} - if (_updateTimeout.occured() && _allState == LedState_t::On) { +void LedSingleClass::setLoop() +{ + if (_allMode == LedState_t::On) { const CONFIG_T& config = Configuration.get(); // Update network status - _ledState[0] = LedState_t::Off; + _ledMode[0] = LedState_t::Off; if (NetworkSettings.isConnected()) { - _ledState[0] = LedState_t::Blink; + _ledMode[0] = LedState_t::Blink; } struct tm timeinfo; - if (getLocalTime(&timeinfo, 5) && (!config.Mqtt_Enabled || (config.Mqtt_Enabled && MqttSettings.getConnected()))) { - _ledState[0] = LedState_t::On; + if (getLocalTime(&timeinfo, 5) && (!config.Mqtt.Enabled || (config.Mqtt.Enabled && MqttSettings.getConnected()))) { + _ledMode[0] = LedState_t::On; } // Update inverter status - _ledState[1] = LedState_t::Off; + _ledMode[1] = LedState_t::Off; if (Hoymiles.getNumInverters() && Datastore.getIsAtLeastOnePollEnabled()) { // set LED status if (Datastore.getIsAllEnabledReachable() && Datastore.getIsAllEnabledProducing()) { - _ledState[1] = LedState_t::On; + _ledMode[1] = LedState_t::On; } if (Datastore.getIsAllEnabledReachable() && !Datastore.getIsAllEnabledProducing()) { - _ledState[1] = LedState_t::Blink; + _ledMode[1] = LedState_t::Blink; } } - _updateTimeout.reset(); - } else if (_updateTimeout.occured() && _allState == LedState_t::Off) { - _ledState[0] = LedState_t::Off; - _ledState[1] = LedState_t::Off; + } else if (_allMode == LedState_t::Off) { + _ledMode[0] = LedState_t::Off; + _ledMode[1] = LedState_t::Off; } +} - auto& pin = PinMapping.get(); +void LedSingleClass::outputLoop() +{ for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - - if (pin.led[i] < 0) { - continue; - } - - switch (_ledState[i]) { + switch (_ledMode[i]) { case LedState_t::Off: - digitalWrite(pin.led[i], LOW); + setLed(i, false); break; case LedState_t::On: - digitalWrite(pin.led[i], HIGH); + setLed(i, true); break; case LedState_t::Blink: if (_blinkTimeout.occured()) { - digitalWrite(pin.led[i], !digitalRead(pin.led[i])); + setLed(i, !_ledStateCurrent[i]); _blinkTimeout.reset(); } break; @@ -98,12 +129,32 @@ void LedSingleClass::loop() } } +void LedSingleClass::setLed(const uint8_t ledNo, const bool ledState) +{ + const auto& pin = PinMapping.get(); + const auto& config = Configuration.get(); + + if (pin.led[ledNo] < 0) { + return; + } + + const uint32_t currentPWM = ledcRead(analogGetChannel(pin.led[ledNo])); + const uint32_t targetPWM = ledState ? pwmTable[config.Led_Single[ledNo].Brightness] : LED_OFF; + + if (currentPWM == targetPWM) { + return; + } + + analogWrite(pin.led[ledNo], targetPWM); + _ledStateCurrent[ledNo] = ledState; +} + void LedSingleClass::turnAllOff() { - _allState = LedState_t::Off; + _allMode = LedState_t::Off; } void LedSingleClass::turnAllOn() { - _allState = LedState_t::On; + _allMode = LedState_t::On; } diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index 79e53a4e5..f602bee15 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -1,105 +1,63 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ -#include #include "MessageOutput.h" +#include + MessageOutputClass MessageOutput; -void MessageOutputClass::register_ws_output(AsyncWebSocket* output) +void MessageOutputClass::init(Scheduler& scheduler) { - std::lock_guard lock(_msgLock); - - _ws = output; + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MessageOutputClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } -void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m) +void MessageOutputClass::register_ws_output(AsyncWebSocket* output) { - // on ESP32-S3, Serial.flush() blocks until a serial console is attached. - // operator bool() of HWCDC returns false if the device is not attached to - // a USB host. in general it makes sense to skip writing entirely if the - // default serial port is not ready. - if (!Serial) { return; } - - size_t written = 0; - while (written < m.size()) { - written += Serial.write(m.data() + written, m.size() - written); - } - Serial.flush(); + _ws = output; } size_t MessageOutputClass::write(uint8_t c) { - std::lock_guard lock(_msgLock); - - auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); - auto iter = res.first; - auto& message = iter->second; - - message.push_back(c); - - if (c == '\n') { - serialWrite(message); - _lines.emplace(std::move(message)); - _task_messages.erase(iter); + if (_buff_pos < BUFFER_SIZE) { + std::lock_guard lock(_msgLock); + _buffer[_buff_pos] = c; + _buff_pos++; + } else { + _forceSend = true; } - return 1; + return Serial.write(c); } -size_t MessageOutputClass::write(const uint8_t *buffer, size_t size) +size_t MessageOutputClass::write(const uint8_t* buffer, size_t size) { std::lock_guard lock(_msgLock); - - auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); - auto iter = res.first; - auto& message = iter->second; - - message.reserve(message.size() + size); - - for (size_t idx = 0; idx < size; ++idx) { - uint8_t c = buffer[idx]; - - message.push_back(c); - - if (c == '\n') { - serialWrite(message); - _lines.emplace(std::move(message)); - message.clear(); - message.reserve(size - idx - 1); - } + if (_buff_pos + size < BUFFER_SIZE) { + memcpy(&_buffer[_buff_pos], buffer, size); + _buff_pos += size; } + _forceSend = true; - if (message.empty()) { _task_messages.erase(iter); } - - return size; + return Serial.write(buffer, size); } void MessageOutputClass::loop() { - std::lock_guard lock(_msgLock); - - // clean up (possibly filled) buffers of deleted tasks - auto map_iter = _task_messages.begin(); - while (map_iter != _task_messages.end()) { - if (eTaskGetState(map_iter->first) == eDeleted) { - map_iter = _task_messages.erase(map_iter); - continue; + // Send data via websocket if either time is over or buffer is full + if (_forceSend || (millis() - _lastSend > 1000)) { + std::lock_guard lock(_msgLock); + if (_ws && _buff_pos > 0) { + _ws->textAll(_buffer, _buff_pos); + _buff_pos = 0; } - - ++map_iter; - } - - if (!_ws) { - while (!_lines.empty()) { - _lines.pop(); // do not hog memory + if (_forceSend) { + _buff_pos = 0; } - return; - } - - while (!_lines.empty() && _ws->availableForWriteAll()) { - _ws->textAll(std::make_shared(std::move(_lines.front()))); - _lines.pop(); + _forceSend = false; } } \ No newline at end of file diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index e406f8752..22b0566f2 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -11,13 +11,17 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass; -void MqttHandleVedirectHassClass::init() +void MqttHandleVedirectHassClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } void MqttHandleVedirectHassClass::loop() { - if (!Configuration.get().Vedirect_Enabled) { + if (!Configuration.get().Vedirect.Enabled) { return; } if (_updateForced) { @@ -42,8 +46,8 @@ void MqttHandleVedirectHassClass::forceUpdate() void MqttHandleVedirectHassClass::publishConfig() { - if ((!Configuration.get().Mqtt_Hass_Enabled) || - (!Configuration.get().Vedirect_Enabled)) { + if ((!Configuration.get().Mqtt.Hass.Enabled) || + (!Configuration.get().Vedirect.Enabled)) { return; } @@ -120,8 +124,8 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* JsonObject deviceObj = root.createNestedObject("dev"); createDeviceInfo(deviceObj); - if (Configuration.get().Mqtt_Hass_Expire) { - root[F("exp_aft")] = Configuration.get().Mqtt_PublishInterval * 3; + if (Configuration.get().Mqtt.Hass.Expire) { + root[F("exp_aft")] = Configuration.get().Mqtt.PublishInterval * 3; } if (deviceClass != NULL) { root[F("dev_cla")] = deviceClass; @@ -188,7 +192,7 @@ void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload) { - String topic = Configuration.get().Mqtt_Hass_Topic; + String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; - MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); } diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index ee5ad417f..c20ddb1e7 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "MqttHandleDtu.h" #include "Configuration.h" @@ -10,27 +10,29 @@ MqttHandleDtuClass MqttHandleDtu; -void MqttHandleDtuClass::init() +void MqttHandleDtuClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleDtuClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); } void MqttHandleDtuClass::loop() { + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); 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())); - MqttSettings.publish("dtu/bssid", String(WiFi.BSSIDstr())); - } - - _lastPublish = millis(); + 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())); + MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); } } \ No newline at end of file diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index dd2f56088..88553e15e 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -1,16 +1,22 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "MqttHandleHass.h" #include "MqttHandleInverter.h" #include "MqttSettings.h" #include "NetworkSettings.h" +#include "Utils.h" +#include "defaults.h" MqttHandleHassClass MqttHandleHass; -void MqttHandleHassClass::init() +void MqttHandleHassClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleHassClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } void MqttHandleHassClass::loop() @@ -37,7 +43,7 @@ void MqttHandleHassClass::forceUpdate() void MqttHandleHassClass::publishConfig() { - if (!Configuration.get().Mqtt_Hass_Enabled) { + if (!Configuration.get().Mqtt.Hass.Enabled) { return; } @@ -47,6 +53,14 @@ void MqttHandleHassClass::publishConfig() const CONFIG_T& config = Configuration.get(); + // publish DTU sensors + publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", ""); + publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi"); + publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", ""); + publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); + + yield(); + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -55,11 +69,11 @@ void MqttHandleHassClass::publishConfig() publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%"); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%"); + publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100); + publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100); - publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 10, 2250); - publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 10, 2250); + publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); + publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); @@ -69,10 +83,10 @@ void MqttHandleHassClass::publishConfig() for (auto& c : inv->Statistics()->getChannelsByType(t)) { for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) { bool clear = false; - if (t == TYPE_DC && !config.Mqtt_Hass_IndividualPanels) { + if (t == TYPE_DC && !config.Mqtt.Hass.IndividualPanels) { clear = true; } - publishField(inv, t, c, deviceFieldAssignment[f], clear); + publishInverterField(inv, t, c, deviceFieldAssignment[f], clear); } } } @@ -81,13 +95,13 @@ void MqttHandleHassClass::publishConfig() } } -void MqttHandleHassClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) +void MqttHandleHassClass::publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear) { if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldType.fieldId)) { return; } - String serial = inv->serialString(); + const String serial = inv->serialString(); String fieldName; if (type == TYPE_AC && fieldType.fieldId == FLD_PDC) { @@ -104,12 +118,12 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch chanNum = channel; } - String configTopic = "sensor/dtu_" + serial + const String configTopic = "sensor/dtu_" + serial + "/" + "ch" + chanNum + "_" + fieldName + "/config"; if (!clear) { - String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); + const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); const char* devCls = deviceClasses[fieldType.deviceClsId]; const char* stateCls = stateClasses[fieldType.stateClsId]; @@ -130,11 +144,10 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch root["unit_of_meas"] = unit_of_measure; } - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); - if (Configuration.get().Mqtt_Hass_Expire) { - root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt_PublishInterval) * inv->getReachableThreshold(); + if (Configuration.get().Mqtt.Hass.Expire) { + root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); } if (devCls != 0) { root["dev_cla"] = devCls; @@ -153,17 +166,17 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch 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(); + const String serial = inv->serialString(); String buttonId = caption; buttonId.replace(" ", "_"); buttonId.toLowerCase(); - String configTopic = "button/dtu_" + serial + const String configTopic = "button/dtu_" + serial + "/" + buttonId + "/config"; - String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; DynamicJsonDocument root(1024); root["name"] = caption; @@ -178,8 +191,7 @@ void MqttHandleHassClass::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) + const int16_t min, const int16_t max) { - String serial = inv->serialString(); + const String serial = inv->serialString(); String buttonId = caption; buttonId.replace(" ", "_"); buttonId.toLowerCase(); - String configTopic = "number/dtu_" + serial + const String configTopic = "number/dtu_" + serial + "/" + buttonId + "/config"; - String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; - String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; DynamicJsonDocument root(1024); root["name"] = caption; @@ -217,8 +229,7 @@ void MqttHandleHassClass::publishInverterNumber( root["min"] = min; root["max"] = max; - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); String buffer; serializeJson(root, buffer); @@ -227,17 +238,17 @@ void MqttHandleHassClass::publishInverterNumber( 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(); + const String serial = inv->serialString(); String sensorId = caption; sensorId.replace(" ", "_"); sensorId.toLowerCase(); - String configTopic = "binary_sensor/dtu_" + serial + const String configTopic = "binary_sensor/dtu_" + serial + "/" + sensorId + "/config"; - String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; DynamicJsonDocument root(1024); root["name"] = caption; @@ -246,27 +257,145 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr inv) +void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::shared_ptr inv) +{ + createDeviceInfo( + root, + inv->name(), + inv->serialString(), + getDtuUrl(), + "OpenDTU", + inv->typeName(), + AUTO_GIT_HASH, + getDtuUniqueId()); +} + +void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) +{ + createDeviceInfo( + root, + NetworkSettings.getHostname(), + getDtuUniqueId(), + getDtuUrl(), + "OpenDTU", + "OpenDTU", + AUTO_GIT_HASH); +} + +void MqttHandleHassClass::createDeviceInfo( + DynamicJsonDocument& root, + const String& name, const String& identifiers, const String& configuration_url, + const String& manufacturer, const String& model, const String& sw_version, + const String& via_device) +{ + auto object = root.createNestedObject("dev"); + + object["name"] = name; + object["ids"] = identifiers; + object["cu"] = configuration_url; + object["mf"] = manufacturer; + object["mdl"] = model; + object["sw"] = sw_version; + + if (via_device != "") { + object["via_device"] = via_device; + } +} + +String MqttHandleHassClass::getDtuUniqueId() +{ + return NetworkSettings.getHostname() + "_" + Utils::getChipId(); +} + +String MqttHandleHassClass::getDtuUrl() { - object["name"] = inv->name(); - object["ids"] = inv->serialString(); - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); - object["mf"] = "OpenDTU"; - object["mdl"] = inv->typeName(); - object["sw"] = AUTO_GIT_HASH; + return String("http://") + NetworkSettings.localIP().toString(); } void MqttHandleHassClass::publish(const String& subtopic, const String& payload) { - String topic = Configuration.get().Mqtt_Hass_Topic; + String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; - MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); } \ No newline at end of file diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 5df671f1e..06e5d22ad 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -18,8 +18,13 @@ MqttHandleHuaweiClass MqttHandleHuawei; -void MqttHandleHuaweiClass::init() +void MqttHandleHuaweiClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; @@ -47,13 +52,13 @@ void MqttHandleHuaweiClass::loop() const CONFIG_T& config = Configuration.get(); - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { return; } const RectifierParameters_t *rp = HuaweiCan.get(); - if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { + if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000)); MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage)); MqttSettings.publish("huawei/input_current", String(rp->input_current)); @@ -78,7 +83,7 @@ void MqttHandleHuaweiClass::onMqttMessage(const espMqttClientTypes::MessagePrope const CONFIG_T& config = Configuration.get(); // ignore messages if Huawei is disabled - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { return; } @@ -86,7 +91,7 @@ void MqttHandleHuaweiClass::onMqttMessage(const espMqttClientTypes::MessagePrope strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* char* setting; - char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + char* rest = &token_topic[strlen(config.Mqtt.Topic)]; strtok_r(rest, "/", &rest); // Remove "huawei" strtok_r(rest, "/", &rest); // Remove "cmd" diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 9048e06fb..881210963 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "MqttHandleInverter.h" #include "MessageOutput.h" @@ -18,7 +18,7 @@ MqttHandleInverterClass MqttHandleInverter; -void MqttHandleInverterClass::init() +void MqttHandleInverterClass::init(Scheduler& scheduler) { using std::placeholders::_1; using std::placeholders::_2; @@ -27,103 +27,106 @@ void MqttHandleInverterClass::init() using std::placeholders::_5; using std::placeholders::_6; - String topic = MqttSettings.getPrefix(); + const 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)); + + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleInverterClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); } void MqttHandleInverterClass::loop() { + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); 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); + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); - String subtopic = inv->serialString(); + const String subtopic = inv->serialString(); - // Name - MqttSettings.publish(subtopic + "/name", inv->name()); + // Name + MqttSettings.publish(subtopic + "/name", inv->name()); - if (inv->DevInfo()->getLastUpdate() > 0) { - // Bootloader Version - MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); + 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 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)); + // 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 part number + MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); - // Hardware version - MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); - } + // 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())); + 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)); - } + 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())); + 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)); - } + 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 lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); - if (inv->Statistics()->getLastUpdate() > 0 && (lastUpdateInternal != _lastPublishStats[i])) { - _lastPublishStats[i] = lastUpdateInternal; - - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - if (t == TYPE_DC) { - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg != nullptr) { - // TODO(tbnobody) - MqttSettings.publish(inv->serialString() + "/" + String(static_cast(c) + 1) + "/name", inv_cfg->channel[c].Name); - } - } - for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(FieldId_t); f++) { - publishField(inv, t, c, _publishFields[f]); + const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (inv->Statistics()->getLastUpdate() > 0 && (lastUpdateInternal != _lastPublishStats[i])) { + _lastPublishStats[i] = lastUpdateInternal; + + // Loop all channels + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg != nullptr) { + // TODO(tbnobody) + MqttSettings.publish(inv->serialString() + "/" + String(static_cast(c) + 1) + "/name", inv_cfg->channel[c].Name); } } + for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(FieldId_t); f++) { + publishField(inv, t, c, _publishFields[f]); + } } } - - yield(); } - _lastPublish = millis(); + yield(); } } -void MqttHandleInverterClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +void MqttHandleInverterClass::publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { - String topic = getTopic(inv, type, channel, fieldId); + const String topic = getTopic(inv, type, channel, fieldId); if (topic == "") { return; } @@ -131,10 +134,10 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv MqttSettings.publish(topic, inv->Statistics()->getChannelFieldValueString(type, channel, fieldId)); } -String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +String MqttHandleInverterClass::getTopic(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { - return String(""); + return ""; } String chanName; @@ -156,7 +159,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr inv, return inv->serialString() + "/" + chanNum + "/" + chanName; } -void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) { const CONFIG_T& config = Configuration.get(); @@ -166,7 +169,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* serial_str; char* subtopic; char* setting; - char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + char* rest = &token_topic[strlen(config.Mqtt.Topic)]; serial_str = strtok_r(rest, "/", &rest); subtopic = strtok_r(rest, "/", &rest); @@ -176,8 +179,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro return; } - uint64_t serial; - serial = strtoull(serial_str, 0, 16); + const uint64_t serial = strtoull(serial_str, 0, 16); auto inv = Hoymiles.getInverterBySerial(serial); @@ -194,7 +196,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* strlimit = new char[len + 1]; memcpy(strlimit, payload, len); strlimit[len] = '\0'; - int32_t payload_val = strtol(strlimit, NULL, 10); + const int32_t payload_val = strtol(strlimit, NULL, 10); delete[] strlimit; if (payload_val < 0) { diff --git a/src/MqttHandleInverterTotal.cpp b/src/MqttHandleInverterTotal.cpp index ac8e6a4ed..db584b264 100644 --- a/src/MqttHandleInverterTotal.cpp +++ b/src/MqttHandleInverterTotal.cpp @@ -10,26 +10,30 @@ MqttHandleInverterTotalClass MqttHandleInverterTotal; -void MqttHandleInverterTotalClass::init() +void MqttHandleInverterTotalClass::init(Scheduler& scheduler) { - _lastPublish.set(Configuration.get().Mqtt_PublishInterval * 1000); + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleInverterTotalClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); } void MqttHandleInverterTotalClass::loop() { + // Update interval from config + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); return; } - if (_lastPublish.occured()) { - MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits())); - MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits())); - MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits())); - MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable())); - MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits())); - MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3)); - MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable())); - - _lastPublish.set(Configuration.get().Mqtt_PublishInterval * 1000); - } + MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits())); + MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits())); + MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits())); + MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable())); + MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits())); + MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3)); + MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable())); } diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index d5d6987ef..9a4a7f77e 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -11,8 +11,13 @@ MqttHandlePowerLimiterClass MqttHandlePowerLimiter; -void MqttHandlePowerLimiterClass::init() +void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; @@ -35,7 +40,7 @@ void MqttHandlePowerLimiterClass::loop() const CONFIG_T& config = Configuration.get(); - if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { + if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { auto val = static_cast(PowerLimiter.getMode()); MqttSettings.publish("powerlimiter/status/mode", String(val)); @@ -51,7 +56,7 @@ void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessagePro const CONFIG_T& config = Configuration.get(); // ignore messages if PowerLimiter is disabled - if (!config.PowerLimiter_Enabled) { + if (!config.PowerLimiter.Enabled) { return; } diff --git a/src/MqttHandlePylontechHass.cpp b/src/MqttHandlePylontechHass.cpp index 7259cf91f..c23539538 100644 --- a/src/MqttHandlePylontechHass.cpp +++ b/src/MqttHandlePylontechHass.cpp @@ -9,14 +9,18 @@ MqttHandlePylontechHassClass MqttHandlePylontechHass; -void MqttHandlePylontechHassClass::init() +void MqttHandlePylontechHassClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandlePylontechHassClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } void MqttHandlePylontechHassClass::loop() { CONFIG_T& config = Configuration.get(); - if (!config.Battery_Enabled) { + if (!config.Battery.Enabled) { return; } if (_updateForced) { @@ -42,7 +46,7 @@ void MqttHandlePylontechHassClass::forceUpdate() void MqttHandlePylontechHassClass::publishConfig() { CONFIG_T& config = Configuration.get(); - if ((!config.Mqtt_Hass_Enabled) || (!config.Battery_Enabled)) { + if ((!config.Mqtt.Hass.Enabled) || (!config.Battery.Enabled)) { return; } @@ -111,29 +115,29 @@ void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char statTopic.concat(subTopic); DynamicJsonDocument root(1024); - root[F("name")] = caption; - root[F("stat_t")] = statTopic; - root[F("uniq_id")] = serial + "_" + sensorId; + root["name"] = caption; + root["stat_t"] = statTopic; + root["uniq_id"] = serial + "_" + sensorId; if (icon != NULL) { - root[F("icon")] = icon; + root["icon"] = icon; } if (unitOfMeasurement != NULL) { - root[F("unit_of_meas")] = unitOfMeasurement; + root["unit_of_meas"] = unitOfMeasurement; } JsonObject deviceObj = root.createNestedObject("dev"); createDeviceInfo(deviceObj); - if (Configuration.get().Mqtt_Hass_Expire) { - root[F("exp_aft")] = Configuration.get().Mqtt_PublishInterval * 3; + if (Configuration.get().Mqtt.Hass.Expire) { + root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; } if (deviceClass != NULL) { - root[F("dev_cla")] = deviceClass; + root["dev_cla"] = deviceClass; } if (stateClass != NULL) { - root[F("stat_cla")] = stateClass; + root["stat_cla"] = stateClass; } char buffer[512]; @@ -162,14 +166,14 @@ void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, cons statTopic.concat(subTopic); DynamicJsonDocument root(1024); - root[F("name")] = caption; - root[F("uniq_id")] = serial + "_" + sensorId; - root[F("stat_t")] = statTopic; - root[F("pl_on")] = payload_on; - root[F("pl_off")] = payload_off; + root["name"] = caption; + root["uniq_id"] = serial + "_" + sensorId; + root["stat_t"] = statTopic; + root["pl_on"] = payload_on; + root["pl_off"] = payload_off; if (icon != NULL) { - root[F("icon")] = icon; + root["icon"] = icon; } JsonObject deviceObj = root.createNestedObject("dev"); @@ -182,17 +186,17 @@ void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, cons void MqttHandlePylontechHassClass::createDeviceInfo(JsonObject& object) { - object[F("name")] = "Battery(" + serial + ")"; - object[F("ids")] = serial; - object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); - object[F("mf")] = F("OpenDTU"); - object[F("mdl")] = Battery.getStats()->getManufacturer(); - object[F("sw")] = AUTO_GIT_HASH; + object["name"] = "Battery(" + serial + ")"; + object["ids"] = serial; + object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["mf"] = "OpenDTU"; + object["mdl"] = Battery.getStats()->getManufacturer(); + object["sw"] = AUTO_GIT_HASH; } void MqttHandlePylontechHassClass::publish(const String& subtopic, const String& payload) { - String topic = Configuration.get().Mqtt_Hass_Topic; + String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; - MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); } diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 7466fc1a6..e5997dc98 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -14,18 +14,30 @@ MqttHandleVedirectClass MqttHandleVedirect; // #define MQTTHANDLEVEDIRECT_DEBUG -void MqttHandleVedirectClass::init() +void MqttHandleVedirectClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + // initially force a full publish + this->forceUpdate(); +} + +void MqttHandleVedirectClass::forceUpdate() { // initially force a full publish _nextPublishUpdatesOnly = 0; _nextPublishFull = 1; } + void MqttHandleVedirectClass::loop() { CONFIG_T& config = Configuration.get(); - if (!MqttSettings.getConnected() || !config.Vedirect_Enabled) { + if (!MqttSettings.getConnected() || !config.Vedirect.Enabled) { return; } @@ -38,7 +50,7 @@ void MqttHandleVedirectClass::loop() if (_nextPublishFull <= _nextPublishUpdatesOnly) { _PublishFull = true; } else { - _PublishFull = !config.Vedirect_UpdatesOnly; + _PublishFull = !config.Vedirect.UpdatesOnly; } #ifdef MQTTHANDLEVEDIRECT_DEBUG @@ -129,14 +141,14 @@ void MqttHandleVedirectClass::loop() } // now calculate next points of time to publish - _nextPublishUpdatesOnly = millis() + (config.Mqtt_PublishInterval * 1000); + _nextPublishUpdatesOnly = millis() + (config.Mqtt.PublishInterval * 1000); if (_PublishFull) { // when Home Assistant MQTT-Auto-Discovery is active, // and "enable expiration" is active, all values must be published at // least once before the announced expiry interval is reached - if ((config.Vedirect_UpdatesOnly) && (config.Mqtt_Hass_Enabled) && (config.Mqtt_Hass_Expire)) { - _nextPublishFull = millis() + (((config.Mqtt_PublishInterval * 3) - 1) * 1000); + if ((config.Vedirect.UpdatesOnly) && (config.Mqtt.Hass.Enabled) && (config.Mqtt.Hass.Expire)) { + _nextPublishFull = millis() + (((config.Mqtt.PublishInterval * 3) - 1) * 1000); #ifdef MQTTHANDLEVEDIRECT_DEBUG uint32_t _tmpNextFullSeconds = (config.Mqtt_PublishInterval * 3) - 1; diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index 2232d53b7..a0b236862 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -19,33 +19,33 @@ void MqttSettingsClass::NetworkEvent(network_event event) break; case network_event::NETWORK_DISCONNECTED: MessageOutput.println("Network lost connection"); - mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi + _mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi break; default: break; } } -void MqttSettingsClass::onMqttConnect(bool sessionPresent) +void MqttSettingsClass::onMqttConnect(const bool sessionPresent) { MessageOutput.println("Connected to MQTT."); const CONFIG_T& config = Configuration.get(); - publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online); + publish(config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { + if (_mqttClient != nullptr) { for (const auto& cb : _mqttSubscribeParser.get_callbacks()) { - mqttClient->subscribe(cb.topic.c_str(), cb.qos); + _mqttClient->subscribe(cb.topic.c_str(), cb.qos); } } } -void MqttSettingsClass::subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) +void MqttSettingsClass::subscribe(const String& topic, const uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) { _mqttSubscribeParser.register_callback(topic.c_str(), qos, cb); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - mqttClient->subscribe(topic.c_str(), qos); + if (_mqttClient != nullptr) { + _mqttClient->subscribe(topic.c_str(), qos); } } @@ -53,8 +53,8 @@ void MqttSettingsClass::unsubscribe(const String& topic) { _mqttSubscribeParser.unregister_callback(topic.c_str()); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - mqttClient->unsubscribe(topic.c_str()); + if (_mqttClient != nullptr) { + _mqttClient->unsubscribe(topic.c_str()); } } @@ -85,11 +85,11 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re default: MessageOutput.println("Unknown"); } - mqttReconnectTimer.once( + _mqttReconnectTimer.once( 2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this); } -void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) { if (_verboseLogging) { MessageOutput.print("Received MQTT message on topic: "); @@ -101,7 +101,7 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie void MqttSettingsClass::performConnect() { - if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) { + if (NetworkSettings.isConnected() && Configuration.get().Mqtt.Enabled) { using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; @@ -110,53 +110,53 @@ void MqttSettingsClass::performConnect() using std::placeholders::_6; std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } MessageOutput.println("Connecting to MQTT..."); const CONFIG_T& config = Configuration.get(); - _verboseLogging = config.Mqtt_VerboseLogging; - willTopic = getPrefix() + config.Mqtt_LwtTopic; - clientId = NetworkSettings.getApName(); - if (config.Mqtt_Tls) { - static_cast(mqttClient)->setCACert(config.Mqtt_RootCaCert); - static_cast(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port); - if (config.Mqtt_TlsCertLogin) { - static_cast(mqttClient)->setCertificate(config.Mqtt_ClientCert); - static_cast(mqttClient)->setPrivateKey(config.Mqtt_ClientKey); + _verboseLogging = config.Mqtt.VerboseLogging; + const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic; + const String clientId = NetworkSettings.getApName(); + if (config.Mqtt.Tls.Enabled) { + static_cast(_mqttClient)->setCACert(config.Mqtt.Tls.RootCaCert); + static_cast(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port); + if (config.Mqtt.Tls.CertLogin) { + static_cast(_mqttClient)->setCertificate(config.Mqtt.Tls.ClientCert); + static_cast(_mqttClient)->setPrivateKey(config.Mqtt.Tls.ClientKey); } else { - static_cast(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); + static_cast(_mqttClient)->setCredentials(config.Mqtt.Username, config.Mqtt.Password); } - static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); - static_cast(mqttClient)->setClientId(clientId.c_str()); - static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); - static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); - static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); - static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + static_cast(_mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); + static_cast(_mqttClient)->setClientId(clientId.c_str()); + static_cast(_mqttClient)->setCleanSession(config.Mqtt.CleanSession); + static_cast(_mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); + static_cast(_mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); + static_cast(_mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } else { - static_cast(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port); - static_cast(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); - static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); - static_cast(mqttClient)->setClientId(clientId.c_str()); - static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); - static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); - static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); - static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + static_cast(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port); + static_cast(_mqttClient)->setCredentials(config.Mqtt.Username, config.Mqtt.Password); + static_cast(_mqttClient)->setWill(willTopic.c_str(), config.Mqtt.Lwt.Qos, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); + static_cast(_mqttClient)->setClientId(clientId.c_str()); + static_cast(_mqttClient)->setCleanSession(config.Mqtt.CleanSession); + static_cast(_mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); + static_cast(_mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); + static_cast(_mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } - mqttClient->connect(); + _mqttClient->connect(); } } void MqttSettingsClass::performDisconnect() { const CONFIG_T& config = Configuration.get(); - publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Offline); + publish(config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Offline); std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } - mqttClient->disconnect(); + _mqttClient->disconnect(); } void MqttSettingsClass::performReconnect() @@ -165,22 +165,22 @@ void MqttSettingsClass::performReconnect() createMqttClientObject(); - mqttReconnectTimer.once( + _mqttReconnectTimer.once( 2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this); } bool MqttSettingsClass::getConnected() { std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return false; } - return mqttClient->connected(); + return _mqttClient->connected(); } -String MqttSettingsClass::getPrefix() +String MqttSettingsClass::getPrefix() const { - return Configuration.get().Mqtt_Topic; + return Configuration.get().Mqtt.Topic; } void MqttSettingsClass::publish(const String& subtopic, const String& payload) @@ -191,16 +191,16 @@ void MqttSettingsClass::publish(const String& subtopic, const String& payload) String value = payload; value.trim(); - publishGeneric(topic, value, Configuration.get().Mqtt_Retain, 0); + publishGeneric(topic, value, Configuration.get().Mqtt.Retain, 0); } -void MqttSettingsClass::publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos) +void MqttSettingsClass::publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos) { std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } - mqttClient->publish(topic.c_str(), qos, retain, payload.c_str()); + _mqttClient->publish(topic.c_str(), qos, retain, payload.c_str()); } void MqttSettingsClass::init() @@ -211,24 +211,18 @@ void MqttSettingsClass::init() createMqttClientObject(); } -void MqttSettingsClass::loop() -{ - if (nullptr == mqttClient) { return; } - mqttClient->loop(); -} - void MqttSettingsClass::createMqttClientObject() { std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - delete mqttClient; - mqttClient = nullptr; + if (_mqttClient != nullptr) { + delete _mqttClient; + _mqttClient = nullptr; } const CONFIG_T& config = Configuration.get(); - if (config.Mqtt_Tls) { - mqttClient = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO); + if (config.Mqtt.Tls.Enabled) { + _mqttClient = static_cast(new espMqttClientSecure); } else { - mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO); + _mqttClient = static_cast(new espMqttClient); } } diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 7e725ff13..92a3156dc 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "NetworkSettings.h" #include "Configuration.h" @@ -12,13 +12,13 @@ #include NetworkSettingsClass::NetworkSettingsClass() - : apIp(192, 168, 4, 1) - , apNetmask(255, 255, 255, 0) + : _apIp(192, 168, 4, 1) + , _apNetmask(255, 255, 255, 0) { - dnsServer.reset(new DNSServer()); + _dnsServer.reset(new DNSServer()); } -void NetworkSettingsClass::init() +void NetworkSettingsClass::init(Scheduler& scheduler) { using std::placeholders::_1; @@ -27,9 +27,14 @@ void NetworkSettingsClass::init() WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); setupMode(); + + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&NetworkSettingsClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } -void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) +void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) { switch (event) { case ARDUINO_EVENT_ETH_START: @@ -87,7 +92,7 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) } } -bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, network_event event) +bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event) { if (!cbEvent) { return pdFALSE; @@ -99,10 +104,10 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, network_event event) return true; } -void NetworkSettingsClass::raiseEvent(network_event event) +void NetworkSettingsClass::raiseEvent(const network_event event) { for (uint32_t i = 0; i < _cbEventList.size(); i++) { - NetworkEventCbList_t entry = _cbEventList[i]; + const NetworkEventCbList_t entry = _cbEventList[i]; if (entry.cb) { if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) { entry.cb(event); @@ -113,13 +118,13 @@ void NetworkSettingsClass::raiseEvent(network_event event) void NetworkSettingsClass::handleMDNS() { - bool mdnsEnabled = Configuration.get().Mdns_Enabled; + const bool mdnsEnabled = Configuration.get().Mdns.Enabled; - if (lastMdnsEnabled == mdnsEnabled) { + if (_lastMdnsEnabled == mdnsEnabled) { return; } - lastMdnsEnabled = mdnsEnabled; + _lastMdnsEnabled = mdnsEnabled; MDNS.end(); @@ -142,17 +147,17 @@ void NetworkSettingsClass::handleMDNS() void NetworkSettingsClass::setupMode() { - if (adminEnabled) { + if (_adminEnabled) { WiFi.mode(WIFI_AP_STA); String ssidString = getApName(); - WiFi.softAPConfig(apIp, apIp, apNetmask); - WiFi.softAP((const char*)ssidString.c_str(), Configuration.get().Security_Password); - dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); - dnsServerStatus = true; + WiFi.softAPConfig(_apIp, _apIp, _apNetmask); + WiFi.softAP(ssidString.c_str(), Configuration.get().Security.Password); + _dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + _dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); + _dnsServerStatus = true; } else { - dnsServerStatus = false; - dnsServer->stop(); + _dnsServerStatus = false; + _dnsServer->stop(); if (_networkMode == network_mode::WiFi) { WiFi.mode(WIFI_STA); } else { @@ -168,13 +173,13 @@ void NetworkSettingsClass::setupMode() void NetworkSettingsClass::enableAdminMode() { - adminEnabled = true; - adminTimeoutCounter = 0; - adminTimeoutCounterMax = Configuration.get().WiFi_ApTimeout * 60; + _adminEnabled = true; + _adminTimeoutCounter = 0; + _adminTimeoutCounterMax = Configuration.get().WiFi.ApTimeout * 60; setupMode(); } -String NetworkSettingsClass::getApName() +String NetworkSettingsClass::getApName() const { return String(ACCESS_POINT_NAME + String(Utils::getChipId())); } @@ -198,26 +203,26 @@ void NetworkSettingsClass::loop() applyConfig(); } - if (millis() - lastTimerCall > 1000) { - if (adminEnabled && adminTimeoutCounterMax > 0) { - adminTimeoutCounter++; - if (adminTimeoutCounter % 10 == 0) { - MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", adminTimeoutCounter, adminTimeoutCounterMax); + if (millis() - _lastTimerCall > 1000) { + if (_adminEnabled && _adminTimeoutCounterMax > 0) { + _adminTimeoutCounter++; + if (_adminTimeoutCounter % 10 == 0) { + MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); } } - connectTimeoutTimer++; - connectRedoTimer++; - lastTimerCall = millis(); + _connectTimeoutTimer++; + _connectRedoTimer++; + _lastTimerCall = millis(); } - if (adminEnabled) { + if (_adminEnabled) { // Don't disable the admin mode when network is not available if (!isConnected()) { - adminTimeoutCounter = 0; + _adminTimeoutCounter = 0; } // If WiFi is connected to AP for more than adminTimeoutCounterMax // seconds, disable the internal Access Point - if (adminTimeoutCounter > adminTimeoutCounterMax) { - adminEnabled = false; + if (_adminTimeoutCounter > _adminTimeoutCounterMax) { + _adminEnabled = false; MessageOutput.println("Admin mode disabled"); setupMode(); } @@ -225,28 +230,28 @@ void NetworkSettingsClass::loop() // WiFi is searching for an AP. So disable searching afer // WIFI_RECONNECT_TIMEOUT and repeat after WIFI_RECONNECT_REDO_TIMEOUT if (isConnected()) { - connectTimeoutTimer = 0; - connectRedoTimer = 0; + _connectTimeoutTimer = 0; + _connectRedoTimer = 0; } else { - if (connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !forceDisconnection) { + if (_connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !_forceDisconnection) { MessageOutput.print("Disable search for AP... "); WiFi.mode(WIFI_AP); MessageOutput.println("done"); - connectRedoTimer = 0; - forceDisconnection = true; + _connectRedoTimer = 0; + _forceDisconnection = true; } - if (connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && forceDisconnection) { + if (_connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && _forceDisconnection) { MessageOutput.print("Enable search for AP... "); WiFi.mode(WIFI_AP_STA); MessageOutput.println("done"); applyConfig(); - connectTimeoutTimer = 0; - forceDisconnection = false; + _connectTimeoutTimer = 0; + _forceDisconnection = false; } } } - if (dnsServerStatus) { - dnsServer->processNextRequest(); + if (_dnsServerStatus) { + _dnsServer->processNextRequest(); } handleMDNS(); @@ -255,15 +260,15 @@ void NetworkSettingsClass::loop() void NetworkSettingsClass::applyConfig() { setHostname(); - if (!strcmp(Configuration.get().WiFi_Ssid, "")) { + if (!strcmp(Configuration.get().WiFi.Ssid, "")) { return; } MessageOutput.print("Configuring WiFi STA using "); - if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi_Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi_Password)) { + if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi.Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi.Password)) { MessageOutput.print("new credentials... "); WiFi.begin( - Configuration.get().WiFi_Ssid, - Configuration.get().WiFi_Password); + Configuration.get().WiFi.Ssid, + Configuration.get().WiFi.Password); } else { MessageOutput.print("existing credentials... "); WiFi.begin(); @@ -298,39 +303,39 @@ void NetworkSettingsClass::setHostname() void NetworkSettingsClass::setStaticIp() { if (_networkMode == network_mode::WiFi) { - if (Configuration.get().WiFi_Dhcp) { + if (Configuration.get().WiFi.Dhcp) { MessageOutput.print("Configuring WiFi STA DHCP IP... "); WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); MessageOutput.println("done"); } else { MessageOutput.print("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)); + 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)); MessageOutput.println("done"); } } else if (_networkMode == network_mode::Ethernet) { - if (Configuration.get().WiFi_Dhcp) { + if (Configuration.get().WiFi.Dhcp) { MessageOutput.print("Configuring Ethernet DHCP IP... "); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); MessageOutput.println("done"); } else { MessageOutput.print("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)); + 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)); MessageOutput.println("done"); } } } -IPAddress NetworkSettingsClass::localIP() +IPAddress NetworkSettingsClass::localIP() const { switch (_networkMode) { case network_mode::Ethernet: @@ -344,7 +349,7 @@ IPAddress NetworkSettingsClass::localIP() } } -IPAddress NetworkSettingsClass::subnetMask() +IPAddress NetworkSettingsClass::subnetMask() const { switch (_networkMode) { case network_mode::Ethernet: @@ -358,7 +363,7 @@ IPAddress NetworkSettingsClass::subnetMask() } } -IPAddress NetworkSettingsClass::gatewayIP() +IPAddress NetworkSettingsClass::gatewayIP() const { switch (_networkMode) { case network_mode::Ethernet: @@ -372,7 +377,7 @@ IPAddress NetworkSettingsClass::gatewayIP() } } -IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no) +IPAddress NetworkSettingsClass::dnsIP(const uint8_t dns_no) const { switch (_networkMode) { case network_mode::Ethernet: @@ -386,7 +391,7 @@ IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no) } } -String NetworkSettingsClass::macAddress() +String NetworkSettingsClass::macAddress() const { switch (_networkMode) { case network_mode::Ethernet: @@ -407,8 +412,8 @@ String NetworkSettingsClass::getHostname() char resultHostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; uint8_t pos = 0; - uint32_t chipId = Utils::getChipId(); - snprintf(preparedHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, config.WiFi_Hostname, chipId); + const uint32_t chipId = Utils::getChipId(); + snprintf(preparedHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, config.WiFi.Hostname, chipId); const char* pC = preparedHostname; while (*pC && pos < WIFI_MAX_HOSTNAME_STRLEN) { // while !null and not over length @@ -439,12 +444,12 @@ String NetworkSettingsClass::getHostname() return resultHostname; } -bool NetworkSettingsClass::isConnected() +bool NetworkSettingsClass::isConnected() const { return WiFi.localIP()[0] != 0 || ETH.localIP()[0] != 0; } -network_mode NetworkSettingsClass::NetworkMode() +network_mode NetworkSettingsClass::NetworkMode() const { return _networkMode; } diff --git a/src/NtpSettings.cpp b/src/NtpSettings.cpp index ce043384d..b89904f35 100644 --- a/src/NtpSettings.cpp +++ b/src/NtpSettings.cpp @@ -19,12 +19,12 @@ void NtpSettingsClass::init() void NtpSettingsClass::setServer() { - configTime(0, 0, Configuration.get().Ntp_Server); + configTime(0, 0, Configuration.get().Ntp.Server); } void NtpSettingsClass::setTimezone() { - setenv("TZ", Configuration.get().Ntp_Timezone, 1); + setenv("TZ", Configuration.get().Ntp.Timezone, 1); tzset(); } diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 2cc48906d..776cde8c3 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -282,7 +282,7 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } -bool PinMappingClass::isValidNrf24Config() +bool PinMappingClass::isValidNrf24Config() const { return _pinMapping.nrf24_clk >= 0 && _pinMapping.nrf24_cs >= 0 @@ -292,7 +292,7 @@ bool PinMappingClass::isValidNrf24Config() && _pinMapping.nrf24_mosi >= 0; } -bool PinMappingClass::isValidCmt2300Config() +bool PinMappingClass::isValidCmt2300Config() const { return _pinMapping.cmt_clk >= 0 && _pinMapping.cmt_cs >= 0 @@ -300,7 +300,7 @@ bool PinMappingClass::isValidCmt2300Config() && _pinMapping.cmt_sdio >= 0; } -bool PinMappingClass::isValidEthConfig() +bool PinMappingClass::isValidEthConfig() const { return _pinMapping.eth_enabled; } diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index feb41dd80..86cb2751b 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -18,7 +18,13 @@ PowerLimiterClass PowerLimiter; -void PowerLimiterClass::init() { } +void PowerLimiterClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&PowerLimiterClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); +} std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status) { @@ -100,7 +106,7 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) if (CMD_PENDING == lastPowerCommandState) { return true; } CONFIG_T& config = Configuration.get(); - commitPowerLimit(_inverter, config.PowerLimiter_LowerPowerLimit, false); + commitPowerLimit(_inverter, config.PowerLimiter.LowerPowerLimit, false); return true; } @@ -108,7 +114,7 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) void PowerLimiterClass::loop() { CONFIG_T const& config = Configuration.get(); - _verboseLogging = config.PowerLimiter_VerboseLogging; + _verboseLogging = config.PowerLimiter.VerboseLogging; // we know that the Hoymiles library refuses to send any message to any // inverter until the system has valid time information. until then we can @@ -126,7 +132,7 @@ void PowerLimiterClass::loop() return; } - if (!config.PowerLimiter_Enabled) { + if (!config.PowerLimiter.Enabled) { shutdown(Status::DisabledByConfig); return; } @@ -137,7 +143,7 @@ void PowerLimiterClass::loop() } std::shared_ptr currentInverter = - Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); + Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); // in case of (newly) broken configuration, shut down // the last inverter we worked with (if any) @@ -192,7 +198,7 @@ void PowerLimiterClass::loop() // the normal mode of operation requires a valid // power meter reading to calculate a power limit - if (!config.PowerMeter_Enabled) { + if (!config.PowerMeter.Enabled) { shutdown(Status::PowerMeterDisabled); return; } @@ -239,7 +245,7 @@ void PowerLimiterClass::loop() } // Check if NTP time is set and next inverter restart not calculated yet - if ((config.PowerLimiter_RestartHour >= 0) && (_nextInverterRestart == 0) ) { + if ((config.PowerLimiter.RestartHour >= 0) && (_nextInverterRestart == 0) ) { // check every 5 seconds if (_nextCalculateCheck < millis()) { struct tm timeinfo; @@ -260,12 +266,12 @@ void PowerLimiterClass::loop() } else { // UI: Solar Passthrough Enabled -> false // Battery discharge can be enabled when start threshold is reached - if (!config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached()) { + if (!config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached()) { _batteryDischargeEnabled = true; } // UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT - if (config.PowerLimiter_SolarPassThroughEnabled && config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { + if (config.PowerLimiter.SolarPassThroughEnabled && config.PowerLimiter.BatteryDrainStategy == EMPTY_AT_NIGHT) { if(isStartThresholdReached()) { // In this case we should only discharge the battery as long it is above startThreshold _batteryDischargeEnabled = true; @@ -278,24 +284,24 @@ void PowerLimiterClass::loop() // UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL // Battery discharge can be enabled when start threshold is reached - if (config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { + if (config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter.BatteryDrainStategy == EMPTY_WHEN_FULL) { _batteryDischargeEnabled = true; } } if (_verboseLogging) { MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s\r\n", - (config.Battery_Enabled?"enabled":"disabled"), + (config.Battery.Enabled?"enabled":"disabled"), Battery.getStats()->getSoC(), - config.PowerLimiter_BatterySocStartThreshold, - config.PowerLimiter_BatterySocStopThreshold, + config.PowerLimiter.BatterySocStartThreshold, + config.PowerLimiter.BatterySocStopThreshold, Battery.getStats()->getSoCAgeSeconds()); - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter_InverterChannelId, FLD_UDC); + float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter.InverterChannelId, FLD_UDC); MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n", dcVoltage, getLoadCorrectedVoltage(), - config.PowerLimiter_VoltageStartThreshold, - config.PowerLimiter_VoltageStopThreshold); + config.PowerLimiter.VoltageStartThreshold, + config.PowerLimiter.VoltageStopThreshold); MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, inverter %s producing\r\n", (isStartThresholdReached()?"yes":"no"), @@ -303,13 +309,13 @@ void PowerLimiterClass::loop() (_inverter->isProducing()?"is":"is NOT")); MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n", - (config.PowerLimiter_SolarPassThroughEnabled?"enabled":"disabled"), - config.PowerLimiter_BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no")); + (config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"), + config.PowerLimiter.BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no")); MessageOutput.printf("[DPL::loop] battery discharging %s, PowerMeter: %d W, target consumption: %d W\r\n", (_batteryDischargeEnabled?"allowed":"prevented"), static_cast(round(PowerMeter.getPowerTotal())), - config.PowerLimiter_TargetPowerConsumption); + config.PowerLimiter.TargetPowerConsumption); } // Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!) @@ -350,7 +356,7 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr float inverterEfficiencyFactor = (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967; // account for losses between solar charger and inverter (cables, junctions...) - float lossesFactor = 1.00 - static_cast(config.PowerLimiter_SolarPassThroughLosses)/100; + float lossesFactor = 1.00 - static_cast(config.PowerLimiter.SolarPassThroughLosses)/100; return dcPower * inverterEfficiencyFactor * lossesFactor; } @@ -402,7 +408,7 @@ bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); - if (!config.PowerLimiter_SolarPassThroughEnabled + if (!config.PowerLimiter.SolarPassThroughEnabled || isBelowStopThreshold() || !VictronMppt.isDataValid()) { return false; @@ -432,7 +438,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve return 0; } - if (config.PowerLimiter_IsInverterBehindPowerMeter) { + if (config.PowerLimiter.IsInverterBehindPowerMeter) { // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. // We don't use FLD_PAC from the statistics, because that @@ -444,7 +450,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // We're not trying to hit 0 exactly but take an offset into account // This means we never fully compensate the used power with the inverter // Case 3 - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; + newPowerLimit -= config.PowerLimiter.TargetPowerConsumption; // At this point we've calculated the required energy to compensate for household consumption. // If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power @@ -514,14 +520,14 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver CONFIG_T& config = Configuration.get(); // Stop the inverter if limit is below threshold. - if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (newPowerLimit < config.PowerLimiter.LowerPowerLimit) { // the status must not change outside of loop(). this condition is // communicated through log messages already. return shutdown(); } // enforce configured upper power limit - int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter_UpperPowerLimit); + int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter.UpperPowerLimit); // scale the power limit by the amount of all inverter channels devided by // the amount of producing inverter channels. the inverters limit each of @@ -544,7 +550,7 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver // Check if the new value is within the limits of the hysteresis auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit); - auto hysteresis = config.PowerLimiter_TargetPowerConsumptionHysteresis; + auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis; // (re-)send power limit in case the last was sent a long time ago. avoids // staleness in case a power limit update was not received by the inverter. @@ -586,7 +592,7 @@ float PowerLimiterClass::getLoadCorrectedVoltage() CONFIG_T& config = Configuration.get(); - auto channel = static_cast(config.PowerLimiter_InverterChannelId); + auto channel = static_cast(config.PowerLimiter.InverterChannelId); float acPower = _inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); @@ -594,7 +600,7 @@ float PowerLimiterClass::getLoadCorrectedVoltage() return 0.0; } - return dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); + return dcVoltage + (acPower * config.PowerLimiter.VoltageLoadCorrectionFactor); } bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold, @@ -603,7 +609,7 @@ bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold, CONFIG_T& config = Configuration.get(); // prefer SoC provided through battery interface - if (config.Battery_Enabled && socThreshold > 0.0 + if (config.Battery.Enabled && socThreshold > 0.0 && Battery.getStats()->isValid() && Battery.getStats()->getSoCAgeSeconds() < 60) { return compare(Battery.getStats()->getSoC(), socThreshold); @@ -620,8 +626,8 @@ bool PowerLimiterClass::isStartThresholdReached() CONFIG_T& config = Configuration.get(); return testThreshold( - config.PowerLimiter_BatterySocStartThreshold, - config.PowerLimiter_VoltageStartThreshold, + config.PowerLimiter.BatterySocStartThreshold, + config.PowerLimiter.VoltageStartThreshold, [](float a, float b) -> bool { return a >= b; } ); } @@ -631,8 +637,8 @@ bool PowerLimiterClass::isStopThresholdReached() CONFIG_T& config = Configuration.get(); return testThreshold( - config.PowerLimiter_BatterySocStopThreshold, - config.PowerLimiter_VoltageStopThreshold, + config.PowerLimiter.BatterySocStopThreshold, + config.PowerLimiter.VoltageStopThreshold, [](float a, float b) -> bool { return a <= b; } ); } @@ -642,8 +648,8 @@ bool PowerLimiterClass::isBelowStopThreshold() CONFIG_T& config = Configuration.get(); return testThreshold( - config.PowerLimiter_BatterySocStopThreshold, - config.PowerLimiter_VoltageStopThreshold, + config.PowerLimiter.BatterySocStopThreshold, + config.PowerLimiter.VoltageStopThreshold, [](float a, float b) -> bool { return a < b; } ); } @@ -654,7 +660,7 @@ void PowerLimiterClass::calcNextInverterRestart() CONFIG_T& config = Configuration.get(); // first check if restart is configured at all - if (config.PowerLimiter_RestartHour < 0) { + if (config.PowerLimiter.RestartHour < 0) { _nextInverterRestart = 1; MessageOutput.println("[DPL::calcNextInverterRestart] _nextInverterRestart disabled"); return; @@ -665,8 +671,8 @@ void PowerLimiterClass::calcNextInverterRestart() if (getLocalTime(&timeinfo, 5)) { // calculation first step is offset to next restart in minutes uint16_t dayMinutes = timeinfo.tm_hour * 60 + timeinfo.tm_min; - uint16_t targetMinutes = config.PowerLimiter_RestartHour * 60; - if (config.PowerLimiter_RestartHour > timeinfo.tm_hour) { + uint16_t targetMinutes = config.PowerLimiter.RestartHour * 60; + if (config.PowerLimiter.RestartHour > timeinfo.tm_hour) { // next restart is on the same day _nextInverterRestart = targetMinutes - dayMinutes; } else { @@ -674,7 +680,7 @@ void PowerLimiterClass::calcNextInverterRestart() _nextInverterRestart = 1440 - dayMinutes + targetMinutes; } if (_verboseLogging) { - MessageOutput.printf("[DPL::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter_RestartHour); + MessageOutput.printf("[DPL::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter.RestartHour); MessageOutput.printf("[DPL::calcNextInverterRestart] dayMinutes %d / targetMinutes %d\r\n", dayMinutes, targetMinutes); MessageOutput.printf("[DPL::calcNextInverterRestart] next inverter restart in %d minutes\r\n", _nextInverterRestart); } @@ -693,18 +699,18 @@ bool PowerLimiterClass::useFullSolarPassthrough() CONFIG_T& config = Configuration.get(); // We only do full solar PT if general solar PT is enabled - if(!config.PowerLimiter_SolarPassThroughEnabled) { + if(!config.PowerLimiter.SolarPassThroughEnabled) { return false; } - if (testThreshold(config.PowerLimiter_FullSolarPassThroughSoc, - config.PowerLimiter_FullSolarPassThroughStartVoltage, + if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc, + config.PowerLimiter.FullSolarPassThroughStartVoltage, [](float a, float b) -> bool { return a >= b; })) { _fullSolarPassThroughEnabled = true; } - if (testThreshold(config.PowerLimiter_FullSolarPassThroughSoc, - config.PowerLimiter_FullSolarPassThroughStopVoltage, + if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc, + config.PowerLimiter.FullSolarPassThroughStopVoltage, [](float a, float b) -> bool { return a < b; })) { _fullSolarPassThroughEnabled = false; } diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index a7b1fee8e..e45b5837d 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -18,8 +18,13 @@ SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN); SoftwareSerial inputSerial; -void PowerMeterClass::init() +void PowerMeterClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&PowerMeterClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + _lastPowerMeterCheck = 0; _lastPowerMeterUpdate = 0; @@ -28,11 +33,11 @@ void PowerMeterClass::init() CONFIG_T& config = Configuration.get(); - if (!config.PowerMeter_Enabled) { + if (!config.PowerMeter.Enabled) { return; } - switch(config.PowerMeter_Source) { + switch(config.PowerMeter.Source) { case SOURCE_MQTT: { auto subscribe = [this](char const* topic, float* target) { if (strlen(topic) == 0) { return; } @@ -45,9 +50,9 @@ void PowerMeterClass::init() _mqttSubscriptions.try_emplace(topic, target); }; - subscribe(config.PowerMeter_MqttTopicPowerMeter1, &_powerMeter1Power); - subscribe(config.PowerMeter_MqttTopicPowerMeter2, &_powerMeter2Power); - subscribe(config.PowerMeter_MqttTopicPowerMeter3, &_powerMeter3Power); + subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerMeter1Power); + subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerMeter2Power); + subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerMeter3Power); break; } @@ -98,7 +103,7 @@ float PowerMeterClass::getPowerTotal(bool forceUpdate) { if (forceUpdate) { CONFIG_T& config = Configuration.get(); - if (config.PowerMeter_Enabled + if (config.PowerMeter.Enabled && (millis() - _lastPowerMeterUpdate) > (1000)) { readPowerMeter(); } @@ -132,11 +137,11 @@ void PowerMeterClass::mqtt() void PowerMeterClass::loop() { CONFIG_T const& config = Configuration.get(); - _verboseLogging = config.PowerMeter_VerboseLogging; + _verboseLogging = config.PowerMeter.VerboseLogging; - if (!config.PowerMeter_Enabled) { return; } + if (!config.PowerMeter.Enabled) { return; } - if (config.PowerMeter_Source == SOURCE_SML) { + if (config.PowerMeter.Source == SOURCE_SML) { if (!smlReadLoop()) { return; } else { @@ -144,7 +149,7 @@ void PowerMeterClass::loop() } } - if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter_Interval * 1000)) { + if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { return; } @@ -161,9 +166,9 @@ void PowerMeterClass::readPowerMeter() { CONFIG_T& config = Configuration.get(); - uint8_t _address = config.PowerMeter_SdmAddress; + uint8_t _address = config.PowerMeter.SdmAddress; - if (config.PowerMeter_Source == SOURCE_SDM1PH) { + if (config.PowerMeter.Source == SOURCE_SDM1PH) { _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); _powerMeter2Power = 0.0; _powerMeter3Power = 0.0; @@ -174,7 +179,7 @@ void PowerMeterClass::readPowerMeter() _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); _lastPowerMeterUpdate = millis(); } - else if (config.PowerMeter_Source == SOURCE_SDM3PH) { + else if (config.PowerMeter.Source == SOURCE_SDM3PH) { _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); _powerMeter2Power = static_cast(sdm.readVal(SDM_PHASE_2_POWER, _address)); _powerMeter3Power = static_cast(sdm.readVal(SDM_PHASE_3_POWER, _address)); @@ -185,7 +190,7 @@ void PowerMeterClass::readPowerMeter() _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); _lastPowerMeterUpdate = millis(); } - else if (config.PowerMeter_Source == SOURCE_HTTP) { + else if (config.PowerMeter.Source == SOURCE_HTTP) { if (HttpPowerMeter.updateValues()) { _powerMeter1Power = HttpPowerMeter.getPower(1); _powerMeter2Power = HttpPowerMeter.getPower(2); diff --git a/src/Scheduler.cpp b/src/Scheduler.cpp new file mode 100644 index 000000000..79dfd9c8b --- /dev/null +++ b/src/Scheduler.cpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Scheduler.h" + +Scheduler scheduler; \ No newline at end of file diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp index 0d4d419ba..a3a9f47fb 100644 --- a/src/SunPosition.cpp +++ b/src/SunPosition.cpp @@ -13,18 +13,23 @@ SunPositionClass::SunPositionClass() { } -void SunPositionClass::init() +void SunPositionClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&SunPositionClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(5 * TASK_SECOND); + _loopTask.enable(); } void SunPositionClass::loop() { - if (getDoRecalc() || checkRecalcDayChanged()) { + if (_doRecalc || checkRecalcDayChanged()) { updateSunData(); } } -bool SunPositionClass::isDayPeriod() +bool SunPositionClass::isDayPeriod() const { if (!_isValidInfo) { return true; @@ -32,28 +37,21 @@ bool SunPositionClass::isDayPeriod() struct tm timeinfo; getLocalTime(&timeinfo, 5); - uint32_t minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; + const uint32_t minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; return (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); } -bool SunPositionClass::isSunsetAvailable() +bool SunPositionClass::isSunsetAvailable() const { return _isSunsetAvailable; } -void SunPositionClass::setDoRecalc(bool doRecalc) +void SunPositionClass::setDoRecalc(const bool doRecalc) { - std::lock_guard lock(_recalcLock); _doRecalc = doRecalc; } -bool SunPositionClass::getDoRecalc() -{ - std::lock_guard lock(_recalcLock); - return _doRecalc; -} - -bool SunPositionClass::checkRecalcDayChanged() +bool SunPositionClass::checkRecalcDayChanged() const { time_t now; struct tm timeinfo; @@ -61,39 +59,31 @@ bool SunPositionClass::checkRecalcDayChanged() time(&now); localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms - uint32_t ymd; - ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; + const uint32_t ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; - if (_lastSunPositionCalculatedYMD != ymd) { - return true; - } - return false; + return _lastSunPositionCalculatedYMD != ymd; } void SunPositionClass::updateSunData() { struct tm timeinfo; - bool gotLocalTime; + const bool gotLocalTime = getLocalTime(&timeinfo, 5); - gotLocalTime = getLocalTime(&timeinfo, 5); _lastSunPositionCalculatedYMD = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; setDoRecalc(false); if (!gotLocalTime) { _sunriseMinutes = 0; _sunsetMinutes = 0; + _isSunsetAvailable = true; _isValidInfo = false; return; } CONFIG_T const& config = Configuration.get(); - int offset = Utils::getTimezoneOffset() / 3600; - - _sun.setPosition(config.Ntp_Latitude, config.Ntp_Longitude, offset); - _sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); double sunset_type; - switch (config.Ntp_SunsetType) { + switch (config.Ntp.SunsetType) { case 0: sunset_type = SunSet::SUNSET_OFFICIAL; break; @@ -108,15 +98,21 @@ void SunPositionClass::updateSunData() break; } - double sunriseRaw = _sun.calcCustomSunrise(sunset_type); - double sunsetRaw = _sun.calcCustomSunset(sunset_type); + const int offset = Utils::getTimezoneOffset() / 3600; + + SunSet sun; + sun.setPosition(config.Ntp.Latitude, config.Ntp.Longitude, offset); + sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); + + const double sunriseRaw = sun.calcCustomSunrise(sunset_type); + const double sunsetRaw = sun.calcCustomSunset(sunset_type); // If no sunset/sunrise exists (e.g. astronomical calculation in summer) // assume it's day period if (std::isnan(sunriseRaw) || std::isnan(sunsetRaw)) { - _isSunsetAvailable = false; _sunriseMinutes = 0; _sunsetMinutes = 0; + _isSunsetAvailable = false; _isValidInfo = false; return; } @@ -128,7 +124,7 @@ void SunPositionClass::updateSunData() _isValidInfo = true; } -bool SunPositionClass::sunsetTime(struct tm* info) +bool SunPositionClass::getSunTime(struct tm* info, const uint32_t offset) const { // Get today's date time_t aTime = time(NULL); @@ -137,29 +133,21 @@ bool SunPositionClass::sunsetTime(struct tm* info) struct tm tm; localtime_r(&aTime, &tm); tm.tm_sec = 0; - tm.tm_min = _sunsetMinutes; + tm.tm_min = offset; tm.tm_hour = 0; tm.tm_isdst = -1; - time_t midnight = mktime(&tm); + const time_t midnight = mktime(&tm); localtime_r(&midnight, info); return _isValidInfo; } -bool SunPositionClass::sunriseTime(struct tm* info) +bool SunPositionClass::sunsetTime(struct tm* info) const { - // Get today's date - time_t aTime = time(NULL); - - // Set the time to midnight - struct tm tm; - localtime_r(&aTime, &tm); - tm.tm_sec = 0; - tm.tm_min = _sunriseMinutes; - tm.tm_hour = 0; - tm.tm_isdst = -1; - time_t midnight = mktime(&tm); + return getSunTime(info, _sunsetMinutes); +} - localtime_r(&midnight, info); - return _isValidInfo; +bool SunPositionClass::sunriseTime(struct tm* info) const +{ + return getSunTime(info, _sunriseMinutes); } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index 609f8bf05..0e5fe082c 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -6,14 +6,24 @@ VictronMpptClass VictronMppt; -void VictronMpptClass::init() +void VictronMpptClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&VictronMpptClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + this->updateSettings(); +} + +void VictronMpptClass::updateSettings() { std::lock_guard lock(_mutex); _controllers.clear(); CONFIG_T& config = Configuration.get(); - if (!config.Vedirect_Enabled) { return; } + if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); int8_t rx = pin.victron_rx; @@ -27,7 +37,7 @@ void VictronMpptClass::init() } auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, config.Vedirect_VerboseLogging); + upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging); _controllers.push_back(std::move(upController)); } diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 82cd140ad..df4c0aa01 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi.h" #include "Configuration.h" @@ -9,44 +9,46 @@ WebApiClass::WebApiClass() : _server(HTTP_PORT) - , _events("/events") { } -void WebApiClass::init() +void WebApiClass::init(Scheduler& scheduler) { - _server.addHandler(&_events); - - _webApiBattery.init(&_server); - _webApiConfig.init(&_server); - _webApiDevice.init(&_server); - _webApiDevInfo.init(&_server); - _webApiDtu.init(&_server); - _webApiEventlog.init(&_server); - _webApiFirmware.init(&_server); - _webApiGridprofile.init(&_server); - _webApiInverter.init(&_server); - _webApiLimit.init(&_server); - _webApiMaintenance.init(&_server); - _webApiMqtt.init(&_server); - _webApiNetwork.init(&_server); - _webApiNtp.init(&_server); - _webApiPower.init(&_server); - _webApiPowerMeter.init(&_server); - _webApiPowerLimiter.init(&_server); - _webApiPrometheus.init(&_server); - _webApiSecurity.init(&_server); - _webApiSysstatus.init(&_server); - _webApiWebapp.init(&_server); - _webApiWsConsole.init(&_server); - _webApiWsLive.init(&_server); - _webApiWsVedirectLive.init(&_server); - _webApiVedirect.init(&_server); - _webApiWsHuaweiLive.init(&_server); - _webApiHuaweiClass.init(&_server); - _webApiWsBatteryLive.init(&_server); + _webApiConfig.init(_server); + _webApiDevice.init(_server); + _webApiDevInfo.init(_server); + _webApiDtu.init(_server); + _webApiEventlog.init(_server); + _webApiFirmware.init(_server); + _webApiGridprofile.init(_server); + _webApiInverter.init(_server); + _webApiLimit.init(_server); + _webApiMaintenance.init(_server); + _webApiMqtt.init(_server); + _webApiNetwork.init(_server); + _webApiNtp.init(_server); + _webApiPower.init(_server); + _webApiPrometheus.init(_server); + _webApiSecurity.init(_server); + _webApiSysstatus.init(_server); + _webApiWebapp.init(_server); + _webApiWsConsole.init(_server); + _webApiWsLive.init(_server); + _webApiBattery.init(_server); + _webApiPowerMeter.init(_server); + _webApiPowerLimiter.init(_server); + _webApiWsVedirectLive.init(_server); + _webApiVedirect.init(_server); + _webApiWsHuaweiLive.init(_server); + _webApiHuaweiClass.init(_server); + _webApiWsBatteryLive.init(_server); _server.begin(); + + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&WebApiClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); } void WebApiClass::loop() @@ -83,7 +85,7 @@ void WebApiClass::loop() bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) { CONFIG_T& config = Configuration.get(); - if (request->authenticate(AUTH_USERNAME, config.Security_Password)) { + if (request->authenticate(AUTH_USERNAME, config.Security.Password)) { return true; } @@ -101,7 +103,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) { CONFIG_T& config = Configuration.get(); - if (config.Security_AllowReadonly) { + if (config.Security.AllowReadonly) { return true; } else { return checkCredentials(request); diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index 691c6504f..586e2ab99 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -12,11 +12,11 @@ #include #include -void WebApiHuaweiClass::init(AsyncWebServer* server) +void WebApiHuaweiClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1)); _server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1)); @@ -32,26 +32,26 @@ void WebApiHuaweiClass::getJsonData(JsonObject& root) { const RectifierParameters_t * rp = HuaweiCan.get(); root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; - root[F("input_voltage")]["v"] = rp->input_voltage; - root[F("input_voltage")]["u"] = "V"; - root[F("input_current")]["v"] = rp->input_current; - root[F("input_current")]["u"] = "A"; - root[F("input_power")]["v"] = rp->input_power; - root[F("input_power")]["u"] = "W"; - root[F("output_voltage")]["v"] = rp->output_voltage; - root[F("output_voltage")]["u"] = "V"; - root[F("output_current")]["v"] = rp->output_current; - root[F("output_current")]["u"] = "A"; - root[F("max_output_current")]["v"] = rp->max_output_current; - root[F("max_output_current")]["u"] = "A"; - root[F("output_power")]["v"] = rp->output_power; - root[F("output_power")]["u"] = "W"; - root[F("input_temp")]["v"] = rp->input_temp; - root[F("input_temp")]["u"] = "°C"; - root[F("output_temp")]["v"] = rp->output_temp; - root[F("output_temp")]["u"] = "°C"; - root[F("efficiency")]["v"] = rp->efficiency * 100; - root[F("efficiency")]["u"] = "%"; + root["input_voltage"]["v"] = rp->input_voltage; + root["input_voltage"]["u"] = "V"; + root["input_current"]["v"] = rp->input_current; + root["input_current"]["u"] = "A"; + root["input_power"]["v"] = rp->input_power; + root["input_power"]["u"] = "W"; + root["output_voltage"]["v"] = rp->output_voltage; + root["output_voltage"]["u"] = "V"; + root["output_current"]["v"] = rp->output_current; + root["output_current"]["u"] = "A"; + root["max_output_current"]["v"] = rp->max_output_current; + root["max_output_current"]["u"] = "A"; + root["output_power"]["v"] = rp->output_power; + root["output_power"]["u"] = "W"; + root["input_temp"]["v"] = rp->input_temp; + root["input_temp"]["u"] = "°C"; + root["output_temp"]["v"] = rp->output_temp; + root["output_temp"]["u"] = "°C"; + root["efficiency"]["v"] = rp->efficiency * 100; + root["efficiency"]["u"] = "%"; } @@ -77,11 +77,11 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); + retMsg["type"] = "warning"; if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - retMsg[F("code")] = WebApiError::GenericNoValueFound; + retMsg["message"] = "No values found!"; + retMsg["code"] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -90,8 +90,8 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) String json = request->getParam("data", true)->value(); if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - retMsg[F("code")] = WebApiError::GenericDataTooLarge; + retMsg["message"] = "Data too large!"; + retMsg["code"] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -104,40 +104,40 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) float minimal_voltage; if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - retMsg[F("code")] = WebApiError::GenericParseError; + retMsg["message"] = "Failed to parse data!"; + retMsg["code"] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; } if (root.containsKey("online")) { - online = root[F("online")].as(); + online = root["online"].as(); if (online) { minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE; } else { minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE; } } else { - retMsg[F("message")] = F("Could not read info if data should be set for online/offline operation!"); - retMsg[F("code")] = WebApiError::LimitInvalidType; + retMsg["message"] = "Could not read info if data should be set for online/offline operation!"; + retMsg["code"] = WebApiError::LimitInvalidType; response->setLength(); request->send(response); return; } if (root.containsKey("voltage_valid")) { - if (root[F("voltage_valid")].as()) { - if (root[F("voltage")].as() < minimal_voltage || root[F("voltage")].as() > 58) { - retMsg[F("message")] = F("voltage not in range between 42 (online)/48 (offline and 58V !"); - retMsg[F("code")] = WebApiError::LimitInvalidLimit; - retMsg[F("param")][F("max")] = 58; - retMsg[F("param")][F("min")] = minimal_voltage; + if (root["voltage_valid"].as()) { + if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 58) { + retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !"; + retMsg["code"] = WebApiError::LimitInvalidLimit; + retMsg["param"]["max"] = 58; + retMsg["param"]["min"] = minimal_voltage; response->setLength(); request->send(response); return; } else { - value = root[F("voltage")].as(); + value = root["voltage"].as(); if (online) { HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE); } else { @@ -148,17 +148,17 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } if (root.containsKey("current_valid")) { - if (root[F("current_valid")].as()) { - if (root[F("current")].as() < 0 || root[F("current")].as() > 60) { - retMsg[F("message")] = F("current must be in range between 0 and 60!"); - retMsg[F("code")] = WebApiError::LimitInvalidLimit; - retMsg[F("param")][F("max")] = 60; - retMsg[F("param")][F("min")] = 0; + if (root["current_valid"].as()) { + if (root["current"].as() < 0 || root["current"].as() > 60) { + retMsg["message"] = "current must be in range between 0 and 60!"; + retMsg["code"] = WebApiError::LimitInvalidLimit; + retMsg["param"]["max"] = 60; + retMsg["param"]["min"] = 0; response->setLength(); request->send(response); return; } else { - value = root[F("current")].as(); + value = root["current"].as(); if (online) { HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT); } else { @@ -168,9 +168,9 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } } - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - retMsg[F("code")] = WebApiError::GenericSuccess; + retMsg["type"] = "success"; + retMsg["message"] = "Settings saved!"; + retMsg["code"] = WebApiError::GenericSuccess; response->setLength(); request->send(response); @@ -189,13 +189,13 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("enabled")] = config.Huawei_Enabled; - root[F("can_controller_frequency")] = config.Huawei_CAN_Controller_Frequency; - root[F("auto_power_enabled")] = config.Huawei_Auto_Power_Enabled; - root[F("voltage_limit")] = static_cast(config.Huawei_Auto_Power_Voltage_Limit * 100) / 100.0; - root[F("enable_voltage_limit")] = static_cast(config.Huawei_Auto_Power_Enable_Voltage_Limit * 100) / 100.0; - root[F("lower_power_limit")] = config.Huawei_Auto_Power_Lower_Power_Limit; - root[F("upper_power_limit")] = config.Huawei_Auto_Power_Upper_Power_Limit; + root["enabled"] = config.Huawei.Enabled; + root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; + root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + root["voltage_limit"] = static_cast(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; + root["enable_voltage_limit"] = static_cast(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0; + root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; + root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; response->setLength(); request->send(response); @@ -209,11 +209,11 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); + retMsg["type"] = "warning"; if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - retMsg[F("code")] = WebApiError::GenericNoValueFound; + retMsg["message"] = "No values found!"; + retMsg["code"] = WebApiError::GenericNoValueFound; response->setLength(); request->send(response); return; @@ -222,8 +222,8 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) String json = request->getParam("data", true)->value(); if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - retMsg[F("code")] = WebApiError::GenericDataTooLarge; + retMsg["message"] = "Data too large!"; + retMsg["code"] = WebApiError::GenericDataTooLarge; response->setLength(); request->send(response); return; @@ -233,8 +233,8 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) DeserializationError error = deserializeJson(root, json); if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - retMsg[F("code")] = WebApiError::GenericParseError; + retMsg["message"] = "Failed to parse data!"; + retMsg["code"] = WebApiError::GenericParseError; response->setLength(); request->send(response); return; @@ -246,26 +246,26 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) !(root.containsKey("voltage_limit")) || !(root.containsKey("lower_power_limit")) || !(root.containsKey("upper_power_limit"))) { - retMsg[F("message")] = F("Values are missing!"); - retMsg[F("code")] = WebApiError::GenericValueMissing; + retMsg["message"] = "Values are missing!"; + retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); request->send(response); return; } CONFIG_T& config = Configuration.get(); - config.Huawei_Enabled = root[F("enabled")].as(); - config.Huawei_CAN_Controller_Frequency = root[F("can_controller_frequency")].as(); - config.Huawei_Auto_Power_Enabled = root[F("auto_power_enabled")].as(); - config.Huawei_Auto_Power_Voltage_Limit = root[F("voltage_limit")].as(); - config.Huawei_Auto_Power_Enable_Voltage_Limit = root[F("enable_voltage_limit")].as(); - config.Huawei_Auto_Power_Lower_Power_Limit = root[F("lower_power_limit")].as(); - config.Huawei_Auto_Power_Upper_Power_Limit = root[F("upper_power_limit")].as(); + config.Huawei.Enabled = root["enabled"].as(); + config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); + config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); + config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); + config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); + config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); + config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); Configuration.write(); - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - retMsg[F("code")] = WebApiError::GenericSuccess; + retMsg["type"] = "success"; + retMsg["message"] = "Settings saved!"; + retMsg["code"] = WebApiError::GenericSuccess; response->setLength(); request->send(response); @@ -280,26 +280,26 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) const PinMapping_t& pin = PinMapping.get(); // Properly turn this on - if (config.Huawei_Enabled) { - MessageOutput.println(F("Initialize Huawei AC charger interface... ")); + if (config.Huawei.Enabled) { + MessageOutput.println("Initialize Huawei AC charger interface... "); if (PinMapping.isValidHuaweiConfig()) { MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - HuaweiCan.init(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - MessageOutput.println(F("done")); + HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + MessageOutput.println("done"); } else { - MessageOutput.println(F("Invalid pin config")); + MessageOutput.println("Invalid pin config"); } } // Properly turn this off - if (!config.Huawei_Enabled) { + if (!config.Huawei.Enabled) { HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT); delay(500); HuaweiCan.setMode(HUAWEI_MODE_OFF); return; } - if (config.Huawei_Auto_Power_Enabled) { + if (config.Huawei.Auto_Power_Enabled) { HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT); return; } diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index a21f09a4f..05897840a 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -13,11 +13,11 @@ #include "WebApi_errors.h" #include "helper.h" -void WebApiBatteryClass::init(AsyncWebServer* server) +void WebApiBatteryClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/battery/status", HTTP_GET, std::bind(&WebApiBatteryClass::onStatus, this, _1)); _server->on("/api/battery/config", HTTP_GET, std::bind(&WebApiBatteryClass::onAdminGet, this, _1)); @@ -38,11 +38,11 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("enabled")] = config.Battery_Enabled; - root[F("verbose_logging")] = config.Battery_VerboseLogging; - root[F("provider")] = config.Battery_Provider; - root[F("jkbms_interface")] = config.Battery_JkBmsInterface; - root[F("jkbms_polling_interval")] = config.Battery_JkBmsPollingInterval; + root[F("enabled")] = config.Battery.Enabled; + root[F("verbose_logging")] = config.Battery.VerboseLogging; + root[F("provider")] = config.Battery.Provider; + root[F("jkbms_interface")] = config.Battery.JkBmsInterface; + root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval; response->setLength(); request->send(response); @@ -101,11 +101,11 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - config.Battery_Enabled = root[F("enabled")].as(); - config.Battery_VerboseLogging = root[F("verbose_logging")].as(); - config.Battery_Provider = root[F("provider")].as(); - config.Battery_JkBmsInterface = root[F("jkbms_interface")].as(); - config.Battery_JkBmsPollingInterval = root[F("jkbms_polling_interval")].as(); + config.Battery.Enabled = root[F("enabled")].as(); + config.Battery.VerboseLogging = root[F("verbose_logging")].as(); + config.Battery.Provider = root[F("provider")].as(); + config.Battery.JkBmsInterface = root[F("jkbms_interface")].as(); + config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].as(); Configuration.write(); retMsg[F("type")] = F("success"); @@ -115,5 +115,5 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - Battery.init(); + Battery.updateSettings(); } diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 08b86d654..e466c79c9 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_config.h" #include "Configuration.h" @@ -10,7 +10,7 @@ #include #include -void WebApiConfigClass::init(AsyncWebServer* server) +void WebApiConfigClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -19,7 +19,7 @@ void WebApiConfigClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1)); _server->on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1)); @@ -70,7 +70,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -81,7 +81,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -173,7 +173,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi request->send(500); return; } - String name = "/" + request->getParam("file")->value(); + const String name = "/" + request->getParam("file")->value(); request->_tempFile = LittleFS.open(name, "w"); } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index f3d3b3427..29508cc99 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_device.h" #include "Configuration.h" @@ -12,11 +12,11 @@ #include "helper.h" #include -void WebApiDeviceClass::init(AsyncWebServer* server) +void WebApiDeviceClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); _server->on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); @@ -73,15 +73,23 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) displayPinObj["reset"] = pin.display_reset; JsonObject ledPinObj = curPin.createNestedObject("led"); - ledPinObj["led0"] = pin.led[0]; - ledPinObj["led1"] = pin.led[1]; + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + ledPinObj["led" + String(i)] = pin.led[i]; + } JsonObject display = root.createNestedObject("display"); - display["rotation"] = config.Display_Rotation; - display["power_safe"] = config.Display_PowerSafe; - display["screensaver"] = config.Display_ScreenSaver; - display["contrast"] = config.Display_Contrast; - display["language"] = config.Display_Language; + display["rotation"] = config.Display.Rotation; + display["power_safe"] = config.Display.PowerSafe; + display["screensaver"] = config.Display.ScreenSaver; + display["contrast"] = config.Display.Contrast; + display["language"] = config.Display.Language; + display["diagramduration"] = config.Display.DiagramDuration; + + JsonArray leds = root.createNestedArray("led"); + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + JsonObject led = leds.createNestedObject(); + led["brightness"] = config.Led_Single[i].Brightness; + } JsonObject victronPinObj = curPin.createNestedObject("victron"); victronPinObj[F("rx")] = pin.victron_rx; @@ -123,7 +131,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > MQTT_JSON_DOC_SIZE) { retMsg["message"] = "Data too large!"; @@ -134,7 +142,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -166,17 +174,24 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping; strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as().c_str(), sizeof(config.Dev_PinMapping)); - config.Display_Rotation = root["display"]["rotation"].as(); - config.Display_PowerSafe = root["display"]["power_safe"].as(); - config.Display_ScreenSaver = root["display"]["screensaver"].as(); - config.Display_Contrast = root["display"]["contrast"].as(); - config.Display_Language = root["display"]["language"].as(); - - Display.setOrientation(config.Display_Rotation); - Display.enablePowerSafe = config.Display_PowerSafe; - Display.enableScreensaver = config.Display_ScreenSaver; - Display.setContrast(config.Display_Contrast); - Display.setLanguage(config.Display_Language); + config.Display.Rotation = root["display"]["rotation"].as(); + config.Display.PowerSafe = root["display"]["power_safe"].as(); + config.Display.ScreenSaver = root["display"]["screensaver"].as(); + config.Display.Contrast = root["display"]["contrast"].as(); + config.Display.Language = root["display"]["language"].as(); + config.Display.DiagramDuration = root["display"]["diagramduration"].as(); + + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + config.Led_Single[i].Brightness = root["led"][i]["brightness"].as(); + config.Led_Single[i].Brightness = min(100, config.Led_Single[i].Brightness); + } + + Display.setOrientation(config.Display.Rotation); + Display.enablePowerSafe = config.Display.PowerSafe; + Display.enableScreensaver = config.Display.ScreenSaver; + Display.setContrast(config.Display.Contrast); + Display.setLanguage(config.Display.Language); + Display.Diagram().updatePeriod(); Configuration.write(); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 31d1d0ca7..04b8a581d 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_devinfo.h" #include "WebApi.h" @@ -8,11 +8,11 @@ #include #include -void WebApiDevInfoClass::init(AsyncWebServer* server) +void WebApiDevInfoClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/devinfo/status", HTTP_GET, std::bind(&WebApiDevInfoClass::onDevInfoStatus, this, _1)); } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 844b4000c..45bfc2dec 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_dtu.h" #include "Configuration.h" @@ -9,11 +9,11 @@ #include #include -void WebApiDtuClass::init(AsyncWebServer* server) +void WebApiDtuClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); _server->on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); @@ -36,16 +36,16 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) // DTU Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; snprintf(buffer, sizeof(buffer), "%0x%08x", - ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); + ((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)), + ((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF))); root["serial"] = buffer; - root["pollinterval"] = config.Dtu_PollInterval; - root["verbose_logging"] = config.Dtu_VerboseLogging; + root["pollinterval"] = config.Dtu.PollInterval; + root["verbose_logging"] = config.Dtu.VerboseLogging; root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized(); - root["nrf_palevel"] = config.Dtu_NrfPaLevel; + root["nrf_palevel"] = config.Dtu.Nrf.PaLevel; root["cmt_enabled"] = Hoymiles.getRadioCmt()->isInitialized(); - root["cmt_palevel"] = config.Dtu_CmtPaLevel; - root["cmt_frequency"] = config.Dtu_CmtFrequency; + root["cmt_palevel"] = config.Dtu.Cmt.PaLevel; + root["cmt_frequency"] = config.Dtu.Cmt.Frequency; response->setLength(); request->send(response); @@ -69,7 +69,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -80,7 +80,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -151,12 +151,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); // Interpret the string as a hex value and convert it to uint64_t - config.Dtu_Serial = strtoll(root["serial"].as().c_str(), NULL, 16); - config.Dtu_PollInterval = root["pollinterval"].as(); - config.Dtu_VerboseLogging = root["verbose_logging"].as(); - config.Dtu_NrfPaLevel = root["nrf_palevel"].as(); - config.Dtu_CmtPaLevel = root["cmt_palevel"].as(); - config.Dtu_CmtFrequency = root["cmt_frequency"].as(); + config.Dtu.Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + config.Dtu.PollInterval = root["pollinterval"].as(); + config.Dtu.VerboseLogging = root["verbose_logging"].as(); + config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as(); + config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as(); + config.Dtu.Cmt.Frequency = root["cmt_frequency"].as(); Configuration.write(); retMsg["type"] = "success"; @@ -166,11 +166,11 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); - Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); - Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); - Hoymiles.setPollInterval(config.Dtu_PollInterval); - Hoymiles.setVerboseLogging(config.Dtu_VerboseLogging); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel); + Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel); + Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); + Hoymiles.setPollInterval(config.Dtu.PollInterval); + Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging); } diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 2b2672722..e0c8b3164 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -1,17 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_eventlog.h" #include "WebApi.h" #include #include -void WebApiEventlogClass::init(AsyncWebServer* server) +void WebApiEventlogClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/eventlog/status", HTTP_GET, std::bind(&WebApiEventlogClass::onEventlogStatus, this, _1)); } @@ -59,7 +59,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) JsonObject eventsObject = eventsArray.createNestedObject(); AlarmLogEntry_t entry; - inv->EventLog()->getLogEntry(logEntry, &entry, locale); + inv->EventLog()->getLogEntry(logEntry, entry, locale); eventsObject["message_id"] = entry.MessageId; eventsObject["message"] = entry.Message; diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 62cf56155..cbc4d7707 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_firmware.h" #include "Configuration.h" @@ -10,7 +10,7 @@ #include "helper.h" #include -void WebApiFirmwareClass::init(AsyncWebServer* server) +void WebApiFirmwareClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -19,7 +19,7 @@ void WebApiFirmwareClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/firmware/update", HTTP_POST, std::bind(&WebApiFirmwareClass::onFirmwareUpdateFinish, this, _1), diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index c9d2adb8d..ee945bc44 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_gridprofile.h" #include "WebApi.h" #include #include -void WebApiGridProfileClass::init(AsyncWebServer* server) +void WebApiGridProfileClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); + _server->on("/api/gridprofile/rawdata", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileRawdata, this, _1)); } void WebApiGridProfileClass::loop() @@ -26,6 +27,50 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) return; } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); + JsonObject root = response->getRoot(); + + uint64_t serial = 0; + if (request->hasParam("inv")) { + String s = request->getParam("inv")->value(); + serial = strtoll(s.c_str(), NULL, 16); + } + + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv != nullptr) { + root["name"] = inv->GridProfile()->getProfileName(); + root["version"] = inv->GridProfile()->getProfileVersion(); + + auto jsonSections = root.createNestedArray("sections"); + auto profSections = inv->GridProfile()->getProfile(); + + for (auto &profSection : profSections) { + auto jsonSection = jsonSections.createNestedObject(); + jsonSection["name"] = profSection.SectionName; + + auto jsonItems = jsonSection.createNestedArray("items"); + + for (auto &profItem : profSection.items) { + auto jsonItem = jsonItems.createNestedObject(); + + jsonItem["n"] = profItem.Name; + jsonItem["u"] = profItem.Unit; + jsonItem["v"] = profItem.Value; + } + } + } + + response->setLength(); + request->send(response); +} + +void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); JsonObject root = response->getRoot(); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index d5ea9b45a..88ddd37e6 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_inverter.h" #include "Configuration.h" @@ -12,11 +12,11 @@ #include #include -void WebApiInverterClass::init(AsyncWebServer* server) +void WebApiInverterClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); _server->on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); @@ -61,6 +61,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; obj["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + obj["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -104,7 +105,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -115,7 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -204,7 +205,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -215,7 +216,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -288,6 +289,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; + inverter.YieldDayCorrection = root["yieldday_correction"] | false; arrayCount++; } @@ -321,6 +323,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inv->setReachableThreshold(inverter.ReachableThreshold); inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(inverter.ZeroYieldDayOnMidnight); + inv->Statistics()->setYieldDayCorrection(inverter.YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); @@ -348,7 +351,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -359,7 +362,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -422,7 +425,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -433,7 +436,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 9470e4cac..1e5b3f212 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -1,18 +1,20 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_limit.h" #include "WebApi.h" #include "WebApi_errors.h" +#include "defaults.h" +#include "helper.h" #include #include -void WebApiLimitClass::init(AsyncWebServer* server) +void WebApiLimitClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1)); _server->on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1)); @@ -73,7 +75,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -84,7 +86,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -112,10 +114,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["limit_value"].as() == 0 || root["limit_value"].as() > 2250) { - retMsg["message"] = "Limit must between 1 and 2250!"; + if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { + retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; - retMsg["param"]["max"] = 2250; + retMsg["param"]["max"] = MAX_INVERTER_LIMIT; response->setLength(); request->send(response); return; diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index ed2d68673..8b65c9352 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_maintenance.h" @@ -9,11 +9,11 @@ #include "WebApi_errors.h" #include -void WebApiMaintenanceClass::init(AsyncWebServer* server) +void WebApiMaintenanceClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); } @@ -40,7 +40,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > MQTT_JSON_DOC_SIZE) { retMsg["message"] = "Data too large!"; @@ -51,7 +51,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 99de0b1b6..c92d787d5 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_mqtt.h" #include "Configuration.h" @@ -15,11 +15,11 @@ #include "PowerMeter.h" #include -void WebApiMqttClass::init(AsyncWebServer* server) +void WebApiMqttClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); _server->on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); @@ -40,26 +40,26 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["mqtt_enabled"] = config.Mqtt_Enabled; - root["mqtt_verbose_logging"] = config.Mqtt_VerboseLogging; - root["mqtt_hostname"] = config.Mqtt_Hostname; - root["mqtt_port"] = config.Mqtt_Port; - root["mqtt_username"] = config.Mqtt_Username; - root["mqtt_topic"] = config.Mqtt_Topic; + root["mqtt_enabled"] = config.Mqtt.Enabled; + root["mqtt_verbose_logging"] = config.Mqtt.VerboseLogging; + root["mqtt_hostname"] = config.Mqtt.Hostname; + root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_username"] = config.Mqtt.Username; + root["mqtt_topic"] = config.Mqtt.Topic; root["mqtt_connected"] = MqttSettings.getConnected(); - root["mqtt_retain"] = config.Mqtt_Retain; - root["mqtt_tls"] = config.Mqtt_Tls; - root["mqtt_root_ca_cert_info"] = getTlsCertInfo(config.Mqtt_RootCaCert); - root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin; - root["mqtt_client_cert_info"] = getTlsCertInfo(config.Mqtt_ClientCert); - root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; - root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; - root["mqtt_clean_session"] = config.Mqtt_CleanSession; - root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; - root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; - root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; - root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic; - root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels; + root["mqtt_retain"] = config.Mqtt.Retain; + root["mqtt_tls"] = config.Mqtt.Tls.Enabled; + root["mqtt_root_ca_cert_info"] = getTlsCertInfo(config.Mqtt.Tls.RootCaCert); + root["mqtt_tls_cert_login"] = config.Mqtt.Tls.CertLogin; + root["mqtt_client_cert_info"] = getTlsCertInfo(config.Mqtt.Tls.ClientCert); + root["mqtt_lwt_topic"] = String(config.Mqtt.Topic) + config.Mqtt.Lwt.Topic; + root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; + root["mqtt_clean_session"] = config.Mqtt.CleanSession; + root["mqtt_hass_enabled"] = config.Mqtt.Hass.Enabled; + root["mqtt_hass_expire"] = config.Mqtt.Hass.Expire; + root["mqtt_hass_retain"] = config.Mqtt.Hass.Retain; + root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; + root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; response->setLength(); request->send(response); @@ -75,29 +75,30 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["mqtt_enabled"] = config.Mqtt_Enabled; - root["mqtt_verbose_logging"] = config.Mqtt_VerboseLogging; - root["mqtt_hostname"] = config.Mqtt_Hostname; - root["mqtt_port"] = config.Mqtt_Port; - root["mqtt_username"] = config.Mqtt_Username; - root["mqtt_password"] = config.Mqtt_Password; - root["mqtt_topic"] = config.Mqtt_Topic; - root["mqtt_retain"] = config.Mqtt_Retain; - root["mqtt_tls"] = config.Mqtt_Tls; - root["mqtt_root_ca_cert"] = config.Mqtt_RootCaCert; - root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin; - root["mqtt_client_cert"] = config.Mqtt_ClientCert; - root["mqtt_client_key"] = config.Mqtt_ClientKey; - root["mqtt_lwt_topic"] = config.Mqtt_LwtTopic; - root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online; - root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline; - root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; - root["mqtt_clean_session"] = config.Mqtt_CleanSession; - root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; - root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; - root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; - root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic; - root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels; + root["mqtt_enabled"] = config.Mqtt.Enabled; + root["mqtt_verbose_logging"] = config.Mqtt.VerboseLogging; + root["mqtt_hostname"] = config.Mqtt.Hostname; + root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_username"] = config.Mqtt.Username; + root["mqtt_password"] = config.Mqtt.Password; + root["mqtt_topic"] = config.Mqtt.Topic; + root["mqtt_retain"] = config.Mqtt.Retain; + root["mqtt_tls"] = config.Mqtt.Tls.Enabled; + root["mqtt_root_ca_cert"] = config.Mqtt.Tls.RootCaCert; + root["mqtt_tls_cert_login"] = config.Mqtt.Tls.CertLogin; + root["mqtt_client_cert"] = config.Mqtt.Tls.ClientCert; + root["mqtt_client_key"] = config.Mqtt.Tls.ClientKey; + root["mqtt_lwt_topic"] = config.Mqtt.Lwt.Topic; + root["mqtt_lwt_online"] = config.Mqtt.CleanSession; + root["mqtt_lwt_offline"] = config.Mqtt.Lwt.Value_Offline; + root["mqtt_lwt_qos"] = config.Mqtt.Lwt.Qos; + root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; + root["mqtt_clean_session"] = config.Mqtt.CleanSession; + root["mqtt_hass_enabled"] = config.Mqtt.Hass.Enabled; + root["mqtt_hass_expire"] = config.Mqtt.Hass.Expire; + root["mqtt_hass_retain"] = config.Mqtt.Hass.Retain; + root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; + root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; response->setLength(); request->send(response); @@ -121,7 +122,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > MQTT_JSON_DOC_SIZE) { retMsg["message"] = "Data too large!"; @@ -132,7 +133,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -157,6 +158,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && 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") @@ -276,6 +278,15 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } + if (root["mqtt_lwt_qos"].as() > 2) { + retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!"; + retMsg["code"] = WebApiError::MqttLwtQos; + retMsg["param"]["max"] = 2; + response->setLength(); + request->send(response); + return; + } + if (root["mqtt_publish_interval"].as() < 5 || root["mqtt_publish_interval"].as() > 65535) { retMsg["message"] = "Publish interval must be a number between 5 and 65535!"; retMsg["code"] = WebApiError::MqttPublishInterval; @@ -307,29 +318,30 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - config.Mqtt_Enabled = root["mqtt_enabled"].as(); - config.Mqtt_VerboseLogging = root["mqtt_verbose_logging"].as(); - config.Mqtt_Retain = root["mqtt_retain"].as(); - config.Mqtt_Tls = root["mqtt_tls"].as(); - strlcpy(config.Mqtt_RootCaCert, root["mqtt_root_ca_cert"].as().c_str(), sizeof(config.Mqtt_RootCaCert)); - config.Mqtt_TlsCertLogin = root["mqtt_tls_cert_login"].as(); - strlcpy(config.Mqtt_ClientCert, root["mqtt_client_cert"].as().c_str(), sizeof(config.Mqtt_ClientCert)); - strlcpy(config.Mqtt_ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt_ClientKey)); - config.Mqtt_Port = root["mqtt_port"].as(); - strlcpy(config.Mqtt_Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt_Hostname)); - strlcpy(config.Mqtt_Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt_Topic)); - strlcpy(config.Mqtt_LwtTopic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt_LwtValue_Offline)); - config.Mqtt_PublishInterval = root["mqtt_publish_interval"].as(); - config.Mqtt_CleanSession = root["mqtt_clean_session"].as(); - config.Mqtt_Hass_Enabled = root["mqtt_hass_enabled"].as(); - config.Mqtt_Hass_Expire = root["mqtt_hass_expire"].as(); - config.Mqtt_Hass_Retain = root["mqtt_hass_retain"].as(); - config.Mqtt_Hass_IndividualPanels = root["mqtt_hass_individualpanels"].as(); - strlcpy(config.Mqtt_Hass_Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt.Enabled = root["mqtt_enabled"].as(); + config.Mqtt.VerboseLogging = root["mqtt_verbose_logging"].as(); + config.Mqtt.Retain = root["mqtt_retain"].as(); + config.Mqtt.Tls.Enabled = root["mqtt_tls"].as(); + strlcpy(config.Mqtt.Tls.RootCaCert, root["mqtt_root_ca_cert"].as().c_str(), sizeof(config.Mqtt.Tls.RootCaCert)); + config.Mqtt.Tls.CertLogin = root["mqtt_tls_cert_login"].as(); + strlcpy(config.Mqtt.Tls.ClientCert, root["mqtt_client_cert"].as().c_str(), sizeof(config.Mqtt.Tls.ClientCert)); + strlcpy(config.Mqtt.Tls.ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt.Tls.ClientKey)); + config.Mqtt.Port = root["mqtt_port"].as(); + strlcpy(config.Mqtt.Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt.Hostname)); + strlcpy(config.Mqtt.Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt.Username)); + strlcpy(config.Mqtt.Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt.Password)); + strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); + strlcpy(config.Mqtt.Lwt.Topic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt.Lwt.Topic)); + strlcpy(config.Mqtt.Lwt.Value_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Online)); + strlcpy(config.Mqtt.Lwt.Value_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Offline)); + config.Mqtt.Lwt.Qos = root["mqtt_lwt_qos"].as(); + config.Mqtt.PublishInterval = root["mqtt_publish_interval"].as(); + config.Mqtt.CleanSession = root["mqtt_clean_session"].as(); + config.Mqtt.Hass.Enabled = root["mqtt_hass_enabled"].as(); + config.Mqtt.Hass.Expire = root["mqtt_hass_expire"].as(); + config.Mqtt.Hass.Retain = root["mqtt_hass_retain"].as(); + config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as(); + strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt.Hass.Topic)); Configuration.write(); retMsg["type"] = "success"; @@ -342,9 +354,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) MqttSettings.performReconnect(); MqttHandleHass.forceUpdate(); MqttHandleVedirectHass.forceUpdate(); - MqttHandleVedirect.init(); - PowerMeter.init(); - PowerLimiter.init(); + MqttHandleVedirect.forceUpdate(); } String WebApiMqttClass::getTlsCertInfo(const char* cert) diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 849c5f8a8..a6e91f3d3 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_network.h" #include "Configuration.h" @@ -10,11 +10,11 @@ #include "helper.h" #include -void WebApiNetworkClass::init(AsyncWebServer* server) +void WebApiNetworkClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); _server->on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); @@ -66,17 +66,17 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["hostname"] = config.WiFi_Hostname; - root["dhcp"] = config.WiFi_Dhcp; - root["ipaddress"] = IPAddress(config.WiFi_Ip).toString(); - root["netmask"] = IPAddress(config.WiFi_Netmask).toString(); - root["gateway"] = IPAddress(config.WiFi_Gateway).toString(); - root["dns1"] = IPAddress(config.WiFi_Dns1).toString(); - root["dns2"] = IPAddress(config.WiFi_Dns2).toString(); - root["ssid"] = config.WiFi_Ssid; - root["password"] = config.WiFi_Password; - root["aptimeout"] = config.WiFi_ApTimeout; - root["mdnsenabled"] = config.Mdns_Enabled; + root["hostname"] = config.WiFi.Hostname; + root["dhcp"] = config.WiFi.Dhcp; + root["ipaddress"] = IPAddress(config.WiFi.Ip).toString(); + root["netmask"] = IPAddress(config.WiFi.Netmask).toString(); + root["gateway"] = IPAddress(config.WiFi.Gateway).toString(); + root["dns1"] = IPAddress(config.WiFi.Dns1).toString(); + root["dns2"] = IPAddress(config.WiFi.Dns2).toString(); + root["ssid"] = config.WiFi.Ssid; + root["password"] = config.WiFi.Password; + root["aptimeout"] = config.WiFi.ApTimeout; + root["mdnsenabled"] = config.Mdns.Enabled; response->setLength(); request->send(response); @@ -100,7 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -111,7 +111,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -208,36 +208,36 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - config.WiFi_Ip[0] = ipaddress[0]; - config.WiFi_Ip[1] = ipaddress[1]; - config.WiFi_Ip[2] = ipaddress[2]; - config.WiFi_Ip[3] = ipaddress[3]; - config.WiFi_Netmask[0] = netmask[0]; - config.WiFi_Netmask[1] = netmask[1]; - config.WiFi_Netmask[2] = netmask[2]; - config.WiFi_Netmask[3] = netmask[3]; - config.WiFi_Gateway[0] = gateway[0]; - config.WiFi_Gateway[1] = gateway[1]; - config.WiFi_Gateway[2] = gateway[2]; - config.WiFi_Gateway[3] = gateway[3]; - config.WiFi_Dns1[0] = dns1[0]; - config.WiFi_Dns1[1] = dns1[1]; - config.WiFi_Dns1[2] = dns1[2]; - config.WiFi_Dns1[3] = dns1[3]; - config.WiFi_Dns2[0] = dns2[0]; - config.WiFi_Dns2[1] = dns2[1]; - config.WiFi_Dns2[2] = dns2[2]; - config.WiFi_Dns2[3] = dns2[3]; - strlcpy(config.WiFi_Ssid, root["ssid"].as().c_str(), sizeof(config.WiFi_Ssid)); - strlcpy(config.WiFi_Password, root["password"].as().c_str(), sizeof(config.WiFi_Password)); - strlcpy(config.WiFi_Hostname, root["hostname"].as().c_str(), sizeof(config.WiFi_Hostname)); + config.WiFi.Ip[0] = ipaddress[0]; + config.WiFi.Ip[1] = ipaddress[1]; + config.WiFi.Ip[2] = ipaddress[2]; + config.WiFi.Ip[3] = ipaddress[3]; + config.WiFi.Netmask[0] = netmask[0]; + config.WiFi.Netmask[1] = netmask[1]; + config.WiFi.Netmask[2] = netmask[2]; + config.WiFi.Netmask[3] = netmask[3]; + config.WiFi.Gateway[0] = gateway[0]; + config.WiFi.Gateway[1] = gateway[1]; + config.WiFi.Gateway[2] = gateway[2]; + config.WiFi.Gateway[3] = gateway[3]; + config.WiFi.Dns1[0] = dns1[0]; + config.WiFi.Dns1[1] = dns1[1]; + config.WiFi.Dns1[2] = dns1[2]; + config.WiFi.Dns1[3] = dns1[3]; + config.WiFi.Dns2[0] = dns2[0]; + config.WiFi.Dns2[1] = dns2[1]; + config.WiFi.Dns2[2] = dns2[2]; + config.WiFi.Dns2[3] = dns2[3]; + strlcpy(config.WiFi.Ssid, root["ssid"].as().c_str(), sizeof(config.WiFi.Ssid)); + strlcpy(config.WiFi.Password, root["password"].as().c_str(), sizeof(config.WiFi.Password)); + strlcpy(config.WiFi.Hostname, root["hostname"].as().c_str(), sizeof(config.WiFi.Hostname)); if (root["dhcp"].as()) { - config.WiFi_Dhcp = true; + config.WiFi.Dhcp = true; } else { - config.WiFi_Dhcp = false; + config.WiFi.Dhcp = false; } - config.WiFi_ApTimeout = root["aptimeout"].as(); - config.Mdns_Enabled = root["mdnsenabled"].as(); + config.WiFi.ApTimeout = root["aptimeout"].as(); + config.Mdns.Enabled = root["mdnsenabled"].as(); Configuration.write(); retMsg["type"] = "success"; diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index c0cfaa441..ed841ba93 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_ntp.h" #include "Configuration.h" @@ -11,11 +11,11 @@ #include "helper.h" #include -void WebApiNtpClass::init(AsyncWebServer* server) +void WebApiNtpClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); _server->on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); @@ -38,9 +38,9 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["ntp_server"] = config.Ntp_Server; - root["ntp_timezone"] = config.Ntp_Timezone; - root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr; + root["ntp_server"] = config.Ntp.Server; + root["ntp_timezone"] = config.Ntp.Timezone; + root["ntp_timezone_descr"] = config.Ntp.TimezoneDescr; struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { @@ -83,12 +83,12 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["ntp_server"] = config.Ntp_Server; - root["ntp_timezone"] = config.Ntp_Timezone; - root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr; - root["longitude"] = config.Ntp_Longitude; - root["latitude"] = config.Ntp_Latitude; - root["sunsettype"] = config.Ntp_SunsetType; + root["ntp_server"] = config.Ntp.Server; + root["ntp_timezone"] = config.Ntp.Timezone; + root["ntp_timezone_descr"] = config.Ntp.TimezoneDescr; + root["longitude"] = config.Ntp.Longitude; + root["latitude"] = config.Ntp.Latitude; + root["sunsettype"] = config.Ntp.SunsetType; response->setLength(); request->send(response); @@ -112,7 +112,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -123,7 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -173,12 +173,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - strlcpy(config.Ntp_Server, root["ntp_server"].as().c_str(), sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, root["ntp_timezone"].as().c_str(), sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, root["ntp_timezone_descr"].as().c_str(), sizeof(config.Ntp_TimezoneDescr)); - config.Ntp_Latitude = root["latitude"].as(); - config.Ntp_Longitude = root["longitude"].as(); - config.Ntp_SunsetType = root["sunsettype"].as(); + strlcpy(config.Ntp.Server, root["ntp_server"].as().c_str(), sizeof(config.Ntp.Server)); + strlcpy(config.Ntp.Timezone, root["ntp_timezone"].as().c_str(), sizeof(config.Ntp.Timezone)); + strlcpy(config.Ntp.TimezoneDescr, root["ntp_timezone_descr"].as().c_str(), sizeof(config.Ntp.TimezoneDescr)); + config.Ntp.Latitude = root["latitude"].as(); + config.Ntp.Longitude = root["longitude"].as(); + config.Ntp.SunsetType = root["sunsettype"].as(); Configuration.write(); retMsg["type"] = "success"; @@ -239,7 +239,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -250,7 +250,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index ca7923642..3fa47984c 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_power.h" #include "WebApi.h" @@ -8,11 +8,11 @@ #include #include -void WebApiPowerClass::init(AsyncWebServer* server) +void WebApiPowerClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); _server->on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); @@ -68,7 +68,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -79,7 +79,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 7aa6f4cc6..361ff5634 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -16,11 +16,11 @@ #include "helper.h" #include "WebApi_errors.h" -void WebApiPowerLimiterClass::init(AsyncWebServer* server) +void WebApiPowerLimiterClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1)); _server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1)); @@ -37,27 +37,27 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("enabled")] = config.PowerLimiter_Enabled; - root[F("verbose_logging")] = config.PowerLimiter_VerboseLogging; - root[F("solar_passthrough_enabled")] = config.PowerLimiter_SolarPassThroughEnabled; - root[F("solar_passthrough_losses")] = config.PowerLimiter_SolarPassThroughLosses; - root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; - root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; - root[F("inverter_id")] = config.PowerLimiter_InverterId; - root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId; - root[F("target_power_consumption")] = config.PowerLimiter_TargetPowerConsumption; - root[F("target_power_consumption_hysteresis")] = config.PowerLimiter_TargetPowerConsumptionHysteresis; - root[F("lower_power_limit")] = config.PowerLimiter_LowerPowerLimit; - root[F("upper_power_limit")] = config.PowerLimiter_UpperPowerLimit; - root[F("battery_soc_start_threshold")] = config.PowerLimiter_BatterySocStartThreshold; - root[F("battery_soc_stop_threshold")] = config.PowerLimiter_BatterySocStopThreshold; - root[F("voltage_start_threshold")] = static_cast(config.PowerLimiter_VoltageStartThreshold * 100 +0.5) / 100.0; - root[F("voltage_stop_threshold")] = static_cast(config.PowerLimiter_VoltageStopThreshold * 100 +0.5) / 100.0;; - root[F("voltage_load_correction_factor")] = config.PowerLimiter_VoltageLoadCorrectionFactor; - root[F("inverter_restart_hour")] = config.PowerLimiter_RestartHour; - root[F("full_solar_passthrough_soc")] = config.PowerLimiter_FullSolarPassThroughSoc; - root[F("full_solar_passthrough_start_voltage")] = static_cast(config.PowerLimiter_FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0; - root[F("full_solar_passthrough_stop_voltage")] = static_cast(config.PowerLimiter_FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0; + root[F("enabled")] = config.PowerLimiter.Enabled; + root[F("verbose_logging")] = config.PowerLimiter.VerboseLogging; + root[F("solar_passthrough_enabled")] = config.PowerLimiter.SolarPassThroughEnabled; + root[F("solar_passthrough_losses")] = config.PowerLimiter.SolarPassThroughLosses; + root[F("battery_drain_strategy")] = config.PowerLimiter.BatteryDrainStategy; + root[F("is_inverter_behind_powermeter")] = config.PowerLimiter.IsInverterBehindPowerMeter; + root[F("inverter_id")] = config.PowerLimiter.InverterId; + root[F("inverter_channel_id")] = config.PowerLimiter.InverterChannelId; + root[F("target_power_consumption")] = config.PowerLimiter.TargetPowerConsumption; + root[F("target_power_consumption_hysteresis")] = config.PowerLimiter.TargetPowerConsumptionHysteresis; + root[F("lower_power_limit")] = config.PowerLimiter.LowerPowerLimit; + root[F("upper_power_limit")] = config.PowerLimiter.UpperPowerLimit; + root[F("battery_soc_start_threshold")] = config.PowerLimiter.BatterySocStartThreshold; + root[F("battery_soc_stop_threshold")] = config.PowerLimiter.BatterySocStopThreshold; + root[F("voltage_start_threshold")] = static_cast(config.PowerLimiter.VoltageStartThreshold * 100 +0.5) / 100.0; + root[F("voltage_stop_threshold")] = static_cast(config.PowerLimiter.VoltageStopThreshold * 100 +0.5) / 100.0;; + root[F("voltage_load_correction_factor")] = config.PowerLimiter.VoltageLoadCorrectionFactor; + root[F("inverter_restart_hour")] = config.PowerLimiter.RestartHour; + root[F("full_solar_passthrough_soc")] = config.PowerLimiter.FullSolarPassThroughSoc; + root[F("full_solar_passthrough_start_voltage")] = static_cast(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0; + root[F("full_solar_passthrough_stop_voltage")] = static_cast(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0; response->setLength(); request->send(response); @@ -124,30 +124,30 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); - config.PowerLimiter_Enabled = root[F("enabled")].as(); + config.PowerLimiter.Enabled = root[F("enabled")].as(); PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation - config.PowerLimiter_VerboseLogging = root[F("verbose_logging")].as(); - config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as(); - config.PowerLimiter_SolarPassThroughLosses = root[F("solar_passthrough_losses")].as(); - config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); - config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); - config.PowerLimiter_InverterId = root[F("inverter_id")].as(); - config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as(); - config.PowerLimiter_TargetPowerConsumption = root[F("target_power_consumption")].as(); - config.PowerLimiter_TargetPowerConsumptionHysteresis = root[F("target_power_consumption_hysteresis")].as(); - config.PowerLimiter_LowerPowerLimit = root[F("lower_power_limit")].as(); - config.PowerLimiter_UpperPowerLimit = root[F("upper_power_limit")].as(); - config.PowerLimiter_BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as(); - config.PowerLimiter_BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as(); - config.PowerLimiter_VoltageStartThreshold = root[F("voltage_start_threshold")].as(); - config.PowerLimiter_VoltageStartThreshold = static_cast(config.PowerLimiter_VoltageStartThreshold * 100) / 100.0; - config.PowerLimiter_VoltageStopThreshold = root[F("voltage_stop_threshold")].as(); - config.PowerLimiter_VoltageStopThreshold = static_cast(config.PowerLimiter_VoltageStopThreshold * 100) / 100.0; - config.PowerLimiter_VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as(); - config.PowerLimiter_RestartHour = root[F("inverter_restart_hour")].as(); - config.PowerLimiter_FullSolarPassThroughSoc = root[F("full_solar_passthrough_soc")].as(); - config.PowerLimiter_FullSolarPassThroughStartVoltage = static_cast(root[F("full_solar_passthrough_start_voltage")].as() * 100) / 100.0; - config.PowerLimiter_FullSolarPassThroughStopVoltage = static_cast(root[F("full_solar_passthrough_stop_voltage")].as() * 100) / 100.0; + config.PowerLimiter.VerboseLogging = root[F("verbose_logging")].as(); + config.PowerLimiter.SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as(); + config.PowerLimiter.SolarPassThroughLosses = root[F("solar_passthrough_losses")].as(); + config.PowerLimiter.BatteryDrainStategy= root[F("battery_drain_strategy")].as(); + config.PowerLimiter.IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); + config.PowerLimiter.InverterId = root[F("inverter_id")].as(); + config.PowerLimiter.InverterChannelId = root[F("inverter_channel_id")].as(); + config.PowerLimiter.TargetPowerConsumption = root[F("target_power_consumption")].as(); + config.PowerLimiter.TargetPowerConsumptionHysteresis = root[F("target_power_consumption_hysteresis")].as(); + config.PowerLimiter.LowerPowerLimit = root[F("lower_power_limit")].as(); + config.PowerLimiter.UpperPowerLimit = root[F("upper_power_limit")].as(); + config.PowerLimiter.BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as(); + config.PowerLimiter.BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as(); + config.PowerLimiter.VoltageStartThreshold = root[F("voltage_start_threshold")].as(); + config.PowerLimiter.VoltageStartThreshold = static_cast(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0; + config.PowerLimiter.VoltageStopThreshold = root[F("voltage_stop_threshold")].as(); + config.PowerLimiter.VoltageStopThreshold = static_cast(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0; + config.PowerLimiter.VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as(); + config.PowerLimiter.RestartHour = root[F("inverter_restart_hour")].as(); + config.PowerLimiter.FullSolarPassThroughSoc = root[F("full_solar_passthrough_soc")].as(); + config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast(root[F("full_solar_passthrough_start_voltage")].as() * 100) / 100.0; + config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast(root[F("full_solar_passthrough_stop_voltage")].as() * 100) / 100.0; diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 53c945d48..383af5b41 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -16,11 +16,11 @@ #include "WebApi.h" #include "helper.h" -void WebApiPowerMeterClass::init(AsyncWebServer* server) +void WebApiPowerMeterClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1)); _server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1)); @@ -38,16 +38,16 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("enabled")] = config.PowerMeter_Enabled; - root[F("verbose_logging")] = config.PowerMeter_VerboseLogging; - root[F("source")] = config.PowerMeter_Source; - root[F("interval")] = config.PowerMeter_Interval; - root[F("mqtt_topic_powermeter_1")] = config.PowerMeter_MqttTopicPowerMeter1; - root[F("mqtt_topic_powermeter_2")] = config.PowerMeter_MqttTopicPowerMeter2; - root[F("mqtt_topic_powermeter_3")] = config.PowerMeter_MqttTopicPowerMeter3; - root[F("sdmbaudrate")] = config.PowerMeter_SdmBaudrate; - root[F("sdmaddress")] = config.PowerMeter_SdmAddress; - root[F("http_individual_requests")] = config.PowerMeter_HttpIndividualRequests; + root[F("enabled")] = config.PowerMeter.Enabled; + root[F("verbose_logging")] = config.PowerMeter.VerboseLogging; + root[F("source")] = config.PowerMeter.Source; + root[F("interval")] = config.PowerMeter.Interval; + root[F("mqtt_topic_powermeter_1")] = config.PowerMeter.MqttTopicPowerMeter1; + root[F("mqtt_topic_powermeter_2")] = config.PowerMeter.MqttTopicPowerMeter2; + root[F("mqtt_topic_powermeter_3")] = config.PowerMeter.MqttTopicPowerMeter3; + root[F("sdmbaudrate")] = config.PowerMeter.SdmBaudrate; + root[F("sdmaddress")] = config.PowerMeter.SdmAddress; + root[F("http_individual_requests")] = config.PowerMeter.HttpIndividualRequests; JsonArray httpPhases = root.createNestedArray(F("http_phases")); @@ -55,15 +55,15 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) JsonObject phaseObject = httpPhases.createNestedObject(); phaseObject[F("index")] = i + 1; - phaseObject[F("enabled")] = config.Powermeter_Http_Phase[i].Enabled; - phaseObject[F("url")] = String(config.Powermeter_Http_Phase[i].Url); - phaseObject[F("auth_type")]= config.Powermeter_Http_Phase[i].AuthType; - phaseObject[F("username")] = String(config.Powermeter_Http_Phase[i].Username); - phaseObject[F("password")] = String(config.Powermeter_Http_Phase[i].Password); - phaseObject[F("header_key")] = String(config.Powermeter_Http_Phase[i].HeaderKey); - phaseObject[F("header_value")] = String(config.Powermeter_Http_Phase[i].HeaderValue); - phaseObject[F("json_path")] = String(config.Powermeter_Http_Phase[i].JsonPath); - phaseObject[F("timeout")] = config.Powermeter_Http_Phase[i].Timeout; + phaseObject[F("enabled")] = config.PowerMeter.Http_Phase[i].Enabled; + phaseObject[F("url")] = String(config.PowerMeter.Http_Phase[i].Url); + phaseObject[F("auth_type")]= config.PowerMeter.Http_Phase[i].AuthType; + phaseObject[F("username")] = String(config.PowerMeter.Http_Phase[i].Username); + phaseObject[F("password")] = String(config.PowerMeter.Http_Phase[i].Password); + phaseObject[F("header_key")] = String(config.PowerMeter.Http_Phase[i].HeaderKey); + phaseObject[F("header_value")] = String(config.PowerMeter.Http_Phase[i].HeaderValue); + phaseObject[F("json_path")] = String(config.PowerMeter.Http_Phase[i].JsonPath); + phaseObject[F("timeout")] = config.PowerMeter.Http_Phase[i].Timeout; } response->setLength(); @@ -169,30 +169,30 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - config.PowerMeter_Enabled = root[F("enabled")].as(); - config.PowerMeter_VerboseLogging = root[F("verbose_logging")].as(); - config.PowerMeter_Source = root[F("source")].as(); - config.PowerMeter_Interval = root[F("interval")].as(); - strlcpy(config.PowerMeter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter3)); - config.PowerMeter_SdmBaudrate = root[F("sdmbaudrate")].as(); - config.PowerMeter_SdmAddress = root[F("sdmaddress")].as(); - config.PowerMeter_HttpIndividualRequests = root[F("http_individual_requests")].as(); + config.PowerMeter.Enabled = root[F("enabled")].as(); + config.PowerMeter.VerboseLogging = root[F("verbose_logging")].as(); + config.PowerMeter.Source = root[F("source")].as(); + config.PowerMeter.Interval = root[F("interval")].as(); + strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1)); + strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2)); + strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3)); + config.PowerMeter.SdmBaudrate = root[F("sdmbaudrate")].as(); + config.PowerMeter.SdmAddress = root[F("sdmaddress")].as(); + config.PowerMeter.HttpIndividualRequests = root[F("http_individual_requests")].as(); JsonArray http_phases = root[F("http_phases")]; for (uint8_t i = 0; i < http_phases.size(); i++) { JsonObject phase = http_phases[i].as(); - config.Powermeter_Http_Phase[i].Enabled = (i == 0 ? true : phase[F("enabled")].as()); - strlcpy(config.Powermeter_Http_Phase[i].Url, phase[F("url")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].Url)); - config.Powermeter_Http_Phase[i].AuthType = phase[F("auth_type")].as(); - strlcpy(config.Powermeter_Http_Phase[i].Username, phase[F("username")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].Username)); - strlcpy(config.Powermeter_Http_Phase[i].Password, phase[F("password")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].Password)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, phase[F("header_key")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].HeaderKey)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, phase[F("header_value")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].HeaderValue)); - config.Powermeter_Http_Phase[i].Timeout = phase[F("timeout")].as(); - strlcpy(config.Powermeter_Http_Phase[i].JsonPath, phase[F("json_path")].as().c_str(), sizeof(config.Powermeter_Http_Phase[i].JsonPath)); + config.PowerMeter.Http_Phase[i].Enabled = (i == 0 ? true : phase[F("enabled")].as()); + strlcpy(config.PowerMeter.Http_Phase[i].Url, phase[F("url")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Url)); + config.PowerMeter.Http_Phase[i].AuthType = phase[F("auth_type")].as(); + strlcpy(config.PowerMeter.Http_Phase[i].Username, phase[F("username")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Username)); + strlcpy(config.PowerMeter.Http_Phase[i].Password, phase[F("password")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Password)); + strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, phase[F("header_key")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); + strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, phase[F("header_value")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); + config.PowerMeter.Http_Phase[i].Timeout = phase[F("timeout")].as(); + strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, phase[F("json_path")].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); } Configuration.write(); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index ab5a4148b..0f46c97f3 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_prometheus.h" #include "Configuration.h" @@ -11,11 +11,11 @@ #include #include "MessageOutput.h" -void WebApiPrometheusClass::init(AsyncWebServer* server) +void WebApiPrometheusClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); } @@ -100,10 +100,10 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques } } -void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName) +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const char* metricName, const char* channelName) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { - const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; + const char* chanName = (channelName == nullptr) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; if (idx == 0 && type == TYPE_AC && channel == 0) { stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); @@ -119,7 +119,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial } } -void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel) +void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel) { if (type != TYPE_DC) { return; diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index a2221f9b6..274d0eb24 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_security.h" #include "Configuration.h" @@ -9,11 +9,11 @@ #include "helper.h" #include -void WebApiSecurityClass::init(AsyncWebServer* server) +void WebApiSecurityClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); _server->on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); @@ -34,8 +34,8 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["password"] = config.Security_Password; - root["allow_readonly"] = config.Security_AllowReadonly; + root["password"] = config.Security.Password; + root["allow_readonly"] = config.Security.AllowReadonly; response->setLength(); request->send(response); @@ -59,7 +59,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) return; } - String json = request->getParam("data", true)->value(); + const String json = request->getParam("data", true)->value(); if (json.length() > 1024) { retMsg["message"] = "Data too large!"; @@ -70,7 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); + const DeserializationError error = deserializeJson(root, json); if (error) { retMsg["message"] = "Failed to parse data!"; @@ -99,8 +99,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - strlcpy(config.Security_Password, root["password"].as().c_str(), sizeof(config.Security_Password)); - config.Security_AllowReadonly = root["allow_readonly"].as(); + strlcpy(config.Security.Password, root["password"].as().c_str(), sizeof(config.Security.Password)); + config.Security.AllowReadonly = root["allow_readonly"].as(); Configuration.write(); retMsg["type"] = "success"; diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index f98b6147d..85324f08f 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_sysstatus.h" #include "Configuration.h" @@ -20,11 +20,11 @@ #define AUTO_GIT_BRANCH "" #endif -void WebApiSysstatusClass::init(AsyncWebServer* server) +void WebApiSysstatusClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); } @@ -59,13 +59,13 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["chipcores"] = ESP.getChipCores(); String reason; - reason = ResetReason.get_reset_reason_verbose(0); + reason = ResetReason::get_reset_reason_verbose(0); root["resetreason_0"] = reason; - reason = ResetReason.get_reset_reason_verbose(1); + reason = ResetReason::get_reset_reason_verbose(1); root["resetreason_1"] = reason; - root["cfgsavecount"] = Configuration.get().Cfg_SaveCount; + root["cfgsavecount"] = Configuration.get().Cfg.SaveCount; char version[16]; snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 1b8842655..0e8e22aad 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -11,11 +11,11 @@ #include "WebApi_errors.h" #include "helper.h" -void WebApiVedirectClass::init(AsyncWebServer* server) +void WebApiVedirectClass::init(AsyncWebServer& server) { using std::placeholders::_1; - _server = server; + _server = &server; _server->on("/api/vedirect/status", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectStatus, this, _1)); _server->on("/api/vedirect/config", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectAdminGet, this, _1)); @@ -36,9 +36,9 @@ void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("vedirect_enabled")] = config.Vedirect_Enabled; - root[F("verbose_logging")] = config.Vedirect_VerboseLogging; - root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly; + root[F("vedirect_enabled")] = config.Vedirect.Enabled; + root[F("verbose_logging")] = config.Vedirect.VerboseLogging; + root[F("vedirect_updatesonly")] = config.Vedirect.UpdatesOnly; response->setLength(); request->send(response); @@ -54,9 +54,9 @@ void WebApiVedirectClass::onVedirectAdminGet(AsyncWebServerRequest* request) JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root[F("vedirect_enabled")] = config.Vedirect_Enabled; - root[F("verbose_logging")] = config.Vedirect_VerboseLogging; - root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly; + root[F("vedirect_enabled")] = config.Vedirect.Enabled; + root[F("verbose_logging")] = config.Vedirect.VerboseLogging; + root[F("vedirect_updatesonly")] = config.Vedirect.UpdatesOnly; response->setLength(); request->send(response); @@ -112,12 +112,12 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) } CONFIG_T& config = Configuration.get(); - config.Vedirect_Enabled = root[F("vedirect_enabled")].as(); - config.Vedirect_VerboseLogging = root[F("verbose_logging")].as(); - config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as(); + config.Vedirect.Enabled = root[F("vedirect_enabled")].as(); + config.Vedirect.VerboseLogging = root[F("verbose_logging")].as(); + config.Vedirect.UpdatesOnly = root[F("vedirect_updatesonly")].as(); Configuration.write(); - VictronMppt.init(); + VictronMppt.updateSettings(); retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); diff --git a/src/WebApi_webapp.cpp b/src/WebApi_webapp.cpp index 90516ad6c..260566fa5 100644 --- a/src/WebApi_webapp.cpp +++ b/src/WebApi_webapp.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_webapp.h" @@ -9,16 +9,18 @@ extern const uint8_t file_favicon_ico_start[] asm("_binary_webapp_dist_favicon_i extern const uint8_t file_favicon_png_start[] asm("_binary_webapp_dist_favicon_png_start"); extern const uint8_t file_zones_json_start[] asm("_binary_webapp_dist_zones_json_gz_start"); extern const uint8_t file_app_js_start[] asm("_binary_webapp_dist_js_app_js_gz_start"); +extern const uint8_t file_site_webmanifest_start[] asm("_binary_webapp_dist_site_webmanifest_start"); extern const uint8_t file_index_html_end[] asm("_binary_webapp_dist_index_html_gz_end"); extern const uint8_t file_favicon_ico_end[] asm("_binary_webapp_dist_favicon_ico_end"); extern const uint8_t file_favicon_png_end[] asm("_binary_webapp_dist_favicon_png_end"); extern const uint8_t file_zones_json_end[] asm("_binary_webapp_dist_zones_json_gz_end"); extern const uint8_t file_app_js_end[] asm("_binary_webapp_dist_js_app_js_gz_end"); +extern const uint8_t file_site_webmanifest_end[] asm("_binary_webapp_dist_site_webmanifest_end"); -void WebApiWebappClass::init(AsyncWebServer* server) +void WebApiWebappClass::init(AsyncWebServer& server) { - _server = server; + _server = &server; _server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); @@ -54,12 +56,17 @@ void WebApiWebappClass::init(AsyncWebServer* server) request->send(response); }); + _server->on("/site.webmanifest", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start); + request->send(response); + }); + _server->on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) { #ifdef AUTO_GIT_HASH // check client If-None-Match header vs ETag/AUTO_GIT_HASH bool eTagMatch = false; if (request->hasHeader("If-None-Match")) { - AsyncWebHeader* h = request->getHeader("If-None-Match"); + const AsyncWebHeader* h = request->getHeader("If-None-Match"); if (strncmp(AUTO_GIT_HASH, h->value().c_str(), strlen(AUTO_GIT_HASH)) == 0) { eTagMatch = true; } diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp index 4cf8eb841..55f83cd6b 100644 --- a/src/WebApi_ws_Huawei.cpp +++ b/src/WebApi_ws_Huawei.cpp @@ -15,7 +15,7 @@ WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass() { } -void WebApiWsHuaweiLiveClass::init(AsyncWebServer* server) +void WebApiWsHuaweiLiveClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -24,7 +24,7 @@ void WebApiWsHuaweiLiveClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1)); _server->addHandler(&_ws); @@ -60,10 +60,10 @@ void WebApiWsHuaweiLiveClass::loop() } if (buffer) { - if (Configuration.get().Security_AllowReadonly) { + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } _ws.textAll(buffer); diff --git a/src/WebApi_ws_battery.cpp b/src/WebApi_ws_battery.cpp index a2a74525d..475b0b667 100644 --- a/src/WebApi_ws_battery.cpp +++ b/src/WebApi_ws_battery.cpp @@ -15,7 +15,7 @@ WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass() { } -void WebApiWsBatteryLiveClass::init(AsyncWebServer* server) +void WebApiWsBatteryLiveClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -24,7 +24,7 @@ void WebApiWsBatteryLiveClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1)); _server->addHandler(&_ws); @@ -58,10 +58,10 @@ void WebApiWsBatteryLiveClass::loop() } if (buffer) { - if (Configuration.get().Security_AllowReadonly) { + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } _ws.textAll(buffer); diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp index 2837fc39d..541593815 100644 --- a/src/WebApi_ws_console.cpp +++ b/src/WebApi_ws_console.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_ws_console.h" #include "Configuration.h" @@ -13,9 +13,9 @@ WebApiWsConsoleClass::WebApiWsConsoleClass() { } -void WebApiWsConsoleClass::init(AsyncWebServer* server) +void WebApiWsConsoleClass::init(AsyncWebServer& server) { - _server = server; + _server = &server; _server->addHandler(&_ws); MessageOutput.register_ws_output(&_ws); } @@ -26,10 +26,10 @@ void WebApiWsConsoleClass::loop() if (millis() - _lastWsCleanup > 1000) { _ws.cleanupClients(); - if (Configuration.get().Security_AllowReadonly) { + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } _lastWsCleanup = millis(); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ffbf6b20a..20308d779 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "WebApi_ws_live.h" #include "Configuration.h" @@ -19,7 +19,7 @@ WebApiWsLiveClass::WebApiWsLiveClass() { } -void WebApiWsLiveClass::init(AsyncWebServer* server) +void WebApiWsLiveClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -28,7 +28,7 @@ void WebApiWsLiveClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); _server->addHandler(&_ws); @@ -75,10 +75,10 @@ void WebApiWsLiveClass::loop() if (buffer) { serializeJson(root, buffer); - if (Configuration.get().Security_AllowReadonly) { + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } _ws.textAll(buffer); @@ -176,14 +176,14 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); - if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) { + if (!strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD)) { hintObj["default_password"] = true; } else { hintObj["default_password"] = false; } JsonObject vedirectObj = root.createNestedObject("vedirect"); - vedirectObj[F("enabled")] = Configuration.get().Vedirect_Enabled; + vedirectObj[F("enabled")] = Configuration.get().Vedirect.Enabled; JsonObject totalVeObj = vedirectObj.createNestedObject("total"); addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); @@ -191,21 +191,21 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); JsonObject huaweiObj = root.createNestedObject("huawei"); - huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled; + huaweiObj[F("enabled")] = Configuration.get().Huawei.Enabled; const RectifierParameters_t * rp = HuaweiCan.get(); addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); JsonObject batteryObj = root.createNestedObject("battery"); - batteryObj[F("enabled")] = Configuration.get().Battery_Enabled; + batteryObj[F("enabled")] = Configuration.get().Battery.Enabled; addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0); JsonObject powerMeterObj = root.createNestedObject("power_meter"); - powerMeterObj[F("enabled")] = Configuration.get().PowerMeter_Enabled; + powerMeterObj[F("enabled")] = Configuration.get().PowerMeter.Enabled; addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); } -void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic) +void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { String chanName; @@ -222,7 +222,7 @@ void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr< } } -void WebApiWsLiveClass::addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits) +void WebApiWsLiveClass::addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits) { root[name]["v"] = value; root[name]["u"] = unit; diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 53c81d122..430eb9ce7 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -16,7 +16,7 @@ WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass() { } -void WebApiWsVedirectLiveClass::init(AsyncWebServer* server) +void WebApiWsVedirectLiveClass::init(AsyncWebServer& server) { using std::placeholders::_1; using std::placeholders::_2; @@ -25,7 +25,7 @@ void WebApiWsVedirectLiveClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; + _server = &server; _server->on("/api/vedirectlivedata/status", HTTP_GET, std::bind(&WebApiWsVedirectLiveClass::onLivedataStatus, this, _1)); _server->addHandler(&_ws); @@ -65,10 +65,10 @@ void WebApiWsVedirectLiveClass::loop() } if (buffer) { - if (Configuration.get().Security_AllowReadonly) { + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } _ws.textAll(buffer); @@ -142,7 +142,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) // power limiter state root["dpl"]["PLSTATE"] = -1; - if (Configuration.get().PowerLimiter_Enabled) + if (Configuration.get().PowerLimiter.Enabled) root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } diff --git a/src/main.cpp b/src/main.cpp index b39845ca0..b02317740 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "Configuration.h" #include "Datastore.h" @@ -24,6 +24,7 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "Scheduler.h" #include "SunPosition.h" #include "Utils.h" #include "WebApi.h" @@ -32,6 +33,7 @@ #include "defaults.h" #include #include +#include void setup() { @@ -44,6 +46,7 @@ void setup() while (!Serial) yield(); #endif + MessageOutput.init(scheduler); MessageOutput.println(); MessageOutput.println("Starting OpenDTU"); @@ -71,11 +74,11 @@ void setup() MessageOutput.print("failed... "); } } - if (Configuration.get().Cfg_Version != CONFIG_VERSION) { + if (Configuration.get().Cfg.Version != CONFIG_VERSION) { MessageOutput.print("migrated... "); Configuration.migrate(); } - CONFIG_T& config = Configuration.get(); + auto& config = Configuration.get(); MessageOutput.println("done"); // Load PinMapping @@ -85,12 +88,12 @@ void setup() } else { MessageOutput.print("using default config "); } - const PinMapping_t& pin = PinMapping.get(); + const auto& pin = PinMapping.get(); MessageOutput.println("done"); // Initialize WiFi MessageOutput.print("Initialize Network... "); - NetworkSettings.init(); + NetworkSettings.init(scheduler); MessageOutput.println("done"); NetworkSettings.applyConfig(); @@ -101,133 +104,89 @@ void setup() // Initialize SunPosition MessageOutput.print("Initialize SunPosition... "); - SunPosition.init(); + SunPosition.init(scheduler); MessageOutput.println("done"); // Initialize MqTT MessageOutput.print("Initialize MqTT... "); MqttSettings.init(); - MqttHandleDtu.init(); - MqttHandleInverter.init(); - MqttHandleInverterTotal.init(); - MqttHandleVedirect.init(); - MqttHandleHass.init(); - MqttHandleVedirectHass.init(); - MqttHandleHuawei.init(); - MqttHandlePowerLimiter.init(); + MqttHandleDtu.init(scheduler); + MqttHandleInverter.init(scheduler); + MqttHandleInverterTotal.init(scheduler); + MqttHandleVedirect.init(scheduler); + MqttHandleHass.init(scheduler); + MqttHandleVedirectHass.init(scheduler); + MqttHandleHuawei.init(scheduler); + MqttHandlePowerLimiter.init(scheduler); MessageOutput.println("done"); // Initialize WebApi MessageOutput.print("Initialize WebApi... "); - WebApi.init(); + WebApi.init(scheduler); MessageOutput.println("done"); // Initialize Display MessageOutput.print("Initialize Display... "); Display.init( + scheduler, static_cast(pin.display_type), pin.display_data, pin.display_clk, pin.display_cs, pin.display_reset); - Display.setOrientation(config.Display_Rotation); - Display.enablePowerSafe = config.Display_PowerSafe; - Display.enableScreensaver = config.Display_ScreenSaver; - Display.setContrast(config.Display_Contrast); - Display.setLanguage(config.Display_Language); + Display.setOrientation(config.Display.Rotation); + Display.enablePowerSafe = config.Display.PowerSafe; + Display.enableScreensaver = config.Display.ScreenSaver; + Display.setContrast(config.Display.Contrast); + Display.setLanguage(config.Display.Language); Display.setStartupDisplay(); MessageOutput.println("done"); // Initialize Single LEDs MessageOutput.print("Initialize LEDs... "); - LedSingle.init(); + LedSingle.init(scheduler); MessageOutput.println("done"); // Check for default DTU serial MessageOutput.print("Check for default DTU serial... "); - if (config.Dtu_Serial == DTU_SERIAL) { + if (config.Dtu.Serial == DTU_SERIAL) { MessageOutput.print("generate serial based on ESP chip id: "); - uint64_t dtuId = Utils::generateDtuSerial(); + const uint64_t dtuId = Utils::generateDtuSerial(); MessageOutput.printf("%0x%08x... ", ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), ((uint32_t)(dtuId & 0xFFFFFFFF))); - config.Dtu_Serial = dtuId; + config.Dtu.Serial = dtuId; Configuration.write(); } MessageOutput.println("done"); MessageOutput.println("done"); - InverterSettings.init(); + InverterSettings.init(scheduler); - Datastore.init(); + Datastore.init(scheduler); - VictronMppt.init(); + VictronMppt.init(scheduler); // Power meter - PowerMeter.init(); + PowerMeter.init(scheduler); // Dynamic power limiter - PowerLimiter.init(); + PowerLimiter.init(scheduler); // Initialize Huawei AC-charger PSU / CAN bus - MessageOutput.println(F("Initialize Huawei AC charger interface... ")); + MessageOutput.println("Initialize Huawei AC charger interface... "); if (PinMapping.isValidHuaweiConfig()) { MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - HuaweiCan.init(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - MessageOutput.println(F("done")); + HuaweiCan.init(scheduler, pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + MessageOutput.println("done"); } else { - MessageOutput.println(F("Invalid pin config")); + MessageOutput.println("Invalid pin config"); } - Battery.init(); + Battery.init(scheduler); } void loop() { - NetworkSettings.loop(); - yield(); - PowerMeter.loop(); - yield(); - PowerLimiter.loop(); - yield(); - InverterSettings.loop(); - yield(); - Datastore.loop(); - yield(); - VictronMppt.loop(); - yield(); - MqttSettings.loop(); - yield(); - MqttHandleDtu.loop(); - yield(); - MqttHandleInverter.loop(); - yield(); - MqttHandleInverterTotal.loop(); - yield(); - MqttHandleVedirect.loop(); - yield(); - MqttHandleHass.loop(); - yield(); - MqttHandleVedirectHass.loop(); - yield(); - MqttHandleHuawei.loop(); - yield(); - MqttHandlePowerLimiter.loop(); - yield(); - WebApi.loop(); - yield(); - Display.loop(); - yield(); - SunPosition.loop(); - yield(); - MessageOutput.loop(); - yield(); - Battery.loop(); - yield(); - MqttHandlePylontechHass.loop(); - yield(); - HuaweiCan.loop(); - yield(); - LedSingle.loop(); - yield(); + scheduler.execute(); } diff --git a/webapp/index.html b/webapp/index.html index 07f73f918..47122be18 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -5,6 +5,7 @@ + OpenDTU-onBattery diff --git a/webapp/package.json b/webapp/package.json index c781b53ea..c5050c466 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -16,32 +16,32 @@ "bootstrap": "^5.3.2", "bootstrap-icons-vue": "^1.11.1", "mitt": "^3.0.1", - "sortablejs": "^1.15.0", + "sortablejs": "^1.15.1", "spark-md5": "^3.0.2", - "vue": "^3.3.8", - "vue-i18n": "^9.7.0", + "vue": "^3.3.13", + "vue-i18n": "^9.8.0", "vue-router": "^4.2.5" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^1.5.0", - "@rushstack/eslint-patch": "^1.5.1", + "@intlify/unplugin-vue-i18n": "^1.6.0", + "@rushstack/eslint-patch": "^1.6.1", "@tsconfig/node18": "^18.2.2", - "@types/bootstrap": "^5.2.9", - "@types/node": "^20.9.0", - "@types/sortablejs": "^1.15.5", + "@types/bootstrap": "^5.2.10", + "@types/node": "^20.10.5", + "@types/sortablejs": "^1.15.7", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^4.5.0", + "@vitejs/plugin-vue": "^4.5.2", "@vue/eslint-config-typescript": "^12.0.0", - "@vue/tsconfig": "^0.4.0", - "eslint": "^8.53.0", - "eslint-plugin-vue": "^9.18.1", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.19.2", "npm-run-all": "^4.1.5", "sass": "^1.69.5", - "terser": "^5.24.0", - "typescript": "^5.2.2", - "vite": "^5.0.0", + "terser": "^5.26.0", + "typescript": "^5.3.3", + "vite": "^5.0.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.3.0", - "vue-tsc": "^1.8.22" + "vue-tsc": "^1.8.25" } } diff --git a/webapp/public/favicon.png b/webapp/public/favicon.png index 3378b661323215355b212542a9fe59b610adeae1..278aac84f197d64e420e057aa7ba62c94c82e512 100644 GIT binary patch literal 4590 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 Qz5oCK07*qoM6N<$f~lH1Y5)KL diff --git a/webapp/public/site.webmanifest b/webapp/public/site.webmanifest new file mode 100644 index 000000000..3be246091 --- /dev/null +++ b/webapp/public/site.webmanifest @@ -0,0 +1,13 @@ +{ + "name": "OpenDTU", + "short_name": "OpenDTU", + "display": "standalone", + "orientation": "portrait", + "icons": [ + { + "src": "/favicon.png", + "sizes": "144x144", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/webapp/public/zones.json b/webapp/public/zones.json index 28bfa0dcf..2d449fe26 100644 --- a/webapp/public/zones.json +++ b/webapp/public/zones.json @@ -11,7 +11,7 @@ "Africa/Blantyre":"CAT-2", "Africa/Brazzaville":"WAT-1", "Africa/Bujumbura":"CAT-2", -"Africa/Cairo":"EET-2", +"Africa/Cairo":"EET-2EEST,M4.5.5/0,M10.5.4/24", "Africa/Casablanca":"<+01>-1", "Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3", "Africa/Conakry":"GMT0", @@ -72,7 +72,7 @@ "America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0", "America/Atikokan":"EST5", "America/Bahia":"<-03>3", -"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0", +"America/Bahia_Banderas":"CST6", "America/Barbados":"AST4", "America/Belem":"<-03>3", "America/Belize":"CST6", @@ -87,7 +87,7 @@ "America/Cayenne":"<-03>3", "America/Cayman":"EST5", "America/Chicago":"CST6CDT,M3.2.0,M11.1.0", -"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0", +"America/Chihuahua":"CST6", "America/Costa_Rica":"CST6", "America/Creston":"MST7", "America/Cuiaba":"<-04>4", @@ -104,7 +104,7 @@ "America/Fort_Nelson":"MST7", "America/Fortaleza":"<-03>3", "America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0", -"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1", +"America/Godthab":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0", "America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0", "America/Grenada":"AST4", @@ -140,14 +140,14 @@ "America/Marigot":"AST4", "America/Martinique":"AST4", "America/Matamoros":"CST6CDT,M3.2.0,M11.1.0", -"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0", +"America/Mazatlan":"MST7", "America/Menominee":"CST6CDT,M3.2.0,M11.1.0", -"America/Merida":"CST6CDT,M4.1.0,M10.5.0", +"America/Merida":"CST6", "America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0", -"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0", +"America/Mexico_City":"CST6", "America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0", "America/Moncton":"AST4ADT,M3.2.0,M11.1.0", -"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0", +"America/Monterrey":"CST6", "America/Montevideo":"<-03>3", "America/Montreal":"EST5EDT,M3.2.0,M11.1.0", "America/Montserrat":"AST4", @@ -159,8 +159,8 @@ "America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0", "America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0", "America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0", -"America/Nuuk":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1", -"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0", +"America/Nuuk":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", +"America/Ojinaga":"CST6CDT,M3.2.0,M11.1.0", "America/Panama":"EST5", "America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0", "America/Paramaribo":"<-03>3", @@ -214,7 +214,7 @@ "Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3", "Asia/Aden":"<+03>-3", "Asia/Almaty":"<+06>-6", -"Asia/Amman":"EET-2EEST,M2.5.4/24,M10.5.5/1", +"Asia/Amman":"<+03>-3", "Asia/Anadyr":"<+12>-12", "Asia/Aqtau":"<+05>-5", "Asia/Aqtobe":"<+05>-5", @@ -231,14 +231,14 @@ "Asia/Chita":"<+09>-9", "Asia/Choibalsan":"<+08>-8", "Asia/Colombo":"<+0530>-5:30", -"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0", +"Asia/Damascus":"<+03>-3", "Asia/Dhaka":"<+06>-6", "Asia/Dili":"<+09>-9", "Asia/Dubai":"<+04>-4", "Asia/Dushanbe":"<+05>-5", "Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Asia/Gaza":"EET-2EEST,M3.4.4/48,M10.5.5/1", -"Asia/Hebron":"EET-2EEST,M3.4.4/48,M10.5.5/1", +"Asia/Gaza":"EET-2EEST,M3.4.4/50,M10.4.4/50", +"Asia/Hebron":"EET-2EEST,M3.4.4/50,M10.4.4/50", "Asia/Ho_Chi_Minh":"<+07>-7", "Asia/Hong_Kong":"HKT-8", "Asia/Hovd":"<+07>-7", @@ -281,7 +281,7 @@ "Asia/Taipei":"CST-8", "Asia/Tashkent":"<+05>-5", "Asia/Tbilisi":"<+04>-4", -"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24", +"Asia/Tehran":"<+0330>-3:30", "Asia/Thimphu":"<+06>-6", "Asia/Tokyo":"JST-9", "Asia/Tomsk":"<+07>-7", @@ -373,7 +373,7 @@ "Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0", "Europe/Kaliningrad":"EET-2", "Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Europe/Kirov":"<+03>-3", +"Europe/Kirov":"MSK-3", "Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0", "Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/London":"GMT0BST,M3.5.0/1,M10.5.0", @@ -406,7 +406,7 @@ "Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Europe/Volgograd":"<+03>-3", +"Europe/Volgograd":"MSK-3", "Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4", @@ -431,7 +431,7 @@ "Pacific/Efate":"<+11>-11", "Pacific/Enderbury":"<+13>-13", "Pacific/Fakaofo":"<+13>-13", -"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99", +"Pacific/Fiji":"<+12>-12", "Pacific/Funafuti":"<+12>-12", "Pacific/Galapagos":"<-06>6", "Pacific/Gambier":"<-09>9", diff --git a/webapp/src/components/FormFooter.vue b/webapp/src/components/FormFooter.vue new file mode 100644 index 000000000..7a18e6022 --- /dev/null +++ b/webapp/src/components/FormFooter.vue @@ -0,0 +1,7 @@ + diff --git a/webapp/src/components/GridProfile.vue b/webapp/src/components/GridProfile.vue index 31a141332..6c93caece 100644 --- a/webapp/src/components/GridProfile.vue +++ b/webapp/src/components/GridProfile.vue @@ -6,36 +6,98 @@ \ No newline at end of file diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 1c29295e5..710177fda 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -70,7 +70,7 @@ - + @@ -79,6 +79,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { DtuConfig } from "@/types/DtuConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -90,6 +91,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, BIconInfoCircle, }, diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 3d80effde..a28bc57fe 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -192,7 +192,7 @@ - + @@ -269,6 +274,7 @@ declare interface Inverter { reachable_threshold: number; zero_runtime: boolean; zero_day: boolean; + yieldday_correction: boolean; channel: Array; } diff --git a/webapp/src/views/MqttAdminView.vue b/webapp/src/views/MqttAdminView.vue index 1c5ba2379..83bcab86b 100644 --- a/webapp/src/views/MqttAdminView.vue +++ b/webapp/src/views/MqttAdminView.vue @@ -103,6 +103,19 @@ v-model="mqttConfigList.mqtt_lwt_offline" type="text" maxlength="20" :placeholder="$t('mqttadmin.LwtOfflineHint')"/> + +
+ +
+ +
+
- + @@ -135,6 +148,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { MqttConfig } from "@/types/MqttConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -145,6 +159,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, }, data() { @@ -154,6 +169,11 @@ export default defineComponent({ alertMessage: "", alertType: "info", showAlert: false, + qosTypeList: [ + { key: 0, value: 'QOS0' }, + { key: 1, value: 'QOS1' }, + { key: 2, value: 'QOS2' }, + ], }; }, created() { diff --git a/webapp/src/views/NetworkAdminView.vue b/webapp/src/views/NetworkAdminView.vue index 1019f3e99..d4ee534a9 100644 --- a/webapp/src/views/NetworkAdminView.vue +++ b/webapp/src/views/NetworkAdminView.vue @@ -63,7 +63,7 @@ :postfix="$t('networkadmin.Minutes')" :tooltip="$t('networkadmin.ApTimeoutHint')"/> - + @@ -72,6 +72,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { NetworkConfig } from "@/types/NetworkConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -82,6 +83,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, }, data() { diff --git a/webapp/src/views/NtpAdminView.vue b/webapp/src/views/NtpAdminView.vue index 69e696c99..468dcac68 100644 --- a/webapp/src/views/NtpAdminView.vue +++ b/webapp/src/views/NtpAdminView.vue @@ -52,7 +52,7 @@ - + @@ -79,6 +79,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; import InputElement from '@/components/InputElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import type { NtpConfig } from "@/types/NtpConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; import { defineComponent } from 'vue'; @@ -89,6 +90,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, BIconInfoCircle, }, diff --git a/webapp/src/views/SecurityAdminView.vue b/webapp/src/views/SecurityAdminView.vue index c6746dd42..6a6ae3a24 100644 --- a/webapp/src/views/SecurityAdminView.vue +++ b/webapp/src/views/SecurityAdminView.vue @@ -23,7 +23,7 @@ type="checkbox" wide/> - + @@ -32,6 +32,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { SecurityConfig } from '@/types/SecurityConfig'; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -42,6 +43,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, }, data() { diff --git a/webapp/tsconfig.config.json b/webapp/tsconfig.config.json index 1fccd77c8..53248ff73 100644 --- a/webapp/tsconfig.config.json +++ b/webapp/tsconfig.config.json @@ -9,6 +9,7 @@ "cypress.config.*" ], "compilerOptions": { + "noEmit": false, "composite": true, "types": [ "node" diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 0a6eabe76..b154652d8 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -17,10 +17,10 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== -"@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== +"@babel/parser@^7.23.5": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== "@esbuild/android-arm64@0.19.5": version "0.19.5" @@ -156,10 +156,10 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d" - integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -171,10 +171,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.53.0": - version "8.53.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" - integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== "@humanwhocodes/config-array@^0.11.13": version "0.11.13" @@ -211,20 +211,20 @@ source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" -"@intlify/core-base@9.7.0": - version "9.7.0" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.7.0.tgz#329f4225365187d9fbe7a3b5c5d8e897fa703b30" - integrity sha512-1tBnfnCI23jXqGW15cagCjn2GgD487VST1dMG8P5LRzrSfx+kUzqFyTrjMNIwgq1tVaF4HnDpFMUuyrzTLKphw== +"@intlify/core-base@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.8.0.tgz#969ca59f55084e23e968ec0bfe71678774e568ec" + integrity sha512-UxaSZVZ1DwqC/CltUZrWZNaWNhfmKtfyV4BJSt/Zt4Or/fZs1iFj0B+OekYk1+MRHfIOe3+x00uXGQI4PbO/9g== dependencies: - "@intlify/message-compiler" "9.7.0" - "@intlify/shared" "9.7.0" + "@intlify/message-compiler" "9.8.0" + "@intlify/shared" "9.8.0" -"@intlify/message-compiler@9.7.0": - version "9.7.0" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.7.0.tgz#6371127c5a2a4f50ec59728f85a7786e3478c931" - integrity sha512-/YdZCio2L2tCM5bZ2eMHbSEIQNPh1QqvZIOLI/yCVKXLscis7O0SsR2nmuU/DfCJ3iSeI8juw82C2wLvfsAeww== +"@intlify/message-compiler@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.8.0.tgz#587d69b302f9b8130a4a949b0ab4add519761787" + integrity sha512-McnYWhcoYmDJvssVu6QGR0shqlkJuL1HHdi5lK7fNqvQqRYaQ4lSLjYmZxwc8tRNMdIe9/KUKfyPxU9M6yCtNQ== dependencies: - "@intlify/shared" "9.7.0" + "@intlify/shared" "9.8.0" source-map-js "^1.0.2" "@intlify/message-compiler@^9.4.0": @@ -240,15 +240,15 @@ resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.4.0.tgz#4a78d462fc82433db900981e12eb5b1aae3d6085" integrity sha512-AFqymip2kToqA0B6KZPg5jSrdcVHoli9t/VhGKE2iiMq9utFuMoGdDC/JOCIZgwxo6aXAk86QyU2XtzEoMuZ6A== -"@intlify/shared@9.7.0": - version "9.7.0" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.7.0.tgz#96166a54b781997db92259772e9621d3f7dff9a5" - integrity sha512-PUkEuk//YKu4CHS5ah3mNa3XL/+TZj6rAY/6yYN+GCNFd2u+uWUkeuwE4Q6t8dydRWlErOePHHS0KyNoof/oBw== +"@intlify/shared@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.8.0.tgz#62adf8f6ef67c8eba6cf8d521e248f3503f237d3" + integrity sha512-TmgR0RCLjzrSo+W3wT0ALf9851iFMlVI9EYNGeWvZFUQTAJx0bvfsMlPdgVtV1tDNRiAfhkFsMKu6jtUY1ZLKQ== -"@intlify/unplugin-vue-i18n@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-1.5.0.tgz#fe2e67d50beefc4b67702a7bcec23062123cb52d" - integrity sha512-jW0MCCdwxybxcwjEfCunAcKjVoxyO3i+cnLL6v+MNGRLUHqrpELF6zQAJUhgAK2afhY7mCliy8RxTFWKdXm26w== +"@intlify/unplugin-vue-i18n@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-1.6.0.tgz#49ab21125c79257e455a6e562a2511c8681b870c" + integrity sha512-IGeFNWxdEvB12E/3Y/+nmIsGeTg5okPsK1XEtUUD/DdkHbVqUbJucMpHKeHF8Px55Qca551pQCs/g+VjNUt6KA== dependencies: "@intlify/bundle-utils" "^7.4.0" "@intlify/shared" "^9.4.0" @@ -408,20 +408,20 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.1.tgz#8311b77e6cce322865ba12ada8c3779369610d18" integrity sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w== -"@rushstack/eslint-patch@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz#5f1b518ec5fa54437c0b7c4a821546c64fed6922" - integrity sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA== +"@rushstack/eslint-patch@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz#9ab8f811930d7af3e3d549183a50884f9eb83f36" + integrity sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw== "@tsconfig/node18@^18.2.2": version "18.2.2" resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.2.tgz#81fb16ecff0d400b1cbadbf76713b50f331029ce" integrity sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw== -"@types/bootstrap@^5.2.9": - version "5.2.9" - resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.9.tgz#5040df5d8d12cb9fb6268a33b8d87234af15e09a" - integrity sha512-Fcg4nORBKaVUAG4F0ePWcatWQVfr3NAT9XIN+hl1PaiAwb4tq55+iua9R3exsbB3yyfhyQlHYg2foTlW86J+RA== +"@types/bootstrap@^5.2.10": + version "5.2.10" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c" + integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g== dependencies: "@popperjs/core" "^2.9.2" @@ -435,10 +435,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== -"@types/node@^20.9.0": - version "20.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298" - integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw== +"@types/node@^20.10.5": + version "20.10.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" + integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== dependencies: undici-types "~5.26.4" @@ -447,10 +447,10 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367" integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg== -"@types/sortablejs@^1.15.5": - version "1.15.5" - resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.5.tgz#c59e51765bc53c920192de0d0202f75b7ce4cb3f" - integrity sha512-qqqbEFbB1EZt08I1Ok2BA3Sx0zlI8oizdIguMsajk4Yo/iHgXhCb3GM6N09JOJqT9xIMYM9LTFy8vit3RNY71Q== +"@types/sortablejs@^1.15.7": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.7.tgz#11f85e98fce2854708e5c6d6011f7a236d79ae9f" + integrity sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ== "@types/spark-md5@^3.0.4": version "3.0.4" @@ -547,31 +547,31 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitejs/plugin-vue@^4.5.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz#b4569fcb1faac054eba4f5efc1aaf4d39f4379e5" - integrity sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ== +"@vitejs/plugin-vue@^4.5.2": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz#1212d81bc83680e14448fefe55abd9fe1ed49ed1" + integrity sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ== -"@volar/language-core@1.10.7", "@volar/language-core@~1.10.5": - version "1.10.7" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.10.7.tgz#9d555bf0a3ca652c525651baba5ecf8a55cf3471" - integrity sha512-6+WI7HGqWCsKJ/bms4V45WP7eDeoGxDtLjYPrHB7QkIWVkRLIeGPzzBoonZz9kERM+Kld3W89Y+IlICejVAKhA== +"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.11.1.tgz#ecdf12ea8dc35fb8549e517991abcbf449a5ad4f" + integrity sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw== dependencies: - "@volar/source-map" "1.10.7" + "@volar/source-map" "1.11.1" -"@volar/source-map@1.10.7", "@volar/source-map@~1.10.5": - version "1.10.7" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.10.7.tgz#f2b5c6b99f3fc91c10d4013eaeb083fbbf4b9e0d" - integrity sha512-anA254XO0lmmeu0p/kvgPOCkrVpqNIHWMvEkPX70PSk4ntg0iBzN/f0Kip6deXvibl6v14Q3Z8RihWrZwdZEEQ== +"@volar/source-map@1.11.1", "@volar/source-map@~1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.11.1.tgz#535b0328d9e2b7a91dff846cab4058e191f4452f" + integrity sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg== dependencies: muggle-string "^0.3.1" -"@volar/typescript@~1.10.5": - version "1.10.7" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.10.7.tgz#2ed47e3260d4161445099ba89c7471fbc51133b6" - integrity sha512-2hvA3vjXVUn1vOpsP/nWLnE5DUmY6YKQhvDRoZVfBrnWwIo0ySxdTUP4XieXGGgSk43xJaeU1zqQS/3Wfm7QgA== +"@volar/typescript@~1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.11.1.tgz#ba86c6f326d88e249c7f5cfe4b765be3946fd627" + integrity sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ== dependencies: - "@volar/language-core" "1.10.7" + "@volar/language-core" "1.11.1" path-browserify "^1.0.1" "@vue/compiler-core@3.2.47": @@ -584,6 +584,16 @@ estree-walker "^2.0.2" source-map "^0.6.1" +"@vue/compiler-core@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.13.tgz#b3d5f8f84caee5de3f31d95cb568d899fd19c599" + integrity sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A== + dependencies: + "@babel/parser" "^7.23.5" + "@vue/shared" "3.3.13" + estree-walker "^2.0.2" + source-map-js "^1.0.2" + "@vue/compiler-core@3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.2.tgz#39567bd15c7f97add97bfc4d44e814df36eb797b" @@ -594,16 +604,6 @@ estree-walker "^2.0.2" source-map-js "^1.0.2" -"@vue/compiler-core@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.8.tgz#301bb60d0245265a88ed5b30e200fbf223acb313" - integrity sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g== - dependencies: - "@babel/parser" "^7.23.0" - "@vue/shared" "3.3.8" - estree-walker "^2.0.2" - source-map-js "^1.0.2" - "@vue/compiler-dom@3.2.47": version "3.2.47" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305" @@ -612,13 +612,13 @@ "@vue/compiler-core" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-dom@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz#09d832514b9b8d9415a3816b065d69dbefcc7e9b" - integrity sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ== +"@vue/compiler-dom@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz#d029e222e545e7ab00be35aafd3abed167f962bf" + integrity sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw== dependencies: - "@vue/compiler-core" "3.3.8" - "@vue/shared" "3.3.8" + "@vue/compiler-core" "3.3.13" + "@vue/shared" "3.3.13" "@vue/compiler-dom@^3.3.0": version "3.3.2" @@ -628,20 +628,20 @@ "@vue/compiler-core" "3.3.2" "@vue/shared" "3.3.2" -"@vue/compiler-sfc@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz#40b18e48aa00260950964d1d72157668521be0e1" - integrity sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA== - dependencies: - "@babel/parser" "^7.23.0" - "@vue/compiler-core" "3.3.8" - "@vue/compiler-dom" "3.3.8" - "@vue/compiler-ssr" "3.3.8" - "@vue/reactivity-transform" "3.3.8" - "@vue/shared" "3.3.8" +"@vue/compiler-sfc@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz#7b397acefd5c0c3808701d2855be88c4be60155c" + integrity sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw== + dependencies: + "@babel/parser" "^7.23.5" + "@vue/compiler-core" "3.3.13" + "@vue/compiler-dom" "3.3.13" + "@vue/compiler-ssr" "3.3.13" + "@vue/reactivity-transform" "3.3.13" + "@vue/shared" "3.3.13" estree-walker "^2.0.2" magic-string "^0.30.5" - postcss "^8.4.31" + postcss "^8.4.32" source-map-js "^1.0.2" "@vue/compiler-sfc@^3.2.47": @@ -668,13 +668,13 @@ "@vue/compiler-dom" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-ssr@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz#136eed54411e4694815d961048a237191063fbce" - integrity sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w== +"@vue/compiler-ssr@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz#ad8748abff8d738ac9c6a3c47be42020f0fbaa63" + integrity sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw== dependencies: - "@vue/compiler-dom" "3.3.8" - "@vue/shared" "3.3.8" + "@vue/compiler-dom" "3.3.13" + "@vue/shared" "3.3.13" "@vue/devtools-api@^6.5.0": version "6.5.0" @@ -690,18 +690,19 @@ "@typescript-eslint/parser" "^6.7.0" vue-eslint-parser "^9.3.1" -"@vue/language-core@1.8.22": - version "1.8.22" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.22.tgz#1ef62645fb9b1f830c6c84a5586e49e74727b1e3" - integrity sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw== +"@vue/language-core@1.8.25": + version "1.8.25" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.25.tgz#b44b4e3c244ba9b1b79cccf9eb7b046535a4676f" + integrity sha512-NJk/5DnAZlpvXX8BdWmHI45bWGLViUaS3R/RMrmFSvFMSbJKuEODpM4kR0F0Ofv5SFzCWuNiMhxameWpVdQsnA== dependencies: - "@volar/language-core" "~1.10.5" - "@volar/source-map" "~1.10.5" + "@volar/language-core" "~1.11.1" + "@volar/source-map" "~1.11.1" "@vue/compiler-dom" "^3.3.0" "@vue/shared" "^3.3.0" computeds "^0.0.1" minimatch "^9.0.3" muggle-string "^0.3.1" + path-browserify "^1.0.1" vue-template-compiler "^2.7.14" "@vue/reactivity-transform@3.2.47": @@ -715,68 +716,68 @@ estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity-transform@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz#6d07649013b0be5c670f0ab6cc7ddd3150ad03f2" - integrity sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw== +"@vue/reactivity-transform@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz#dc8e9be961865dc666e367e1aaaea0716afa5c90" + integrity sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q== dependencies: - "@babel/parser" "^7.23.0" - "@vue/compiler-core" "3.3.8" - "@vue/shared" "3.3.8" + "@babel/parser" "^7.23.5" + "@vue/compiler-core" "3.3.13" + "@vue/shared" "3.3.13" estree-walker "^2.0.2" magic-string "^0.30.5" -"@vue/reactivity@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.8.tgz#cce8a03a3fd3539c3eeda53e277ba365d160dd4d" - integrity sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw== +"@vue/reactivity@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.13.tgz#9b1dff3b523a69997b66cba2f86f83839e8285fb" + integrity sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ== dependencies: - "@vue/shared" "3.3.8" + "@vue/shared" "3.3.13" -"@vue/runtime-core@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.8.tgz#fba5a632cbf2b5d29e171489570149cb6975dcdb" - integrity sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw== +"@vue/runtime-core@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.13.tgz#e8414218e8c7db94acfcec6fd12044704adda9cf" + integrity sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA== dependencies: - "@vue/reactivity" "3.3.8" - "@vue/shared" "3.3.8" + "@vue/reactivity" "3.3.13" + "@vue/shared" "3.3.13" -"@vue/runtime-dom@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz#e2d7aa795cf50914dda9a951887765a594b38af4" - integrity sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA== +"@vue/runtime-dom@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz#36b42b479d5a394972f305ca8e95c5f648bf55ef" + integrity sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ== dependencies: - "@vue/runtime-core" "3.3.8" - "@vue/shared" "3.3.8" - csstype "^3.1.2" + "@vue/runtime-core" "3.3.13" + "@vue/shared" "3.3.13" + csstype "^3.1.3" -"@vue/server-renderer@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.8.tgz#9b1779010e75783edeed8fcfb97d9c95fc3ac5d2" - integrity sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg== +"@vue/server-renderer@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.13.tgz#fccdd0787798173be8929f40f23161c17b60ed36" + integrity sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg== dependencies: - "@vue/compiler-ssr" "3.3.8" - "@vue/shared" "3.3.8" + "@vue/compiler-ssr" "3.3.13" + "@vue/shared" "3.3.13" "@vue/shared@3.2.47": version "3.2.47" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c" integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ== +"@vue/shared@3.3.13": + version "3.3.13" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.13.tgz#4cb73cda958d77ffd389c8640cf7d93a10ac676f" + integrity sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA== + "@vue/shared@3.3.2", "@vue/shared@^3.3.0": version "3.3.2" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3" integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ== -"@vue/shared@3.3.8": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.8.tgz#f044942142e1d3a395f24132e6203a784838542d" - integrity sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw== - -"@vue/tsconfig@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.4.0.tgz#f01e2f6089b5098136fb084a0dd0cdd4533b72b0" - integrity sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg== +"@vue/tsconfig@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz#3124ec16cc0c7e04165b88dc091e6b97782fffa9" + integrity sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -1006,10 +1007,10 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== de-indent@^1.0.2: version "1.0.2" @@ -1146,10 +1147,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.18.1: - version "9.18.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.18.1.tgz#73cf29df7450ce5913296465f8d1dc545344920c" - integrity sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg== +eslint-plugin-vue@^9.19.2: + version "9.19.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz#7ab83a001a1ac8bccae013c5b9cb5d2c644fb376" + integrity sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" natural-compare "^1.4.0" @@ -1190,15 +1191,15 @@ eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.53.0: - version "8.53.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce" - integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag== +eslint@^8.56.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.3" - "@eslint/js" "8.53.0" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.56.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -1903,10 +1904,10 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" @@ -2128,12 +2129,12 @@ postcss@^8.1.10: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@^8.4.32: + version "8.4.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" + integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw== dependencies: - nanoid "^3.3.6" + nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -2314,10 +2315,10 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -sortablejs@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a" - integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w== +sortablejs@^1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.1.tgz#9a35f52cdff449fb42ea8ecf222f3468d76e0a47" + integrity sha512-P5Cjvb0UG1ZVNiDPj/n4V+DinttXG6K8n7vM/HQf0C25K3YKQTQY6fsr/sEGsJGpQ9exmPxluHxKBc0mLKU1lQ== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" @@ -2436,10 +2437,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -terser@^5.24.0: - version "5.24.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364" - integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw== +terser@^5.26.0: + version "5.26.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" + integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2482,10 +2483,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== ufo@^1.1.2: version "1.1.2" @@ -2556,13 +2557,13 @@ vite-plugin-css-injected-by-js@^3.3.0: resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.3.0.tgz#c19480a9e42a95c5bced976a9dde1446f9bd91ff" integrity sha512-xG+jyHNCmUqi/TXp6q88wTJGeAOrNLSyUUTp4qEQ9QZLGcHWQQsCsSSKa59rPMQr8sOzfzmWDd8enGqfH/dBew== -vite@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.0.tgz#3bfb65acda2a97127e4fa240156664a1f234ce08" - integrity sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw== +vite@^5.0.10: + version "5.0.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.10.tgz#1e13ef5c3cf5aa4eed81f5df6d107b3c3f1f6356" + integrity sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw== dependencies: esbuild "^0.19.3" - postcss "^8.4.31" + postcss "^8.4.32" rollup "^4.2.0" optionalDependencies: fsevents "~2.3.3" @@ -2580,13 +2581,13 @@ vue-eslint-parser@^9.3.1: lodash "^4.17.21" semver "^7.3.6" -vue-i18n@^9.7.0: - version "9.7.0" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.7.0.tgz#c88592ade72c651d6879895244d348f2892c5646" - integrity sha512-8Z8kSz9U2juzuAf+6mjW1HTd5pIlYuFJZkC+HvYOglFdpzwc2rTUGjxKwN8xGdtGur1MFnyJ44TSr+TksJtY8A== +vue-i18n@^9.8.0: + version "9.8.0" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.8.0.tgz#54339daf377a31b234b027c5158e774728b6bc24" + integrity sha512-Izho+6PYjejsTq2mzjcRdBZ5VLRQoSuuexvR8029h5CpN03FYqiqBrShMyf2I1DKkN6kw/xmujcbvC+4QybpsQ== dependencies: - "@intlify/core-base" "9.7.0" - "@intlify/shared" "9.7.0" + "@intlify/core-base" "9.8.0" + "@intlify/shared" "9.8.0" "@vue/devtools-api" "^6.5.0" vue-router@^4.2.5: @@ -2604,25 +2605,25 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^1.8.22: - version "1.8.22" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.22.tgz#421e73c38b50802a6716ca32ed87b5970c867323" - integrity sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A== +vue-tsc@^1.8.25: + version "1.8.25" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.25.tgz#90cd03e71d28c5c4a8068167b232eb97cc96b77f" + integrity sha512-lHsRhDc/Y7LINvYhZ3pv4elflFADoEOo67vfClAfF2heVHpHmVquLSjojgCSIwzA4F0Pc4vowT/psXCYcfk+iQ== dependencies: - "@volar/typescript" "~1.10.5" - "@vue/language-core" "1.8.22" + "@volar/typescript" "~1.11.1" + "@vue/language-core" "1.8.25" semver "^7.5.4" -vue@^3.3.8: - version "3.3.8" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.8.tgz#532ff071af24f6a69e5ecc53a66858a9ee874ffc" - integrity sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w== +vue@^3.3.13: + version "3.3.13" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.13.tgz#f03098fa1b4e7cc88c133bef92260b55e3767002" + integrity sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q== dependencies: - "@vue/compiler-dom" "3.3.8" - "@vue/compiler-sfc" "3.3.8" - "@vue/runtime-dom" "3.3.8" - "@vue/server-renderer" "3.3.8" - "@vue/shared" "3.3.8" + "@vue/compiler-dom" "3.3.13" + "@vue/compiler-sfc" "3.3.13" + "@vue/runtime-dom" "3.3.13" + "@vue/server-renderer" "3.3.13" + "@vue/shared" "3.3.13" webpack-sources@^3.2.3: version "3.2.3" diff --git a/webapp_dist/favicon.png b/webapp_dist/favicon.png index 3378b661323215355b212542a9fe59b610adeae1..278aac84f197d64e420e057aa7ba62c94c82e512 100644 GIT binary patch literal 4590 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
jEVgNPu;m^l(wtDw;O zQztS30W=ed*X$XPl;&jiTcKrYboHmx>DR#lAmR8Y4jc}LvP!6yLbZ16{@hxlom1oN z3$uA*y0vzYDaIH8fSFzXP2S&i-C!+8BJrqenlbVv={RO~#u(^J7n>86N%Qx7rGl~~ zA0rnLpELUN`K&#@0nq5A??lSeq`Uxtgy$b4*zI-$nE*iF_miZ&-|s(dG-l8Ny;kPT z{K$CwF(b+2C|Wj%%jL3l&8IPEg4f7f*b#+JopLRD2Yrf(z?q8wkc>e!?@P~)W-U35ZvO;%Mm>D);IdSt?j#+LNBR2t(lcCg zx==iHwypz!%N!zWt$a%FFLC29!5>!qrv0I!+Z<&kMWS9bd_m}EwaZ%1oM=&P!7MD%nnDD5^s8I#}p|6T?E0Pw3|@?Jmc Qz5oCK07*qoM6N<$f~lH1Y5)KL diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz index 7272e15f7cff7ce4961a98609c05a961842211e1..be84fc7db40d1bb95eb2307c5b553caf0f95204e 100644 GIT binary patch literal 392 zcmV;30eAi%iwFP!000026KzsEZ`?2p-t#M1Ew|KGyLGTVv}uPf3D66Qjyj8HGm#}h z;+^4tU)pl|KyHymll&f}ns@gPUyi>Xzrx6q>+YKM1J3xNRp?cB2hfaY?0S*wglwQ6 zP0GlvdOCiqJ}J<9(vg3GgsxT82d`jEcy5(Go9i0aa@?xTV@TXz7)lc_5d~l}1M299 zHz@1BHy9>HhZToQKUO!nZ+1<_(-|}G)d!u4v7YhtnmGKCyZQEJ5@}b$If`Tj0?g=T z4l`LETZ_Vl)fT)5^2B6Zm3!l`{aAna>z#=mb`KH#{qd;^{T0;Mc&6T z1$%KQj49+C5)I@br(O&`XD#OI=d8L$m!1N!E>6F0t~mv}?7%+!TUNL@1wV8@@sJP% zKP_;Qh3pE!2F(7zFo*O4F@=c9Eie<0L8`u+tNBsHqBkdpwuVQCCgUChiH)#Li~I#^ m(OLM49{s zEjH_?;#PoM4y|!t5Yg7cwB8BmBkpP;cj_?JB`^PBQ|}|SdxG3VV?ZVtREILS?u=#n zcgCSYuvn1aw|()i_DRSBy?!RiIm*u@ zno8!LOfbzr{tThodOa@0&Y!}v7Dey6<{d910`tcawtTmXL(m?RyCgXJ_yr;QfYFX{ zG}L=WJ*$Iy%VyE3twmkJt3{P?sljjzKi`&ddo0;!*2{k6Uqh8P2QYdb&y>jwzbb*ID!EAc}zqR4k-Oj^;E!<9n~aLmsAXbE;FFMt|(H#XU9vggHRvbj|desT4^!3t!C5 zZ8uurOeZrfY`&A37Btt%ObeOkWTpkocQVt&A96C&oF8;D)7%$3f0=0w3!TigmPJlx zTGL`DGp$YFWTqtwoXoUPsgs$OD|9l`qGe8ITDsK9OdGJs$y}Hj7de@YX>p;GxwI%c zJDH!?oy;;Xv&hXX_cJf$Xm<8AJGz>e^EJzy%|dVU67FV^zgggLcJ?@nT+U8DXGf>A z!0YVnc9!{_g^p(jf6ueC>v;v=^UBU=q4!znes=IbJ2;@F9%vUA^dtD7S9LsFM26w^wQqwrQOl1_@kF~NH6V?UfCtRqEC8Rr?jJ2dO5eW zyI*<<$Mlk(X;;^@#5cXbIlaI;y}&)az(2jfLA}63{jiJre<2_BLMQbiFZBX9^+G@O z0!OvLQ@w(#`cZtLOde(lAM?S-E0m0a78TScTpOLx}%dMPFic%L8dn*e_@VFTt7`=_86m}{f*x3#-G!*b_}qs z^*{VSUk&nzT)!rc5DZ#hwe;_QuRCcz z!DXe@f4A1gv7d)}0>fj^hn1|uYLk}vh>8cQ_|9K)o!oYH2s4b6-7suD!B71wK)!BZ zfBE+(>+4xdum2&^H|iwMJe>MjFV)GlPFgMf`n$?UBK(M6hk35`al>%dYp-9wjyl=x zM6Wky6LsK))&6Ph>vVnX_~`T}9IHE-H$3&mu+GL*=#TL4-~S#b^l5#)6Z1S;c3tiJ z;O1vfHi@_#UPRe#w%{z_ZI<7iXmHA!V=c zKmCD6E8PKF({RPI+u#4bm6boeuKZ=ajguV!b?D?g4ud|At^ewEbIf{sEl=UzB-)4d z1YVf(vinJ#rf0y7gXnuA6Z_}aYeCcxb6>CRta(83x8pcp3=@xoY;C9U%M>4Z8OW+QU|Dl%x}*GVTG1%nJeglE)>)`7<1x>blLPyrCyI{fxugMVND zz7@fL{_B4|qj_|^G=;YazIG44f8DwN^a(yrBib~2eP`x4{M)B>d%l^S?5B40I&0m3 zYTx^M=N>35-?={yuJ!vs-|Qgi(}eD$I2pqe2yaaG>9sDg{OcMH-q?-Opg+pi{vGgj zEys~SzX{;)z(Mn9s3QVD)!9v)TsdI@n7bRDTrq;nb2s42bAmhh`SQ{Ue-Hr(@+bmE z0dj#8^atG^r8-P--*jSjuAk$*a>COE1Ueb%fOr4k@|_bnjAuA~9rX2y7kNXSh#(KW z#J}+pP2le}$NM;a`~92F0YMRp^B9isM7!=IfhmF`G<#l_5$AWn!7u?YWw*aS@{*zB zGBjBkTvj642rGECrG zB7_!ZWA`e{VMrS2#Ny*KztFun&V=_YCo$Vl{U8=W;XtPETe$0E8CBEJDnnR`5jZmd zw~N2e@{#mpacu3jEOASAdvPv<`~3g*%#}Efhf<*3-dKmeVZ{I!e?&5{s091e#^8;e z`(L!|=KSdKX~n`54kELMNiR+{%yEd;c+o8LbnyA_qa;L@sm_x)i?8A^tlWrW5W=L` z@VdhjN^ln>zR4z0-b1s^`(6Y}gz$KJKnWuQ*!8YuwAYL5 zfhW9P2YGa*L*6N}e+7!(33%R_bn`40o>LZ&fdi)f5l`5gzy(Mr00wmRbZhIWyKJF` zfPi-)`--Lmc(!FX7f4R45BS6b7Lc2P&0qo-_D(=s$cTKD55Fhe1Rx5-a}K4=TOWt8-8)e`0!_l-NoMTG+eCxz8Qt;J?{1Kc z;i%TyQ44hFe=)ExP{7v|kWukF?<4>2>hMkhJ3?~BKf^NBb)dt62N+Uy4VvBdi!B_~ z(-~}%o%A#5ft}*fz;^2J^WR`rSpmN7E}Q^0C!hb`zk+)Ok8ajcU6c&8`ZNBdBAr{_ z_>GB`D!^d zwF?7if4*4;&4mtteg<(k)YH$=i!XUiUaWXcfG-~h;~*2=lk#f3aAnVU_LsMlCSkph z5_w5jk+NEO6WL2Kw=~+0^7EuWf|nm<$E-lI?S{nb!5oqp_l;3}J^#QZXM#$|Mwe`q zt8lNcSECFNB=YPIBuiwXmFGW%dp9%>_Pm+Ye|e%{puf`B zW!$&&ohuSbkF`$j3_cxYBQMM<9{ zX8A|t@D1{O`QpX17xg`OwIYZ5BV6}j5L|hy5yCesOyl^&<;Kdy@N`S2f7ZVxAc&_= ze^*@E8M~%VK^~>=9q z;a&g&)Gx0=GwAnW^Uq_T26Y*Xb)07#Xsnp^jc-=C9A^)!4u(LoPQB&*G56%O{JH%jKp5pKNno4FJ%leXi?%N}nkt8l(cZ}@lah3fF~>Rp$n; zT(|+wV_WJ07-7)HAh=`ijM53(O%gXM+h=e@Fr^Jk7^FVZ1=qQ?R4h^OYwF95b1g#LhDvhT52; z3>sX*?oj3mJ4W*~2p2VPxWUua0PUwMoXmAxm~cV*=OZxe!0g&6zUk`B+NR_yhl3ny zwF%g=JKZn~hlqvHU&8RS2s9(viS$VzZyK4=?GU49$;;DX(sssTuRl+?e~x+sp_4KB z4=Nk-<{DL}c<6xl4RC@X2#a2Hhd&dlcTu7}{{)0aZz%LVzauMns)0wJ=+G~tkREgd zFXRB|0Ph$S6&8SqX|2b?wa!}Qei)}(dc0wvK_!Z6QA*+yTzGawy-raw1hFw4^mXi| znZ*XGPL`(*mpM@s>VO_Gf7HEGhPSb3@+-zyi^p59KB*!iXfs%~ez$G{HPSc)nMCN< zr@^ZAVZGeh!$24`^lk4u7<;3Gbm!a6-gj{6q6idYYqLjv*n9xpD|o-*e&7LQ>Ly71 zHvO;i1M>>tHCc*euuRl`%?YUxsE2>Ew9lPh}e=roZG-=QiPt*5u zos7IerqH2SyfWEo94&<}(-;^1`PZb1kD|rgA`~N?3T^#W#`gVd&{Ae=mx%;*t6x349j3w8J}vP>R6_lPi`6{HOM{2_0hr?lFWg8zJa` z#e%5!croK>2{|nM-z$f4=%e^4{DHzcfd>>%Ad_wqPv92c3U4b=Jl?qh6V-b!%(d_# zHX)bqBH>d8oS}#aMmgjx&n7u>DJ?ZO0z;yRRuBS*o(Yd5e*|ZPGgvfjf-u%71Dfll zcgQr%#l1|t{*_J&L?^K@oV9_B9x8*3WmVvR7urjObNeAdrD^c_?+Lw^O{Vw;^IrC) zbK^Lzs_D$!irX$l1od66n2C!(G5iu(rD|<;I?rA{U$4eEOIaqKpK944XGRedMeN+4 z3m+{LF4cnhe}h;U>5S357sej~E4BM^B&5J3=J?+?GKwFNtsuHzQ9JCp(xk$e-d8hiT5!W=VKDWeEANJn=JI_R0W&6G>7Wnq`&_rE#vD$%5Z1#Cek7QxdM+bZWti8*&oHce{ zmJbg)f4=7Tr?L1|=fK{YUaLSyhUZ`iG-jgG@z2MxP(IxY(h+R=P0j&*F;Fl&Ld&ou z4h6o3OWQQNj3)yORf;+!R^=F@t}!?e58nn;8W|SfOVOozQV6nuilD-;)LxLGt(+qE zRTqc{38={DQI{H|Qx*}~IE_yu!>ZO@XRo6Ue@`}^z1Uh;-+Btu=3cLjvTTy>Y;F#N zY?Svp{dl~Y^`f{J`?s6$$VUg4?>2)p&2_q_GEAQf~h2jY* zDMKt!y-BP#p8CKQSyHeRd=7lgLDaHf8&$%u;&WMSfvmFFPi@RqpBT08K@PRhv}pKT zhmkali;K9GBX&g8`~s+3G8+L5w0Sz>f6c2B$eRe$>-{AcauW=Klry<$K?Yl6H3vcZ zm>;p|8ejRLJp3*vvI|WRMXWb-=r>Vz6ZEe_d$FhSO5dp`ALvnOzI|x`%-)Ho-vn2| zCeCBS7&SKF?xpx{%;oZTxf%qhhZB$g)J`eWkKb<2`V+t3JX}p1rVSmD`sSQsf3w?5 z@&P^3k+pB6!MsnL3bUk7bzWFYDl*1bip@ly|2syhlWp;8Jtn;f?+D1_E8=(gGX&DxEt!wOGhj* z1o4~bS|%?XovC-@q3$(Pd(Cvjf2RMRLf{BSio+|`R(az>a4v;9f=gYP^b1P#qPv_d z{!t^r*mr)BXXF>x(XF;7WPp`hQu@7dVM(zd5?A5p2<{s?4Cbx$0<)IbP_!je zd(r;+JGIqB^K>aRPb-9t>74s&@eZ# z;*BdeIjmr*bCdm1Fp*2jIC`TZF5}M^Ahd|@Fd?uI1&aMR5qgXX{wL7ujW>3Zn|!~% z%uWD@zF{*2SxXq*$#jInJ3f%H9>>WoAIAy(+x0MPI`H&iT%w>ON*cJGFlmXkb-0K( zI`RwNfK_Wm3z+qfRC;Kd1|ITdN}MkofJ9}DjN^q67I*E@@za%T zd}KZ$FaHU|Y1wr|O1fyPL0agQj;GT&a|7q7{W_9;!|;VDuFUYjfBKPl5-uos7E_2^ zT&j81rTVEIaaj*zwBsLKso0kLm?Gj&?S^i)bN>N;(dA#rBVdkqPwR0ns%3DjFT5)= zU22O}+XfhjY{B)~Vr6yad6mH3G>`#yyT%f`mr6nQ3Yec!0u;JKd8{p#a{*tI2~hB{ z@`SxfdAPfimClvZe+?An8!SFtXWM`|{zfp^|1ma>`Sj(!Q-G?8o&PMmX#Ye8>`0G{ z8SfKw{DIfkvw4pB;|k#hmJfkO62aq6rb(?@CITTq?DUST3-srO`&Cu%JB4~(->{>} zu1~p3j+suj;1VV*xn(yQ#C{x{e|Bx`N0g6Y-`TO(ciEAX z-Tms8R7l8j92xb<=wzDpEI7Y~G%}0gI|b4Pu`Jq~<-S&8t$z+){><)SHgSIOFVANL zqV87};BG!1d&1y|y5@YBr`~w1qk)&ER!`yNHmMKl+IS8yUkwwfq>*Bjbyb5r&}lyj zCgP2mnYvXCf3m-nngDE!J(?!76{MW*ZPXV^D=7>eCBXmFs)*`m_@!qiDlQ0y3EM^q82<$$amkVa|8ivpe^dfv6>hMQvl&*^nT2wOjIy+} zXusnwC4M1w;Q_8EUkvlZ`eM$d?k^l@?LLXv-B7JZVnXFoO=;=ELZE4oDjLrw{k#_j z>FCI$^Ggj~^Qr+nPIN1YHmUB<2y5LBwHI~a%@vzND*)VMFgEkK3rz@1b z5tqkZfBs4wXZuvMqYtYUD{ly<_IEeRavw8z0L%qZyH>EIbmt&bFvUta5wo=c9O)AC zWkB`dbg_DZi`;S;kG0xOF)6|W+{6g6YT^dmdg@_+s}_qkZ$4t8qVQ!3J#>niL?S=9 z4t&txDY8;r==n~AR;Ca}n>wf$d4!$`mz5vGe?0JUp0yxjSVI;qmsqflXh3#plZLUf ziI2{}1$?M8kkG7Q%trY0yJ$>MJ%N5$?atfkKLpf zdxf5HBIL^j=+;{P2p0gBR%wUmIPC>_$zwRGkZ zzf@zLjs37l0;pk@2jMo*hSeC{lgT7oW+P6{&o3X^ggRd7225iu7hhnMFT8@TwrJal zA?}OosaJEvP{x%{qDZ^gRA^;o47qos133;JudtOvWgSy2XkxGAz5Dd|TE<}}e+`?c zB?TE%o?`-)Nh3MfUz3&e+TWKsQ`OY*b1dtVjF)IZ!bN$hBySPY=ZwTg z+dkBRK_-PWhK9V#GcvK#vP^Xzi~CgFCPwr#hBS(8qo4nVX+ufruNxNL0^jmoORy7Ed1n7`Oj^|lbE_PUqDfOr(?5LKy6I|P@n;)i3EU363rH;HFk|-wm zRX^KN2RvY-)**u`tx%c)=5`=45WU>xgU`Q)VSrhqk*4@63T2*qe|w6;_V(tOH!BRC zY}MRsLl*|>oweW4IPst%e0EJe6GN8_&b?!OEsPmHy?DF*yx#VM*SSB=T=49lTy8(x z+S;9ks+^LT#!Nu0&ba=-;mJmw)Ucxl*WW>gky6BN8(DeE!TcoPVJbqLU=bN)=Di?9 z0?LP2Bee~5hIKG(e}ZSd?}<)EpZ_+pgj4Rs^3(5b9-dtO?_GmqD7HOMD>m*-;gK^8 z4&|t=(9bD5+hXVBVzX!F*}2gnruiZ}h_ZCTy`At6$5`9~UOX@FWt5lol{&T0T#1r- z0B!{aR${hrFG!K6KYal_oGU6t^gH5DrKwwuu)Rj52GsNxe}3?iNLrTXfF4FrOm9B_ zI>Ic}q6bb-Q?geEf5zI(BQNYx&5nS37rl80vmI)DvNS%Rdzvad>XdSz0Ux|-n5X8$ zFU_t}sVICf6D-$pgE$nT-^dMh1`W*!N~)^4tF2 zctw-Ea2i2q#jc#`bmcHAbZrW%`mUw1<7C6uV&ymA1&@Wa_3b9w z%a%d_OanyoWE8`V+o{cC>8f?q{A=bqS!&`cJ#ef4f0z%GU@$;YoF0V_y4MJ(E2@S( zp)-84-d0|Z3R(hPF`l5t4vDFN{$P|O%mO{%pjeoaXM^UD&a8g>^7(X`)s9tf#{v1) zg?&h=ic1y@OIo?mogn#g%cFfEwo@->hhh!g)2(fbZU5-~kqp%yaN?mc>(nRr zf6^k`?OvXpo}Gwiw+nAa9L3{VGc4A7;zc<}g?4Sk=$%8N1(t4%w6eS0v zdw^W<*29mm@sG4u(0c~(yOcfI^l>2RIH8uaqbX#0p~%?r!2G;C7+%o^XI^rJ2tasA zY;|7W0}ap`ei9Ap()jbQpZ^2oZ?fUmf}X8nNqoj8!@udCKil3o39gbDP9>u#e}Kf` zlh*EV*&nY^a^()1h`ADIaR*aT1~|qpJm`wAXe@b#!;K za(kz%k~pHEsq1J8@e?GOdeo zzJ+1pm~i2QlY)$=^*hAleq#yQ^i4H;1t`T>ntSydIWKso7`Odc0o%efVZ7ssA_bP2}d@l5t`{Ti=P_-i;*|`2Jh7Ih$mVgpqLp{N6IG1 z4W?o{+n~B{Edeooe+^aq;``h~SiX>E&*7a0rr&HS{JH8{evW0t_H*1-EmNrl@L<#m z2J@MV`SV#FA9yg;gy7U5e@;|3LLa%6B(oxa712M= zTY{LJ69xx)L`AV@AuF>8m(Cw7bLEUqW0U2a^qMkLZd$pq2;CK&66-uI$KsY6b9n<* zfw(q7HyxpzT%D-@vt*^sNKVldOViUqSp^i{CAz4pN7{}`b=hXoyI!`uj*WoqAdY5g z*aQQ)X}=7`e^F%j3I4(@JOC}ncNi>#2DT}(E3qzP3#*`>ro9rigVy?v>^Q5nxH#ajSFp&i1){5y9!98)pQ9yMMPM%JLwIdHs)KfK~?6elb zZEtOEPh zjd(U60Ulvs&=F=uoOlsBU2OEzO@Qr>(0R!}qyO=>O@APSfPLeq*f%}BujpTe&$9Kq zj~~}NY8O-<>IH~)d1{k6Nk0F5fBTxjs{I?;hR_kA>JsqTz00c@k40;)c&|(^fiQmE{ubL6uppLrlWJA>w zhKZkY=Ms}Rj5Bbl|HJ64xSdt0LDqEQ;<$i0)Y$$-Y6-F2Wv51&YD7q2p@3%^vnz!n ze@O(DTae8B7&*VOa=h{}{J2769f~rfzmsusDj+)_n^Ybu6%YT#PV{kN!aM8-##n!3 zsC!j9;Wf!Kt{3gshwAbDn8wk$m&x8{qOb)vlzAWN@{Z$PaF=bqe+A_OWj>r&XYmzu@gthCEJ(nH9s;~!vcfFT zG_&3^lJ-sawW_>t`?$GC{49tX$ax~a`LnywU&lWE=qz7WR%NMyUMfA*H9 zYiD{`Q`b5ACUy@@^f08xiWs44N*B=Uc#JVXK-br1t`w`7+)%1$N4;YXsj!DLzwiE!eV6 zsiJ!|WOk2yO;SqPM-&i!{x!R!e`Hw>GQ+>WrR;8Gc3429XnJn4Fb@(hh`JcQu7}ci z4;mccE|)upre9qafvjTx06UI*Dojd8o13v6U&p2bT@~GKCNJE`*l40f4ac@=m0BFE zn0gQBh%2116k9lUgF~qm_o^CtJr3tEfqq2G$0Zpf%L?J~!8kjjaGNCNe?|0iHPSfE zI}gMcef}FZW>np>Z3jE4?)l8j`d4R;RZaGoGUa%>Cx-YjXYK<4|~NC-HwB zm8%0z8^>bkCL9d~P6OhlmJZAYF-I?EkzmCZ4jCqqFo>q0fAL7`z@*#MfoqrTIQIN~ z+!3l;>ric~PEE&Nnl(1I!7rEADbXfeaA#_Mq%mj&ggp=_))l*Et=7gaTI#-L&apjx zAR9l$<8dCL*_PTB)>3y%6@X6c!_;Y_<=K`INQ#UFlSIdb0C-MA?1K>WuPE)ZUDWvr zQ%d3KdHt&me>TCfxyqhfEkC7_{96~z(>*-`Nu_-0g?BNm3Fmm-Mr1=)56A#gdf23M znonI)5}3*>p+hKcH|xrk7t`wvHQ7Q9Uv3L&>cBZ6T-WK}HUwW{91e^IR>->%rI)`xZV&rSU6oG)WZ=|WS*sAcp; zTWxA>2EM~M7d9=)Fq)OMhX)Z#vy#dhSRfrYX3RlaD*OEFuwqtGwW@*U`f_~%N?_h`KODax40M6HeQ3y-MmZYTcWLQm0L#B$#iTEb>SQp zD`*w>ZyMZ@#Mgl@WTFQi)gtT#Wh93IKfeuQV5a@iz9+PrTL1;>pW5F&-Pkkb-z~}? zulB~NL*cO>gR2-X{*OPctG$yh;Z2bm$XC_Oe-e;zem+ z2hw5@7u}8LuU@DMSQ9@MR|DEzslJ+^0f?M^F`8ykwJ*dNZ5oE*jP`YKVvUX0;LVJ} ze>lEHEHy?=n>tDxJt+`oi-sB1kVZaRFIW!gw<9e+nYz4=-N^X}abliLR`!ryQMdu#cITRAC0i zRkAfJQ%BFLO{@%cu@eb8>yQbxN#&ST)iO`HJc*$qz?a=2jqFCmz@oxfTPvmJ+%9~# zFE`jX6OXK}A!aPkY7JsbC6r%z z1i~ums!{wC)1GIj&uMA3sF55`>cso_K$&@IX~a%xy;!^t_LJhhpf}6-@yA$)kqUMe zU-ZqL!qAe~R0JiX?>P4JP*+#`3ntvn?W-wT?HjT;V3bShWp{$*+HcCMf5)-CnV~AB zpLg>#B;O{Uakb?Eh1KbF?%CTyglshxG$K(Ykd5 zOs|qnzm-)M{z#mVwCxB_q+&)y5;{SrOA-~4N(Q0)`hu2YGZ@Jwe}V1|r**I(Zr@Z6 z0BOJtHM4^9Mv|Yxr${R{g;pw%wlDzt0xvN&MF@wPO^X|=vZ=-TmuzaWf`NgM&d9W2 zx%?zup`q84E_(T9oXgr1!_ZMd2af(^%@ljB%b>~(OG%>Tioivy3^*f_`z_!6zM@SC zYg|^;2HQ`k^vt++f7VG0U*8;r^0MWycn-M3A{^xx+ALD@-fS#Awoq^UHV8Gwf9;DKm;OYY9{F2Pt^Zz~ zkHY~pXSJi6zgf6%vg%+=j3)9cj6J`bQ;jg8_<($u*t{CW*un;bscUDjwC#rixH%x> zeVmYarwGTGvFu9ez8kw!wt7wjz!vMH(1l8uq4FA&)J4TD>kV>1?&3~3dNeL2FBF62 zCwmkH8L(ATe>QXn`NUP4&@(pG#)*RJ)oc9J-8}L zc@C7|tA#ZC(AXkqoz4f(<*w+?)S22A`G(LQm+PMU#7^-Qn8j4b4lBABrB>?{W^H4% zgc*cX)*_$U2lG6zgBP>&s4CR=<;01}CS%8if63-vf1lbR^%lQhqQ$tWn+qE-mP7M+ zuZCmks%SN6Opgi@A8S=(xf_va`$G4Q^t#{3Cjyv*gz0 ziH|Kee}<_#7)o)j6}JuoJ3QO~ZImW@yg7NucE!We>a}KZbrBa?8c|q45r$~QQ@c6* zZ_hjkCR_zmW8QP=WNv4qu|1L6qi$~|%8A}+?guyBnZdAr5-FH{m1KvKiWAk5d{Rm$ zjNv?qhvIDeLxTWI{$?hQk*|l>!;H-PUHlKqe-nO|i!f4dnw3Co7{s4JVobChT?1~JUUJ)+$Tt4RkT$$bf#sDTzJmp{zPMvaV;TNip*mB#&&a!a~%Z%<+NdMu){+Wj~jPj1J zH~qMuqaK-V4kzKpc4rHI*%@Wya7H)8f75r$_}&y%m+Y(Kps%BpEH89w+jv6jb57)$ zxVAz!9%OVcdDAEvv4`^d7&M(UHKNlG-1RD|Zxd+Z*c6FuzHDGS^af-#Cocm12KjhV za*Jvm8KdBCtkJX>HPiSe_g>Ee!09xYss$H>d@kyAPgBPyR;FU3Xskw2ddkb z_?_W!^TXZfFLh>1qhn4IZv6HBvn^& z6cs`Fm1bL}_05>x2-`l0qN^kI7T6bi`|2$UO};mi7?Bedg{e4{-~)c*e^Kd}vdI!Q zCy|%F86}o8BdcX`t=?V(-aTxcMJqB!SnHNyqYL3MEjVaY;rp4Lhit=YI5~yb`ho4?GDYqXTa=-y zIit3Sm!$dul~Cj^=PE7~gOQ*b^kfo5e;Rue_|LHO=QNI<>{#cyf5uOl0!HUSgk@ES zk=Uxg8=;^H9BMkc-5RHX5e))qRgy|yQW7-*veuo$1zAzjx;D3UD=8r!v279Vg;hTj z?NNG3QJAXU?0Kw5iSeK!?Pw~3bQO;$psNBywyRRKN@SBqku@V@u2WHg%K&S|{-{b4 zpqCb5>Z%B#uGwZgfA%e$ghHRJ8C#}AcfEl_mBn=8A#*D$$L`cI$$eZlYY10qCWT5~ zwHH(~I*sBfv5;AAY)RdIqCq9_q~YXyLFi-tSu%$IjU%pGQCmm_qeEAM8;mbN&W2=- zp)^<#_0ME`sGFEfg$uO7_TZZw8jO4s^VX0HwXQqL>oBW4e>;A2uwk|)Rnh0aa{%$W zIdgBAc;f@74(<#yR_8!fAg%1o!sHT1Ll4M=kyW}T7vJ%{S^bQcuj`Krx|gmJUi6be zQ8b$En_rM~*;L!Uh%@=77J~^Cl=>|PC!Gbhu;0jlzu5vU-t*N#qDR)!0?aiCMb9f$Im{B@OK!P&lsD(A>B4EjIi@DRY@N*!0S&)H@W4r|& z{SNqY@u_1iq+GbXth2uP3owAa1Vbt#sfF(cF5;vPHJX90iXMUR^+lLO*8nDG#ez`# z99U6)tGFD!;#-yL@u>~MMSy9m_aH``U1Onoed8uXe{lMCU}J#xvDi{9v{wJbg9E)} zE(jF^baJTgHk{~$0fXkR9ZInA){SxxWC)NXz0BJc-V#oyZoDKQ)-BFjl90l6Tr?oN zwlOv}u6q#@;-HG<2e{?!G;QwOphl{I1oUC&|m_d^k5M5 z10D6HPN(DwQ|r-fNANLGjMKIn%BgRw!Q-;81)gGG$KJ<3ErIC7gij}G8l^!eKP!u( zc2)+9_^>RJ#$k0w@l97}^1@1g#29(C39KQzf2)?TqJKH%8xq6}eHf59HGZK|9vqy| z#x!BIm#3vSuU9fKL>#S%w~mpMdE2EdV}Vz>l2?UidB3yHR1R6 zTeQL95@iQZnzJWN`4SZe?UP@}^bc80z?50px$iktDzS&fMu2}DwPv}b^_BFKe_Nfd z5?A(%ZW2NiuY&J1ZlL?E^rvpTFJ{mfgX;l*Y5@>iK2xlAb~ZTFUMMWRZ%MR*>yUA| zC>!0XlHsBhbPE+gF8kM*Nh!Mv^ZM(ues7#!O4WtElkP?XoW-m64mMwdPxI5Q0Dw+^GmwC)tgGVTmhlmr!R#C1TR@V6|j^cAzxETJj^T%?Jy=T5q5~uvhjSy5(FxK=rH_ zf53$OEVVhdl%0HsTwW{Qe<5kp2j(sZ}Hkf41MkF)f8}{_Uwu zao=@*$MYt)zue3!90SzyOGk45K~wt{)sR3@X-iyj&AKtTQ?7V$pPCcGeR9P!i%Gy1 zbOt+*)?^BcKH3Wce~hI7YwwowMpQvzF(GuSxtwmp^>os70MZ8 zOo*yV$!$Lrii>m>f45gQZ;Ii4YDse|JXN~KMH`#CzTkq%vVqV1=Wt#owWivKeeb_OqKfxpG2aU>N@L+zs~fyl4RI1XwijU8;(v1+wO= zJOL@%an~4#4QZF}oPcfGmV1OMLu?q3TKn?l8n!q4IJ@`be>*9P2rpmi`PNi6T+$t6 zz$)G!$0`^Mq)Mnp+Ms*3sxm;8bH32MIL;i7Suunmb7AuV99T(?cF2paGJs_+zVn55 zyXe?+DHg^Uo3XX~vBbTg%uHdCV%sqDI37xYcxahtEd>fpdNtaGckX`?K7YU1tSlKF zoyOww<;^{0e-LF0l`l-xo?0P|?R0FR3~|yf4_#QXph+xTn&4qyP4mW}L&lUa=zAc6 z+yohhoMWt*@b=(9<3JTR55$p7nRynAJ-Ej8J^{i`Comp}=IPefQzz*_VU)Gc>C^zc zEul3+QLkI+~u!;#h^q-3wq@^5k1De+ab?;A(=I8_EY{1&S9kI_9Y( zGamNi&-Z8b$v8y;C8YbF9Cue|rn>!N0;Rn1s|hXZyt#4B+WLlb*(Dj9I%_Z_EeOx7nyz@iXaR)FMk1)kncB zbMsDP>B}g-v3%?K+=(r8_*JfIr{eW%uOaDKDK8vom5>BQo6kC;mr}mQ%tvKOFvMKe5`5bQ93H8DXo0_; ze}AT;8pt5BE0Y_$ER*?qwUf7XnfUwo{32?YPd2K&K+`iQKE4WvD+($t`|*|DU#zCG z^p|CfvhfYf%JPB;C>Y}!XJCElid9psplqbl&`U?EwYgs4`dTP9C0Rvyi1Kldx&>6} z@a2ma&t4RGU#+m``*4Hd>h`g*-?BUZf6XdhC4|+e?u9TbfBHiU$?oNf zC;szUxBQw_w`Q%2zxYb}S|6P&&-bx-m8w80PM&q|m4mksgLttDpP?DH^t~rDUEGGORl)!0wj#sA>^KRWadPX>64?gEbsw z0Vo#5RmH%O`oWdnU{%A36?2r1@4KLJQzRpI7`1`=y;cfug1G@i>&!G*ucs|o3Hl%p zHztU|f|5}0gWRsA&NCQFLYC0dTEE+x(LDXK&d!|~(>o`6tcSTCeQtmE{xAM`y!&=J zI;_(rpeRk`FcuJ`U(R0wR~RMb&vRF}JV=S^Qfv8TyXAJjz&;sNWqPS^&D-My z;XZtJAO`A;qqO6^h>vzuL4lwJlK!_=)gSO6qf_O^&zG)m!vrJ+S3rA+4i5Q{Oa0fk z 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%0D4uIY@nW)E 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

W} z@o|`ii=m7ghZ2A9SLWv<`jHY|7nrAiL(*0Mh0cCezR-O8Xs#eV**_GG4SRSiFpj^E z4ohf%ZX6`%xQh*bN+{Q7cc|<{H|&^i!J0Bv*Yc8Mub^fpmM8<%>lEHom`z|ztpE4_ zS+aQ7n>stIgQ#;WcW*eqd$T&0;N0Ltuuo?SH@ALr;2nRr6?5kZ_syN-ZkBeG$u8w5 z-}CM9d$Bzl+F|Hc1Id$l3QAMm70fgeTbQL-R!i@^zpt@Fnf?;#FFo;c`GdvA`we|F zCxKA0zppno6t!?puRu=_6!lliJ1Rl-vZ>WqLfcbNYFT8k-?-0^rleZpV!v_I7jmyj z->IipI-q~f^_SaygSonHs=W{}?$2cYz_I8Dfkm~}m?lnB7plu@ES&|bW2t5|bSj`D zhW!PgAViPmp^L3~)mpN|;9&({E+Um-$r8t;H2K)u3(n{*2+kilIDec4&N{}`0HvYd zGXPm)TNj0^;8z#r_9P$0fT24=ey)L?gX#wCj;4R9w!SWG%zH6ECC3a2wzikrKHezT z{wIg;f6juhR!;$DIvb54q1iQFEUegkoNa+OlfxNgPDXwlP?xF7EJwA)EZile>8Ohpwf0 zqwlG^=AKHHl|Ky1dqx#Bnpc|lBF+}jPQ>nDk96bJ&FQ`Nq@sqoI}#**-q&DxtPk=A zUP>F*94!g$K(%lvDN5>bI`E!z(ZUNCS;NNtU zYh+v=Gdr@DMP_J6pP@Z{tOrdqG((G5nxg_B&^fe-%$gg_Ji=~!L5*q0thGa5$oG*N zSW+K*bcuBMB}o5J1OHG=vuZ?gaE6Gj+n?4H*E6%_Klr3tzX1(?x6j`0BT%30SI~dx z#Z1h(%@ew`=8@?0Dd%_JJPv(iEsLPfYaM+Kb@X}vc=RbjWdR(z4TGqEmM6I%o$<2s zkV&?2KaBdTz^H1I4??Y`en6Yn=ie?ZX!Pa2QxGU6uWOs8!J!x3HbE`gp1EJj9j$}` zf9{+5(ZUL6>_aIFU$8jDG7vpPT)|0c87ib2YuwHC#%>tZWRUL0P z3qo4AzbR-s7>9UKu=M@gW{?Qm8~n}#2~_hCa6nfd3W_Rk@~NzsYckZ6mQH`O))Xqb zO`)UPER=M*bEj}4b(&55O=>(I`J_ezHM^6_RHJoKD1hMC)2*R}Q!TfMYEHJ?DmqKU zSg5&7kiOFQu%GppU-XyXHErRqO-iq_PMJXQ(a;wLJ2T53WEZ%X;{lEtdK1Q#QtnKZ z)wGxiYB}F|Jnjfp(&mpda7TZ5LH~UiBz65-KZQyLehmcTUE|0j{KRJ?jy!;&qs}S& z@q1^Adw#l8?|hu>2=6!y>Pd>TDGM9&`jCE^52Kfue+1vJlN9~XvWEQ84!Q!10Bt~$ zzl&U~Q{f)dBlnocoZ2xC&lrbYHu+(f1MXab&KdN6Oj8oZgT!L!#KG%@+}tI9<$W4q zRtW)omZQUG?sbhv8JB@&)sDB&aaewOLsjJ2JS4uYPI}av46be743obSKY&sN)>3_O#O+ zch<+X!vF@6LCb5uh502ckdFs{Z(is5NTgeAXPi^uc77rS-G;*6lC^rCC%!$7fpzpp zfgQwn5aa#f{;r&SJ6MIvRspl|q`bV|Ks#;%%X^%L`R%$t#G{!7o?t9AjApl=2Sc0IMdctsjQR1;l%OYn+NX0q#^IJ0 z{(@YUU|5K?5dE>PKJYMwCfeW$x8lr8eQ`h}OFy|Hu-rHYgP87F%A!aZBM}}az2>;m z)Dhf%*^X|9LY@d6HB2aomd;=nk% zg%vlHAgT2q5tCihAratzN%!v3Jh=+{r|HS1QytuX3@S9wqbuDpGzm$;LGbasm%B`@ zkxf^!E$G!Hi673Ih@~r=V1;XfDaYSgVFruwif-*fT#+<&Wo5aj5&aI5_?O@o<18BP zR6uA5WC-G^2@ICPmdAZ~P8qIeJWXp`pMzT%E{l)|?LLY-b_Q2a8D_tV~JGJ`t69!RvzV{1f;$Vj6j~e`Sf|SLG zopbLB_R%N->dk8Q29S3@5ITEr;Ntn*CP{6oFL;|}#2{L8(M>=Kj1gKbw?oEZmS4d9kf zpW;c;xo1dm`uaPivm3*my3-QoOqLHzjCPt1OI+?W9+nvFG#!?>*l9Q{akcX|9hNw6 zI4p5H=di?oHA#cxoxhv=EFC~7aiZFq;7bCe>#MLA$aVRDJ9AovPuC<|^IV;K6m44J z_6&dLrp>KJlVWm)roW-N_BAC+btrNSrb?>L>d*VIh{?aHm68#ozw=m!!r_kY#HYVp z>Mw(xLPdD7GZz>L$lJ? zL3n|OBan%^FV|L$GjpKoEC-s=zs!v^gI!ziQSPrAXT}_|)Z0|FG5r1ot;fWHl!J~d z5z^p)0`u8iU`;p5+@LY`d2C?*zcU;7)s$MLbB$3AuhE;M>FSmE3xG1$+T%2TwS$fS zr|}i_im5H3nSx)9P?~FkE*vpei6okmozu7#1KK?MIQ99CbpOMWoEy@OzC{Dv?-83R z+kDMcN3X&)vA?LUYep51actziH;qI35TxgSu%*69E-uJxfBvwpc!{%!7hww?P+Xv@ z#|%?wR%QV4bUrZn{Jw_GbP+&YLcY0^TvBAstLK}8p!S?22g|Hw^&(9^2Wc0Hbg;lF z&Y%{rJNVPk&g*i@b3(g^6JPh@=LZEqI<}Rvj~@ z_Q&sM&#dh7*PmIXDqmoBMczfve$1+)fJLE2zXv>Ix4$|(kij}mMqzIefaI8Q(E}ol z70f`3r(3hZpnCiD@nVbtyDnv%*oDx4gBLjqMVUpQM0GIZZ2dnQgVEj@dAqV&h5)|>IAfU2UFa4W5$g}RHPlU`IU~bbGjz(1 zp!4f`$do@C2)h(*d^cE(ALFsf=ir6rUpj;AsEYnpw9vr4FmQeeZtnq`&%q3T=(30B z?0WucBy`t-R?3eEL+BkWtnt4oE8gRy*nAtl9Y?fxgXWpVwV#x|c|=kNyfs(-NfmvI z5$(tQ@G9(&7n7r@GodO%;a8gllr6#5Z_QQjmaBZgNFPX6(3Ub!>nx9rN?Z3A&FEhU z^2O1mYQVfEzrdrNS;6Ul-;};C@jRD!W0QDBwt)VyKEJQ@#a zLw{i8R~}4c#|SC;M!xb8igh6+Yoju%4pxIn>fsW7cE^!OP+2+krd<@H`=8P1i+Z_k z0qPp~QC9idk*CcxS}CwqJ50Ig7|N#4hm56c4E=YH=_p9A)OpWR5W~9uOUsuQa&^(B zxLs<|hS0k7fJRDxCrTD*NE&<|(3oa_ShO|bQXc}mvWmaH!P4AvO*$ZC(5uD9vgUs3 zJfLf)g8d))dD;#ADSQ-Ip^AkD)89`3~L zaPbwUYsJs7HqL>HsD6t3t&IEh{xDV=?gs-~*LMGJ0QW+FVf_%uYkKGV0KTjoJ#C1i z%9`09l-m~MdhQcfd)H)wKL0zJAj5g+-%Vd0u<;c-ng?t#k0HsrCgtA^H>oXoI0A6} z$-jFXg^r|4+KESusG>Xh24i~=O%cI>z5!V|Hp$A(W625#e{s;0H^o1c!1Q=64b){< zer=uuRlx&)=3$O<2o-8vNS1n{PWjl~{(}xV?EI~V9O59qPSVd``<%mDz1p%er}S~I zMo0AVTO4?(41dm<2ea#6`Pf6HGo_Oc6*&C&;fHT?_(1?#+zG;8{`&}oaRfs5Ych{O z7)wN+f-u)j^Fas$^1p?H@Ee?ksI+mEg%$tR%35rH0nisd2T@;owk7ZZR-(#Cn!ItV z{h8~FTe+L5sfPKUxQ`uKlPi6-PjemVOGg=+YnV4zGkB@TT=i5t?vmZNQ#ykCAeeIY zwR71U&4+3qaxHo^p}zFjG8NYO78t`qZZqudkM2b_9^_?UG`m0A>Gxq*%;p0#9Qi!! zqURicW>oQQ2@25cp`U%8KctvvT=a-TV+G%KID$cY1^v6O20X2H>Vjun^squ>1>bm7 zaVEl5^Dr^97jm3!E_&RczVzQUn5~5dv$g0Ef%?*q8%(n=!*UZD6gRZYpkD{jFOAdu z#$-#J1%)=}FNY!IntK|wd;(!+HD7B}m<`c?9sIj9`u4!w86AhifQLYw151CD^Q?;> zeV9>6!`YS%akk~3_+R;Ki-~k$3=$yWn-PAwes~Ldp^1|)W(4M%+0%V?_6wgKb60=<5RQYaZm2V$X<%^kBzJ5rRhqFVYkqLMJOJ`?<<{7s}e^riu zl{QL~5hROg=1M}BYs=o9{~7f1;=FD4&%* zQ?GjPK)q_Yj~c&OZ|pGO3Z&vJ94&6+D7SXt_wL)=(C-<6Uusr9Em8$#1@-@9(MR;9 z9*5Xnh`(+bRO0OnY3KAYi1xumv?q^$MKpahh5Wt9?8_nD-z}%M@q41WZ2dv_UYbHK z{oJQjA;>(k>9VA9pB@OI=(r-AGLSBGfCRp2a+_cV~g7&D;paBqT3<0dTVWe!P@t) zV(He$K?>^yw_?;hFTN~F&;pEUTnbNOy8%#83f3>P`auuoIVuH#o3%JZn}5#=F^rOj zM@Rd9_UqV>!u%F3dr^Q!ybX!+th$ zS#|>CYVB!2eAEJbVJ^Vek63_zdKVwM3L&{+8S1q+u0=i5S6h(AxE5ZOG?IHSOTDcZ z*}WP&e_U%~Rm>y=)g@HZj}-dhFpNChBiw#?mkyz(dk`ks_&sUT0-h7|PPAx(O@klA zg6CSK_5T|jZ7B_9_UV@QJYch7>Kf*2Kj^WR|5_l=%LVfMIX{okQ2a)JUNeXHjM7}C zmYGUd984s<462f<@+>GRKp=fQ$KzmJ>v3QTk6+>Fj#gK*dxE)N24!^6Xrz%7wRi7# zgiikvb^Ns>RJf0*<0aM6dsH2N?%dnu>r{HX2vnPX0S3m>?qOncQ2nc7Vj16MC2`a z*lhVV-7{U{#9~eMzP5`Sd$Hf+Yj`(Id$6q1Xcq0x#hmEmJRg}(a4Jvf{fI7!L+6`e zZ;X@cqN5ieSitXRGLx|3or^#ol5scgdH+{7jajLCw(vSbawluo(sub> z7g$23hH*%qrZ6#obg!AVEtfwP+p;!`9^Snrp7l$$1VVUn_H8{CFYFS>pUm`&BHdZt zl$S#&Tq@P3(kt~9U&=MYgl+~;VVD)j@qU+~ zQa$&lH0NtR|C5iti3g~TkmVk8y!RHu+*QKZ)j4?^y-Uk~n7R-==tkSGddRtq!9nXb z@KZb|lf?Pu+9pHx;}-s2CSyt_+acZuLUz)$Lh{lb<=sb-Bf&gi`o1? z`evKTMT2F3ye?d+)!P5sZq)kXs{!+WyH7vRecEKPYJ?ot_r{PQE&g+bo_r)c&GONF z_v*Q1j%sWFRIY09kmbS1wfy+@LEfYVB!MAVU|cfW z!p_+B#hXU9`I8(PJ>6q$iZuLgQk7rfRqQRsfRD!U5!Vv{ZCSOJEOBw^&(*_C)gl#| z$8}%i&sx6H^W!5)RoNNb04UdXAS7oKEmJFh!tA`6WZ~ttl3d~Qv`UphXycb)(w(Bg zUK0sx>g(x@!@RnCW4>2yyX4w9gE$=c@t8{*N4IhBGEHJ}k@>eJ5Q2H9`_*z14l6O} zxXie49D5;RmWb>04MJi>`mg*m|ZEo7MPR+%vWwbd#It zIJ_Qjp5yg2%dzH0@NMGig?omZhrjoKFgFj`zuDp2w29V)w#EGbWGgk2)Xa7yZ~yR? zI;i9gM|nd?v006B1@SzG1J*p{1%>~bK|eBc;-kk-{7k=|O!w<4BSFQhN7V_NO4*r5 zEy5%hpC9AJBWu$*hQ%hA7z3Q)`=b8iG@M53;jxaWtC7V4P>^&rm&Bfl| z8OG+GM)jaKPQ(1xH2BN#1&6_Z2-qeho^>3Q46#rj+NS(aX|NP0`BLx~Mo2H)P%O3L zu9C2CS8OSduryUiQ!+PIXZOEtPTwYEkA$?ku#MjSJ7|w#LMpVhrU}V^T*A_hR9e{0 z#v~K=bl_SGMDrEQNoE60L2hOdumz1uGphI|VQ$QUagx_GJd-dkY6>b-`bQ7qt0;}? zC$yw8y4+jPep0F}NNKUr$(3)Q87r8v3TD_S$kna6p*s1Tpetq)J ztbXmy?AO6V0Q_NZX3&9`VLAlXF1Y8B@$5FsgW>$MVOSL~S$*z*1iK%Tdl(5Hz-%4e z0?UJjDd>MD4iCIfBjERSepPR(brI$uGh%?*%eHly=g^n+bx6D%p94QOT)zyWiy%nP zgLN{s9KT$!h1v*yR^U1vH4nJTY= zm{k&o^(iiNtq<3KbY7VXuk!~gyo^51I~;F-^!{h!jkb>ZpFePfTd1|H|5@PY1KZIqWH+!}TN4 z;V{U89QevCi8mISpcD(FRN!pmf%Bv_x_R$LHy=e`keA#|#&JILJf}%!1{Wxy_OMjt zNaww~@tz`oX*IR3ZU5EDCr$N2r`un{p7`z9m2Vi}=2CQA+-gyb;%n^4U(6l(?14M7 z(Z}7(aD-a@Xt5m&a+6TBIPz3lI}EPE#kWW(kctA2XmdA7gKXgjdzky7t&PSn5!r&c zce_}FqwxR=kQ>@+ZIuU?Ult4saS#lX^mf5`5HB--J)?{U0sNMGM2cIMC%GSqW%QOj z^^9f2#u+jE4WCP@etw9(;Q2Q^kE>j%mG*{v;OCdE6lj#tI%DnYMUL+Z&o8<2f&##*^&e;}DT%$M zO#0`4VHjH!piocqZ!Emm98aX`K?PI=7+%8+9q<&SZEcH%vF`54vKs7>h9kV!0-6D= zXzP3E&*v~o&TlJB$>+K{c+oi}kLycPo6=`YMBdDdSFU#-s2{P>?=W z+3duLe56^GvabW5;+PTt?2?mh76pD)4^F!IM|uj4i#H;_{|q%;R1U z)#UT4n^oHs_sf$FQ^HDvX&H*GCCc$B*AT&ZA&aZG5w-agIba?f(ih zXQB4_lx;l_QE0RL+t7rMp&l|LVYFTtNn~Y<)uk4hyZlh3TA;y3l;V(mv$45q&BmTZ zK-7pksuk5pCFfQXmc5TZ1G66M2=*|vs?eU@gX68I8yh?KC&Im9{{JT;JR1;yk6K_R zq^js6zAs9L%hk=Qt#--Wr-H9dhwlZ8Z$gJ39!rjQbl%~OBg$2GenbB<-{p`pe~VKb z<}m6Ha;UgGTq`d|BO;*aX?H;OmY|C5nyPhcs%KqTo@e7~f*;}aR+F79Hf{6)T-rd~ z8J}}+K6`_ypgRr4yg6NHi^lVR#Tw7G#y1^zR+*XIY5~2-_dyc4c{i}FHOqPKZLF-= z4;l&8S9I%M&8}`WgHKLU;b$3p%2Ey3*wU=kX)sE%d4}+jp9ZG6y3)Z_tJP{XXW@3U zZN4O89f`?tHTf)^PGyuMz(5SOhVG$zX_}-`-@9Rbz zW?yILw0x*hda@@RF&|pNyFKB8 z_|OX8?ulTP53S(E-n~zC9PWA67_ev%#=yobd#GSa220mr+)u8z|Ht}w%l~6(>AR(? zaj?d>_ob`vTU)IkDAn#cOIsUTPnY)kW5A_k>Bx_l4*WC^;U`IdlRTho|0@c6=)$#h zuz!pR>l`!R?|EF4)dF-6FXC;9#!G+gQM{2mxiQ>Lw+BF8`D?lawX2Vbm!G7k+cEs{ z{@k+@r=7Hh{>To14}`a=K+kx}v%lQ)CkSa8rs7J7KX1pbJnnt6FEl>6a7pbE?z z>51)P0cFpkm6eR|)RBiYA$q>H2`?!W$Pj|SkVLR@)N8c5$uh{(&VX~^uDT9%B(Ot3 zf=m`pCzGOl=0N}CYsOSDhozoQM8sJQppj{w{RTU+jbl}R`` zf#V^2S|sF7QSA3Ug$iPwf;nD^zC!1~`hz zhZDY1$J|YL42q0+f&&{N>EnRq2P{5d>A|+0A9%sRWMT&gKm!0lM#;#t;194>2ZYe@ z27fJoW~T>Uj5%YG^W5)!_ALBsK}{`y7VO4-&k_%mIOckof~X`1-uk~z{`HU3)qkuP zG$}kVMqCiD!ci({?}UjnNQ+yS}hVytv)( z+j8CFAyAcvRu&H+1sE4QZcG#^5!khzmx!C}sH#9dh?cO?7igu0ZtX@Pq#Q!kHm7L~ z-vk2Rod=ixRhR%xXvs%LutqvE$k$^2L(6rLPbPRn1b{q+Wj%tA_?OoB2tQ4yb^`c+ zJq;q*J%O>^wz6KDM9~XTGXlPG5M-Ngi@D_GjuTi3ZaIn5G2Wpz|Ea?anGO2ZroT0K zqLGitKuH@oahRN2oc}y%8*i{=vJlIx9ru+^mG1&}%W0nPe+C#ETQ~9-$H7P!d3Q6PSZUl4Xe*K$L-E#z044Unb^Z6*di^{V}o^a$lnNz*$XK zlLC~(1IgK=q9lPo60CPfJ=}oxcq9PWhy?L~5egE}hcQY3%Ndj4=lx|I6WpYK?H|~% zcwnBj-DyP2U5uf;R0G&-vfJT#5@~5-`YwUY(bfk;WdwDu=U{ZcFvFv@d`pA}?x~KF zjMzf4%n(IB=>N$nDz$vfmj?YXC6(LDG#QEtr)+TG9Z@fcc|a0EnBV zvXt6Itet?J3=U4o+egRZ+Y(`ag?$WgHtyj-srbsu;D9zaoK;fyz@lDWw)t_%6+9Ye zUmbWCNcb-};Xk=LX!++!3ZwA8KlLo}Y(bClQxB_;@FLWo;G=qzpZjBAs(Ppb^frHl z48^*QbSr>kv?eSs!3V7_7cuL3&b3^Ul{r=5`!ql^VoB#rxj47Qe&*6Mfd ze?7UI+Rn*;ov!~E{JSW?cqE{K>mdeC3^(}g_*t<{#?R#5x-H3UN*n4L?)kW&Qpq-^ z+wtn^RBc$ilq%jfx8I-8i3<1N_S&Yq>Bas(*^}Jrs7R$1hS8?h|w1g z7FW)n$ONs*gLkZHj21qGLk=b0_5SW6Y}jSSWZ^BLV`Fm-IITAfb7@eIK8s69I45&lU0(>s3Lh~Q~pe5*v>(4P<@V0a^54~ z`+j2;c?`K?+O4f0fbGW#*&tZ}f#bjmm-RFFO?i|!#dDnj&-Gvb^<)LU`m0dmHf`Sm z6GRnaL*hUVkX8qOK6yO=;pTzr{K)M=`bBUva>r9I-43~UB%EHFicF=VsPBy@lj{TQ zdLK5P>jUQwdb0^oNN?|u`W*%rdp9Hd5Bteof9m|TM?TumI z7>bfh0NN#@`q=^B>ez7LZvx?t;q#J8EF>m^eBqCA#Sl* zVBE`~2t$juRTw%W#R@XYDx3mtU~0vr9t^gi=V`HG0#drb)^7p53{O%dG4V-4KBlK0 z?1V|Hz|OX=1^}^CX=Ip+nox99QD&$bnH`M|B!Uiqq+)Uky#$I>Vl|e^1Xw0<#Omc+ zvIBVozG97q8PqJyP+w(!vC6Wk7qG|+Nfp)3Pr_3qUr-T zOD2RR6J|?(yyT+>YUz9o#4pDrj9O8&ykvj31WRpoX_)lGK^UYNtX6?0J&&4fM}=a- zjc8JTd$h4EXVsn@;4$dC|3d0kAg|LE(W+>c^e6kpnZ z^Z?Y|%`vJ6pD7admhxl?;R~WFApBrJ5x@~_8q+;QkgY;_;=v-tUvA?{&VaXxR##7P zk41?p6F_sa>yTUS?s`5f>kXGx!y>HW-W_J4C8K@vtf$qO~=nl`YqDc!f9&&l{hN zi~XcM_Qv`mTg}|qd+Lq>BzhfB-NhQ_2+0ddqX|rlaPi^OVhJS!pg+|3z2q1PvV>>v z<3SjQ8OUR^xPTrmLMDLCk|_rK@lh`wqoxPAKoGY;eoH_=4RDj}GG~>wF;f8{xq^8pImh(~)^s*%@;>es}4-`^$4@{O6+DesvLFGzN5y(=RE%TOB zSlBup6T2=+wtRF-Ds_&sa$>^@BqabJ8;8{wf>$rCPC_)7S71dmmt>T$f zduu zTOI5ACS=}8Q3~sb>wv}}m}>Xj$UbmX^2DB~%WuT-ty|kUX`V~zITHF zjad&^A?Pdhn**Q^bZY1gCll!JGBLwL^E1m(9gg@4>uu)%QdU+%d2q;Kk-|D%6q9Ks zF)Pr;ELc(?U$H(T1dt_vAPJanG*9)J-LKBW%8G7WsL3F;ZBT*yh+WlQy%QIOP{`Kg{XnJj-$MVxI1@L*dT$_5K6gF&1N92``J;MPB)3S3>c z3?$>OP-1c271LE*sKA5DZxQ$HZ{?L)Ma9`$Kl~@adM05Z=yk{mx0Z` zFDgj`&h3O)Q38{nL1iRhB36|K=a5XrA(T@}n*)l!IBso!)tsh1U&#*e5quoL|u&A`W*nLBY0gPcZpEX-eMM4hWHPs;1JsTvW8DnKQ^v+8+Z+ zk{8^(C(AK$YDh1pnox3ZFwZF^a0sNS)C#~*5WF>i(EVUq6}0^#ONP*!+;K766*O<_TheLc=g@^a>@E1hw1{=}yrH{j_veELt!_lcf%;pr7UVTqsdX=_8b2K-`xGREkU?+^EF%UTT_r40Ra zz^sXXRlL84_Y>{?FL*!Ig}>kRTB!+@_Ym^_$JPt2^v{s8s=Ys<98a~Bca-v@mhva0 z{Gq-72Jd&;`zv^#YVR-M{a<}(5M@Zj;jpI%m+UcM_b^-YImy+^mit{ArcsL?;+~Dky9~C5|T0cbHA2j2CE=!EJ(0KEWw@iO4s8;mBmMC%}X)`j0h7wxs z4}>bfPm;(`IJgx2{&q}^dZ1(h;L~BdkMO*P%_FH&y2B_kj=tYx2lU;$_n~m$m4FQi zlxfS9*H04IW~ynZ50INVwvEDmH%*3NhC3Yaw8bDIkFdp0m!j=uQh&EI8+fjN@7dh| zE$cDM6J)Vql~+vOZFB;2%R;#gRl-t>K*}xe>k+SeOVo`{M#vb0L>eN$Umct0q5qr& zj3)PkNGk0$q|TcHUj~c?l(k8^RLT&jg+Li`U&65`;Ls9$zuf~)RqDB)0*F!NMvnQw zaI>-IZl2MK!}XyiYOE%za6>hJY?eN^Rp`nHSS`NJ0TBvjo}LEOiO4b601`)wWNch|dO=6UnT) z3Au}8+og%{!fm-w>`;eFNKg^u0b~cLL<%Z&VhP1+tTU9kG$mx;AgKM+4nLp<7asf% zXuY~kI0SQrr*Co!6+%*fT%x`U{gRY5f~hn@gUG~vka9Fp!Pa_`uWg>fmKwK2X6_mX zTzqKyYp!i7%VS6~8Xe`af7xrDqrgT^=Lz3veRPTvgY%WM^XLOAde*iIc_K%<1tcfv zW(u-ucoG&$EvY)l+KtVL>Kxafk_;OrPF|rPl>y!8=rVaJ-nPj z1w-Hg_GZ+m48$aKri|j}$u?weOhn*jJ3Z2$gz&g`v&;KWdLG z+4#7c`qg*`o@-z~UBw53=^v+HEtRfG}PjJetGL`1{liq}&!1eWNU`yxrAz>k?v=&?&jl|;FqKpP{S z+lz{lY3Pfai+EIQ-nz&v>9U1hrPh>UZ{Z-^{s+7(emsI@c9%s-o*{dpSTaAiiME2! z&J|~wn@%Nvfi38!eRp#C;s5LG+LqfklIZ(>g~X&(z-ml8`>>TFgRrzV6T2cNjwCbA zDq01iAPFlHP;ikfQ~3Ao(>*tUpq*@$%OVCdgSqzf<#Z1a@UE4#ZmOpo>+a0#68yN`b+mZ1F zy58J>E%P*aKvephq)3?tl+ZEz0pt4Ds{S#c@nBjjJE+;R@9Z*;`3sej<(xI{w4#Mu zwia$OtF1+HspnAdWy!8?(e>vl$aHufR_3?d@hB3p5Us z{e8Q*aQv3t!tjVTayTrCrD}I2RN)Fow7wpJ8naFKUj)(S73mqgeEo(r3 zqwn&vLNL7+P0$m;z;)Jt$B`MXMHKJtwRqa}Ov8fAxjiS=K`&gjwpt{BK7?PeNWz+= z6y5{yvAT@v$u6)gHA(ea>D?o#)svb8|4}V8Wfc)oS}0M@U()QY^K70$f0eBes}(FP zyF=Ud60x%HfZOxtHa&|_X^1oFitVs}aV-ztc$nTH(--p{C%MB?ss7?3lrY{Y;ckK1;zSobYa(llwin8tJm zq_Nu8Wmb(YWtuYXNdHx+-2;e!+V1U%;dTcP2dKYxv~t*V%Jl+U(AB9J1R0)=Gx<{ zE&{){`yO~Noyy6ExX1?p^N$u8q4fwBz?1>2B3k?&qMl7aCaryLQ-&6gW9KG?l>(N< zC44amCq;CFvq5isBI9O%wUSiHt4L-|PmxTeEN)|EwY#;LtecV9a>9)|pP%;XUPSN; zClbB%PEE$X5jkSGzyb#3T|o-#HL2?89$YpJTy3WbV4@p(mN-?!Ja+Y+^+Z!gRm&Gb z607+GBu*PaipY;5_8@b3{ftL`WmdEUu*jl}DzzK86&}9hMYS=1-)zY5CYss{)-GUY zXa>bDoZ?K`Vy8Jc!+%d5R>7znDA~~o3>h80@_+@#v0cD;^F<*67_}?}Xg`8o|DOs% z=cVGMp3^wI33AG@`$Fv^6IVb zD^|#aH6Ks9+S5K&fUWDnrwTJ!@?{VXv*ZJ@+gc8A2+N+`PGq9a0Kg{s1yNEa-2@4; z;C=I>=UXMKnpyPnY|DYe5X@7k<$z-pC#hBZA?Pk;)um;B&e|A9FUIjV={R=Y38~u( zr!gzA16SUHPuM9Qkd{edz}%zE{0gm7Gx4h`S$^oR3Oqj)gj_>ut!bxh%{gmiDzm7b z2Y?XbIuh7~d|m$`gpPsYjE;9mcUU=@w!{`IDipN1Gpn92xG;3kR{tmiO5tN}L-FEhr-dCil=fTKqFfIO?|rYKEl}^D9yfp4dN$sqBS5lGsp2s%)u!=? zzo**Y2|qtF@&7U9ZPKj122t)1Ef#An!8kXESgF5%)|%=Et*O?T!dB5^Yb*6a9chXQ z6UC-|0*D$Cy%y2YwC#UBS~p|(i;;9*PIAkRCRXrZrbIhQ6Af6PSj#on>BMXqZMQk~nWssK zNht(>0%5w%3@jgSsj>$gE^8$o$?1~Ei>ReIi`aPK4Q@fcRtI@7ovpX55UO_=MXrH8DeU{5^|jf z#MKaw(D({*)mdV&E8AM6kbXo_1qK1lY;y>I^k4Gybj)%km=K2E>NMdW(y{ zgQ@&A2E>uhlzSPs))LiI3QWh_32*IOXaW8J01RFuiKv36x~CHK0zMk^XsxyP0vlZ_ zO9uFEa|34fn5)bNq%sX=F#Cj~NOe(vr6LXy8<`^{=g`y_U~{$stDEDV_16bF3h~kw z#mN8(ZnGY33ESdU9?^+}~8kQEx zL~HWVf#O&jnDv7I@wrT-4N0K{eaO>R8KoFaop7Yv5_4L7MTauyDfwB4h8%r=;HR|2 z#j%e)eZGZHG!y7u9Ag_2QaEa(eZ&~OK0&ptF+Y=Ns;oy{F41dG-5Rvag{wLH5OiOP zb>v#VgiT|3z05D-C4KcSDXKKFL0~xyq&6fDCAyr>^cZX{tOingpw>xNuabh@4QZw0 zS~raHVFfHTg4PjHp4tK7bDao(_7o9quMuKFW5iogBiXRl3uR-#?#(RhZ*E0IY;9Vg zzX%z*)kNq5p(BTfZ;;M8@FXJ*by7T6Vk`F2$~n+U4pUZ(zI#05DPso-ofLV?!6$Ar zHb#5{KM^RB)#2XWArbCF){k5MgI-J#H|7>`V^qfWVt2%R`6)Pb;*q+42Qw3PCW^-4{0{yclk{~rC8fq_Wj^P73lADxDD74p&ln&UG2=UqpZhj~q zwTw7aULf&-_V%{Kjv8FVWikjoHlo1Kh^-G zqVJ_tpH#A%+1@1#NTtTu=EpW7)c#iuTt~5R3&Tz~JtQ2hbd{TbmeEIytr8H)hxxw^&)$D_tc*@l|%71a{I&uPV<|L3=d z{n2mFe|vuVpWmLJ?LX&mQChnMuRuS%RKnqTb+mxjya^Guus=sHw97K>(Q)2}K_})raQS3*eN@YB=)$NgQLLhtdQyM; zVv^bTdn5WO`^`q*YxwB&AjCnJfF*`%jT6I5S{*Wzg}_*UhxDFl?G!%%n2yjzN5B-w zp+n~Qh$liwLGjdv7`Zi{vy;kpyl zGek>dw2tRu?mI3vh8s;Spt@?*Wl~l}-ePt-?z?FS34j-*?y<0ze`08QGj>KNtv||?&u_K<)-$jUnv4%o_*dAm$kd0Wu%zmSs{c4oXeEl4~ zgN$HfD);gug<~Y-N;OPxPFIYHMYBh}QS=lQZq ze`)Z`E5nL@^f7$!RQC+byXS~tBK5z?{wo}R2q%P2nhOP_PJtLzf zMF}ZtttmMcmoat<708#4X;7EJq8z^aar*x4^ySI))%o=3=%*vIKu-%*opI0{lD;le z_FA-_v(P{x{##--g5rNKqR@c#r8~6xfAWWyF4@zV7GUh-@-bWN7!6>hh%AWP3vJEw z`Mc@)o3Fn9@%;GL*}r}|JU@?O#8J@7s@@zxFx;j=fp|7vycBvQG3Z=&dzlo|4@BlJ zvHv?s3c2p_-UjMVd)c;KPA7&EWu}_c0Usd(~{w1RD{SHe*=qy_-RBCACXS&zw zT=gf4o#K;K^ZOag^5#rvFxfhI^LirtTXgfc5F}n(SqM~nf!lwd#h#?Ba%iaPUP(qXXia#Sm(qFezBR7r1`8T*2 z;5uGB!tbF&BOw+-tVFt{SR!1 Jr}Idj4FH9SETX^7`n6>K_n*JcUIbmj!P$cPufYEL=dY`Y*Zu47 z4Pm(yU>Wv(n$FJ_uu#OyGp}=4r^W##B1OvR((ClW%k}a)Hno@ahceJXaF&_5?C6d z4K1p;gMg>Dak>y>mv7ziglwzlP6G3-Ysdwj?j93TLYPet_Y^6)jew3CEkmAV z+bjK$fE(m8;Fjz#<>(8Yd%m{mX$E}mSuN!Wy5ibXbOxwXB%`(j_)M7}auPMPo{%D` zf*QY`dA;uSg%19{4*uTP-q&t}li?u+1ZqoXOUA1Hs_ zH`RnNpTFY{{xrfS{c*Ma(_A(YXGM|wY4$d2lya|n0rKvU*e+I=EGhtJGA zZZe1k261ZHC~w!;-8Iw>2Y&`4de`JL18c)PR?yqha%aoTh5M z$;|sU`7kg?kSqlC1A>RnT_slNg+l4bx~~zQN!rvt!qU2gbcaw*7$a8oAuX}maTMau zfES7#eZ1|EqT_`+(X`Dl;;3T`)1{S&StZlvQMzeRB!z|DHY2Hy$}Kj%0EP)D!YaOQ z0F#RRmRc7xggNd;=k6^sVQV6s40u5@!D&|L^YUX}o=|4=(mu$gpK!KO%*Sh9QXv^x z5v#W%h{J5P#;_6={eQ>GIKL;cT`h_VG1-whuVPr1o~`zO_TeV4xSxfvFf;Xsef-%Q z76|yD)2l#mONghRSVD!(+>;`5Qva1e{NdZQeZH6< zaLo80rxZ~NOt1l9I<2@bG9?){oJDWoF6NOqo{wmje`Fev92T#_|e~zSc!m2^G2dWfU06Pvjr1hFox zXlv?|9VSMyAP_tGvSP+|T!78G_fXt+?VsnKyv=`d$I5q4SLjd7jn}lsF6O#d*FGUs zCB8+=!ubV6h^i5?%NRl@XKKieh%(CO<&<&|1nCCCK~&1z{E9UY5oMxYbO8b_S zi-dqtkxAxO<)#Su5$w{1-3zB7Cyf5FM6

45B*{N=Wmf{FZPK3f5`^-}tRW&T9s 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 CbL=|+

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 delta 165538 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œ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