From 5eae82b71ced9bda1797c32c948098ff42730c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Leforestier?= Date: Sat, 14 Dec 2024 12:22:15 +0100 Subject: [PATCH] Merge with v24.11.7 (#4) * massage file handling * fix comment * Update patch_apply.py * webapp: Update dependencies * Add serial prefix 1410 to HMS_2CH inverters This is related to #2235 and fixes #2230 * Output WiFi disconnect reason in console * Upgrade ESPAsyncWebServer from 3.1.2 to 3.2.0 * Upgrade olikraus/U8g2 from 2.35.19 to 2.35.21 * Upgrade arkhipenko/TaskScheduler from git #testing to 3.8.5 * webapp: update dependencies * Feature: Add support for HERF 1 channel inverters * Upgrade ESPAsyncWebServer from 3.2.0 to 3.3.1 * Upgrade olikraus/U8g2 from 2.35.21 to 2.35.27 * webapp: Update dependencies * webapp: Upgrade tsconfig node18 to node22 * webapp: Parse version string event if update search is not allowed * issue template: asks for firmware variant * actions: use setup-node@v4 as v3 causes warning the "Yarn Linting" action causes a warning to appear about a deprecated Node version. switch to actions/setup-node@v4, which is already in use by the action building the web app for the firmware, to avoid this warning. * actions: switch to node version 20 for linting use version consistent with the version used when building the web application. * actions: run yarn prettier to check web app formatting * actions: fix a typo * changelogs: group webapp-related changes * Doc: Remove inverter list and add a link to the documentation This reduces redundant effort when a inverter is added. * Upgrade olikraus/U8g2 from 2.35.27 to 2.35.30 * webapp: Update dependencies * webapp: add app.js.gz * actions: enable corepack to use fixed version of yarn this allows us to fix the version of yarn, the Node.js package manager, to a particular version. using corepack is the recommended way to use yarn these days. * webapp: Fix html error in eventlog * Fix: WebApp was not reloaded after firmware update With the upgrade from ESPAsyncWebServer to 3.3.1 it seems that something has changed. Have to trigger the reboot from the main context. * Update bblanchon/ArduinoJson from 7.1.0 to 7.2.0 * webapp: add app.js.gz * Github Action: Update node version from 20 to 22 * Publish ESP heap and temperature details on MQTT I noticed that some useful ESP stats are missing on the MQTT feed, so this adds: - ESP temperature - ESP heap stats (size, free, minFree, maxAlloc) * Fix: Wrong topic in home assistant auto discovery for maxalloc and minfree * Fix: Saving DTU config values just returned "Values are missing" * Publish temperature only if its not NAN * Feature: Inverter radio statistics (rx/tx statistics) The statistics are shown in the WebApp and published via MQTT. Statistics are reset at midnight. * Added icon to radio statistics * webapp: add app.js.gz * Fix: Unable to CMT transmit power in WebApp The pa_level was sent as string instead of a number. fixes #2299 * Fix: Restart was triggered before all website data was sent This led to the effect that e.g. the confirmation messages where not shown. It is somehow related to ESPAsyncWebServer 3.3.0 * webapp: Update dependencies * webapp: Fix data type for all range inputs * webapp: add app.js.gz * Decrease restart delay to 1 second This prevents a reload of the webapp (during firmware update) before the esp is online again * Optimize MQTT subscription handling * Move inverter housekeeping tasks inside the InverterAbstract class * Feature: Allow reset of radio statistics via mqtt * Feature: Publish Radio statistics to home assistant * MQTT Hass: Change char* to String& * MQTT Hass: Rename caption parameter to name * MQTT Hass: Change parameter order for publishInverterSensor * MQTT Hass: Change parameter order for publishDtuSensor * MQTT Hass: Make publish methods static * MQTT Hass: Change parameter order for publishDtuBinarySensor * MQTT Hass: Change parameter order for publishInverterButton * MQTT Hass: Change parameter order for publishInverterNumber * MQTT Hass: Harmonise parameter names * MQTT Hass: Remove no more required checks * MQTT Hass: Move publishBinarySensor logic into separate method * MQTT Hass: Reorder binary sensor methods * MQTT Hass: Move publishSensor logic into separate method * MQTT Hass: Move yield into the publish method * MQTT Hass: Add device_type and category to publishInverterBinarySensor * MQTT Hass: Reorder defines * MQTT Hass: Move serialization and allocation check into own method * MQTT Hass: Append dtu prefix topic for each single sensor * Feature: Publish YieldTotal, YieldDay and Power of all inverters to Home Assistant * MQTT Hass: Implement category as enum instead of String * MQTT Hass: Implement device class as enum instead of String * MQTT Hass: Implement method to add common metadata to json output * Remove unnecessary CMT SPI inversions * Fix cs_ena_posttrans calculation * Remove unnecessary delays * Implement W5500 support * Add SpiManager library * Optimize CMT FIFO access * Change cmt_spi3 implementation from C to C++ * Add Arduino SPI translation * Use SpiManager for nRF, CMT and W5500 * Use shared SPI bus for CMT and W5500 * Only use a single SPI device for CMT * Feature: Allow reset of radio statistics via WebApp * webapp: Update dependencies * Embed current branch into building process * Slight adjustments to github bug_report template * Upgrade github actions/checkout to v4 * GitHub Build Action: Automatically generate littlefs image If a data directory exists, the content of this directory will be placed in the littlefs image and embedded into the factory.bin file * Fix: Only count RF RX packets when packets where sent This mainly occours after a reset of the statistics that receive count is higher then transmit count * webapp: Apply auto format * webapp: Update dependencies * Simplify network callback handling * Simplify inverter handling * webapp: add app.js.gz * Apply license headers and automatic code formatting to SpiManager * Apply automatic code formatting * Added device profile for OpenDTU Fusion v2 PoE * increase chunkSizeWarningLimit for webapp build (#1287) increase from 500k (default) to 1024k in order to get rid of the warning messages. * Rename NetworkEventCb to DtuNetworkEventCb to prevent further upgrade issues * Add default values for ethernet pins in case they are not defined for a specific board * Take care of different signature of ETH.begin method in Arduino Core 3.x * Added required include to work with IDF 5 * Update espressif32 from 6.8.1 to 6.9.0 * webapp: Update dependencies * webapp: add app.js.gz * issue template: fix typo * Add connection check for W5500 before full initialization * Prevent warning on GPIO ISR service registration * Adjust name of OpenDTU Fusion v2 PoE build environment * Add device profiles for OpenDTU Fusion v2 PoE with displays * Fix: avoid deprecated setAuthentication() to fix memory exhaustion with ESPAsyncWebServer 3.3.0, the setAuthentication() method became deprecated and a replacement method was provided which acts as a shim and uses the new middleware-based approach to setup authentication. in order to eventually apply a changed "read-only access allowed" setting, the setAuthentication() method was called periodically. the shim implementation each time allocates a new AuthenticationMiddleware and adds it to the chain of middlewares, eventually exhausting the memory. we now use the new middleware-based approach ourselves and only add the respective AuthenticatonMiddleware instance once to the respective websocket server instance. a regression where enabling unauthenticated read-only access is not applied until reboot is also fixed. all the AuthenticationMiddleware instances were never removed from the chain of middlewares when calling setAuthentication("", ""). * Fix: force websocket clients to authenticate when changing the security settings (disabling read-only access or changing the password), existing websocket connections are now closed, forcing the respective clients to authenticate (with the new password). otherwise, existing websocket clients keep connected even though the security settings now expect authentication with a (changed) password. * webapp: Update dependencies * Upgrade ESPAsyncWebServer from 3.3.1 to 3.3.7 * Remove icon because device_class is set * Remove unused DEVICE_CLASS_TEMP * Fix: Add state_class to several Home Assistant sensors state_class was added to yieldtotal, yieldday ac power and temperature for the whole dtu closes: #2324 * Update UpgradePartition.md Fixed typo * Feature: Show RSSI of last received packet in radio stats The value is also published via MQTT * Upgrade ESPAsyncWebServer from 3.3.7 to 3.3.11 * webapp: Update dependencies * Rename NetworkEventCbList_t to DtuNetworkEventCbList_t for further upgrades * Replace format strings by platform independent macros * webapp: Update dependencies * webapp: add app.js.gz * webapp: Fix eslint issues * Remove EMAC related code for devices that don't have one * Initialize the last rssi value with -127 instead of 0 to indicate a non existing connection of no data was received yet * Fix: "Equal brightness" in LED settings does not work correctly fixes: #2332 * Upgrade ESPAsyncWebServer from 3.3.11 to 3.3.12 * webapp: add app.js.gz * webapp: pin assignment: hide unsupported pins if the pin_mapping.json includes unsupported pins, e.g., `eth` pins on an ESP32-S3, the whole category should still be hidden in the device manager. * webapp: Update dependencies * Don't set TX timeout to 0 anymore for HW/USB CDC Due to a change in the Espressif Arduino core, the TX timeout for the HW CDC (used in the ESP32-S3, for example) must not be set to 0, as otherwise, an integer underflow occurs. Removing the TX timeout is not necessary anymore anyways, because it is now detected when CDC is not active, and attempts to write will return immediately until the host read something again. Only when the transmit buffer becomes full initially, the default timeout of just 100ms takes effect once. For USB CDC (used with the ESP32-S2, for example), the timeout is not relevant either. * Feature: show task details in system info view shows whether or not known tasks are alive, and in particular shows how much of the respective stack is still available. * Hotfix to not use DMA on SPI3 of ESP32-S2 See issue #2343. * Upgrade ESPAsyncWebServer from 3.3.12 to 3.3.13 * Fix: Correct output of wifi disconnect reason code * webapp: Update dependencies * Upgrade ESPAsyncWebServer from 3.3.13 to 3.3.14 * Upgrade ESPAsyncWebServer from 3.3.14 to 3.3.15 * Upgrade olikraus/U8g2 from 2.35.30 to 2.36.2 * webapp: Update dependencies * Upgrade ESPAsyncWebServer from 3.3.15 to 3.3.16 * webapp: add app.js.gz * Fix: cpplint errors * Update nrf24/RF24 from 1.4.9 to 1.4.10 * Upgrade ESPAsyncWebServer from 3.3.16 to 3.3.17 * Rename config API to file API * Refactor file handling API and add endpoint to delete files * Feature: Refactor config management interface * webapp: Use global AlertResponse interface * Add API endpoint to retrieve custom languages and complete language pack * webapp: Allow upload of language packs * Feature: Allow custom language pack for webapp * Feature: Added spanish language pack * Feature: Added italian language pack * Move lookup for translation path to separate method * Check if language pack metadata are valid * webapp: Added global reboot wait screen * webapp: Rename interface to prevent lint errors * add and use configuration write guard the configuration write guard is now required when the configuration struct shall be mutated. the write guards locks multiple writers against each other and also, more importantly, makes the writes synchronous to the main loop. all code running in the main loop can now be sure that (1) reads from the configuration struct are non-preemtive and (2) the configuration struct as a whole is in a consistent state when reading from it. NOTE that acquiring a write guard from within the main loop's task will immediately cause a deadlock and the watchdog will trigger a reset. if writing from inside the main loop should ever become necessary, the write guard must be updated to only lock the mutex but not wait for a signal. * webapp: Fix: WaitRetstartView showed basic auth dialog * Rewrite display language handling to work with locale strings instead of magic numbers. This is required to implement further i18n functions using the language packs * Feature: Implement language pack support for display texts * Feature: Added spanish display translation * Feature: Added italian display translation * Added README.md to lang folder * Feature: Added device info for HMS-700 * Fix: Take DST into account when recalculating the sunrise sunset time If it is not considered the correct sunset / sunrise time is only calculated at the next day Fixes: #2377 * Feature: Validate JSON before uploading * webapp: Update dependencies * Upgrade ESPAsyncWebServer from 3.3.17 to 3.3.21 * Fix: Lint Error * Fix: skip BOM in JSON files (pin_mapping and config) based on #2387 * webapp: right-align labels for inputs on non-sm viewports this change tries to achieve a pleasing look of input forms by right-aligning the texts of labels. the input form now looks similar to a table, achieving a cleaner look, especially for forms where the labels have varying text lenghts. * webapp: last table row shall have no bottom border similar to the first row which has no border at the top. * webapp: remove table's bottom margin we don't need a margin at the bottom of tables in general. not sure why this is even a thing in bootstrap. this change, in particular, makes the space between a table and a parent card symmetric on all sides. * webapp: add gap between inverter selectors * webapp: avoid inline style in inverter channel info card * webapp: equalize style of cards with tables in live view this change adjusts the style of cards showing tables such that they look the same as inverter channel info tables. * webapp: use reasonable name for radio stats accordion * webapp: align table headers with card headers set the left margin of table header cells to the same marging the card header use, such that the text align on the same axis. * webapp: apply card-table class to info view cards the cards in all information views still used a div.card-body around the table, which added a margin on all sides of the table. to achieve a unified look, these cards and tables now look the same as the inverter channel cards. * webapp: adjust look of tables in accordions to live view cards this is relevant for the radio statistics table, as well as the tables in the grid profile modal. * webapp: beautify radio statistics reset button it would be nice to have this in the header of the accordion, which is hard, but doable. however, clicking the button then also toggles the accordion, which is unacceptable. preventing that seems non-trivial, as the @click.stop() is not enough. also, nesting interactive elements is simply bad practice. the button can also go to the right of header, with reasonable effort, but the corner radii are then messed up and would need to react interactively (accordion collapsed or not), which is also a pain. we now "float" the reset button to the right, add a nice icon, and give the button some space so it at least looks like it belongs there. * webapp: fix inverter "add" and "save order" button positions the source tells us that the buttons are supposed to be on the right of tha card, but the CSS broke at some point. * webapp: optimize spacing on bottom of cards if the last child in a card (div.card > div.card-body) adds bottom marging, we don't want the card to add more space through its padding-bottom. most cards have children that add sufficient space at the bottom anyways. * webapp: MQTT: use v-if in favor of v-show if we hide elements (which is done using style="display:none;"), they are still part of the DOM and mess with CSS rules that shall apply to the last element of a card or the last row of a table. * webapp: MQTT: no login with cert if TLS disabled in the settings view we hide the "login with cert" setting while TLS is disabled, so we should also hide that info in the info view when TLS is disabled. * webapp: avoid inline style for inverter channel info value * webapp: properly space alert with hint for hostname * webapp: optimize look of firmware update cards * webapp: inverter advanced tab needs space at the top this avoids the input text box from colliding with the tab navigation bottom border. * webapp: optimize look of login page improve spacing and align login buton to the right, where all our buttons are. * webapp: optimize body bottom padding and length long forms, when scrolled to the bottom, would leave no space between the bottom of the viewport and the buttons, which is unpleasent. short views would still createa large (high) body, for apparently no reason. * webapp: consistently use no colon in form labels there are no colons for table headers as well. some form labels had no colon already, so this change uses a unified look among form labels. * webapp: optimize placement of device profile doc buttons * remove empty container for device profile links. if a device profile has no links, no buttons are generated, but a row was still part of the DOM, adding spurious space between the select and the alert with the hint. * webapp: show pin mapping categories as cards on a desktop browser, this approach allows to display all categories at once. we also increase readability as the values are much closer to their label. previously, the values were far to the right of the screen and it was unpleasent to read which value belonged to which setting. the grouping of values per category was also not very well conceived. by using cards, we also avoid some styling issues, namely the use of rowspan, which caused a spurious table cell border at the end of the old table layout. * webapp: device manager: optimize cards for tab nav the top border of the card was breaking the design of the tabs, where the active tab would be "visually connected" to the content. also, the rounded border at the top did not blend in with the navbar's bottom border. * webapp: always scroll up when navigating to another view * webapp: fix inverter selection button breaking on small viewports, the icon and the inverter label would be displayed in two lines. this change keeps the icon and the label tied together in any case, and the icon is centered vertically around the label. * keep console.log() when serving webapp the removal of console and debugger statements by esbuild even when not building for production seems to be a regression, as these were definitely working in the past. this change uses the command parameter to configure esbuild to either keep or indeed remove the respective statements. they are only kept if command is not "serve". to avoid having to indent everything in defineConfig() by one block, the return statement and closing curly brace were added "inline". * Remove not required include * Replace multiline print by printf * Remove not required include * webapp: declare emitted event in FormFooter component fixes an annoying warning (visible in the browser console): [Vue warn]: Extraneous non-emits event listeners (reload) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option. * webapp: Update dependencies * Upgrade ESPAsyncWebServer from 3.3.21 to 3.3.22 * Fix lint errors * Build factory.bin in every compile attempt This is required to apply changes which are maybe only related to the data directory. * webapp: add app.js.gz * Merge with v24.11.7 * Fix cpplint --------- Co-authored-by: Marc-Philip Co-authored-by: Thomas Basler Co-authored-by: Bernhard Kirchen Co-authored-by: Tobias Diedrich Co-authored-by: LennartF22 <18723691+LennartF22@users.noreply.github.com> Co-authored-by: vaterlangen Co-authored-by: mbo18 Co-authored-by: janrombold <92722795+janrombold@users.noreply.github.com> Co-authored-by: CommanderRedYT --- .github/ISSUE_TEMPLATE/bug_report.yml | 13 +- .github/workflows/build.yml | 17 +- .../config/release-notes-config.json | 6 + .github/workflows/cpplint.yml | 2 +- .github/workflows/yarnlint.yml | 16 +- .github/workflows/yarnprettier.yml | 28 + README.md | 39 +- docs/DeviceProfiles/opendtu_fusion.json | 137 +- docs/UpgradePartition.md | 2 +- include/Configuration.h | 29 +- include/Display_Graphic.h | 17 +- include/I18n.h | 37 + include/MqttHandleHass.h | 56 +- include/MqttHandleInverter.h | 26 +- include/NetworkSettings.h | 20 +- include/PinMapping.h | 17 +- include/RestartHelper.h | 18 + include/Utils.h | 4 +- include/W5500.h | 29 + include/WebApi.h | 7 +- include/WebApi_config.h | 17 - include/WebApi_errors.h | 8 +- include/WebApi_file.h | 18 + include/WebApi_i18n.h | 14 + include/WebApi_inverter.h | 1 + include/WebApi_ws_console.h | 2 + include/WebApi_ws_live.h | 2 + include/__compiled_constants.h | 1 + include/defaults.h | 6 +- lang/README.md | 9 + lang/es.lang.json | 690 ++ lang/it.lang.json | 690 ++ lib/CMT2300a/cmt_spi3.c | 142 - lib/CMT2300a/cmt_spi3.cpp | 155 + lib/CMT2300a/cmt_spi3.h | 10 +- lib/Hoymiles/src/Hoymiles.cpp | 23 +- lib/Hoymiles/src/HoymilesRadio.cpp | 27 + lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 12 +- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 8 +- .../commands/ActivePowerControlCommand.cpp | 4 +- .../src/commands/DevControlCommand.cpp | 4 +- .../src/commands/MultiDataCommand.cpp | 12 +- .../src/commands/RealTimeRunDataCommand.cpp | 2 +- .../src/commands/SystemConfigParaCommand.cpp | 2 +- lib/Hoymiles/src/inverters/HERF_1CH.cpp | 55 + lib/Hoymiles/src/inverters/HERF_1CH.h | 13 + lib/Hoymiles/src/inverters/HMS_2CH.cpp | 2 +- lib/Hoymiles/src/inverters/HM_1CH.cpp | 6 +- lib/Hoymiles/src/inverters/HM_2CH.cpp | 6 +- lib/Hoymiles/src/inverters/HM_4CH.cpp | 6 +- lib/Hoymiles/src/inverters/HM_Abstract.cpp | 4 +- .../src/inverters/InverterAbstract.cpp | 34 +- lib/Hoymiles/src/inverters/InverterAbstract.h | 30 +- lib/Hoymiles/src/inverters/README.md | 27 +- lib/Hoymiles/src/parser/AlarmLogParser.cpp | 6 +- lib/Hoymiles/src/parser/DevInfoParser.cpp | 21 +- lib/Hoymiles/src/parser/GridProfileParser.cpp | 2 +- .../src/parser/SystemConfigParaParser.cpp | 6 +- lib/SpiManager/library.json | 13 + lib/SpiManager/src/SpiBus.cpp | 60 + lib/SpiManager/src/SpiBus.h | 49 + lib/SpiManager/src/SpiBusConfig.cpp | 71 + lib/SpiManager/src/SpiBusConfig.h | 21 + lib/SpiManager/src/SpiCallback.cpp | 69 + lib/SpiManager/src/SpiCallback.h | 15 + lib/SpiManager/src/SpiManager.cpp | 114 + lib/SpiManager/src/SpiManager.h | 41 + pio-scripts/auto_firmware_version.py | 13 + pio-scripts/create_factory_bin.py | 52 +- pio-scripts/patch_apply.py | 26 +- platformio.ini | 44 +- src/Configuration.cpp | 79 +- src/Display_Graphic.cpp | 73 +- src/Display_Graphic_Diagram.cpp | 2 +- src/I18n.cpp | 176 + src/InverterSettings.cpp | 28 +- src/JsyMk.cpp | 8 +- src/MqttHandleDtu.cpp | 10 + src/MqttHandleHass.cpp | 381 +- src/MqttHandleInverter.cpp | 113 +- src/MqttSettings.cpp | 3 +- src/NetworkSettings.cpp | 47 +- src/PinMapping.cpp | 90 +- src/RestartHelper.cpp | 36 + src/SunPosition.cpp | 6 +- src/Utils.cpp | 59 +- src/W5500.cpp | 153 + src/WebApi.cpp | 15 +- src/WebApi_device.cpp | 57 +- src/WebApi_dtu.cpp | 38 +- src/{WebApi_config.cpp => WebApi_file.cpp} | 133 +- src/WebApi_firmware.cpp | 3 +- src/WebApi_i18n.cpp | 76 + src/WebApi_inverter.cpp | 119 +- src/WebApi_limit.cpp | 6 +- src/WebApi_maintenance.cpp | 6 +- src/WebApi_mqtt.cpp | 110 +- src/WebApi_network.cpp | 84 +- src/WebApi_ntp.cpp | 40 +- src/WebApi_power.cpp | 10 +- src/WebApi_prometheus.cpp | 24 +- src/WebApi_security.cpp | 16 +- src/WebApi_sysstatus.cpp | 14 + src/WebApi_ws_console.cpp | 26 +- src/WebApi_ws_live.cpp | 32 +- src/main.cpp | 39 +- webapp/env.d.ts | 2 +- webapp/eslint.config.js | 14 +- webapp/package.json | 39 +- webapp/src/App.vue | 2 +- webapp/src/components/CardElement.vue | 3 +- webapp/src/components/EventLog.vue | 10 +- webapp/src/components/FirmwareInfo.vue | 2 +- webapp/src/components/FormFooter.vue | 6 + webapp/src/components/GridProfile.vue | 6 +- webapp/src/components/HardwareInfo.vue | 2 +- webapp/src/components/HeapDetails.vue | 2 +- webapp/src/components/InterfaceApInfo.vue | 2 +- .../src/components/InterfaceNetworkInfo.vue | 1 + webapp/src/components/InverterChannelInfo.vue | 7 +- webapp/src/components/LocaleSwitcher.vue | 27 +- webapp/src/components/MemoryInfo.vue | 2 +- webapp/src/components/NavBar.vue | 4 +- webapp/src/components/PinInfo.vue | 67 +- webapp/src/components/RadioInfo.vue | 2 +- webapp/src/components/TaskDetails.vue | 40 + webapp/src/components/WifiApInfo.vue | 2 +- webapp/src/components/WifiStationInfo.vue | 2 +- webapp/src/emitter.d.ts | 2 +- webapp/src/i18n.ts | 145 + webapp/src/locales/de.json | 152 +- webapp/src/locales/en.json | 146 +- webapp/src/locales/fr.json | 60 +- webapp/src/locales/index.ts | 78 - webapp/src/main.ts | 14 +- webapp/src/router/index.ts | 13 + webapp/src/scss/styles.scss | 60 + webapp/src/types/AlertResponse.ts | 6 + webapp/src/types/Config.ts | 7 - webapp/src/types/DeviceConfig.ts | 2 +- webapp/src/types/File.ts | 4 + webapp/src/types/LiveDataStatus.ts | 11 + webapp/src/types/PinMapping.ts | 2 +- webapp/src/types/SystemStatus.ts | 8 + webapp/src/utils/structure.ts | 25 + webapp/src/utils/waitRestart.ts | 7 + webapp/src/views/ConfigAdminView.vue | 290 +- webapp/src/views/ConsoleInfoView.vue | 10 +- webapp/src/views/DeviceAdminView.vue | 68 +- webapp/src/views/DtuAdminView.vue | 8 +- webapp/src/views/FirmwareUpgradeView.vue | 66 +- webapp/src/views/HomeView.vue | 157 +- webapp/src/views/InverterAdminView.vue | 20 +- webapp/src/views/LoginView.vue | 4 +- webapp/src/views/MaintenanceRebootView.vue | 2 + webapp/src/views/MqttAdminView.vue | 16 +- webapp/src/views/MqttInfoView.vue | 12 +- webapp/src/views/NetworkAdminView.vue | 6 +- webapp/src/views/NtpInfoView.vue | 4 +- webapp/src/views/SystemInfoView.vue | 12 +- webapp/src/views/WaitRestartView.vue | 64 + webapp/tsconfig.config.json | 4 +- webapp/tsconfig.json | 2 +- webapp/vite.config.ts | 12 +- webapp/yarn.lock | 5618 +++++++++-------- webapp_dist/js/app.js.gz | Bin 183817 -> 192821 bytes 166 files changed, 8402 insertions(+), 4256 deletions(-) create mode 100644 .github/workflows/yarnprettier.yml create mode 100644 include/I18n.h create mode 100644 include/RestartHelper.h create mode 100644 include/W5500.h delete mode 100644 include/WebApi_config.h create mode 100644 include/WebApi_file.h create mode 100644 include/WebApi_i18n.h create mode 100644 lang/README.md create mode 100644 lang/es.lang.json create mode 100644 lang/it.lang.json delete mode 100644 lib/CMT2300a/cmt_spi3.c create mode 100644 lib/CMT2300a/cmt_spi3.cpp create mode 100644 lib/Hoymiles/src/inverters/HERF_1CH.cpp create mode 100644 lib/Hoymiles/src/inverters/HERF_1CH.h create mode 100644 lib/SpiManager/library.json create mode 100644 lib/SpiManager/src/SpiBus.cpp create mode 100644 lib/SpiManager/src/SpiBus.h create mode 100644 lib/SpiManager/src/SpiBusConfig.cpp create mode 100644 lib/SpiManager/src/SpiBusConfig.h create mode 100644 lib/SpiManager/src/SpiCallback.cpp create mode 100644 lib/SpiManager/src/SpiCallback.h create mode 100644 lib/SpiManager/src/SpiManager.cpp create mode 100644 lib/SpiManager/src/SpiManager.h create mode 100644 src/I18n.cpp create mode 100644 src/RestartHelper.cpp create mode 100644 src/W5500.cpp rename src/{WebApi_config.cpp => WebApi_file.cpp} (59%) create mode 100644 src/WebApi_i18n.cpp create mode 100644 webapp/src/components/TaskDetails.vue create mode 100644 webapp/src/i18n.ts delete mode 100644 webapp/src/locales/index.ts create mode 100644 webapp/src/types/AlertResponse.ts delete mode 100644 webapp/src/types/Config.ts create mode 100644 webapp/src/types/File.ts create mode 100644 webapp/src/utils/structure.ts create mode 100644 webapp/src/utils/waitRestart.ts create mode 100644 webapp/src/views/WaitRestartView.vue diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5e73566e7..8f3579300 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -47,7 +47,8 @@ body: label: Install Method description: How did you install OpenDTU? options: - - Pre-Compiled binary from GitHub + - Pre-Compiled binary from GitHub releases + - Pre-Compiled binary from GitHub actions/pull-request - Self-Compiled validations: required: true @@ -59,6 +60,14 @@ body: placeholder: "e.g. 359d513" validations: required: true + - type: input + id: environment + attributes: + label: What firmware variant (PIO Environment) are you using? + description: You can find this in by going to Info -> System + placeholder: "generic_esp32s3_usb" + validations: + required: true - type: textarea id: logs attributes: @@ -84,5 +93,5 @@ body: required: true - label: I have updated the title field above with a concise description. required: true - - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported + - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported. required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32bbad64e..29a30d9e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: environments: ${{ steps.envs.outputs.environments }} build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: @@ -79,18 +79,27 @@ jobs: python -m pip install --upgrade pip pip install --upgrade platformio setuptools + - name: Enable Corepack + run: | + cd webapp + corepack enable + - name: Setup Node.js and yarn uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: | + cd webapp + yarn install --frozen-lockfile - name: Build WebApp - run: yarn --cwd webapp build + run: | + cd webapp + yarn build - name: Build firmware run: pio run -e ${{ matrix.environment }} diff --git a/.github/workflows/config/release-notes-config.json b/.github/workflows/config/release-notes-config.json index 3fa9e3bd1..8699820e3 100644 --- a/.github/workflows/config/release-notes-config.json +++ b/.github/workflows/config/release-notes-config.json @@ -18,6 +18,12 @@ "fix" ] }, + { + "title": "## 🌎 Web Application", + "labels": [ + "webapp" + ] + }, { "title": "## 📚 Documentation", "labels": [ diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index 4ee4b4a82..84d9fd069 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/yarnlint.yml b/.github/workflows/yarnlint.yml index f1c912c96..dd438a053 100644 --- a/.github/workflows/yarnlint.yml +++ b/.github/workflows/yarnlint.yml @@ -6,17 +6,23 @@ jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: webapp + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable - name: Setup Node.js and yarn - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: yarn install --frozen-lockfile - name: Linting - run: yarn --cwd webapp lint \ No newline at end of file + run: yarn lint diff --git a/.github/workflows/yarnprettier.yml b/.github/workflows/yarnprettier.yml new file mode 100644 index 000000000..c521f88d6 --- /dev/null +++ b/.github/workflows/yarnprettier.yml @@ -0,0 +1,28 @@ +name: Yarn Prettier + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: webapp + + steps: + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable + - name: Setup Node.js and yarn + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "yarn" + cache-dependency-path: "webapp/yarn.lock" + + - name: Install WebApp dependencies + run: yarn install --frozen-lockfile + + - name: Check Formatting + run: yarn prettier --check src/ diff --git a/README.md b/README.md index 3ce269cce..494639486 100644 --- a/README.md +++ b/README.md @@ -44,41 +44,4 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre ## Currently supported Inverters -| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases | -| ---------------------| ------------------ | --------- | ----------- | --------- | -| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-350-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-400-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-600-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-700-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-800-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-1000-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HM-1200-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HM-1500-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HMS-300-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-350-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-400-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-450-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-500-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-600-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-700-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-800-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-900-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-1000-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-1600-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMS-1800-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMS-2000-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMT-1600-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-1800-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-2000-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-1800-6T | CMT2300A | 6 | 3 | 3 | -| Hoymiles HMT-2250-6T | CMT2300A | 6 | 3 | 3 | -| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 | -| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 | -| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 | -| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | -| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | -| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | -| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 | -| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 | -| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 | +A list of all currently supported inverters can be found [here](https://www.opendtu.solar/hardware/inverter_overview/) diff --git a/docs/DeviceProfiles/opendtu_fusion.json b/docs/DeviceProfiles/opendtu_fusion.json index f33dc47db..990f4c46b 100644 --- a/docs/DeviceProfiles/opendtu_fusion.json +++ b/docs/DeviceProfiles/opendtu_fusion.json @@ -1,6 +1,9 @@ [ { "name": "OpenDTU Fusion v1", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -25,6 +28,9 @@ }, { "name": "OpenDTU Fusion v1 with SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -54,6 +60,9 @@ }, { "name": "OpenDTU Fusion v1 with SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -83,6 +92,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A and NRF24", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -115,6 +127,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -152,6 +167,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -186,5 +204,122 @@ "data": 2, "clk": 1 } + }, + { + "name": "OpenDTU Fusion v2 PoE", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 0, + "data": 2, + "clk": 1 + } + }, + { + "name": "OpenDTU Fusion v2 PoE with SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 3, + "data": 2, + "clk": 1 + } + }, + { + "name": "OpenDTU Fusion v2 PoE with SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 2, + "data": 2, + "clk": 1 + } } -] \ No newline at end of file +] diff --git a/docs/UpgradePartition.md b/docs/UpgradePartition.md index 782463f56..f919844c7 100644 --- a/docs/UpgradePartition.md +++ b/docs/UpgradePartition.md @@ -1,3 +1,3 @@ # Upgrade Partition -This documentation will has been moved and can be found here: +This documentation has been moved and can be found here: diff --git a/include/Configuration.h b/include/Configuration.h index 05cd1aea9..a87080830 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -3,9 +3,12 @@ #include "PinMapping.h" #include +#include +#include +#include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change +#define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -30,6 +33,7 @@ #define CHAN_MAX_NAME_STRLEN 31 #define DEV_MAX_MAPPING_NAME_STRLEN 63 +#define LOCALE_STRLEN 2 #define PWRMTR_MAX_CHAN_COUNT 2 @@ -146,7 +150,7 @@ struct CONFIG_T { bool ScreenSaver; uint8_t Rotation; uint8_t Contrast; - uint8_t Language; + char Locale[LOCALE_STRLEN + 1]; struct { uint32_t Duration; uint8_t Mode; @@ -175,15 +179,32 @@ struct CONFIG_T { class ConfigurationClass { public: - void init(); + void init(Scheduler& scheduler); bool read(); bool write(); void migrate(); - CONFIG_T& get(); + CONFIG_T const& get(); + + class WriteGuard { + public: + WriteGuard(); + CONFIG_T& getConfig(); + ~WriteGuard(); + + private: + std::unique_lock _lock; + }; + + WriteGuard getWriteGuard(); INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); void deleteInverterById(const uint8_t id); + +private: + void loop(); + + Task _loopTask; }; extern ConfigurationClass Configuration; diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index e49bf9f61..81fcba3bc 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -40,7 +40,7 @@ class DisplayGraphicClass { 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 setLocale(const String& locale); void setDiagramMode(DiagramMode_t mode); void setStartupDisplay(); @@ -65,7 +65,7 @@ class DisplayGraphicClass { DisplayType_t _display_type = DisplayType_t::None; DiagramMode_t _diagram_mode = DiagramMode_t::Off; - uint8_t _display_language = DISPLAY_LANGUAGE; + String _display_language = DISPLAY_LOCALE; uint8_t _mExtra; const uint16_t _period = 1000; const uint16_t _interval = 60000; // interval at which to power save (milliseconds) @@ -73,6 +73,19 @@ class DisplayGraphicClass { char _fmtText[32]; bool _isLarge = false; uint8_t _lineOffsets[5]; + + String _i18n_offline; + String _i18n_yield_today_kwh; + String _i18n_yield_today_wh; + String _i18n_date_format; + String _i18n_current_power_kw; + String _i18n_current_power_w; + String _i18n_yield_total_mwh; + String _i18n_yield_total_kwh; + String _i18n_powermeter_power_w; + String _i18n_powermeter_power_kw; + String _i18n_pm_positive_today_kwh; + String _i18n_pm_negative_today_kwh; }; extern DisplayGraphicClass Display; diff --git a/include/I18n.h b/include/I18n.h new file mode 100644 index 000000000..4c9f488ee --- /dev/null +++ b/include/I18n.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +struct LanguageInfo_t { + String code; + String name; + String filename; +}; + +class I18nClass { +public: + I18nClass(); + void init(Scheduler& scheduler); + std::list getAvailableLanguages(); + String getFilenameByLocale(const String& locale) const; + void readDisplayStrings( + const String& locale, + String& date_format, + String& offline, + String& power_w, String& power_kw, + String& yield_today_wh, String& yield_today_kwh, + String& yield_total_kwh, String& yield_total_mwh, + String& yield_powermeter_power_w, String& yield_powermeter_power_kw, + String& yield_pm_positive_today_kwh, String& yield_pm_negative_today_kwh); + +private: + void readLangPacks(); + void readConfig(String file); + + std::list _availLanguages; +}; + +extern I18nClass I18n; diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 66f4e83d8..054e4faf8 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -7,29 +7,42 @@ #include // mqtt discovery device classes -enum { +enum DeviceClassType { DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, - DEVICE_CLS_TEMP, DEVICE_CLS_POWER_FACTOR, - DEVICE_CLS_REACTIVE_POWER + DEVICE_CLS_REACTIVE_POWER, + DEVICE_CLS_CONNECTIVITY, + DEVICE_CLS_DURATION, + DEVICE_CLS_SIGNAL_STRENGTH, + DEVICE_CLS_TEMPERATURE, + DEVICE_CLS_RESTART }; -const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power" }; -enum { +const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" }; + +enum StateClassType { STATE_CLS_NONE = 0, STATE_CLS_MEASUREMENT, STATE_CLS_TOTAL_INCREASING }; -const char* const stateClasses[] = { 0, "measurement", "total_increasing" }; +const char* const stateClass_name[] = { 0, "measurement", "total_increasing" }; + +enum CategoryType { + CATEGORY_NONE = 0, + CATEGORY_CONFIG, + CATEGORY_DIAGNOSTIC +}; +const char* const category_name[] = { 0, "config", "diagnostic" }; + typedef struct { FieldId_t fieldId; // field id - uint8_t deviceClsId; // device class - uint8_t stateClsId; // state class + DeviceClassType deviceClsId; // device class + StateClassType stateClsId; // state class } byteAssign_fieldDeviceClass_t; const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { @@ -42,7 +55,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { { FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, { FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, { FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT }, - { FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT }, + { FLD_T, DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT }, { FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }, @@ -59,13 +72,24 @@ class MqttHandleHassClass { private: void loop(); - void publish(const String& subtopic, const String& payload); - 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, const int16_t min = 1, const int16_t max = 100, float step = 1.0); - void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); + static void publish(const String& subtopic, const String& payload); + static void publish(const String& subtopic, const JsonDocument& doc); + + static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + // Binary Sensor + static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterBinarySensor(std::shared_ptr inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + // Sensor + static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterSensor(std::shared_ptr inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + static void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); + static void publishInverterButton(std::shared_ptr inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterNumber(std::shared_ptr inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const StateClassType state_class, const CategoryType category); void publishPowerMeterField(size_t channel, JsyMkClass::Field_t fieldId, const bool clear = false); diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index 7c86a8098..ea3a6e381 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include class MqttHandleInverterClass { public: @@ -19,7 +21,6 @@ class MqttHandleInverterClass { private: 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; @@ -41,6 +42,29 @@ class MqttHandleInverterClass { FLD_IRR, FLD_Q }; + + enum class Topic : unsigned { + LimitPersistentRelative, + LimitPersistentAbsolute, + LimitNonPersistentRelative, + LimitNonPersistentAbsolute, + Power, + Restart, + ResetRfStats, + }; + + static constexpr frozen::string _cmdtopic = "+/cmd/"; + static constexpr frozen::map _subscriptions = { + { "limit_persistent_relative", Topic::LimitPersistentRelative }, + { "limit_persistent_absolute", Topic::LimitPersistentAbsolute }, + { "limit_nonpersistent_relative", Topic::LimitNonPersistentRelative }, + { "limit_nonpersistent_absolute", Topic::LimitNonPersistentAbsolute }, + { "power", Topic::Power }, + { "restart", Topic::Restart }, + { "reset_rf_stats", Topic::ResetRfStats }, + }; + + void onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); }; extern MqttHandleInverterClass MqttHandleInverter; diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index 40ddc914d..90d3962b4 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "W5500.h" #include #include #include @@ -23,18 +24,18 @@ enum class network_event { NETWORK_EVENT_MAX }; -typedef std::function NetworkEventCb; +typedef std::function DtuNetworkEventCb; -typedef struct NetworkEventCbList { - NetworkEventCb cb; +typedef struct DtuNetworkEventCbList { + DtuNetworkEventCb cb; network_event event; - NetworkEventCbList() + DtuNetworkEventCbList() : cb(nullptr) , event(network_event::NETWORK_UNKNOWN) { } -} NetworkEventCbList_t; +} DtuNetworkEventCbList_t; class NetworkSettingsClass { public: @@ -53,7 +54,7 @@ class NetworkSettingsClass { bool isConnected() const; network_mode NetworkMode() const; - bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); + bool onEvent(DtuNetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); void raiseEvent(const network_event event); private: @@ -62,7 +63,7 @@ class NetworkSettingsClass { void setStaticIp(); void handleMDNS(); void setupMode(); - void NetworkEvent(const WiFiEvent_t event); + void NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info); Task _loopTask; @@ -81,8 +82,9 @@ class NetworkSettingsClass { bool _dnsServerStatus = false; network_mode _networkMode = network_mode::Undefined; bool _ethConnected = false; - std::vector _cbEventList; + std::vector _cbEventList; bool _lastMdnsEnabled = false; + std::unique_ptr _w5500; }; -extern NetworkSettingsClass NetworkSettings; \ No newline at end of file +extern NetworkSettingsClass NetworkSettings; diff --git a/include/PinMapping.h b/include/PinMapping.h index 5ead9185e..5a079063e 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -12,6 +12,7 @@ struct PinMapping_t { char name[MAPPING_NAME_STRLEN + 1]; + int8_t nrf24_miso; int8_t nrf24_mosi; int8_t nrf24_clk; @@ -26,6 +27,14 @@ struct PinMapping_t { int8_t cmt_gpio3; int8_t cmt_sdio; + int8_t w5500_mosi; + int8_t w5500_miso; + int8_t w5500_sclk; + int8_t w5500_cs; + int8_t w5500_int; + int8_t w5500_rst; + +#if CONFIG_ETH_USE_ESP32_EMAC int8_t eth_phy_addr; bool eth_enabled; int eth_power; @@ -33,11 +42,14 @@ struct PinMapping_t { int eth_mdio; eth_phy_type_t eth_type; eth_clock_mode_t eth_clk_mode; +#endif + uint8_t display_type; uint8_t display_data; uint8_t display_clk; uint8_t display_cs; uint8_t display_reset; + int8_t led[PINMAPPING_LED_COUNT]; int8_t serial_modbus_tx; @@ -52,11 +64,14 @@ class PinMappingClass { bool isValidNrf24Config() const; bool isValidCmt2300Config() const; + bool isValidW5500Config() const; +#if CONFIG_ETH_USE_ESP32_EMAC bool isValidEthConfig() const; +#endif bool isValidSerialModbusConfig() const; private: PinMapping_t _pinMapping; }; -extern PinMappingClass PinMapping; \ No newline at end of file +extern PinMappingClass PinMapping; diff --git a/include/RestartHelper.h b/include/RestartHelper.h new file mode 100644 index 000000000..80f5f6758 --- /dev/null +++ b/include/RestartHelper.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class RestartHelperClass { +public: + RestartHelperClass(); + void init(Scheduler& scheduler); + void triggerRestart(); + +private: + void loop(); + + Task _rebootTask; +}; + +extern RestartHelperClass RestartHelper; diff --git a/include/Utils.h b/include/Utils.h index f81e73180..39994f323 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include class Utils { @@ -9,7 +10,8 @@ class Utils { static uint32_t getChipId(); static uint64_t generateDtuSerial(); static int getTimezoneOffset(); - static void restartDtu(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); + static String generateMd5FromFile(String file); + static void skipBom(File& f); }; diff --git a/include/W5500.h b/include/W5500.h new file mode 100644 index 000000000..d85cb016c --- /dev/null +++ b/include/W5500.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include // required for esp_eth_handle_t +#include + +#include + +class W5500 { +private: + explicit W5500(spi_device_handle_t spi, gpio_num_t pin_int); + +public: + W5500(const W5500&) = delete; + W5500& operator=(const W5500&) = delete; + ~W5500(); + + static std::unique_ptr setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst); + String macAddress(); + +private: + static bool connection_check_spi(spi_device_handle_t spi); + static bool connection_check_interrupt(gpio_num_t pin_int); + + esp_eth_handle_t eth_handle; + esp_netif_t* eth_netif; +}; diff --git a/include/WebApi.h b/include/WebApi.h index b6fdbd089..6e85bafde 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -1,14 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "WebApi_config.h" #include "WebApi_device.h" #include "WebApi_devinfo.h" #include "WebApi_dtu.h" #include "WebApi_errors.h" #include "WebApi_eventlog.h" +#include "WebApi_file.h" #include "WebApi_firmware.h" #include "WebApi_gridprofile.h" +#include "WebApi_i18n.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" #include "WebApi_maintenance.h" @@ -30,6 +31,7 @@ class WebApiClass { public: WebApiClass(); void init(Scheduler& scheduler); + void reload(); static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); @@ -45,13 +47,14 @@ class WebApiClass { private: AsyncWebServer _server; - WebApiConfigClass _webApiConfig; WebApiDeviceClass _webApiDevice; WebApiDevInfoClass _webApiDevInfo; WebApiDtuClass _webApiDtu; WebApiEventlogClass _webApiEventlog; + WebApiFileClass _webApiFile; WebApiFirmwareClass _webApiFirmware; WebApiGridProfileClass _webApiGridprofile; + WebApiI18nClass _webApiI18n; WebApiInverterClass _webApiInverter; WebApiLimitClass _webApiLimit; WebApiMaintenanceClass _webApiMaintenance; diff --git a/include/WebApi_config.h b/include/WebApi_config.h deleted file mode 100644 index f29dc8fcf..000000000 --- a/include/WebApi_config.h +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include - -class WebApiConfigClass { -public: - void init(AsyncWebServer& server, Scheduler& scheduler); - -private: - void onConfigGet(AsyncWebServerRequest* request); - void onConfigDelete(AsyncWebServerRequest* request); - void onConfigListGet(AsyncWebServerRequest* request); - void onConfigUploadFinish(AsyncWebServerRequest* request); - void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); -}; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index 0da8d3d9f..68e107d42 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -18,9 +18,10 @@ enum WebApiError { DtuInvalidCmtFrequency, DtuInvalidCmtCountry, - ConfigBase = 3000, - ConfigNotDeleted, - ConfigSuccess, + FileBase = 3000, + FileNotDeleted, + FileSuccess, + FileDeleteSuccess, InverterBase = 4000, InverterSerialZero, @@ -32,6 +33,7 @@ enum WebApiError { InverterChanged, InverterDeleted, InverterOrdered, + InverterStatsResetted, LimitBase = 5000, LimitSerialZero, diff --git a/include/WebApi_file.h b/include/WebApi_file.h new file mode 100644 index 000000000..ce958b059 --- /dev/null +++ b/include/WebApi_file.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiFileClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void onFileGet(AsyncWebServerRequest* request); + void onFileDelete(AsyncWebServerRequest* request); + void onFileDeleteAll(AsyncWebServerRequest* request); + void onFileListGet(AsyncWebServerRequest* request); + void onFileUploadFinish(AsyncWebServerRequest* request); + void onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); +}; diff --git a/include/WebApi_i18n.h b/include/WebApi_i18n.h new file mode 100644 index 000000000..237e44739 --- /dev/null +++ b/include/WebApi_i18n.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiI18nClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void onI18nLanguages(AsyncWebServerRequest* request); + void onI18nLanguage(AsyncWebServerRequest* request); +}; diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index c316622e5..6ba6c5e8e 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -14,4 +14,5 @@ class WebApiInverterClass { void onInverterEdit(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request); void onInverterOrder(AsyncWebServerRequest* request); + void onInverterStatReset(AsyncWebServerRequest* request); }; diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index cf7beecce..b3194319d 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -8,9 +8,11 @@ class WebApiWsConsoleClass { public: WebApiWsConsoleClass(); void init(AsyncWebServer& server, Scheduler& scheduler); + void reload(); private: AsyncWebSocket _ws; + AuthenticationMiddleware _simpleDigestAuth; Task _wsCleanupTask; void wsCleanupTaskCb(); diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 98f91ac99..04af6968d 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -11,6 +11,7 @@ class WebApiWsLiveClass { public: WebApiWsLiveClass(); void init(AsyncWebServer& server, Scheduler& scheduler); + void reload(); private: static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); @@ -25,6 +26,7 @@ class WebApiWsLiveClass { void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); AsyncWebSocket _ws; + AuthenticationMiddleware _simpleDigestAuth; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; diff --git a/include/__compiled_constants.h b/include/__compiled_constants.h index ac8991e9f..a00caf6d0 100644 --- a/include/__compiled_constants.h +++ b/include/__compiled_constants.h @@ -5,4 +5,5 @@ extern const char *__COMPILED_GIT_HASH__; +extern const char *__COMPILED_GIT_BRANCH__; // extern const char *__COMPILED_DATE_TIME_UTC_STR__; diff --git a/include/defaults.h b/include/defaults.h index d6bc387f5..9ba8dfac1 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -99,7 +99,7 @@ #define DISPLAY_SCREENSAVER true #define DISPLAY_ROTATION 2U #define DISPLAY_CONTRAST 60U -#define DISPLAY_LANGUAGE 0U +#define DISPLAY_LOCALE "en" #define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) #define DISPLAY_DIAGRAM_MODE 1U @@ -109,8 +109,10 @@ #define MAX_INVERTER_LIMIT 2250 +#define LANG_PACK_SUFFIX ".lang.json" + #define SERIAL_MODBUS_POLL_INTERVAL 1U #ifndef SERIAL_MODBUS_BAUDRATE #define SERIAL_MODBUS_BAUDRATE 9600U -#endif \ No newline at end of file +#endif diff --git a/lang/README.md b/lang/README.md new file mode 100644 index 000000000..530e87e26 --- /dev/null +++ b/lang/README.md @@ -0,0 +1,9 @@ +# Language Packs + +This folder contains language packs for OpenDTU which can be uploaded to the +device using the "Config Management" function. +Select "Language Pack" in the restore section, select a `.json` file containing +your language and press "Restore". Afterwards all language selection drop down +menues contain the new language. + +Create a pull to request to share your own language pack (or corrections) with the community. diff --git a/lang/es.lang.json b/lang/es.lang.json new file mode 100644 index 000000000..629b97990 --- /dev/null +++ b/lang/es.lang.json @@ -0,0 +1,690 @@ +{ + "meta": { + "name": "Español", + "code": "es" + }, + "display": { + "date_format": "%d/%m/%Y %H:%M", + "offline": "Apagado", + "power_w": "%.0f W", + "power_kw": "%.1f kW", + "yield_today_wh": "Hoy: %4.0f Wh", + "yield_today_kwh": "Hoy: %.1f kWh", + "yield_total_kwh": "Total: %.1f kWh", + "yield_total_mwh": "Total: %.0f kWh" + }, + "webapp": { + "menu": { + "LiveView": "Vista en directo", + "Settings": "Ajustes", + "NetworkSettings": "Ajustes de Red", + "NTPSettings": "Ajustes NTP", + "MQTTSettings": "Ajustes MQTT", + "InverterSettings": "Ajustes Inversor", + "SecuritySettings": "Ajustes Seguridad", + "DTUSettings": "Ajustes DTU", + "DeviceManager": "Administrador Dispositivos", + "ConfigManagement": "Gestión configuración", + "FirmwareUpgrade": "Actualización Firmware", + "DeviceReboot": "Reinicio Dispositivo", + "Info": "Info", + "System": "Sistema", + "Network": "Red", + "NTP": "NTP", + "MQTT": "MQTT", + "Console": "Consola", + "About": "Acerca", + "Logout": "Logout", + "Login": "Login" + }, + "base": { + "Loading": "Cargando...", + "Reload": "Recargar", + "Cancel": "Cancelar", + "Save": "Guardar", + "Refreshing": "Refrescando", + "Pull": "Tira hacia abajo para refrescar", + "Release": "Soltar para refrescar", + "Close": "Cerrar" + }, + "wait": { + "NotReady": "OpenDTU is not yet ready", + "PleaseWait": "Please wait. You will be automatically redirected to the home page." + }, + "Error": { + "Oops": "Oops!" + }, + "localeswitcher": { + "Dark": "Oscuro", + "Light": "Claro", + "Auto": "Automático" + }, + "apiresponse": { + "1001": "¡Opciones guardadas!", + "1002": "No se encontraron valores", + "1003": "Datos demasiado grandes", + "1004": "Fallo al procesar los datos", + "1005": "Faltan valores", + "1006": "Fallo en la escritura", + "2001": "¡El número de serie no puede ser cero!", + "2002": "Intervalo de Poll interval debe ser mayor que cero!", + "2003": "Configuración de potencia incorrecta!", + "2004": "La frecuencia debe estar entre {min} y {max} kHz y debe ser un múltiplo de 250 kHz!", + "2005": "Modelo desconocido! Por favor, informe el \"Modelo de pieza de hardware\" y el modelo (por ejemplo, HM-350) como un problema en aquí.", + "3001": "No se eliminó nada", + "3002": "Configuración borrada. Reinicio en curso...", + "4001": "@:apiresponse.2001", + "4002": "El nombre debe tener entre 1 y {max} caracteres de longitud!", + "4003": "Solo se admiten {max} inversores!", + "4004": "Inversor creado!", + "4005": "ID no válido especificado", + "4006": "Cantidad de canales máxima incorrecta dada!", + "4007": "Inversor modificado!", + "4008": "Inversor eliminado!", + "4009": "Orden de inversores guardado!", + "5001": "@:apiresponse.2001", + "5002": "Límite debe estar entre 1 y {max}!", + "5003": "Tipo incorrecto especificado!", + "5004": "Inversor incorrecto especificado!", + "6001": "Reinicio desencadenado!", + "6002": "Reinicio cancelado!", + "7001": "¡El servidor MQTT debe tener entre 1 y {max} caracteres de longitud!", + "7002": "¡El nombre de usuario debe no tener más de {max} caracteres!", + "7003": "¡La contraseña debe no tener más de {max} caracteres!", + "7004": "¡El tema debe tener entre 1 y {max} caracteres de longitud!", + "7005": "¡El tema no debe contener caracteres de espacio!", + "7006": "¡El tema debe terminar con barra inclinada (/)!", + "7007": "¡El puerto debe ser un número entre 1 y 65535!", + "7008": "¡El certificado debe tener entre 1 y {max} caracteres de longitud!", + "7009": "¡El tema LWT debe tener entre 1 y {max} caracteres de longitud!", + "7010": "¡El tema LWT no debe contener caracteres de espacio!", + "7011": "¡El valor LWT en línea debe tener entre 1 y {max} caracteres de longitud!", + "7012": "¡El valor LWT fuera de línea debe tener entre 1 y {max} caracteres de longitud!", + "7013": "¡El intervalo de publicación debe ser un número entre {min} y {max}!", + "7014": "¡El tema Hass debe tener entre 1 y {max} caracteres de longitud!", + "7015": "¡El tema Hass no debe contener caracteres de espacio!", + "7016": "¡La QoS LWT no debe ser mayor que {max}!", + "7017": "Client ID must not longer then {max} characters!", + "8001": "¡La dirección IP no es válida!", + "8002": "¡La máscara de red no es válida!", + "8003": "¡El gateway no es válido!", + "8004": "¡La dirección IP del servidor DNS 1 no es válida!", + "8005": "¡La dirección IP del servidor DNS 2 no es válida!", + "8006": "¡El valor de tiempo de espera del punto de acceso administrativo es inválido!", + "9001": "¡El servidor NTP debe tener entre 1 y {max} caracteres de longitud!", + "9002": "¡La zona horaria debe tener entre 1 y {max} caracteres de longitud!", + "9003": "¡La descripción de la zona horaria debe tener entre 1 y {max} caracteres de longitud!", + "9004": "¡El año debe ser un número entre {min} y {max}!", + "9005": "¡El mes debe ser un número entre {min} y {max}!", + "9006": "¡El día debe ser un número entre {min} y {max}!", + "9007": "¡La hora debe ser un número entre {min} y {max}!", + "9008": "¡Los minutos deben ser un número entre {min} y {max}!", + "9009": "¡Los segundos deben ser un número entre {min} y {max}!", + "9010": "¡Hora actualizada!", + "10001": "¡La contraseña debe tener entre 8 y {max} caracteres de longitud!", + "10002": "¡Autenticación exitosa!", + "11001": "¡@:apiresponse.2001", + "11002": "¡@:apiresponse:5004", + "12001": "¡El perfil debe tener entre 1 y {max} caracteres de longitud!" + }, + "home": { + "LiveData": "Datos en Vivo", + "SerialNumber": "Número de Serie: ", + "CurrentLimit": "Límite de Corriente: ", + "DataAge": "Edad de los Datos: ", + "Seconds": "{val} segundos", + "ShowSetInverterLimit": "Ver / Establecer Límite del Inversor", + "TurnOnOff": "Encender/Apagar el Inversor", + "ShowInverterInfo": "Ver Información del Inversor", + "ShowEventlog": "Ver Registro de Eventos", + "UnreadMessages": "mensajes sin leer", + "Loading": "@:base.Cargando", + "EventLog": "Registro de Eventos", + "InverterInfo": "Información del Inversor", + "LimitSettings": "Configuración de Límites", + "LastLimitSetStatus": "Último Estado de Configuración del Límite:", + "SetLimit": "Establecer Límite:", + "Relative": "Relativo (%)", + "Absolute": "Absoluto (W)", + "LimitHint": "Consejo: Si establece el límite como un valor absoluto, la visualización del valor actual solo se actualizará después de ~4 minutos.", + "SetPersistent": "Establecer Límite Permanente", + "SetNonPersistent": "Establecer Límite No Permanente", + "PowerSettings": "Configuración de Energía", + "LastPowerSetStatus": "Último Estado de Configuración de Energía:", + "TurnOn": "Encender", + "TurnOff": "Apagar", + "Restart": "Reiniciar", + "Failure": "Fallo", + "Pending": "Pendiente", + "Ok": "Aceptar", + "Unknown": "Desconocido", + "ShowGridProfile": "Ver Perfil de la Red", + "GridProfile": "Perfil de la Red", + "LoadingInverter": "Waiting for data... (can take up to 10 seconds)", + "RadioStats": "Radio Statistics", + "TxRequest": "TX Request Count", + "RxSuccess": "RX Success", + "RxFailNothing": "RX Fail: Receive Nothing", + "RxFailPartial": "RX Fail: Receive Partial", + "RxFailCorrupt": "RX Fail: Receive Corrupt", + "TxReRequest": "TX Re-Request Fragment", + "StatsReset": "Reset Statistics", + "StatsResetting": "Resetting...", + "Rssi": "RSSI of last received packet", + "RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.", + "dBm": "{dbm} dBm" + }, + "eventlog": { + "Start": "Iniciar", + "Stop": "Parar", + "Id": "ID", + "Message": "Mensaje" + }, + "devinfo": { + "NoInfo": "Sin información disponible", + "NoInfoLong": "No se ha recibido ningún dato válido del inversor hasta ahora. Todavía estamos intentando...", + "UnknownModel": "¡Modelo desconocido! Por favor, informe el \"Número de parte de hardware\" y el modelo (por ejemplo, HM-350) como un problema aquí.", + "Serial": "Número de serie", + "ProdYear": "Año de producción", + "ProdWeek": "Semana de producción", + "Model": "Modelo", + "DetectedMaxPower": "Potencia máxima detectada", + "BootloaderVersion": "Versión del cargador de arranque", + "FirmwareVersion": "Versión del firmware", + "FirmwareBuildDate": "Fecha de construcción del firmware", + "HardwarePartNumber": "Número de parte de hardware", + "HardwareVersion": "Versión de hardware" + }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "Name": "Nombre", + "Version": "Versión", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "GridprofileSupport": "Apoyar el desarrollo", + "GridprofileSupportLong": "Por favor, consulte aquí para obtener más información." + }, + "systeminfo": { + "SystemInfo": "Información del sistema", + "VersionError": "Error al obtener información de la versión", + "VersionNew": "¡Nueva versión disponible! ¡Mostrar cambios!", + "VersionOk": "¡Actualizado!" + }, + "firmwareinfo": { + "FirmwareInformation": "Información del firmware", + "Hostname": "Hostname", + "SdkVersion": "Versión del SDK", + "ConfigVersion": "Versión de la configuración", + "FirmwareVersion": "Versión del firmware / Hash de Git", + "PioEnv": "Entorno PIO", + "FirmwareVersionHint": "Haga clic aquí para mostrar información sobre su versión actual", + "FirmwareUpdate": "Actualización de firmware", + "FirmwareUpdateHint": "Haga clic aquí para ver las diferencias entre su versión y la última versión", + "FrmwareUpdateAllow": "Al activar la comprobación de actualización, se envía una solicitud a GitHub.com cada vez que se llama a la página para recuperar la versión actualmente disponible. Si no está de acuerdo con esto, deje esta función desactivada.", + "ResetReason0": "Razón de reinicio CPU 0", + "ResetReason1": "Razón de reinicio CPU 1", + "ConfigSaveCount": "Contador de guardado de configuración", + "Uptime": "Tiempo de actividad", + "UptimeValue": "0 días {time} | 1 día {time} | {count} días {time}" + }, + "hardwareinfo": { + "HardwareInformation": "Información del hardware", + "ChipModel": "Modelo de chip", + "ChipRevision": "Revisión de chip", + "ChipCores": "Núcleos del chip", + "CpuFrequency": "Frecuencia de la CPU", + "Mhz": "MHz", + "CpuTemperature": "CPU Temperature", + "FlashSize": "Flash Memory Size" + }, + "memoryinfo": { + "MemoryInformation": "Información de la memoria", + "Type": "Tipo", + "Usage": "Uso", + "Free": "Libre", + "Used": "Usado", + "Size": "Tamaño", + "Heap": "Montón", + "PsRam": "PSRAM", + "LittleFs": "LittleFs", + "Sketch": "Boceto" + }, + "heapdetails": { + "HeapDetails": "Detalles del montón", + "TotalFree": "Total libre", + "LargestFreeBlock": "Bloque libre contiguo más grande", + "MaxUsage": "Uso máximo desde el inicio", + "Fragmentation": "Nivel de fragmentación" + }, + "taskdetails": { + "TaskDetails": "Task Details", + "Name": "Name", + "StackFree": "Stack Free", + "Priority": "Priority", + "Task_idle0": "Idle (CPU Core 0)", + "Task_idle1": "Idle (CPU Core 1)", + "Task_wifi": "Wi-Fi", + "Task_tit": "TCP/IP", + "Task_looptask": "Arduino Main Loop", + "Task_asynctcp": "Async TCP", + "Task_mqttclient": "MQTT Client", + "Task_huaweican0": "AC Charger CAN", + "Task_pmsdm": "PowerMeter (SDM)", + "Task_pmhttpjson": "PowerMeter (HTTP+JSON)", + "Task_pmsml": "PowerMeter (Serial SML)", + "Task_pmhttpsml": "PowerMeter (HTTP+SML)" + }, + "radioinfo": { + "RadioInformation": "Información de la radio", + "Status": "Estado de {module}", + "ChipStatus": "Estado del chip de {module}", + "ChipType": "Tipo de chip de {module}", + "Connected": "conectado", + "NotConnected": "no conectado", + "Configured": "configurado", + "NotConfigured": "no configurado", + "Unknown": "Desconocido" + }, + "networkinfo": { + "NetworkInformation": "Información de la red" + }, + "wifistationinfo": { + "WifiStationInfo": "Información de WiFi (Estación)", + "Status": "Estado", + "Enabled": "habilitado", + "Disabled": "deshabilitado", + "Ssid": "SSID", + "Bssid": "BSSID", + "Quality": "Calidad", + "Rssi": "RSSI" + }, + "wifiapinfo": { + "WifiApInfo": "Información de WiFi (Punto de acceso)", + "Status": "@:wifistationinfo.Status", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "Ssid": "@:wifistationinfo.Ssid", + "Stations": "# Estaciones" + }, + "interfacenetworkinfo": { + "NetworkInterface": "Interfaz de red ({iface})", + "Hostname": "@:firmwareinfo.Hostname", + "IpAddress": "Dirección IP", + "Netmask": "Máscara de red", + "DefaultGateway": "Puerta de enlace predeterminada", + "Dns": "DNS {num}", + "MacAddress": "Dirección MAC" + }, + "interfaceapinfo": { + "NetworkInterface": "Interfaz de red (Punto de acceso)", + "IpAddress": "@:interfacenetworkinfo.IpAddress", + "MacAddress": "@:interfacenetworkinfo.MacAddress" + }, + "ntpinfo": { + "NtpInformation": "Información de NTP", + "ConfigurationSummary": "Resumen de configuración", + "Server": "Servidor", + "Timezone": "Zona horaria", + "TimezoneDescription": "Descripción de la zona horaria", + "CurrentTime": "Hora actual", + "Status": "Estado", + "Synced": "sincronizado", + "NotSynced": "no sincronizado", + "LocalTime": "Hora local", + "Sunrise": "Amanecer", + "Sunset": "Atardecer", + "NotAvailable": "No disponible", + "Mode": "Modo", + "Day": "Día", + "Night": "Noche" + }, + "mqttinfo": { + "MqttInformation": "Información de MQTT", + "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", + "Status": "@:ntpinfo.Status", + "Enabled": "Habilitado", + "Disabled": "Deshabilitado", + "Server": "@:ntpinfo.Server", + "Port": "Puerto", + "ClientId": "Client ID", + "Username": "Nombre de usuario", + "BaseTopic": "Tema base", + "PublishInterval": "Intervalo de publicación", + "Seconds": "{sec} segundos", + "CleanSession": "Bandera CleanSession", + "Retain": "Retener", + "Tls": "TLS", + "RootCertifcateInfo": "Información del certificado raíz de CA", + "TlsCertLogin": "Iniciar sesión con certificado TLS", + "ClientCertifcateInfo": "Información del Certificado del Cliente", + "HassSummary": "Resumen de la Configuración de Descubrimiento Automático MQTT de Home Assistant", + "Expire": "Expirar", + "IndividualPanels": "Paneles Individuales", + "RuntimeSummary": "Resumen de Tiempo de Ejecución", + "ConnectionStatus": "Estado de Conexión", + "Connected": "conectado", + "Disconnected": "desconectado" + }, + "console": { + "Console": "Consola", + "VirtualDebugConsole": "Consola de Depuración Virtual", + "EnableAutoScroll": "Habilitar Desplazamiento Automático", + "ClearConsole": "Limpiar Consola", + "CopyToClipboard": "Copiar al Portapapeles" + }, + "inverterchannelinfo": { + "String": "Cadena {num}", + "Phase": "Fase {num}", + "General": "General" + }, + "invertertotalinfo": { + "TotalYieldTotal": "Total de Rendimiento Acumulado", + "TotalYieldDay": "Total de Rendimiento del Día", + "TotalPower": "Potencia Total" + }, + "inverterchannelproperty": { + "Power": "Potencia", + "Voltage": "Voltaje", + "Current": "Corriente", + "Power DC": "Potencia DC", + "YieldDay": "Rendimiento del Día", + "YieldTotal": "Rendimiento Total", + "Frequency": "Frecuencia", + "Temperature": "Temperatura", + "PowerFactor": "Factor de Potencia", + "ReactivePower": "Potencia Reactiva", + "Efficiency": "Eficiencia", + "Irradiation": "Irradiación" + }, + "maintenancereboot": { + "DeviceReboot": "Reinicio del Dispositivo", + "PerformReboot": "Realizar Reinicio", + "Reboot": "¡Reiniciar!", + "Cancel": "@:base.Cancel", + "RebootOpenDTU": "Reiniciar OpenDTU", + "RebootQuestion": "¿Realmente desea reiniciar el dispositivo?", + "RebootHint": "Nota: Normalmente no es necesario realizar un reinicio manual. OpenDTU realiza cualquier reinicio necesario (por ejemplo, después de una actualización de firmware) automáticamente. También se adoptan configuraciones sin reiniciar. Si necesita reiniciar debido a un error, considere informarlo en https://github.com/tbnobody/OpenDTU/issues." + }, + "dtuadmin": { + "DtuSettings": "Configuración de DTU", + "DtuConfiguration": "Configuración de DTU", + "Serial": "Serial", + "SerialHint": "Tanto el inversor como el DTU tienen un número de serie. El número de serie del DTU se genera aleatoriamente en el primer inicio y generalmente no es necesario cambiarlo.", + "PollInterval": "Intervalo de Sondeo", + "Seconds": "Segundos", + "NrfPaLevel": "Potencia de Transmisión NRF24", + "CmtPaLevel": "Potencia de Transmisión CMT2300A", + "NrfPaLevelHint": "Utilizado para inversores HM. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.", + "CmtPaLevelHint": "Utilizado para inversores HMS/HMT. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.", + "CmtCountry": "Región/País CMT2300A", + "CmtCountryHint": "Cada país tiene asignaciones de frecuencia diferentes.", + "country_0": "Europa ({min}MHz - {max}MHz)", + "country_1": "América del Norte ({min}MHz - {max}MHz)", + "country_2": "Brasil ({min}MHz - {max}MHz)", + "CmtFrequency": "Frecuencia CMT2300A", + "CmtFrequencyHint": "¡Asegúrese de utilizar solo frecuencias permitidas en el país respectivo! Después de un cambio de frecuencia, puede tardar hasta 15 minutos en establecer una conexión.", + "CmtFrequencyWarning": "La frecuencia seleccionada está fuera del rango permitido en su región/país seleccionado. Asegúrese de que esta selección no infrinja ninguna regulación local.", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", + "Min": "Mínimo ({db} dBm)", + "Low": "Bajo ({db} dBm)", + "High": "Alto ({db} dBm)", + "Max": "Máximo ({db} dBm)" + }, + "securityadmin": { + "SecuritySettings": "Configuración de Seguridad", + "AdminPassword": "Contraseña de Administrador", + "Password": "Contraseña", + "RepeatPassword": "Repetir Contraseña", + "PasswordHint": "Consejo: La contraseña de administrador se utiliza para acceder a esta interfaz web (usuario 'admin'), pero también para conectarse al dispositivo cuando está en modo AP. Debe tener 8 a 64 caracteres.", + "Permissions": "Permisos", + "ReadOnly": "Permitir acceso de solo lectura a la interfaz web sin contraseña" + }, + "ntpadmin": { + "NtpSettings": "Configuración de NTP", + "NtpConfiguration": "Configuración de NTP", + "TimeServer": "Servidor de Tiempo", + "TimeServerHint": "El valor predeterminado es adecuado siempre que OpenDTU tenga acceso directo a Internet.", + "Timezone": "Zona Horaria", + "TimezoneConfig": "Configuración de Zona Horaria", + "LocationConfiguration": "Configuración de Ubicación", + "Longitude": "Longitud", + "Latitude": "Latitud", + "SunSetType": "Tipo de Atardecer", + "SunSetTypeHint": "Afecta al cálculo día/noche. Puede tardar hasta un minuto en aplicarse el nuevo tipo.", + "OFFICIAL": "Amanecer estándar (90.8°)", + "NAUTICAL": "Amanecer náutico (102°)", + "CIVIL": "Amanecer civil (96°)", + "ASTONOMICAL": "Amanecer astronómico (108°)", + "ManualTimeSynchronization": "Sincronización Manual del Tiempo", + "CurrentOpenDtuTime": "Hora Actual de OpenDTU", + "CurrentLocalTime": "Hora Local Actual", + "SynchronizeTime": "Sincronizar Tiempo", + "SynchronizeTimeHint": "Consejo: Puede utilizar la sincronización manual del tiempo para establecer la hora actual de OpenDTU si no hay un servidor NTP disponible. Pero tenga en cuenta que en caso de un ciclo de energía, se perderá la hora. Además, tenga en cuenta que la precisión del tiempo se verá gravemente afectada, ya que no se puede resincronizar regularmente y el microcontrolador ESP32 no tiene un reloj en tiempo real." + }, + "networkadmin": { + "NetworkSettings": "Configuración de Red", + "WifiConfiguration": "Configuración de WiFi", + "WifiSsid": "SSID de WiFi", + "WifiPassword": "Contraseña de WiFi", + "Hostname": "Nombre de Host", + "HostnameHint": "Consejo: El texto %06X se remplazará con los últimos 6 dígitos del ChipID de ESP en formato hexadecimal.", + "EnableDhcp": "Habilitar DHCP", + "StaticIpConfiguration": "Configuración de IP Estática", + "IpAddress": "Dirección IP", + "Netmask": "Máscara de Red", + "DefaultGateway": "Puerta de Enlace Predeterminada", + "Dns": "Servidor DNS {num}", + "AdminAp": "Configuración de WiFi (Punto de Acceso de Administrador)", + "ApTimeout": "Tiempo de espera del Punto de Acceso", + "ApTimeoutHint": "Tiempo que se mantiene abierto el Punto de Acceso. Un valor de 0 significa infinito.", + "Minutes": "minutos", + "EnableMdns": "Habilitar mDNS", + "MdnsSettings": "Configuración de mDNS" + }, + "mqttadmin": { + "MqttSettings": "Configuración de MQTT", + "MqttConfiguration": "Configuración de MQTT", + "EnableMqtt": "Habilitar MQTT", + "EnableHass": "Habilitar Descubrimiento Automático MQTT de Home Assistant", + "MqttBrokerParameter": "Parámetros del Broker MQTT", + "Hostname": "Nombre de Host", + "HostnameHint": "Nombre de host o dirección IP", + "Port": "Puerto", + "ClientId": "Client ID", + "Username": "Nombre de Usuario", + "UsernameHint": "Nombre de usuario, dejar vacío para conexión anónima", + "Password": "Contraseña", + "PasswordHint": "Contraseña, dejar vacío para conexión anónima", + "BaseTopic": "Tema Base", + "BaseTopicHint": "Tema base, se antepondrá a todos los temas publicados (por ejemplo, inverter/)", + "PublishInterval": "Intervalo de Publicación", + "Seconds": "segundos", + "CleanSession": "Habilitar Bandera CleanSession", + "EnableRetain": "Habilitar Bandera Retain", + "EnableTls": "Habilitar TLS", + "RootCa": "Certificado Raíz CA (predeterminado Letsencrypt)", + "TlsCertLoginEnable": "Habilitar Inicio de Sesión con Certificado TLS", + "ClientCert": "Certificado del Cliente TLS", + "ClientKey": "Clave del Cliente TLS", + "LwtParameters": "Parámetros de LWT", + "LwtTopic": "Tema de LWT", + "LwtTopicHint": "Tema de LWT, se añadirá al tema base", + "LwtOnline": "Mensaje de LWT en línea", + "LwtOnlineHint": "Mensaje que se publicará en el tema de LWT cuando esté en línea", + "LwtOffline": "Mensaje de LWT fuera de línea", + "LwtOfflineHint": "Mensaje que se publicará en el tema de LWT cuando esté fuera de línea", + "LwtQos": "QoS (Calidad de Servicio)", + "QOS0": "0 (Como máximo una vez)", + "QOS1": "1 (Al menos una vez)", + "QOS2": "2 (Exactamente una vez)", + "HassParameters": "Parámetros de Descubrimiento Automático MQTT de Home Assistant", + "HassPrefixTopic": "Tema de Prefijo", + "HassPrefixTopicHint": "El prefijo para el tema de descubrimiento", + "HassRetain": "Habilitar Bandera Retain", + "HassExpire": "Habilitar Expiración", + "HassIndividual": "Paneles Individuales" + }, + "inverteradmin": { + "InverterSettings": "Configuración del Inversor", + "AddInverter": "Agregar un nuevo Inversor", + "Serial": "Serial", + "Name": "Nombre", + "Add": "Agregar", + "AddHint": "Consejo: Puede configurar parámetros adicionales después de haber creado el inversor. Use el ícono de lápiz en la lista de inversores.", + "InverterList": "Lista de Inversores", + "Status": "Estado", + "Send": "Enviar", + "Receive": "Recibir", + "StatusHint": "Consejo: El inversor se alimenta con su entrada de CC. Si no hay sol, el inversor está apagado. Aún se pueden enviar solicitudes.", + "Type": "Tipo", + "Action": "Acción", + "SaveOrder": "Guardar orden", + "DeleteInverter": "Eliminar inversor", + "EditInverter": "Editar inversor", + "General": "General", + "String": "Cadena", + "Advanced": "Avanzado", + "InverterSerial": "Serial del Inversor:", + "InverterName": "Nombre del Inversor:", + "InverterNameHint": "Aquí puede especificar un nombre personalizado para su inversor.", + "InverterStatus": "Recibir / Enviar", + "PollEnable": "Sondear datos del inversor", + "PollEnableNight": "Sondear datos del inversor por la noche", + "CommandEnable": "Enviar comandos", + "CommandEnableNight": "Enviar comandos por la noche", + "StringName": "Nombre de cadena {num}:", + "StringNameHint": "Aquí puede especificar un nombre personalizado para el puerto respectivo de su inversor.", + "StringMaxPower": "Potencia máxima de cadena {num}:", + "StringMaxPowerHint": "Ingrese la potencia máxima de los paneles solares conectados.", + "StringYtOffset": "Compensación total de rendimiento de cadena {num}:", + "StringYtOffsetHint": "Esta compensación se aplica al valor total de rendimiento leído del inversor. Esto se puede usar para ajustar el rendimiento total del inversor a cero si se utiliza un inversor usado. Pero aún puede intentar sondear datos.", + "InverterHint": "*) Ingrese Wp del canal para calcular la irradiación.", + "ReachableThreshold": "Umbral de Alcanzabilidad", + "ReachableThresholdHint": "Define cuántas solicitudes se permiten fallar hasta que el inversor se considere no alcanzable.", + "ZeroRuntime": "Datos de tiempo cero", + "ZeroRuntimeHint": "Datos de tiempo cero (sin datos de rendimiento) si el inversor se vuelve inalcanzable.", + "ZeroDay": "Rendimiento diario cero a medianoche", + "ZeroDayHint": "Esto solo funciona si el inversor es inalcanzable. Si se leen datos del inversor, se usarán sus valores. (El reinicio solo ocurre en el ciclo de energía)", + "ClearEventlog": "Clear Eventlog at midnight", + "Cancel": "@:base.Cancel", + "Save": "@:base.Save", + "DeleteMsg": "¿Está seguro de que desea eliminar el inversor \"{name}\" con número de serie {serial}?", + "Delete": "Eliminar", + "YieldDayCorrection": "Corrección de Rendimiento Diario", + "YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche" + }, + "fileadmin": { + "ConfigManagement": "Gestión de Configuración", + "BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración", + "BackupConfig": "Copia de seguridad del archivo de configuración", + "Backup": "Copia de seguridad", + "Restore": "Restaurar", + "NoFileSelected": "Ningún archivo seleccionado", + "RestoreHeader": "Restaurar: Restaurar el Archivo de Configuración", + "Back": "Atrás", + "UploadSuccess": "Carga Exitosa", + "RestoreHint": "Nota: Esta operación reemplaza el archivo de configuración con la configuración restaurada y reinicia OpenDTU para aplicar todas las configuraciones.", + "ResetHeader": "Inicializar: Realizar Restablecimiento de Fábrica", + "FactoryResetButton": "Restaurar Configuraciones Predeterminadas de Fábrica", + "ResetHint": "Nota: Haga clic en Restaurar Configuraciones Predeterminadas de Fábrica para restaurar e inicializar las configuraciones predeterminadas de fábrica y reiniciar.", + "FactoryReset": "Restablecimiento de Fábrica", + "ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?", + "ResetConfirm": "Restablecimiento de Fábrica", + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON file is formatted incorrectly.", + "InvalidJsonContent": "JSON file has the wrong content." + }, + "login": { + "Login": "Iniciar Sesión", + "SystemLogin": "Inicio de Sesión en el Sistema", + "Username": "Nombre de Usuario", + "UsernameRequired": "Se requiere el nombre de usuario", + "Password": "Contraseña", + "PasswordRequired": "Se requiere la contraseña", + "LoginButton": "Iniciar Sesión" + }, + "firmwareupgrade": { + "FirmwareUpgrade": "Actualización de Firmware", + "Loading": "@:base.Loading", + "OtaError": "Error OTA", + "Back": "Atrás", + "Retry": "Reintentar", + "OtaStatus": "Estado OTA", + "OtaSuccess": "La carga de firmware fue exitosa. El dispositivo se reinició automáticamente. Cuando el dispositivo vuelva a ser accesible, la interfaz se recargará automáticamente.", + "FirmwareUpload": "Carga de Firmware", + "UploadProgress": "Progreso de Carga" + }, + "about": { + "AboutOpendtu": "Acerca de OpenDTU", + "Documentation": "Documentation", + "DocumentationBody": "The firmware and hardware documentation can be found here: https://www.opendtu.solar", + "ProjectOrigin": "Origen del Proyecto", + "ProjectOriginBody1": "Este proyecto se inició a partir de esta discusión. (Mikrocontroller.net)", + "ProjectOriginBody2": "El protocolo de Hoymiles fue descifrado mediante los esfuerzos voluntarios de muchos participantes. OpenDTU, entre otros, se desarrolló basado en este trabajo. El proyecto está bajo una Licencia de Código Abierto (Licencia Pública General de GNU versión 2).", + "ProjectOriginBody3": "El software se desarrolló según nuestro mejor conocimiento y creencia. Sin embargo, no se acepta ninguna responsabilidad por un mal funcionamiento o pérdida de garantía del inversor.", + "ProjectOriginBody4": "OpenDTU está disponible de forma gratuita. Si pagaste dinero por el software, probablemente te estafaron.", + "NewsUpdates": "Noticias y Actualizaciones", + "NewsUpdatesBody": "Las nuevas actualizaciones se pueden encontrar en Github: https://github.com/tbnobody/OpenDTU", + "ErrorReporting": "Reporte de Errores", + "ErrorReportingBody": "Por favor, informa problemas utilizando la función proporcionada por Github", + "Discussion": "Discusión", + "DiscussionBody": "Discute con nosotros en Discord o Github" + }, + "hints": { + "RadioProblem": "No se pudo conectar a un módulo de radio configurado. Por favor, verifica la conexión.", + "TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.", + "TimeSyncLink": "Por favor, verifica la configuración de tu hora.", + "DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.", + "DefaultPasswordLink": "Por favor, cambia la contraseña." + }, + "deviceadmin": { + "DeviceManager": "Administrador de Dispositivos", + "ParseError": "Error de análisis en 'pin_mapping.json': {error}", + "PinAssignment": "Configuración de Conexión", + "SelectedProfile": "Perfil Seleccionado", + "DefaultProfile": "(Configuraciones predeterminadas)", + "ProfileHint": "Tu dispositivo puede dejar de responder si seleccionas un perfil incompatible. En este caso, debes realizar una eliminación a través de la interfaz serial.", + "Display": "Pantalla", + "PowerSafe": "Habilitar Ahorro de Energía", + "PowerSafeHint": "Apaga la pantalla si no hay un inversor produciendo.", + "Screensaver": "Habilitar Protector de Pantalla", + "ScreensaverHint": "Mueve la pantalla un poco en cada actualización para evitar el quemado. (Útil especialmente para pantallas OLED)", + "DiagramMode": "Modo de Diagrama", + "off": "Apagar", + "small": "Pequeño", + "fullscreen": "Pantalla Completa", + "DiagramDuration": "Duración del Diagrama", + "DiagramDurationHint": "El período de tiempo que se muestra en el diagrama.", + "Seconds": "Segundos", + "Contrast": "Contraste ({contrast})", + "Rotation": "Rotación", + "rot0": "Sin rotación", + "rot90": "Rotación de 90 grados", + "rot180": "Rotación de 180 grados", + "rot270": "Rotación de 270 grados", + "DisplayLanguage": "Idioma de la Pantalla", + "en": "Inglés", + "de": "Alemán", + "fr": "Francés", + "Leds": "LEDs", + "EqualBrightness": "Brillo Equitativo", + "LedBrightness": "Brillo del LED {led} ({brightness})" + }, + "pininfo": { + "Category": "Categoría", + "Name": "Nombre", + "Number": "Número", + "ValueSelected": "Seleccionado", + "ValueActive": "Activo" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" + } + } +} diff --git a/lang/it.lang.json b/lang/it.lang.json new file mode 100644 index 000000000..eaafda630 --- /dev/null +++ b/lang/it.lang.json @@ -0,0 +1,690 @@ +{ + "meta": { + "name": "Italiano", + "code": "it" + }, + "display": { + "date_format": "%d/%m/%Y %H:%M", + "offline": "Offline", + "power_w": "%.0f W", + "power_kw": "%.1f kW", + "yield_today_wh": "oggi: %4.0f Wh", + "yield_today_kwh": "oggi: %.1f kWh", + "yield_total_kwh": "totale: %.1f kWh", + "yield_total_mwh": "totale: %.0f kWh" + }, + "webapp": { + "menu": { + "LiveView": "Dati in tempo reale", + "Settings": "Impostazioni", + "NetworkSettings": "Impostazioni di rete", + "NTPSettings": "Impostazioni NTP", + "MQTTSettings": "Impostazioni MQTT", + "InverterSettings": "Impostazioni Inverter", + "SecuritySettings": "Impostazioni di Sicurezza", + "DTUSettings": "Impostazioni DTU", + "DeviceManager": "Gestione Dispositivi", + "ConfigManagement": "Gestione Configurazione", + "FirmwareUpgrade": "Aggiornamento Firmware", + "DeviceReboot": "Riavvio DTU", + "Info": "Info", + "System": "Sistema", + "Network": "Rete", + "NTP": "NTP", + "MQTT": "MQTT", + "Console": "Console", + "About": "Informazioni DTU", + "Logout": "Esci", + "Login": "Login" + }, + "base": { + "Loading": "Caricamento...", + "Reload": "Ricarica", + "Cancel": "Cancella", + "Save": "Salva", + "Refreshing": "Aggiorna", + "Pull": "Trascina in basso per aggiornare", + "Release": "Rilascia per aggiornare", + "Close": "Chiudi" + }, + "wait": { + "NotReady": "OpenDTU is not yet ready", + "PleaseWait": "Please wait. You will be automatically redirected to the home page." + }, + "Error": { + "Oops": "Oops!" + }, + "localeswitcher": { + "Dark": "Scuro", + "Light": "Chiaro", + "Auto": "Automatico" + }, + "apiresponse": { + "1001": "Settings saved!", + "1002": "No values found!", + "1003": "Data too large!", + "1004": "Failed to parse data!", + "1005": "Values are missing!", + "1006": "Write failed!", + "2001": "Serial cannot be zero!", + "2002": "Poll interval must be greater zero!", + "2003": "Invalid power level setting!", + "2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!", + "2005": "Invalid country selection!", + "3001": "Not deleted anything!", + "3002": "Configuration resettet. Rebooting now...", + "4001": "@:apiresponse.2001", + "4002": "Name must between 1 and {max} characters long!", + "4003": "Only {max} inverters are supported!", + "4004": "Inverter created!", + "4005": "Invalid ID specified!", + "4006": "Invalid amount of max channel setting given!", + "4007": "Inverter changed!", + "4008": "Inverter deleted!", + "4009": "Inverter order saved!", + "5001": "@:apiresponse.2001", + "5002": "Limit must between 1 and {max}!", + "5003": "Invalid type specified!", + "5004": "Invalid inverter specified!", + "6001": "Reboot triggered!", + "6002": "Reboot cancled!", + "7001": "MQTT Server must between 1 and {max} characters long!", + "7002": "Username must not longer then {max} characters!", + "7003": "Password must not longer then {max} characters!", + "7004": "Topic must not longer then {max} characters!", + "7005": "Topic must not contain space characters!", + "7006": "Topic must end with slash (/)!", + "7007": "Port must be a number between 1 and 65535!", + "7008": "Certificate must not longer then {max} characters!", + "7009": "LWT topic must not longer then {max} characters!", + "7010": "LWT topic must not contain space characters!", + "7011": "LWT online value must not longer then {max} characters!", + "7012": "LWT offline value must not longer then {max} characters!", + "7013": "Publish interval must be a number between {min} and {max}!", + "7014": "Hass topic must not longer then {max} characters!", + "7015": "Hass topic must not contain space characters!", + "7016": "LWT QOS must not greater then {max}!", + "7017": "Client ID must not longer then {max} characters!", + "8001": "IP address is invalid!", + "8002": "Netmask is invalid!", + "8003": "Gateway is invalid!", + "8004": "DNS Server IP 1 is invalid!", + "8005": "DNS Server IP 2 is invalid!", + "8006": "Administrative AccessPoint Timeout value is invalid", + "9001": "NTP Server must between 1 and {max} characters long!", + "9002": "Timezone must between 1 and {max} characters long!", + "9003": "Timezone description must between 1 and {max} characters long!", + "9004": "Year must be a number between {min} and {max}!", + "9005": "Month must be a number between {min} and {max}!", + "9006": "Day must be a number between {min} and {max}!", + "9007": "Hour must be a number between {min} and {max}!", + "9008": "Minute must be a number between {min} and {max}!", + "9009": "Second must be a number between {min} and {max}!", + "9010": "Time updated!", + "10001": "Password must between 8 and {max} characters long!", + "10002": "Authentication successful!", + "11001": "@:apiresponse.2001", + "11002": "@:apiresponse:5004", + "12001": "Profil must between 1 and {max} characters long!" + }, + "home": { + "LiveData": "Dati in tempo reale", + "SerialNumber": "Numero seriale: ", + "CurrentLimit": "Limite attuale: ", + "DataAge": "Aggiornamento Dati: ", + "Seconds": "{val} secondi", + "ShowSetInverterLimit": "Mostra / Imposta Limite di Potenza", + "TurnOnOff": "Accendi/Spegni Inverter", + "ShowInverterInfo": "Mostra info Inverter", + "ShowEventlog": "Mostra Log Eventi", + "UnreadMessages": "msg non letti", + "Loading": "@:base.Loading", + "EventLog": "Log Eventi", + "InverterInfo": "Info Inverter", + "LimitSettings": "Impostazioni Limite Potenza", + "LastLimitSetStatus": "Stato ultimo limite impostato:", + "SetLimit": "Imposta Limite a:", + "Relative": "Percentuale (%)", + "Absolute": "Assoluto (W)", + "LimitHint": "Nota: Se imposti il limite assoluto, il valore sul display sarà aggiornato dopo circa 4 minuti.", + "SetPersistent": "Imposta Limite in Modo Persistente", + "SetNonPersistent": "Imposta Limite Temporaneamente", + "PowerSettings": "Impostazioni Potenza", + "LastPowerSetStatus": "Ultimo Stato dell'Inverter:", + "TurnOn": "Accendi Inverter", + "TurnOff": "Spegni Inverter", + "Restart": "Riavvia Inverter", + "Failure": "Fallito", + "Pending": "In Attesa", + "Ok": "Ok", + "Unknown": "Sconosciuto", + "ShowGridProfile": "Mostra Settaggi Inverter", + "GridProfile": "Settaggi Inverter", + "LoadingInverter": "In attesa dei dati... (puo' richiedere fino a 10 secondi)", + "RadioStats": "Radio Statistics", + "TxRequest": "TX Request Count", + "RxSuccess": "RX Success", + "RxFailNothing": "RX Fail: Receive Nothing", + "RxFailPartial": "RX Fail: Receive Partial", + "RxFailCorrupt": "RX Fail: Receive Corrupt", + "TxReRequest": "TX Re-Request Fragment", + "StatsReset": "Reset Statistics", + "StatsResetting": "Resetting...", + "Rssi": "RSSI of last received packet", + "RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.", + "dBm": "{dbm} dBm" + }, + "eventlog": { + "Start": "Inizio", + "Stop": "Fine", + "Id": "ID", + "Message": "Messaggio" + }, + "devinfo": { + "NoInfo": "Informazioni non disponibili", + "NoInfoLong": "Ancora nessuna informazione dall'inverter. Sto riprovando...", + "UnknownModel": "Modello sconosciuto! Per favore fornisci \"Hardware Part Number\" ed il modello (esempio HM-350) in una Issue su GitHub.", + "Serial": "Seriale", + "ProdYear": "Produzione Annua", + "ProdWeek": "Produzione Settimanale", + "Model": "Modello", + "DetectedMaxPower": "Rilevata potenza massima", + "BootloaderVersion": "Versione Bootloader", + "FirmwareVersion": "Versione Firmware", + "FirmwareBuildDate": "Data Firmware", + "HardwarePartNumber": "Hardware Part Number", + "HardwareVersion": "Hardware Version" + }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "Name": "Nome", + "Version": "Versione", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "GridprofileSupport": "Supporto sviluppatori", + "GridprofileSupportLong": "Clicca qui per ulteriori informazioni." + }, + "systeminfo": { + "SystemInfo": "Info Sistema", + "VersionError": "Errore ricezione della versione", + "VersionNew": "Nuova versione disponibile! Mostra aggiornamenti!", + "VersionOk": "Già aggiornato!" + }, + "firmwareinfo": { + "FirmwareInformation": "Info Firmware", + "Hostname": "Hostname", + "SdkVersion": "SDK Version", + "ConfigVersion": "Config Version", + "FirmwareVersion": "Firmware Version / Git Hash", + "PioEnv": "PIO Environment", + "FirmwareVersionHint": "Click here to show information about your current version", + "FirmwareUpdate": "Firmware Update", + "FirmwareUpdateHint": "Click here to view the changes between your version and the latest version", + "FrmwareUpdateAllow": "By activating the update check, a request is sent to GitHub.com each time the page is called up to retrieve the currently available version. If you do not agree with this, leave this function deactivated.", + "ResetReason0": "Reset Reason CPU 0", + "ResetReason1": "Reset Reason CPU 1", + "ConfigSaveCount": "Config save count", + "Uptime": "Uptime", + "UptimeValue": "0 days {time} | 1 day {time} | {count} days {time}" + }, + "hardwareinfo": { + "HardwareInformation": "Info Hardware", + "ChipModel": "Chip Model", + "ChipRevision": "Chip Revision", + "ChipCores": "Chip Cores", + "CpuFrequency": "CPU Frequency", + "Mhz": "MHz", + "CpuTemperature": "CPU Temperature", + "FlashSize": "Flash Memory Size" + }, + "memoryinfo": { + "MemoryInformation": "Info Memoria", + "Type": "Tipo", + "Usage": "Uso", + "Free": "Libera", + "Used": "Usata", + "Size": "Dimensione", + "Heap": "Heap", + "PsRam": "PSRAM", + "LittleFs": "LittleFs", + "Sketch": "Sketch" + }, + "heapdetails": { + "HeapDetails": "Dettagli memoria Heap", + "TotalFree": "Libera totale", + "LargestFreeBlock": "Blocco contiguo libero più grande", + "MaxUsage": "Massima utilizzata dall'avvio", + "Fragmentation": "Livello frammentazione" + }, + "taskdetails": { + "TaskDetails": "Task Details", + "Name": "Name", + "StackFree": "Stack Free", + "Priority": "Priority", + "Task_idle0": "Idle (CPU Core 0)", + "Task_idle1": "Idle (CPU Core 1)", + "Task_wifi": "Wi-Fi", + "Task_tit": "TCP/IP", + "Task_looptask": "Arduino Main Loop", + "Task_asynctcp": "Async TCP", + "Task_mqttclient": "MQTT Client", + "Task_huaweican0": "AC Charger CAN", + "Task_pmsdm": "PowerMeter (SDM)", + "Task_pmhttpjson": "PowerMeter (HTTP+JSON)", + "Task_pmsml": "PowerMeter (Serial SML)", + "Task_pmhttpsml": "PowerMeter (HTTP+SML)" + }, + "radioinfo": { + "RadioInformation": "Info Transceiver Radio", + "Status": "{module} Stato", + "ChipStatus": "{module} Chip Stato", + "ChipType": "{module} Chip Tipo", + "Connected": "connesso", + "NotConnected": "non connesso", + "Configured": "configurato", + "NotConfigured": "no configurato", + "Unknown": "Sconosciuto" + }, + "networkinfo": { + "NetworkInformation": "Informazioni Rete" + }, + "wifistationinfo": { + "WifiStationInfo": "Info WiFi (Station)", + "Status": "Stato", + "Enabled": "abilitato", + "Disabled": "disabilitato", + "Ssid": "SSID", + "Bssid": "BSSID", + "Quality": "Qualità", + "Rssi": "RSSI" + }, + "wifiapinfo": { + "WifiApInfo": "Info WiFi (Access Point)", + "Status": "@:wifistationinfo.Status", + "Enabled": "@:wifistationinfo.Enabled", + "Disabled": "@:wifistationinfo.Disabled", + "Ssid": "@:wifistationinfo.Ssid", + "Stations": "Numero Stazioni" + }, + "interfacenetworkinfo": { + "NetworkInterface": "Interfaccia di Rete ({iface})", + "Hostname": "@:firmwareinfo.Hostname", + "IpAddress": "Indirizzo IP", + "Netmask": "Netmask", + "DefaultGateway": "Gateway", + "Dns": "DNS {num}", + "MacAddress": "Indirizzo MAC" + }, + "interfaceapinfo": { + "NetworkInterface": "Interfaccia di Rete (Access Point)", + "IpAddress": "@:interfacenetworkinfo.IpAddress", + "MacAddress": "@:interfacenetworkinfo.MacAddress" + }, + "ntpinfo": { + "NtpInformation": "Informazioni NTP", + "ConfigurationSummary": "Riepilogo Configurazione", + "Server": "Server", + "Timezone": "Timezone", + "TimezoneDescription": "Descrizione Timezone", + "CurrentTime": "Data/Ora attuale", + "Status": "Stato", + "Synced": "sincronizzata", + "NotSynced": "non sincronizzata", + "LocalTime": "Ora Locale", + "Sunrise": "Alba", + "Sunset": "Tramonto", + "NotAvailable": "Non Disponibile", + "Mode": "Modalità", + "Day": "Giorno", + "Night": "Notte" + }, + "mqttinfo": { + "MqttInformation": "Informazioni MQTT", + "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", + "Status": "@:ntpinfo.Status", + "Enabled": "Abilitato", + "Disabled": "Disabilitato", + "Server": "@:ntpinfo.Server", + "Port": "Porta", + "ClientId": "Client ID", + "Username": "Username", + "BaseTopic": "Topic Base", + "PublishInterval": "Intervallo Publish", + "Seconds": "{sec} secondi", + "CleanSession": "CleanSession", + "Retain": "Retain", + "Tls": "TLS", + "RootCertifcateInfo": "Info Certificato Root CA", + "TlsCertLogin": "Entra con Certificato TLS", + "ClientCertifcateInfo": "Info Certificato Client", + "HassSummary": "Riepilogo Configurazione Home Assistant MQTT Auto Discovery", + "Expire": "Scade", + "IndividualPanels": "Pannello Individuale", + "RuntimeSummary": "Riepilogo Runtime", + "ConnectionStatus": "Stato Connessione", + "Connected": "connesso", + "Disconnected": "disconnesso" + }, + "console": { + "Console": "Console", + "VirtualDebugConsole": "Virtual Debug Console", + "EnableAutoScroll": "Abilita AutoScroll", + "ClearConsole": "Pulisci Console", + "CopyToClipboard": "Copia nella clipboard" + }, + "inverterchannelinfo": { + "String": "Stringa {num}", + "Phase": "Fase {num}", + "General": "Generale" + }, + "invertertotalinfo": { + "TotalYieldTotal": "Totale Energia", + "TotalYieldDay": "Energia Giornaliera", + "TotalPower": "Potenza Totale" + }, + "inverterchannelproperty": { + "Power": "Potenza", + "Voltage": "Tensione", + "Current": "Corrente", + "Power DC": "PotenzaDC", + "YieldDay": "EnergiaOggi", + "YieldTotal": "EnergiaTotale", + "Frequency": "Frequenza", + "Temperature": "Temperatura", + "PowerFactor": "FattorePotenza", + "ReactivePower": "PotenzaReattiva", + "Efficiency": "Efficienza", + "Irradiation": "Irragiamento" + }, + "maintenancereboot": { + "DeviceReboot": "Riavvio DTU", + "PerformReboot": "Fai il riavvio", + "Reboot": "Riavvio!", + "Cancel": "@:base.Cancel", + "RebootOpenDTU": "Riavvio OpenDTU", + "RebootQuestion": "Vuoi veramente riavvia il DTU?", + "RebootHint": "Nota: Normalmente non serve riavviare OpenDTU, in quanto esegue automaticamente il ravvio quando necessario (ad esempio dopo aggiornamento firmware). Modifiche alla configurazione vengono apprese subito, senza richiedere riavvio. Se devi riavviare a causa di un errore, ti preghiamo di segnalarcelo cliccando su https://github.com/tbnobody/OpenDTU/issues." + }, + "dtuadmin": { + "DtuSettings": "Impostazioni DTU", + "DtuConfiguration": "Configurazione DTU", + "Serial": "Seriale", + "SerialHint": "Sia il DTU che l'inverter hanno un numero seriale. Il numero seriale del DTU è generato casualmente al primo avvio e normalmente non serve modificarlo.", + "PollInterval": "Intervallo Interrogazione", + "Seconds": "Secondi", + "NrfPaLevel": "Potenza Trasmettitore NRF24", + "CmtPaLevel": "Potenza Trasmettitore CMT2300A", + "NrfPaLevelHint": "Usato per inverter HM. Considera che aumentando la potenza aumentano il consumo di corrente.", + "CmtPaLevelHint": "Usato per inverter HMS/HMT. Considera che aumentando la potenza aumentano il consumo di corrente.", + "CmtCountry": "CMT2300A Zona/Paese", + "CmtCountryHint": "Ogni zona ha una differente allocazione di frequenze utilizzabili.", + "country_0": "Europa ({min}MHz - {max}MHz)", + "country_1": "Nord America ({min}MHz - {max}MHz)", + "country_2": "Brasile ({min}MHz - {max}MHz)", + "CmtFrequency": "Frequenza CMT2300A", + "CmtFrequencyHint": "Fai attenzione ad usare solo frequenze ammesse nel tuo Paese! Dopo la modifica frequenza, servono fino a 15 minuti affinché la connessione si ristabilisca.", + "CmtFrequencyWarning": "La frequenza selezionata è fuori dal range selezionato dal tuo Paese. Verifica che la frequenza selezionata non violi le normative del tuo Paese.", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", + "Min": "Minima ({db} dBm)", + "Low": "Bassa ({db} dBm)", + "High": "Alta ({db} dBm)", + "Max": "Massima ({db} dBm)" + }, + "securityadmin": { + "SecuritySettings": "Impostazioni di Sicurezza", + "AdminPassword": "Password Admin", + "Password": "Password", + "RepeatPassword": "Ripeti Password", + "PasswordHint": "Nota: La password di amministrazione viene utilizzata non solo per accedere a questa interfaccia web (con user 'admin'), ma anche per connettersi al dispositivo in modalità AP. Deve avere da 8 a 64 caratteri.", + "Permissions": "Permessi", + "ReadOnly": "Permetti accessi web in sola lettura senza richiedere la password" + }, + "ntpadmin": { + "NtpSettings": "Impostazioni NTP (Data / Ora)", + "NtpConfiguration": "Configurazione NTP", + "TimeServer": "Server NTP", + "TimeServerHint": "Puoi lasciare il valore di default, nel caso in cui OpenDTU abbia accesso ad internet.", + "Timezone": "Timezone", + "TimezoneConfig": "Timezone Config", + "LocationConfiguration": "Configurazione Posizione", + "Longitude": "Longitudine", + "Latitude": "Latitudine", + "SunSetType": "Tipo di Alba", + "SunSetTypeHint": "Influenza il calcolo dell'ora di Alba/Tramonto. Dopo la conferma, è richiesto fino ad un minuto perché la modifica venga applicata.", + "OFFICIAL": "Standard dawn (90.8°)", + "NAUTICAL": "Nautical dawn (102°)", + "CIVIL": "Civil dawn (96°)", + "ASTONOMICAL": "Astronomical dawn (108°)", + "ManualTimeSynchronization": "Sincronizzazione Manuale Data/Ora", + "CurrentOpenDtuTime": "Ora OpenDTU attuale", + "CurrentLocalTime": "Ora Locale attuale", + "SynchronizeTime": "Sincronizza Data/Ora", + "SynchronizeTimeHint": "Nota: Puoi usare la sincronizzazione manuale per impostare Data/Ora nel caso che non sia disponibile un server NTP. In questo caso la data/ora viene persa in caso di mancata alimentazione. Inoltre, con la sincronizzazione manuale ci sarà una progressiva deriva della Data/Ora in quanto l'ESP32 non ha un Real Time Clock interno." + }, + "networkadmin": { + "NetworkSettings": "Impostazioni di Rete", + "WifiConfiguration": "Configurazione WiFi", + "WifiSsid": "WiFi SSID", + "WifiPassword": "WiFi Password", + "Hostname": "Hostname", + "HostnameHint": "Nota: Il testo %06X sarà rimpiazzato con le ultime 6 cifre del ChipID dell'ESP32 in formato esadecimale.", + "EnableDhcp": "Abilita DHCP", + "StaticIpConfiguration": "Configurazione IP Statico", + "IpAddress": "Indirizzo IP", + "Netmask": "Netmask", + "DefaultGateway": "Default Gateway", + "Dns": "DNS Server {num}", + "AdminAp": "Configurazione WiFi (Admin AccessPoint)", + "ApTimeout": "Timeout AccessPoint", + "ApTimeoutHint": "Tempo in cui la modalità AccessPoint rimarrà attiva. 0=per sempre.", + "Minutes": "minuti", + "EnableMdns": "Abilita mDNS", + "MdnsSettings": "Configurazione mDNS" + }, + "mqttadmin": { + "MqttSettings": "Impostazioni MQTT", + "MqttConfiguration": "Configurazione MQTT", + "EnableMqtt": "Abilita MQTT", + "EnableHass": "Abilita Home Assistant MQTT Auto Discovery", + "MqttBrokerParameter": "Parametri Broker MQTT", + "Hostname": "Hostname", + "HostnameHint": "Hostname o Indirizzo IP", + "Port": "Porta", + "ClientId": "Client ID", + "Username": "Username", + "UsernameHint": "Username, lascia vuoto per connessione anonima", + "Password": "Password", + "PasswordHint": "Password, lascia vuota per connessione anonima", + "BaseTopic": "Topic Base", + "BaseTopicHint": "Topic Base, prefisso da aggiungere (ad esempio inverter/)", + "PublishInterval": "Intervallo pubblicazione", + "Seconds": "secondi", + "CleanSession": "Abilita CleanSession", + "EnableRetain": "Abilita Retain", + "EnableTls": "Abilita TLS", + "RootCa": "CA-Root-Certificate (default Letsencrypt)", + "TlsCertLoginEnable": "Abilita Login con certificato TLS", + "ClientCert": "TLS Client-Certificate", + "ClientKey": "TLS Client-Key", + "LwtParameters": "Parametri LWT", + "LwtTopic": "Topic LWT", + "LwtTopicHint": "Topic LWT, da aggiungere al Topic Base", + "LwtOnline": "Messaggio 'Online0 LWT", + "LwtOnlineHint": "Messaggio pubblicato quando online", + "LwtOffline": "Messaggio 'Offline' LWT", + "LwtOfflineHint": "Messaggio che sarà pubblicato quando offline", + "LwtQos": "QoS (Quality of Service)", + "QOS0": "0 (Al massimo una volta)", + "QOS1": "1 (Almeno una volta)", + "QOS2": "2 (Esattamente una volta)", + "HassParameters": "Parametri Home Assistant MQTT Auto Discovery", + "HassPrefixTopic": "Prefisso Topic", + "HassPrefixTopicHint": "Prefisso per Topic autodiscovery", + "HassRetain": "Abilita Retain", + "HassExpire": "Abilita Scadenza", + "HassIndividual": "Pannelli Individuale" + }, + "inverteradmin": { + "InverterSettings": "Impostazioni Inverter", + "AddInverter": "Aggiungi nuovo Inverter", + "Serial": "Seriale", + "Name": "Nome", + "Add": "Aggiungi", + "AddHint": "Nota: Potrai aggiungere ulteriori parametri dopo aver creato l'inverter, cliccando sull'icona 'Matita' nella lista inverter.", + "InverterList": "Lista Inverter", + "Status": "Stato", + "Send": "Invia", + "Receive": "Riceve", + "StatusHint": "Nota: L'inverter viene alimentato dal fotovoltaico. Durante la notte, l'inverter risulterà spento. Le richieste potranno comunque essere trasmesse.", + "Type": "Tipo", + "Action": "Azione", + "SaveOrder": "Salva ordine", + "DeleteInverter": "Rimuovi inverter", + "EditInverter": "Modifica inverter", + "General": "Generale", + "String": "Stringa", + "Advanced": "Avanzate", + "InverterSerial": "Seriale Inverter:", + "InverterName": "Nome Inverter:", + "InverterNameHint": "Puoi specificare un nome qualsiasi da assegnare all'inverter.", + "InverterStatus": "Riceve / Invia", + "PollEnable": "Interroga inverter", + "PollEnableNight": "Interroga inverter di notte", + "CommandEnable": "Invia comandi", + "CommandEnableNight": "Invia comandi di notte", + "StringName": "Nome stringa {num}:", + "StringNameHint": "Qui puoi specificare un nome qualsiasi per la porta dell'inverter o per il pannello fotovoltaico collegato.", + "StringMaxPower": "Massima potenza stringa {num}:", + "StringMaxPowerHint": "Inserisci la potenza massima associata ai panelli fotovoltaici collegati a questa stringa.", + "StringYtOffset": "Offset Energia totale per la stringa {num}:", + "StringYtOffsetHint": "Questo offset viene utilizzato per azzerare il contatore qualora venga usato un inverter usato.", + "InverterHint": "*) Inserisci la potenza Wp dei pannelli fotovoltaici collegati alla stringa: servirà per calcolare l'irragiamento.", + "ReachableThreshold": "Reachable Threshold", + "ReachableThresholdHint": "Definisce il numero di richieste fallite prima che l'inverter sia considerato irraggiungibile.", + "ZeroRuntime": "Azzera dati in tempo reale", + "ZeroRuntimeHint": "Azzera i dati in tempo reale (tranne l'Energia) se l'inverter diventa irraggiunbile.", + "ZeroDay": "Azzera dati energia alla mezzanotte", + "ZeroDayHint": "Questo vale se l'inverter risulta irraggiungibile. Se l'inverter risponde anche di notte, verranno mostrati i suoi valori. (Il Reset si verifica al riavvio)", + "ClearEventlog": "Clear Eventlog at midnight", + "Cancel": "@:base.Cancel", + "Save": "@:base.Save", + "DeleteMsg": "Sicuro di voler rimuovere l'inverter \"{name}\" con numero seriale {serial}?", + "Delete": "Rimuovi", + "YieldDayCorrection": "Correzione energia giornaliera", + "YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte" + }, + "fileadmin": { + "ConfigManagement": "Configurazione Gestione", + "BackupHeader": "Backup: Configurazione File Backup", + "BackupConfig": "Esegui il backup del file", + "Backup": "Backup", + "Restore": "Ripristina", + "NoFileSelected": "Nessun file selezionato", + "RestoreHeader": "Ripristina: Ripristina File Configurazione", + "Back": "Indietro", + "UploadSuccess": "Invio File con successo", + "RestoreHint": "Nota: questa operazione rimpiazza la configurazione con quella contenuta nel file, e poi riavvia automaticamente OpenDTU per applicare la nuova configurazione.", + "ResetHeader": "Inizializza: Esegui il Factory Reset", + "FactoryResetButton": "Ripristina Configurazione Factory-Default", + "ResetHint": "Nota: Clicca 'Ripristina Configurazione Factory-Default' per stabilire le impostazioni di fabbrica e riavviare automaticamente OpenDTU.", + "FactoryReset": "Factory Reset", + "ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?", + "ResetConfirm": "Factory Reset!", + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON file is formatted incorrectly.", + "InvalidJsonContent": "JSON file has the wrong content." + }, + "login": { + "Login": "Login", + "SystemLogin": "System Login", + "Username": "Username", + "UsernameRequired": "Inserisci Username", + "Password": "Password", + "PasswordRequired": "Inserisci Password", + "LoginButton": "Login" + }, + "firmwareupgrade": { + "FirmwareUpgrade": "Aggiornamento Firmware", + "Loading": "@:base.Loading", + "OtaError": "Errore aggiornamento OTA", + "Back": "Indietro", + "Retry": "Riprova", + "OtaStatus": "Stato OTA", + "OtaSuccess": "Aggiornamento firmware eseguito con successo. Il dispositivo si riavvierà automaticamente. Quando sarà nuovamente disponibile, l'interfacca sarà ricaricata automaticamente.", + "FirmwareUpload": "Invia Firmware", + "UploadProgress": "Upload in corso" + }, + "about": { + "AboutOpendtu": "About OpenDTU", + "Documentation": "Documentazione", + "DocumentationBody": "La documentazione firmware e hardware sono disponibili qui: https://www.opendtu.solar", + "ProjectOrigin": "Origine Progetto", + "ProjectOriginBody1": "Questo progetto è partito da questa discussione. (Mikrocontroller.net)", + "ProjectOriginBody2": "Il protocollo Hoymiles è stato decriptato grazie al contributo volontario di molti programmatori. OpenDTU, fra gli altri, è stato sviluppato grazie a questo lavoro. Il progetto è distribuito con Licenza Open Source (GNU General Public License version 2).", + "ProjectOriginBody3": "Il software è stato sviluppato con le nostre migliori conoscenze e convinzioni. Tuttavia, non si assume alcuna responsabilità per malfunzionamenti o perdita di garanzia dell'inverter.", + "ProjectOriginBody4": "OpenDTU è disponibile gratuitamente. Se hai pagato per questo software, probabilmente sei stato truffato.", + "NewsUpdates": "Novità e Aggiornamenti", + "NewsUpdatesBody": "Nuovi aggiornamenti sono disponibili su Github: https://github.com/tbnobody/OpenDTU", + "ErrorReporting": "Segnalazione Errori", + "ErrorReportingBody": "Per favore segnala eventuali problemi utilizzando le funzionalità della piattaforma Github", + "Discussion": "Discussioni", + "DiscussionBody": "Puoi avviare una discussione con noi su Discord o Github" + }, + "hints": { + "RadioProblem": "Non è possibile dialogare con il modulo radio selezionato. Controlla i collegamenti alla radio.", + "TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.", + "TimeSyncLink": "Controlla le impostazioni Data/Ora.", + "DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.", + "DefaultPasswordLink": "Per favore cambia la password." + }, + "deviceadmin": { + "DeviceManager": "Device-Manager", + "ParseError": "Parse error in 'pin_mapping.json': {error}", + "PinAssignment": "Impostazioni Connessione", + "SelectedProfile": "Profilo selezionato", + "DefaultProfile": "(Impostazioni di Default)", + "ProfileHint": "Il tuo dispositivo potrebbe smettere di rispondere selezionando un profilo incompatibile. In questo caso, dovrai eseguire una cancellazione collegandoti all'interfaccia seriale.", + "Display": "Display", + "PowerSafe": "Abilita Risparmio Energetico", + "PowerSafeHint": "Spegni il display se l'inverter non produce.", + "Screensaver": "Abilita Screensaver", + "ScreensaverHint": "Muove il testo nel display per prevenire danneggiamento pixel. (Utile in caso di display OLED)", + "DiagramMode": "Modalità grafica", + "off": "Off", + "small": "Small", + "fullscreen": "Fullscreen", + "DiagramDuration": "Durata grafico", + "DiagramDurationHint": "Periodo che viene mostrato nel grafico.", + "Seconds": "Secondi", + "Contrast": "Contrasto ({contrast})", + "Rotation": "Rotazione", + "rot0": "Nessuna rotazione", + "rot90": "Rotazione 90 gradi", + "rot180": "Rotazione 180 gradi", + "rot270": "Rotazione 270 gradi", + "DisplayLanguage": "Linuga Display", + "en": "English", + "de": "German", + "fr": "French", + "Leds": "LEDs", + "EqualBrightness": "Equalizza luminosità", + "LedBrightness": "LED {led}, Luminosità ({brightness})" + }, + "pininfo": { + "Category": "Categoria", + "Name": "Nome", + "Number": "Numero", + "ValueSelected": "Selezionato", + "ValueActive": "Attivo" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" + } + } +} diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c deleted file mode 100644 index 59aad36f7..000000000 --- a/lib/CMT2300a/cmt_spi3.c +++ /dev/null @@ -1,142 +0,0 @@ -#include "cmt_spi3.h" -#include -#include -#include // for esp_rom_gpio_connect_out_signal - -SemaphoreHandle_t paramLock = NULL; -#define SPI_PARAM_LOCK() \ - do { \ - } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) -#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) - -// for ESP32 this is the so-called HSPI -// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, -// it is simply the first externally usable hardware SPI master controller -#define SPI_CMT SPI2_HOST - -spi_device_handle_t spi_reg, spi_fifo; - -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) -{ - paramLock = xSemaphoreCreateMutex(); - - spi_bus_config_t buscfg = { - .mosi_io_num = pin_sdio, - .miso_io_num = -1, // single wire MOSI/MISO - .sclk_io_num = pin_clk, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = 32, - }; - spi_device_interface_config_t devcfg = { - .command_bits = 1, - .address_bits = 7, - .dummy_bits = 0, - .mode = 0, // SPI mode 0 - .cs_ena_pretrans = 1, - .cs_ena_posttrans = 1, - .clock_speed_hz = spi_speed, - .spics_io_num = pin_cs, - .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, - .queue_size = 1, - .pre_cb = NULL, - .post_cb = NULL, - }; - - ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED)); - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg)); - - // FiFo - spi_device_interface_config_t devcfg2 = { - .command_bits = 0, - .address_bits = 0, - .dummy_bits = 0, - .mode = 0, // SPI mode 0 - .cs_ena_pretrans = 2, - .cs_ena_posttrans = (uint8_t)(1 / (spi_speed * 10e6 * 2) + 2), // >2 us - .clock_speed_hz = spi_speed, - .spics_io_num = pin_fcs, - .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, - .queue_size = 1, - .pre_cb = NULL, - .post_cb = NULL, - }; - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); - - esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false); - delay(100); -} - -void cmt_spi3_write(const uint8_t addr, const uint8_t dat) -{ - uint8_t tx_data; - tx_data = ~dat; - spi_transaction_t t = { - .cmd = 1, - .addr = ~addr, - .length = 8, - .tx_buffer = &tx_data, - .rx_buffer = NULL - }; - SPI_PARAM_LOCK(); - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); - SPI_PARAM_UNLOCK(); - delayMicroseconds(100); -} - -uint8_t cmt_spi3_read(const uint8_t addr) -{ - uint8_t rx_data; - spi_transaction_t t = { - .cmd = 0, - .addr = ~addr, - .length = 8, - .rxlength = 8, - .tx_buffer = NULL, - .rx_buffer = &rx_data - }; - SPI_PARAM_LOCK(); - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); - SPI_PARAM_UNLOCK(); - delayMicroseconds(100); - return rx_data; -} - -void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len) -{ - uint8_t tx_data; - - spi_transaction_t t = { - .length = 8, - .tx_buffer = &tx_data, // reference to write data - .rx_buffer = NULL - }; - - SPI_PARAM_LOCK(); - for (uint8_t i = 0; i < len; i++) { - tx_data = ~buf[i]; // negate buffer contents - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); - delayMicroseconds(4); // > 4 us - } - SPI_PARAM_UNLOCK(); -} - -void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len) -{ - uint8_t rx_data; - - spi_transaction_t t = { - .length = 8, - .rxlength = 8, - .tx_buffer = NULL, - .rx_buffer = &rx_data - }; - - SPI_PARAM_LOCK(); - for (uint8_t i = 0; i < len; i++) { - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); - delayMicroseconds(4); // > 4 us - buf[i] = rx_data; - } - SPI_PARAM_UNLOCK(); -} diff --git a/lib/CMT2300a/cmt_spi3.cpp b/lib/CMT2300a/cmt_spi3.cpp new file mode 100644 index 000000000..28fdc8aee --- /dev/null +++ b/lib/CMT2300a/cmt_spi3.cpp @@ -0,0 +1,155 @@ +#include "cmt_spi3.h" +#include +#include +#include + +SemaphoreHandle_t paramLock = NULL; +#define SPI_PARAM_LOCK() \ + do { \ + } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) +#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) + +static void IRAM_ATTR pre_cb(spi_transaction_t *trans) { + gpio_set_level(*reinterpret_cast(trans->user), 0); +} + +static void IRAM_ATTR post_cb(spi_transaction_t *trans) { + gpio_set_level(*reinterpret_cast(trans->user), 1); +} + +spi_device_handle_t spi; +gpio_num_t cs_reg, cs_fifo; + +void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed) +{ + paramLock = xSemaphoreCreateMutex(); + + auto bus_config = std::make_shared( + static_cast(pin_sdio), + GPIO_NUM_NC, + static_cast(pin_clk) + ); + + spi_device_interface_config_t device_config { + .command_bits = 0, // set by transactions individually + .address_bits = 0, // set by transactions individually + .dummy_bits = 0, + .mode = 0, // SPI mode 0 + .duty_cycle_pos = 0, + .cs_ena_pretrans = 2, // only 1 pre and post cycle would be required for register access + .cs_ena_posttrans = static_cast(2 * spi_speed / 1000000), // >2 us + .clock_speed_hz = spi_speed, + .input_delay_ns = 0, + .spics_io_num = -1, // CS handled by callbacks + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = pre_cb, + .post_cb = post_cb, + }; + + spi = SpiManagerInst.alloc_device("", bus_config, device_config); + if (!spi) + ESP_ERROR_CHECK(ESP_FAIL); + + cs_reg = static_cast(pin_cs); + ESP_ERROR_CHECK(gpio_reset_pin(cs_reg)); + ESP_ERROR_CHECK(gpio_set_level(cs_reg, 1)); + ESP_ERROR_CHECK(gpio_set_direction(cs_reg, GPIO_MODE_OUTPUT)); + + cs_fifo = static_cast(pin_fcs); + ESP_ERROR_CHECK(gpio_reset_pin(cs_fifo)); + ESP_ERROR_CHECK(gpio_set_level(cs_fifo, 1)); + ESP_ERROR_CHECK(gpio_set_direction(cs_fifo, GPIO_MODE_OUTPUT)); +} + +void cmt_spi3_write(const uint8_t addr, const uint8_t data) +{ + spi_transaction_ext_t trans { + .base { + .flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR, + .cmd = 0, + .addr = addr, + .length = 8, + .rxlength = 0, + .user = &cs_reg, // CS for register access + .tx_buffer = &data, + .rx_buffer = nullptr, + }, + .command_bits = 1, + .address_bits = 7, + .dummy_bits = 0, + }; + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast(&trans))); + SPI_PARAM_UNLOCK(); +} + +uint8_t cmt_spi3_read(const uint8_t addr) +{ + uint8_t data; + spi_transaction_ext_t trans { + .base { + .flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR, + .cmd = 1, + .addr = addr, + .length = 0, + .rxlength = 8, + .user = &cs_reg, // CS for register access + .tx_buffer = nullptr, + .rx_buffer = &data, + }, + .command_bits = 1, + .address_bits = 7, + .dummy_bits = 0, + }; + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast(&trans))); + SPI_PARAM_UNLOCK(); + return data; +} + +void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len) +{ + spi_transaction_t trans { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = 8, + .rxlength = 0, + .user = &cs_fifo, // CS for FIFO access + .tx_buffer = nullptr, + .rx_buffer = nullptr, + }; + + SPI_PARAM_LOCK(); + spi_device_acquire_bus(spi, portMAX_DELAY); + for (uint8_t i = 0; i < len; i++) { + trans.tx_buffer = buf + i; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + } + spi_device_release_bus(spi); + SPI_PARAM_UNLOCK(); +} + +void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len) +{ + spi_transaction_t trans { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = 0, + .rxlength = 8, + .user = &cs_fifo, // CS for FIFO access + .tx_buffer = nullptr, + .rx_buffer = nullptr, + }; + + SPI_PARAM_LOCK(); + spi_device_acquire_bus(spi, portMAX_DELAY); + for (uint8_t i = 0; i < len; i++) { + trans.rx_buffer = buf + i; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + } + spi_device_release_bus(spi); + SPI_PARAM_UNLOCK(); +} diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index 6d3a67b62..16655dbad 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -3,7 +3,11 @@ #include -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); +#ifdef __cplusplus +extern "C" { +#endif + +void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed); void cmt_spi3_write(const uint8_t addr, const uint8_t dat); uint8_t cmt_spi3_read(const uint8_t addr); @@ -11,4 +15,8 @@ uint8_t cmt_spi3_read(const uint8_t addr); void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len); void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len); +#ifdef __cplusplus +} +#endif + #endif diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 1416a73ab..7f1a3c1f3 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -4,6 +4,7 @@ */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_1CH.h" #include "inverters/HERF_2CH.h" #include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" @@ -135,15 +136,7 @@ void HoymilesClass::loop() 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(); - } - if (inv->getClearEventlogOnMidnight()) { - inv->EventLog()->clearBuffer(); - } + inv->performDailyTask(); } lastWeekDay = currentWeekDay; @@ -173,6 +166,8 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, c i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_1CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_2CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_4CH::isValidSerial(serial)) { @@ -200,9 +195,9 @@ std::shared_ptr HoymilesClass::getInverterByPos(const uint8_t std::shared_ptr HoymilesClass::getInverterBySerial(const uint64_t serial) { - for (uint8_t i = 0; i < _inverters.size(); i++) { - if (_inverters[i]->serial() == serial) { - return _inverters[i]; + for (auto& inv : _inverters) { + if (inv->serial() == serial) { + return inv; } } return nullptr; @@ -214,9 +209,7 @@ std::shared_ptr HoymilesClass::getInverterByFragment(const fra return nullptr; } - std::shared_ptr inv; - for (uint8_t i = 0; i < _inverters.size(); i++) { - inv = _inverters[i]; + for (auto& inv : _inverters) { serial_u p; p.u64 = inv->serial(); diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 9d288554e..55281ab05 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -66,16 +66,31 @@ void HoymilesRadio::handleReceivedPackage() } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); + // Statistics: Count RX Fail No Answer + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailNoAnswer++; + } + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { Hoymiles.getMessageOutput()->println("Retransmit timeout"); + // Statistics: Count RX Fail Partial Answer + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailPartialAnswer++; + } + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { Hoymiles.getMessageOutput()->println("Packet handling error"); + // Statistics: Count RX Fail Corrupt Data + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailCorruptData++; + } + _commandQueue.pop(); _busyFlag = false; @@ -83,17 +98,26 @@ void HoymilesRadio::handleReceivedPackage() // Perform Retransmit Hoymiles.getMessageOutput()->print("Request retransmit: "); Hoymiles.getMessageOutput()->println(verifyResult); + // Statistics: Count TX Re-Request Fragment + inv->RadioStats.TxReRequestFragment++; + sendRetransmitPacket(verifyResult); } else { // Successful received all packages Hoymiles.getMessageOutput()->println("Success"); + // Statistics: Count RX Success + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxSuccess++; + } + _commandQueue.pop(); _busyFlag = false; } } else { // If inverter was not found, assume the command is invalid Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); + // Statistics: Count RX Fail Unknown Data _commandQueue.pop(); _busyFlag = false; } @@ -105,6 +129,9 @@ void HoymilesRadio::handleReceivedPackage() auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); if (nullptr != inv) { inv->clearRxFragmentBuffer(); + // Statistics: TX Requests + inv->RadioStats.TxRequestData++; + sendEsbPacket(*cmd); } else { Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index bbd31f212..e58221cd9 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -34,7 +34,7 @@ uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const { if ((frequency % getChannelWidth()) != 0) { - Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth()); + Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %" PRId32 " kHz!\r\n", frequency / 1000000.0, getChannelWidth()); return 0xFF; // ERROR } if (frequency < getMinFrequency() || frequency > getMaxFrequency()) { @@ -43,7 +43,7 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) con return 0xFF; // ERROR } if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) { - Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n", + Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%" PRId32 " - %" PRId32 " MHz)\r\n", frequency / 1000000.0, static_cast(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6), static_cast(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6)); @@ -167,9 +167,9 @@ void HoymilesRadio_CMT::loop() // Save packet in inverter rx buffer Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0); dumpBuf(f.fragment, f.len, false); - Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); + Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi); - inv->addRxFragment(f.fragment, f.len); + inv->addRxFragment(f.fragment, f.len, f.rssi); } else { Hoymiles.getMessageOutput()->println("Inverter Not found!"); } @@ -194,9 +194,9 @@ void HoymilesRadio_CMT::setPALevel(const int8_t paLevel) } if (_radio->setPALevel(paLevel)) { - Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel); + Hoymiles.getMessageOutput()->printf("CMT TX power set to %" PRId8 " dBm\r\n", paLevel); } else { - Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); + Hoymiles.getMessageOutput()->printf("CMT TX power %" PRId8 " dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); } } diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 4bf104ade..0019a4bdb 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -76,11 +76,11 @@ void HoymilesRadio_NRF::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel); + Hoymiles.getMessageOutput()->printf("RX Channel: %" PRId8 " --> ", f.channel); dumpBuf(f.fragment, f.len, false); - Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); + Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi); - inv->addRxFragment(f.fragment, f.len); + inv->addRxFragment(f.fragment, f.len, f.rssi); } else { Hoymiles.getMessageOutput()->println("Inverter Not found!"); } @@ -183,7 +183,7 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd) openWritingPipe(s); _radio->setRetries(3, 15); - Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", + Hoymiles.getMessageOutput()->printf("TX %s Channel: %" PRId8 " --> ", cmd.getCommandName().c_str(), _radio->getChannel()); cmd.dumpDataPayload(Hoymiles.getMessageOutput()); _radio->write(cmd.getDataPayload(), cmd.getDataSize()); diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index dcd2370c1..4ce3c6e55 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -85,13 +85,13 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons float ActivePowerControlCommand::getLimit() const { - const float l = (((uint16_t)_payload[12] << 8) | _payload[13]); + const float l = (static_cast(_payload[12]) << 8) | _payload[13]; return l / 10; } PowerLimitControlType ActivePowerControlCommand::getType() { - return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]); + return (PowerLimitControlType)((static_cast(_payload[14]) << 8) | _payload[15]); } void ActivePowerControlCommand::gotTimeout() diff --git a/lib/Hoymiles/src/commands/DevControlCommand.cpp b/lib/Hoymiles/src/commands/DevControlCommand.cpp index b73f74f0c..8dc419d60 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.cpp +++ b/lib/Hoymiles/src/commands/DevControlCommand.cpp @@ -35,8 +35,8 @@ DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t route void DevControlCommand::udpateCRC(const uint8_t len) { const uint16_t crc = crc16(&_payload[10], len); - _payload[10 + len] = (uint8_t)(crc >> 8); - _payload[10 + len + 1] = (uint8_t)(crc); + _payload[10 + len] = static_cast(crc >> 8); + _payload[10 + len + 1] = static_cast(crc); } bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index 0e7bf51f1..dd5d72ea0 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -63,10 +63,10 @@ uint8_t MultiDataCommand::getDataType() const void MultiDataCommand::setTime(const time_t time) { - _payload[12] = (uint8_t)(time >> 24); - _payload[13] = (uint8_t)(time >> 16); - _payload[14] = (uint8_t)(time >> 8); - _payload[15] = (uint8_t)(time); + _payload[12] = static_cast(time >> 24); + _payload[13] = static_cast(time >> 16); + _payload[14] = static_cast(time >> 8); + _payload[15] = static_cast(time); udpateCRC(); } @@ -112,8 +112,8 @@ bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t void MultiDataCommand::udpateCRC() { const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password - _payload[24] = (uint8_t)(crc >> 8); - _payload[25] = (uint8_t)(crc); + _payload[24] = static_cast(crc >> 8); + _payload[25] = static_cast(crc); } uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id) diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index b1396a4dd..9f5563bf9 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -48,7 +48,7 @@ bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const u const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { - Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", + Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); return false; diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 0c142afc8..70dcffa98 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -48,7 +48,7 @@ bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { - Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", + Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); return false; diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.cpp b/lib/Hoymiles/src/inverters/HERF_1CH.cpp new file mode 100644 index 000000000..49531d99c --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.cpp @@ -0,0 +1,55 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_1CH.h" + +static const byteAssign_t byteAssignment[] = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 40, 2, 10, false, 1 }, // to be verified + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, // to be verified + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, // to be verified + + { TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 }, + { TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 }, + { TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 }, + { TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 } +}; + +HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_1CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x284100000000 && serial <= 0x2841ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2841; +} + +String HERF_1CH::typeName() const +{ + return "HERF-300-1T"; +} + +const byteAssign_t* HERF_1CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_1CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.h b/lib/Hoymiles/src/inverters/HERF_1CH.h new file mode 100644 index 000000000..8220272e3 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_1CH : public HM_Abstract { +public: + explicit HERF_1CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index 4ad0157f5..4cbc686cd 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; - return preSerial == 0x1144 || preSerial == 0x1143; + return preSerial == 0x1144 || preSerial == 0x1143 || preSerial == 0x1410; } String HMS_2CH::typeName() const diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 0f0c64c23..eef82c5c3 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -36,10 +36,10 @@ bool HM_1CH::isValidSerial(const uint64_t serial) // serial >= 0x112100000000 && serial <= 0x1121ffffffff uint8_t preId[2]; - preId[0] = (uint8_t)(serial >> 40); - preId[1] = (uint8_t)(serial >> 32); + preId[0] = static_cast(serial >> 40); + preId[1] = static_cast(serial >> 32); - if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) { + if (static_cast((((static_cast(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x12) { return true; } diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 02dd8ae4f..91228ff31 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -44,10 +44,10 @@ bool HM_2CH::isValidSerial(const uint64_t serial) // serial >= 0x114100000000 && serial <= 0x1141ffffffff uint8_t preId[2]; - preId[0] = (uint8_t)(serial >> 40); - preId[1] = (uint8_t)(serial >> 32); + preId[0] = static_cast(serial >> 40); + preId[1] = static_cast(serial >> 32); - if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) { + if (static_cast((((static_cast(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x14) { return true; } diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index 586248b59..45ebab40a 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -57,10 +57,10 @@ bool HM_4CH::isValidSerial(const uint64_t serial) // serial >= 0x116100000000 && serial <= 0x1161ffffffff uint8_t preId[2]; - preId[0] = (uint8_t)(serial >> 40); - preId[1] = (uint8_t)(serial >> 32); + preId[0] = static_cast(serial >> 40); + preId[1] = static_cast(serial >> 32); - if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) { + if (static_cast((((static_cast(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x16) { return true; } diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 45efc99db..324f6a27d 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -50,13 +50,13 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force) if (!force) { if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { - if ((uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) { + if (static_cast(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt)) { return false; } } } - _lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG); + _lastAlarmLogCnt = static_cast(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)); time_t now; time(&now); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 68d611836..26a89c131 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -14,8 +14,8 @@ InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial) char serial_buff[sizeof(uint64_t) * 8 + 1]; snprintf(serial_buff, sizeof(serial_buff), "%0x%08x", - ((uint32_t)((serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(serial & 0xFFFFFFFF))); + static_cast((serial >> 32) & 0xFFFFFFFF), + static_cast(serial & 0xFFFFFFFF)); _serialString = serial_buff; _alarmLogParser.reset(new AlarmLogParser()); @@ -137,6 +137,11 @@ bool InverterAbstract::getClearEventlogOnMidnight() const return _clearEventlogOnMidnight; } +int8_t InverterAbstract::getLastRssi() const +{ + return _lastRssi; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; @@ -185,8 +190,10 @@ void InverterAbstract::clearRxFragmentBuffer() _rxFragmentRetransmitCnt = 0; } -void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) +void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi) { + _lastRssi = rssi; + if (len < 11) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); return; @@ -208,7 +215,7 @@ void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len } if (fragmentId >= MAX_RF_FRAGMENT_COUNT) { - Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId); + Hoymiles.getMessageOutput()->printf("ERROR: fragment id %" PRId8 " is too large for buffer and ignored\r\n", fragmentId); return; } @@ -272,3 +279,22 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) return FRAGMENT_OK; } + +void InverterAbstract::performDailyTask() +{ + // Have to reset the offets first, otherwise it will + // Substract the offset from zero which leads to a high value + Statistics()->resetYieldDayCorrection(); + if (getZeroYieldDayOnMidnight()) { + Statistics()->zeroDailyData(); + } + if (getClearEventlogOnMidnight()) { + EventLog()->clearBuffer(); + } + resetRadioStats(); +} + +void InverterAbstract::resetRadioStats() +{ + RadioStats = {}; +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 2a51079ba..29fba12fa 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -61,10 +61,36 @@ class InverterAbstract { void setClearEventlogOnMidnight(const bool enabled); bool getClearEventlogOnMidnight() const; + int8_t getLastRssi() const; + void clearRxFragmentBuffer(); - void addRxFragment(const uint8_t fragment[], const uint8_t len); + void addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi); uint8_t verifyAllFragments(CommandAbstract& cmd); + void performDailyTask(); + + void resetRadioStats(); + + struct { + // TX Request Data + uint32_t TxRequestData; + + // TX Re-Request Fragment + uint32_t TxReRequestFragment; + + // RX Success + uint32_t RxSuccess; + + // RX Fail Partial Answer + uint32_t RxFailPartialAnswer; + + // RX Fail No Answer + uint32_t RxFailNoAnswer; + + // RX Fail Corrupt Data + uint32_t RxFailCorruptData; + } RadioStats = {}; + virtual bool sendStatsRequest() = 0; virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendDevInfoRequest() = 0; @@ -107,6 +133,8 @@ class InverterAbstract { bool _zeroYieldDayOnMidnight = false; bool _clearEventlogOnMidnight = false; + int8_t _lastRssi = -127; + std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; std::unique_ptr _gridProfileParser; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index 8d913deb5..b55445328 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -1,15 +1,16 @@ # Class overview -| Class | Models | Serial range | -| --------------| --------------------------- | ------------ | -| HM_1CH | HM-300/350/400-1T | 1121 | -| HM_2CH | HM-600/700/800-2T | 1141 | -| HM_4CH | HM-1000/1200/1500-4T | 1161 | -| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | -| HMS_1CHv2 | HMS-500-1T v2 | 1125 | -| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 | -| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | -| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | -| HMT_6CH | HMT-1800/2250-6T | 1382 | -| HERF_2CH | HERF 800 | 2821 | -| HERF_4CH | HERF 1800 | 2801 | +| Class | Models | Serial range | +| --------------| --------------------------- | ------------- -- | +| HM_1CH | HM-300/350/400-1T | 1121 | +| HM_2CH | HM-600/700/800-2T | 1141 | +| HM_4CH | HM-1000/1200/1500-4T | 1161 | +| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | +| HMS_1CHv2 | HMS-500-1T v2 | 1125 | +| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 | +| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | +| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | +| HMT_6CH | HMT-1800/2250-6T | 1382 | +| HERF_1CH | HERF 300 | 2841 | +| HERF_2CH | HERF 800 | 2821 | +| HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 30b813647..027dd84b5 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -243,7 +243,7 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, HOY_SEMAPHORE_TAKE(); - const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; + const uint32_t wcode = static_cast(_payloadAlarmLog[entryStartOffset]) << 8 | _payloadAlarmLog[entryStartOffset + 1]; uint32_t startTimeOffset = 0; if (((wcode >> 13) & 0x01) == 1) { startTimeOffset = 12 * 60 * 60; @@ -255,8 +255,8 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, } 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.StartTime = ((static_cast(_payloadAlarmLog[entryStartOffset + 4]) << 8) | static_cast(_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; + entry.EndTime = (static_cast(_payloadAlarmLog[entryStartOffset + 6]) << 8) | static_cast(_payloadAlarmLog[entryStartOffset + 7]); HOY_SEMAPHORE_GIVE(); diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 0c2e15e8d..fb0fe3e89 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -61,6 +61,7 @@ const devInfo_t devInfo[] = { { { 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, 0x21, ALL }, 700, "HMS-700-2T" }, // 00 { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 { { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00 { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 @@ -149,7 +150,7 @@ void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate) uint16_t DevInfoParser::getFwBuildVersion() const { HOY_SEMAPHORE_TAKE(); - const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; + const uint16_t ret = (static_cast(_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; HOY_SEMAPHORE_GIVE(); return ret; } @@ -158,13 +159,13 @@ time_t DevInfoParser::getFwBuildDateTime() const { struct tm timeinfo = {}; HOY_SEMAPHORE_TAKE(); - timeinfo.tm_year = ((((uint16_t)_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900; + timeinfo.tm_year = ((static_cast(_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900; - timeinfo.tm_mon = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1; - timeinfo.tm_mday = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100; + timeinfo.tm_mon = ((static_cast(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1; + timeinfo.tm_mday = ((static_cast(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100; - timeinfo.tm_hour = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100; - timeinfo.tm_min = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100; + timeinfo.tm_hour = ((static_cast(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100; + timeinfo.tm_min = ((static_cast(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100; HOY_SEMAPHORE_GIVE(); return timegm(&timeinfo); @@ -181,7 +182,7 @@ String DevInfoParser::getFwBuildDateTimeStr() const uint16_t DevInfoParser::getFwBootloaderVersion() const { HOY_SEMAPHORE_TAKE(); - const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; + const uint16_t ret = (static_cast(_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; HOY_SEMAPHORE_GIVE(); return ret; } @@ -189,11 +190,11 @@ uint16_t DevInfoParser::getFwBootloaderVersion() const uint32_t DevInfoParser::getHwPartNumber() const { HOY_SEMAPHORE_TAKE(); - const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; - const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; + const uint16_t hwpn_h = (static_cast(_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; + const uint16_t hwpn_l = (static_cast(_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; HOY_SEMAPHORE_GIVE(); - return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); + return (static_cast(hwpn_h) << 16) | static_cast(hwpn_l); } String DevInfoParser::getHwVersion() const diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 489565e19..2f50bd506 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -443,7 +443,7 @@ std::list GridProfileParser::getProfile() const 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]); + float value = static_cast((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]); value /= itemDefinition.Divider; GridProfileItem_t v; diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index 346b5d468..f01af7070 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -45,7 +45,7 @@ void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t* float SystemConfigParaParser::getLimitPercent() const { HOY_SEMAPHORE_TAKE(); - const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; + const float ret = ((static_cast(_payload[2]) << 8) | _payload[3]) / 10.0; HOY_SEMAPHORE_GIVE(); return ret; } @@ -53,8 +53,8 @@ float SystemConfigParaParser::getLimitPercent() const void SystemConfigParaParser::setLimitPercent(const float value) { HOY_SEMAPHORE_TAKE(); - _payload[2] = ((uint16_t)(value * 10)) >> 8; - _payload[3] = ((uint16_t)(value * 10)); + _payload[2] = static_cast(value * 10) >> 8; + _payload[3] = static_cast(value * 10); HOY_SEMAPHORE_GIVE(); } diff --git a/lib/SpiManager/library.json b/lib/SpiManager/library.json new file mode 100644 index 000000000..22e5ddc99 --- /dev/null +++ b/lib/SpiManager/library.json @@ -0,0 +1,13 @@ +{ + "name": "SpiManager", + "keywords": "spi", + "description": "Library for managing the allocation of dedicated or shared SPI buses on the ESP32.", + "authors": { + "name": "Lennart Ferlemann" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/SpiManager/src/SpiBus.cpp b/lib/SpiManager/src/SpiBus.cpp new file mode 100644 index 000000000..6161507ed --- /dev/null +++ b/lib/SpiManager/src/SpiBus.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiBus.h" +#include "SpiBusConfig.h" +#include "SpiCallback.h" + +SpiBus::SpiBus(const std::string& _id, spi_host_device_t _host_device) + : id(_id) + , host_device(_host_device) + , cur_config(nullptr) +{ + spi_bus_config_t bus_config { + .mosi_io_num = -1, + .miso_io_num = -1, + .sclk_io_num = -1, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = 0, // defaults to SPI_MAX_DMA_LEN (=4092) or SOC_SPI_MAXIMUM_BUFFER_SIZE (=64) + .flags = 0, + .intr_flags = 0 + }; + +#if !CONFIG_IDF_TARGET_ESP32S2 + spi_dma_chan_t dma_channel = SPI_DMA_CH_AUTO; +#else + // DMA for SPI3 on ESP32-S2 is shared with ADC/DAC, so we cannot use it here + spi_dma_chan_t dma_channel = (host_device != SPI3_HOST ? SPI_DMA_CH_AUTO : SPI_DMA_DISABLED); +#endif + + ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, dma_channel)); +} + +SpiBus::~SpiBus() +{ + ESP_ERROR_CHECK(spi_bus_free(host_device)); +} + +spi_device_handle_t SpiBus::add_device(const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + if (!SpiCallback::patch(shared_from_this(), bus_config, device_config)) + return nullptr; + + spi_device_handle_t device; + ESP_ERROR_CHECK(spi_bus_add_device(host_device, &device_config, &device)); + return device; +} + +// TODO: add remove_device (with spi_device_acquire_bus) + +void SpiBus::apply_config(SpiBusConfig* config) +{ + if (cur_config) + cur_config->unpatch(host_device); + cur_config = config; + if (cur_config) + cur_config->patch(host_device); +} diff --git a/lib/SpiManager/src/SpiBus.h b/lib/SpiManager/src/SpiBus.h new file mode 100644 index 000000000..1ca79c7ca --- /dev/null +++ b/lib/SpiManager/src/SpiBus.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +class SpiBusConfig; + +class SpiBus : public std::enable_shared_from_this { +public: + explicit SpiBus(const std::string& id, spi_host_device_t host_device); + SpiBus(const SpiBus&) = delete; + SpiBus& operator=(const SpiBus&) = delete; + ~SpiBus(); + + inline __attribute__((always_inline)) void require_config(SpiBusConfig* config) + { + if (config == cur_config) + return; + apply_config(config); + } + + inline __attribute__((always_inline)) void free_config(SpiBusConfig* config) + { + if (config != cur_config) + return; + apply_config(nullptr); + } + + inline const std::string& get_id() const + { + return id; + } + + inline spi_host_device_t get_host_device() const + { + return host_device; + } + + spi_device_handle_t add_device(const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); + +private: + void apply_config(SpiBusConfig* config); + + std::string id; + spi_host_device_t host_device; + SpiBusConfig* cur_config; +}; diff --git a/lib/SpiManager/src/SpiBusConfig.cpp b/lib/SpiManager/src/SpiBusConfig.cpp new file mode 100644 index 000000000..64234d658 --- /dev/null +++ b/lib/SpiManager/src/SpiBusConfig.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiBusConfig.h" + +#include +#include +#include + +SpiBusConfig::SpiBusConfig(gpio_num_t _pin_mosi, gpio_num_t _pin_miso, gpio_num_t _pin_sclk) + : pin_mosi(_pin_mosi) + , pin_miso(_pin_miso) + , pin_sclk(_pin_sclk) +{ + if (pin_mosi != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi)); + ESP_ERROR_CHECK(gpio_set_direction(pin_mosi, GPIO_MODE_INPUT_OUTPUT)); + } + + if (pin_miso != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_miso)); + ESP_ERROR_CHECK(gpio_set_direction(pin_miso, GPIO_MODE_INPUT)); + } + + if (pin_sclk != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk)); + ESP_ERROR_CHECK(gpio_set_direction(pin_sclk, GPIO_MODE_INPUT_OUTPUT)); + } +} + +SpiBusConfig::~SpiBusConfig() +{ + if (pin_mosi != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi)); + + if (pin_miso != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_miso)); + + if (pin_sclk != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk)); +} + +void SpiBusConfig::patch(spi_host_device_t host_device) +{ + if (pin_mosi != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_mosi, spi_periph_signal[host_device].spid_out, false, false); + esp_rom_gpio_connect_in_signal(pin_mosi, spi_periph_signal[host_device].spid_in, false); + } + + if (pin_miso != GPIO_NUM_NC) + esp_rom_gpio_connect_in_signal(pin_miso, spi_periph_signal[host_device].spiq_in, false); + + if (pin_sclk != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_sclk, spi_periph_signal[host_device].spiclk_out, false, false); + esp_rom_gpio_connect_in_signal(pin_sclk, spi_periph_signal[host_device].spiclk_in, false); + } +} + +void SpiBusConfig::unpatch(spi_host_device_t host_device) +{ + if (pin_mosi != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_mosi, SIG_GPIO_OUT_IDX, false, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spid_in, false); + } + + if (pin_miso != GPIO_NUM_NC) + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiq_in, false); + + if (pin_sclk != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_sclk, SIG_GPIO_OUT_IDX, false, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiclk_in, false); + } +} diff --git a/lib/SpiManager/src/SpiBusConfig.h b/lib/SpiManager/src/SpiBusConfig.h new file mode 100644 index 000000000..736b89519 --- /dev/null +++ b/lib/SpiManager/src/SpiBusConfig.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class SpiBusConfig { +public: + explicit SpiBusConfig(gpio_num_t pin_mosi, gpio_num_t pin_miso, gpio_num_t pin_sclk); + SpiBusConfig(const SpiBusConfig&) = delete; + SpiBusConfig& operator=(const SpiBusConfig&) = delete; + ~SpiBusConfig(); + + void patch(spi_host_device_t host_device); + void unpatch(spi_host_device_t host_device); + +private: + gpio_num_t pin_mosi; + gpio_num_t pin_miso; + gpio_num_t pin_sclk; +}; diff --git a/lib/SpiManager/src/SpiCallback.cpp b/lib/SpiManager/src/SpiCallback.cpp new file mode 100644 index 000000000..e353d04bf --- /dev/null +++ b/lib/SpiManager/src/SpiCallback.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiCallback.h" + +#include "SpiBus.h" +#include +#include + +namespace SpiCallback { +namespace { + struct CallbackData { + std::shared_ptr bus; + std::shared_ptr config; + transaction_cb_t inner_pre_cb; + transaction_cb_t inner_post_cb; + }; + + std::array, SPI_MANAGER_CALLBACK_COUNT> instances; + + template + void IRAM_ATTR fn_pre_cb(spi_transaction_t* trans) + { + instances[N]->bus->require_config(instances[N]->config.get()); + if (instances[N]->inner_pre_cb) + instances[N]->inner_pre_cb(trans); + } + + template + void IRAM_ATTR fn_post_cb(spi_transaction_t* trans) + { + if (instances[N]->inner_post_cb) + instances[N]->inner_post_cb(trans); + } + + template + inline __attribute__((always_inline)) bool alloc(CallbackData*& instance, transaction_cb_t& pre_cb, transaction_cb_t& post_cb) + { + if constexpr (N > 0) { + if (alloc(instance, pre_cb, post_cb)) + return true; + if (!instances[N - 1]) { + instances[N - 1].emplace(); + instance = &*instances[N - 1]; + pre_cb = fn_pre_cb; + post_cb = fn_post_cb; + return true; + } + } + return false; + } +} + +bool patch(const std::shared_ptr& bus, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + CallbackData* instance; + transaction_cb_t pre_cb; + transaction_cb_t post_cb; + if (!alloc(instance, pre_cb, post_cb)) + return false; + + instance->bus = bus; + instance->config = bus_config; + instance->inner_pre_cb = device_config.pre_cb; + instance->inner_post_cb = device_config.post_cb; + device_config.pre_cb = pre_cb; + device_config.post_cb = post_cb; + + return true; +} +} diff --git a/lib/SpiManager/src/SpiCallback.h b/lib/SpiManager/src/SpiCallback.h new file mode 100644 index 000000000..98222b1a9 --- /dev/null +++ b/lib/SpiManager/src/SpiCallback.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +// Pre and post callbacks for 2 buses with 3 devices each +#define SPI_MANAGER_CALLBACK_COUNT 6 + +class SpiBus; +class SpiBusConfig; + +namespace SpiCallback { +bool patch(const std::shared_ptr& bus, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); +} diff --git a/lib/SpiManager/src/SpiManager.cpp b/lib/SpiManager/src/SpiManager.cpp new file mode 100644 index 000000000..d727a96ef --- /dev/null +++ b/lib/SpiManager/src/SpiManager.cpp @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiManager.h" + +#ifdef ARDUINO +#include +#endif + +SpiManager::SpiManager() +{ +} + +#ifdef ARDUINO + +std::optional SpiManager::to_arduino(spi_host_device_t host_device) +{ + switch (host_device) { +#if CONFIG_IDF_TARGET_ESP32 + case SPI1_HOST: + return FSPI; + case SPI2_HOST: + return HSPI; + case SPI3_HOST: + return VSPI; +#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + case SPI2_HOST: + return FSPI; + case SPI3_HOST: + return HSPI; +#elif CONFIG_IDF_TARGET_ESP32C3 + case SPI2_HOST: + return FSPI; +#endif + default: + return std::nullopt; + } +} + +#endif + +bool SpiManager::register_bus(spi_host_device_t host_device) +{ + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (available_buses[i]) + continue; + + available_buses[i] = host_device; + return true; + } + + return false; +} + +bool SpiManager::claim_bus(spi_host_device_t& host_device) +{ + for (int i = SPI_MANAGER_NUM_BUSES - 1; i >= 0; --i) { + if (!available_buses[i]) + continue; + + host_device = *available_buses[i]; + available_buses[i].reset(); + return true; + } + + return false; +} + +#ifdef ARDUINO + +std::optional SpiManager::claim_bus_arduino() +{ + spi_host_device_t host_device; + if (!claim_bus(host_device)) + return std::nullopt; + return to_arduino(host_device); +} + +#endif + +spi_device_handle_t SpiManager::alloc_device(const std::string& bus_id, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + std::shared_ptr shared_bus = get_shared_bus(bus_id); + if (!shared_bus) + return nullptr; + + return shared_bus->add_device(bus_config, device_config); +} + +std::shared_ptr SpiManager::get_shared_bus(const std::string& bus_id) +{ + // look for existing shared bus + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (!shared_buses[i]) + continue; + if (shared_buses[i]->get_id() == bus_id) + return shared_buses[i]; + } + + // create new shared bus + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (shared_buses[i]) + continue; + + spi_host_device_t host_device; + if (!claim_bus(host_device)) + return nullptr; + + shared_buses[i] = std::make_shared(bus_id, host_device); + return shared_buses[i]; + } + + return nullptr; +} + +SpiManager SpiManagerInst; diff --git a/lib/SpiManager/src/SpiManager.h b/lib/SpiManager/src/SpiManager.h new file mode 100644 index 000000000..1e8f6e1b0 --- /dev/null +++ b/lib/SpiManager/src/SpiManager.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "SpiBus.h" +#include "SpiBusConfig.h" + +#include + +#include +#include +#include +#include + +#define SPI_MANAGER_NUM_BUSES SOC_SPI_PERIPH_NUM + +class SpiManager { +public: + explicit SpiManager(); + SpiManager(const SpiManager&) = delete; + SpiManager& operator=(const SpiManager&) = delete; + +#ifdef ARDUINO + static std::optional to_arduino(spi_host_device_t host_device); +#endif + + bool register_bus(spi_host_device_t host_device); + bool claim_bus(spi_host_device_t& host_device); +#ifdef ARDUINO + std::optional claim_bus_arduino(); +#endif + + spi_device_handle_t alloc_device(const std::string& bus_id, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); + +private: + std::shared_ptr get_shared_bus(const std::string& bus_id); + + std::array, SPI_MANAGER_NUM_BUSES> available_buses; + std::array, SPI_MANAGER_NUM_BUSES> shared_buses; +}; + +extern SpiManager SpiManagerInst; diff --git a/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index 26e1bd65a..c1fa9771d 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -36,9 +36,20 @@ def get_build_version(): return build_version +def get_build_branch(): + try: + branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir + except Exception as err: + branch_name = "master" + print("Firmware Branch: " + branch_name) + return branch_name + + def get_firmware_specifier_build_flag(): build_version = get_build_version() build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" + build_branch = get_build_branch() + build_flag += " -D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\"" return (build_flag) @@ -64,6 +75,8 @@ def do_main(): if 1: # Add the description of the current git revision lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version()) + # ... and git branch + lines += 'const char *__COMPILED_GIT_BRANCH__ = "%s";\n' % (get_build_branch()) updateFileIfChanged(targetfile, bytes(lines, "utf-8")) diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py index d394998b2..ec8274e21 100644 --- a/pio-scripts/create_factory_bin.py +++ b/pio-scripts/create_factory_bin.py @@ -18,20 +18,64 @@ Import("env") +env = DefaultEnvironment() platform = env.PioPlatform() import sys -from os.path import join, getsize +import csv +import subprocess +import shutil +from os.path import join, getsize, exists, isdir +from os import listdir sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) import esptool +def esp32_build_filesystem(fs_name, fs_size): + filesystem_dir = env.subst("$PROJECT_DATA_DIR") + print("Creating %dKiB filesystem with content:" % (int(fs_size, 0)/1024) ) + if not isdir(filesystem_dir) or not listdir(filesystem_dir): + print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!") + return False + # this does not work on GitHub, results in 'mklittlefs: No such file or directory' + tool = shutil.which(env.subst(env["MKFSTOOL"])) + if tool is None or not exists(tool): + print("Using fallback mklittlefs") + tool = "~/.platformio/packages/tool-mklittlefs/mklittlefs" + + cmd = (tool, "-c", filesystem_dir, "-s", fs_size, fs_name) + returncode = subprocess.call(cmd, shell=False) + print("Return Code:", returncode) + return True + def esp32_create_combined_bin(source, target, env): print("Generating combined binary for serial flashing") # The offset from begin of the file where the app0 partition starts # This is defined in the partition .csv file app_offset = 0x10000 + fs_offset = -1 + fs_name = env.subst("$BUILD_DIR/littlefs.bin") + + with open(env.BoardConfig().get("build.partitions")) as csv_file: + print("Read partitions from ", env.BoardConfig().get("build.partitions")) + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + for row in csv_reader: + if line_count == 0: + print(f'{", ".join(row)}') + line_count += 1 + else: + if (len(row) < 4): + continue + print(f'{row[0]} {row[1]} {row[2]} {row[3]} {row[4]}') + line_count += 1 + if(row[0] == 'app0'): + app_offset = int(row[3], base=16) + elif(row[0] == 'spiffs'): + partition_size = row[4] + if esp32_build_filesystem(fs_name, partition_size): + fs_offset = int(row[3], base=16) new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) @@ -77,9 +121,13 @@ def esp32_create_combined_bin(source, target, env): print(f" - {hex(app_offset)} | {firmware_name}") cmd += [hex(app_offset), firmware_name] + if fs_offset != -1: + print(f" - {hex(fs_offset)} | {fs_name}") + cmd += [hex(fs_offset), fs_name] + print('Using esptool.py arguments: %s' % ' '.join(cmd)) esptool.main(cmd) -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) \ No newline at end of file +env.AddPostAction("buildprog", esp32_create_combined_bin) diff --git a/pio-scripts/patch_apply.py b/pio-scripts/patch_apply.py index 5734c4aa0..5c77f6839 100644 --- a/pio-scripts/patch_apply.py +++ b/pio-scripts/patch_apply.py @@ -23,20 +23,20 @@ def is_tool(name): return which(name) is not None def replaceInFile(in_file, out_file, text, subs, flags=0): - """ - Function for replacing content for the given file - Taken from https://www.studytonight.com/python-howtos/search-and-replace-a-text-in-a-file-in-python - """ + """Function for replacing content for the given file.""" + if os.path.exists(in_file): - with open(in_file, "rb") as infile: - with open(out_file, "wb") as outfile: - #read the file contents - file_contents = infile.read() - text_pattern = re.compile(re.escape(text), flags) - file_contents = text_pattern.sub(subs, file_contents.decode('utf-8')) - outfile.seek(0) - outfile.truncate() - outfile.write(file_contents.encode()) + # read the file contents + with open(in_file, "r", encoding="utf-8") as infile: + file_contents = infile.read() + + # do replacement + text_pattern = re.compile(re.escape(text), flags) + file_contents = text_pattern.sub(subs, file_contents) + + # write the result + with open(out_file, "w", encoding="utf-8") as outfile: + outfile.write(file_contents) def main(): if (env.GetProjectOption('custom_patches', '') == ''): diff --git a/platformio.ini b/platformio.ini index 472b965a3..1a9e4d89a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,9 @@ extra_configs = custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.8.1 +platform = espressif32@6.9.0 +platform_packages = + platformio/tool-mklittlefs build_flags = -DPIOENV=\"$PIOENV\" @@ -39,13 +41,13 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESPAsyncWebServer @ 3.1.2 - bblanchon/ArduinoJson @ 7.1.0 + mathieucarbou/ESPAsyncWebServer @ 3.3.22 + bblanchon/ArduinoJson @ 7.2.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0 - nrf24/RF24 @ 1.4.9 - olikraus/U8g2 @ 2.35.19 + nrf24/RF24 @ 1.4.10 + olikraus/U8g2 @ 2.36.2 buelowp/sunset @ 1.1.7 - https://github.com/arkhipenko/TaskScheduler#testing + arkhipenko/TaskScheduler @ 3.8.5 extra_scripts = pre:pio-scripts/auto_firmware_version.py @@ -236,6 +238,7 @@ build_flags = ${env.build_flags} -DLED0=17 -DLED1=18 -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 [env:opendtufusionv2] board = esp32-s3-devkitc-1 @@ -259,3 +262,32 @@ build_flags = ${env.build_flags} -DCMT_SDIO=5 -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 + +[env:opendtufusionv2_poe] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +debug_tool = esp-builtin +debug_speed = 12000 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=48 + -DHOYMILES_PIN_MOSI=35 + -DHOYMILES_PIN_SCLK=36 + -DHOYMILES_PIN_IRQ=47 + -DHOYMILES_PIN_CE=38 + -DHOYMILES_PIN_CS=37 + -DLED0=17 + -DLED1=18 + -DCMT_CLK=6 + -DCMT_CS=4 + -DCMT_FCS=21 + -DCMT_GPIO2=3 + -DCMT_GPIO3=8 + -DCMT_SDIO=5 + -DW5500_MOSI=40 + -DW5500_MISO=41 + -DW5500_SCLK=39 + -DW5500_CS=42 + -DW5500_INT=44 + -DW5500_RST=43 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index ee0d02633..9ebc50c12 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -13,8 +13,17 @@ CONFIG_T config; -void ConfigurationClass::init() +static std::condition_variable sWriterCv; +static std::mutex sWriterMutex; +static unsigned sWriterCount = 0; + +void ConfigurationClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&ConfigurationClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + memset(&config, 0x0, sizeof(config)); } @@ -107,7 +116,7 @@ bool ConfigurationClass::write() display["screensaver"] = config.Display.ScreenSaver; display["rotation"] = config.Display.Rotation; display["contrast"] = config.Display.Contrast; - display["language"] = config.Display.Language; + display["locale"] = config.Display.Locale; display["diagram_duration"] = config.Display.Diagram.Duration; display["diagram_mode"] = config.Display.Diagram.Mode; @@ -171,6 +180,7 @@ bool ConfigurationClass::write() bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); + Utils::skipBom(f); JsonDocument doc; @@ -294,7 +304,7 @@ bool ConfigurationClass::read() 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; + strlcpy(config.Display.Locale, display["locale"] | DISPLAY_LOCALE, sizeof(config.Display.Locale)); config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE; @@ -341,6 +351,20 @@ bool ConfigurationClass::read() } f.close(); + + // Check for default DTU serial + MessageOutput.print("Check for default DTU serial... "); + if (config.Dtu.Serial == DTU_SERIAL) { + MessageOutput.print("generate serial based on ESP chip id: "); + const uint64_t dtuId = Utils::generateDtuSerial(); + MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ", + static_cast((dtuId >> 32) & 0xFFFFFFFF), + static_cast(dtuId & 0xFFFFFFFF)); + config.Dtu.Serial = dtuId; + write(); + } + MessageOutput.println("done"); + return true; } @@ -406,6 +430,22 @@ void ConfigurationClass::migrate() } } + if (config.Cfg.Version < 0x00011d00) { + JsonObject device = doc["device"]; + JsonObject display = device["display"]; + switch (display["language"] | 0U) { + case 0U: + strlcpy(config.Display.Locale, "en", sizeof(config.Display.Locale)); + break; + case 1U: + strlcpy(config.Display.Locale, "de", sizeof(config.Display.Locale)); + break; + case 2U: + strlcpy(config.Display.Locale, "fr", sizeof(config.Display.Locale)); + break; + } + } + f.close(); config.Cfg.Version = CONFIG_VERSION; @@ -413,11 +453,16 @@ void ConfigurationClass::migrate() read(); } -CONFIG_T& ConfigurationClass::get() +CONFIG_T const& ConfigurationClass::get() { return config; } +ConfigurationClass::WriteGuard ConfigurationClass::getWriteGuard() +{ + return WriteGuard(); +} + INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -462,4 +507,30 @@ void ConfigurationClass::deleteInverterById(const uint8_t id) } } +void ConfigurationClass::loop() +{ + std::unique_lock lock(sWriterMutex); + if (sWriterCount == 0) { return; } + + sWriterCv.notify_all(); + sWriterCv.wait(lock, [] { return sWriterCount == 0; }); +} + +CONFIG_T& ConfigurationClass::WriteGuard::getConfig() +{ + return config; +} + +ConfigurationClass::WriteGuard::WriteGuard() + : _lock(sWriterMutex) +{ + sWriterCount++; + sWriterCv.wait(_lock); +} + +ConfigurationClass::WriteGuard::~WriteGuard() { + sWriterCount--; + if (sWriterCount == 0) { sWriterCv.notify_all(); } +} + ConfigurationClass Configuration; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index fa2cc6ad6..df4334121 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -4,6 +4,7 @@ */ #include "Display_Graphic.h" #include "Datastore.h" +#include "I18n.h" #include "JsyMk.h" #include #include @@ -17,18 +18,11 @@ std::map { DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } }, }; -// Language defintion, respect order in languages[] and translation lists +// Language defintion, respect order in translation lists #define I18N_LOCALE_EN 0 #define I18N_LOCALE_DE 1 #define I18N_LOCALE_FR 2 -// Languages supported. Note: the order is important and must match locale_translations.h -const uint8_t languages[] = { - I18N_LOCALE_EN, - I18N_LOCALE_DE, - I18N_LOCALE_FR -}; - static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; @@ -173,9 +167,42 @@ void DisplayGraphicClass::setOrientation(const uint8_t rotation) calcLineHeights(); } -void DisplayGraphicClass::setLanguage(const uint8_t language) +void DisplayGraphicClass::setLocale(const String& locale) { - _display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; + _display_language = locale; + uint8_t idx = I18N_LOCALE_EN; + if (locale == "de") { + idx = I18N_LOCALE_DE; + } else if (locale == "fr") { + idx = I18N_LOCALE_FR; + } + + _i18n_date_format = i18n_date_format[idx]; + _i18n_offline = i18n_offline[idx]; + _i18n_current_power_w = i18n_current_power_w[idx]; + _i18n_current_power_kw = i18n_current_power_kw[idx]; + _i18n_yield_today_wh = i18n_yield_today_wh[idx]; + _i18n_yield_today_kwh = i18n_yield_today_kwh[idx]; + _i18n_yield_total_kwh = i18n_yield_total_kwh[idx]; + _i18n_yield_total_mwh = i18n_yield_total_mwh[idx]; + _i18n_powermeter_power_w = i18n_powermeter_power_w[idx]; + _i18n_powermeter_power_kw = i18n_powermeter_power_kw[idx]; + _i18n_pm_positive_today_kwh = i18n_pm_positive_today_kwh[idx]; + _i18n_pm_negative_today_kwh = i18n_pm_negative_today_kwh[idx]; + + I18n.readDisplayStrings(locale, + _i18n_date_format, + _i18n_offline, + _i18n_current_power_w, + _i18n_current_power_kw, + _i18n_yield_today_wh, + _i18n_yield_today_kwh, + _i18n_yield_total_kwh, + _i18n_yield_total_mwh, + _i18n_powermeter_power_w, + _i18n_powermeter_power_kw, + _i18n_pm_positive_today_kwh, + _i18n_pm_negative_today_kwh); } void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode) @@ -216,9 +243,11 @@ void DisplayGraphicClass::loop() const char direction = (JsyMk.getFieldValue(0, JsyMkClass::Field_t::NEGATIVE) > 0) ? 'O' : 'I'; if (watts > 999) { - snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_kw[_display_language], direction, watts / 1000); + // snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_kw[_display_language], direction, watts / 1000); + snprintf(_fmtText, sizeof(_fmtText), _i18n_powermeter_power_kw.c_str(), direction, watts / 1000); } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_w[_display_language], direction, watts); + // snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_w[_display_language], direction, watts); + snprintf(_fmtText, sizeof(_fmtText), _i18n_powermeter_power_w.c_str(), direction, watts); } printText(_fmtText, 0); } @@ -245,9 +274,9 @@ void DisplayGraphicClass::loop() if (showText) { const float watts = Datastore.getTotalAcPowerEnabled(); if (watts > 999) { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000); + snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_kw.c_str(), watts / 1000); } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts); + snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_w.c_str(), watts); } printText(_fmtText, 0); } @@ -257,7 +286,7 @@ void DisplayGraphicClass::loop() //=====> Offline =========== else { - printText(i18n_offline[_display_language], 0); + printText(_i18n_offline.c_str(), 0); // check if it's time to enter power saving mode if (millis() - _previousMillis >= (_interval * 2)) { displayPowerSave = enablePowerSafe; @@ -269,27 +298,27 @@ void DisplayGraphicClass::loop() if (displayPowerMeter) { // Daily Input float wattsInput = JsyMk.getFieldValue(0, JsyMkClass::Field_t::TODAY_POSITIVE_ENERGY); - snprintf(_fmtText, sizeof(_fmtText), i18n_pm_positive_today_kwh[_display_language], wattsInput); + snprintf(_fmtText, sizeof(_fmtText), _i18n_pm_positive_today_kwh.c_str(), wattsInput); printText(_fmtText, 1); // Daily Output float wattsOutput = JsyMk.getFieldValue(0, JsyMkClass::Field_t::TODAY_NEGATIVE_ENERGY); - snprintf(_fmtText, sizeof(_fmtText), i18n_pm_negative_today_kwh[_display_language], wattsOutput); + snprintf(_fmtText, sizeof(_fmtText), _i18n_pm_negative_today_kwh.c_str(), wattsOutput); printText(_fmtText, 2); } else { // Daily production float wattsToday = Datastore.getTotalAcYieldDayEnabled(); if (wattsToday >= 10000) { - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); + snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_kwh.c_str(), wattsToday / 1000); } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); + snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_wh.c_str(), wattsToday); } printText(_fmtText, 1); // Total production const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); - auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; - snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); + auto const format = (wattsTotal >= 1000) ? _i18n_yield_total_mwh : _i18n_yield_total_kwh; + snprintf(_fmtText, sizeof(_fmtText), format.c_str(), wattsTotal); printText(_fmtText, 2); } @@ -300,7 +329,7 @@ void DisplayGraphicClass::loop() } else { // Get current time time_t now = time(nullptr); - strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); + strftime(_fmtText, sizeof(_fmtText), _i18n_date_format.c_str(), localtime(&now)); printText(_fmtText, 3); } } diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp index b52968829..fb0b68fea 100644 --- a/src/Display_Graphic_Diagram.cpp +++ b/src/Display_Graphic_Diagram.cpp @@ -87,7 +87,7 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos if (maxWatts > 999) { snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000); } else { - snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); + snprintf(fmtText, sizeof(fmtText), "%" PRId16 "W", static_cast(maxWatts)); } if (isFullscreen) { diff --git a/src/I18n.cpp b/src/I18n.cpp new file mode 100644 index 000000000..147cfbc3e --- /dev/null +++ b/src/I18n.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "I18n.h" +#include "MessageOutput.h" +#include "Utils.h" +#include "defaults.h" +#include +#include + +I18nClass I18n; + +I18nClass::I18nClass() +{ +} + +void I18nClass::init(Scheduler& scheduler) +{ + readLangPacks(); +} + +std::list I18nClass::getAvailableLanguages() +{ + return _availLanguages; +} + +String I18nClass::getFilenameByLocale(const String& locale) const +{ + auto it = std::find_if(_availLanguages.begin(), _availLanguages.end(), [locale](const LanguageInfo_t& elem) { + return elem.code == locale; + }); + + if (it != _availLanguages.end()) { + return it->filename; + } else { + return String(); + } +} + +void I18nClass::readDisplayStrings( + const String& locale, + String& date_format, + String& offline, + String& power_w, String& power_kw, + String& yield_today_wh, String& yield_today_kwh, + String& yield_total_kwh, String& yield_total_mwh, + String& yield_powermeter_power_w, String& yield_powermeter_power_kw, + String& yield_pm_positive_today_kwh, String& yield_pm_negative_today_kwh) +{ + auto filename = getFilenameByLocale(locale); + if (filename == "") { + return; + } + + JsonDocument filter; + filter["display"] = true; + + File f = LittleFS.open(filename, "r", false); + + JsonDocument doc; + + // Deserialize the JSON document + const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter)); + if (error) { + MessageOutput.printf("Failed to read file %s\r\n", filename.c_str()); + f.close(); + return; + } + + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + + auto displayData = doc["display"]; + + if (displayData["date_format"].as() != "null") { + date_format = displayData["date_format"].as(); + } + + if (displayData["offline"].as() != "null") { + offline = displayData["offline"].as(); + } + + if (displayData["power_w"].as() != "null") { + power_w = displayData["power_w"].as(); + } + + if (displayData["power_kw"].as() != "null") { + power_kw = displayData["power_kw"].as(); + } + + if (displayData["yield_today_wh"].as() != "null") { + yield_today_wh = displayData["yield_today_wh"].as(); + } + + if (displayData["yield_today_kwh"].as() != "null") { + yield_today_kwh = displayData["yield_today_kwh"].as(); + } + + if (displayData["yield_total_kwh"].as() != "null") { + yield_total_kwh = displayData["yield_total_kwh"].as(); + } + + if (displayData["yield_total_mwh"].as() != "null") { + yield_total_mwh = displayData["yield_total_mwh"].as(); + } + + if (displayData["yield_powermeter_power_w"].as() != "null") { + yield_powermeter_power_w = displayData["yield_powermeter_power_w"].as(); + } + + if (displayData["yield_powermeter_power_kw"].as() != "null") { + yield_powermeter_power_kw = displayData["yield_powermeter_power_kw"].as(); + } + + if (displayData["yield_pm_positive_today_kwh"].as() != "null") { + yield_pm_positive_today_kwh = displayData["yield_pm_positive_today_kwh"].as(); + } + + if (displayData["yield_pm_negative_today_kwh"].as() != "null") { + yield_pm_negative_today_kwh = displayData["yield_pm_negative_today_kwh"].as(); + } + + f.close(); +} + +void I18nClass::readLangPacks() +{ + auto root = LittleFS.open("/"); + auto file = root.getNextFileName(); + + while (file != "") { + if (file.endsWith(LANG_PACK_SUFFIX)) { + MessageOutput.printf("Read File %s\r\n", file.c_str()); + readConfig(file); + } + file = root.getNextFileName(); + } + root.close(); +} + +void I18nClass::readConfig(String file) +{ + JsonDocument filter; + filter["meta"] = true; + + File f = LittleFS.open(file, "r", false); + + JsonDocument doc; + + // Deserialize the JSON document + const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter)); + if (error) { + MessageOutput.printf("Failed to read file %s\r\n", file.c_str()); + f.close(); + return; + } + + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + + LanguageInfo_t lang; + lang.code = String(doc["meta"]["code"] | ""); + lang.name = String(doc["meta"]["name"] | ""); + lang.filename = file; + + if (lang.code != "" && lang.name != "") { + _availLanguages.push_back(lang); + } else { + MessageOutput.printf("Invalid meta data\r\n"); + } + + f.close(); +} diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 0e903187d..4b5d52e7b 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -8,20 +8,7 @@ #include "PinMapping.h" #include "SunPosition.h" #include - -// the NRF shall use the second externally usable HW SPI controller -// for ESP32 that is the so-called VSPI, for ESP32-S2/S3 it is now called implicitly -// HSPI, as it has shifted places for these chip generations -// for all generations, this is equivalent to SPI3_HOST in the lower level driver -// For ESP32-C2, the only externally usable HW SPI controller is SPI2, its signal names -// being prefixed with FSPI. -#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 -#define SPI_NRF HSPI -#elif CONFIG_IDF_TARGET_ESP32C3 -#define SPI_NRF FSPI -#else -#define SPI_NRF VSPI -#endif +#include InverterSettingsClass InverterSettings; @@ -44,7 +31,10 @@ void InverterSettingsClass::init(Scheduler& scheduler) if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { if (PinMapping.isValidNrf24Config()) { - SPIClass* spiClass = new SPIClass(SPI_NRF); + auto spi_bus = SpiManagerInst.claim_bus_arduino(); + ESP_ERROR_CHECK(spi_bus ? ESP_OK : ESP_FAIL); + + SPIClass* spiClass = new SPIClass(*spi_bus); spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); } @@ -70,10 +60,10 @@ void InverterSettingsClass::init(Scheduler& scheduler) for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { - MessageOutput.print(" Adding inverter: "); - MessageOutput.print(config.Inverter[i].Serial, HEX); - MessageOutput.print(" - "); - MessageOutput.print(config.Inverter[i].Name); + MessageOutput.printf(" Adding inverter: %0" PRIx32 "%08" PRIx32 " - %s", + static_cast((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF), + static_cast(config.Inverter[i].Serial & 0xFFFFFFFF), + config.Inverter[i].Name); auto inv = Hoymiles.addInverter( config.Inverter[i].Name, config.Inverter[i].Serial); diff --git a/src/JsyMk.cpp b/src/JsyMk.cpp index 501903ecc..1ea4bb131 100644 --- a/src/JsyMk.cpp +++ b/src/JsyMk.cpp @@ -31,7 +31,8 @@ constexpr std::arraytm_hour == 0 && lt->tm_min == 0 && lt->tm_sec <= Configuration.get().SerialModbus.PollInterval) { + if (lt.tm_hour == 0 && lt.tm_min == 0 && lt.tm_sec <= Configuration.get().SerialModbus.PollInterval) { _todayPositiveRef = 0; _todayNegativeRef = 0; } diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index e8192b2e2..df025f12c 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -7,6 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include +#include MqttHandleDtuClass MqttHandleDtu; @@ -34,8 +35,17 @@ void MqttHandleDtuClass::loop() MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); + MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize())); + MqttSettings.publish("dtu/heap/free", String(ESP.getFreeHeap())); + MqttSettings.publish("dtu/heap/minfree", String(ESP.getMinFreeHeap())); + MqttSettings.publish("dtu/heap/maxalloc", String(ESP.getMaxAllocHeap())); if (NetworkSettings.NetworkMode() == network_mode::WiFi) { MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); } + + float temperature = CpuTemperature.read(); + if (!std::isnan(temperature)) { + MqttSettings.publish("dtu/temperature", String(temperature)); + } } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 84edf6e48..930808785 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -7,8 +7,8 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "Utils.h" -#include "defaults.h" #include "__compiled_constants.h" +#include "defaults.h" MqttHandleHassClass MqttHandleHass; @@ -58,29 +58,46 @@ 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); + publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Temperature", "dtu/temperature", "°C", "", DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); - yield(); + publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); + publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); + publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE); + + publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); - publishInverterButton(inv, "Turn Inverter Off", "mdi:power-plug-off", "config", "", "cmd/power", "0"); - publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); - publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); + publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); + + publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1); + publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); - 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", DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_NONE); - publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); - publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); + publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RSSI", "radio/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { @@ -94,8 +111,6 @@ void MqttHandleHassClass::publishConfig() } } } - - yield(); } if (!JsyMk.isInitialised()) @@ -149,8 +164,6 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr if (!clear) { const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); - const char* devCls = deviceClasses[fieldType.deviceClsId]; - const char* stateCls = stateClasses[fieldType.stateClsId]; String name; if (type != TYPE_DC) { @@ -159,46 +172,34 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr name = "CH" + chanNum + " " + fieldName; } + String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId); + JsonDocument root; + createInverterInfo(root, inv); + addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, fieldType.stateClsId, CATEGORY_NONE); root["name"] = name; root["stat_t"] = stateTopic; root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName; - String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId); - if (unit_of_measure != "") { - root["unit_of_meas"] = unit_of_measure; - } - - createInverterInfo(root, inv); - 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; - } - if (stateCls != 0) { - root["stat_cla"] = stateCls; - } - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); + publish(configTopic, root); } else { publish(configTopic, ""); } } -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) +void MqttHandleHassClass::publishInverterButton( + std::shared_ptr inv, const String& name, const String& state_topic, const String& payload, + const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -206,41 +207,30 @@ 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, - const int16_t min, const int16_t max, float step) + std::shared_ptr inv, const String& name, + const String& stateTopic, const String& command_topic, + const int16_t min, const int16_t max, float step, + const String& unit_of_measure, const String& icon, + const StateClassType state_class, const CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -248,150 +238,22 @@ void MqttHandleHassClass::publishInverterNumber( + "/" + buttonId + "/config"; - const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + command_topic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; JsonDocument root; + createInverterInfo(root, inv); + addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, state_class, category); - root["name"] = caption; + root["name"] = name; root["uniq_id"] = serial + "_" + buttonId; - if (strcmp(icon, "")) { - root["ic"] = icon; - } - root["ent_cat"] = category; root["cmd_t"] = cmdTopic; root["stat_t"] = statTopic; - root["unit_of_meas"] = unitOfMeasure; root["min"] = min; root["max"] = max; root["step"] = step; - createInverterInfo(root, inv); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) -{ - const String serial = inv->serialString(); - - String sensorId = caption; - sensorId.replace(" ", "_"); - sensorId.toLowerCase(); - - const String configTopic = "binary_sensor/dtu_" + serial - + "/" + sensorId - + "/config"; - - const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; - - JsonDocument root; - - root["name"] = caption; - root["uniq_id"] = serial + "_" + sensorId; - root["stat_t"] = statTopic; - root["pl_on"] = payload_on; - root["pl_off"] = payload_off; - - createInverterInfo(root, inv); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic) -{ - String id = name; - id.toLowerCase(); - id.replace(" ", "_"); - String topic = subTopic; - if (topic == "") { - topic = id; - } - - JsonDocument root; - - root["name"] = name; - root["uniq_id"] = getDtuUniqueId() + "_" + id; - if (strcmp(device_class, "")) { - root["dev_cla"] = device_class; - } - if (strcmp(category, "")) { - root["ent_cat"] = category; - } - if (strcmp(icon, "")) { - root["ic"] = icon; - } - if (strcmp(unit_of_measure, "")) { - root["unit_of_meas"] = unit_of_measure; - } - root["stat_t"] = MqttSettings.getPrefix() + "dtu" + "/" + topic; - - root["avty_t"] = MqttSettings.getPrefix() + Configuration.get().Mqtt.Lwt.Topic; - - const CONFIG_T& config = Configuration.get(); - root["pl_avail"] = config.Mqtt.Lwt.Value_Online; - root["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline; - - createDtuInfo(root); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config"; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic) -{ - String id = name; - id.toLowerCase(); - id.replace(" ", "_"); - - String topic = subTopic; - if (!strcmp(subTopic, "")) { - topic = String("dtu/") + "/" + id; - } - - JsonDocument root; - - root["name"] = name; - root["uniq_id"] = getDtuUniqueId() + "_" + id; - root["stat_t"] = MqttSettings.getPrefix() + topic; - root["pl_on"] = payload_on; - root["pl_off"] = payload_off; - - if (strcmp(device_class, "")) { - root["dev_cla"] = device_class; - } - if (strcmp(category, "")) { - root["ent_cat"] = category; - } - - createDtuInfo(root); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config"; - serializeJson(root, buffer); - publish(configTopic, buffer); + publish(configTopic, root); } void MqttHandleHassClass::publishPowerMeterField(size_t channel, JsyMkClass::Field_t fieldId, const bool clear) @@ -442,7 +304,7 @@ void MqttHandleHassClass::publishPowerMeterField(size_t channel, JsyMkClass::Fie serializeJson(root, buffer); publish(configTopic, buffer); } else { - publish(configTopic, {}); + publish(configTopic, String {}); } } @@ -523,4 +385,129 @@ void MqttHandleHassClass::publish(const String& subtopic, const String& payload) String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain); + yield(); +} + +void MqttHandleHassClass::publish(const String& subtopic, const JsonDocument& doc) +{ + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + String buffer; + serializeJson(doc, buffer); + publish(subtopic, buffer); +} + +void MqttHandleHassClass::addCommonMetadata( + JsonDocument& doc, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + if (unit_of_measure != "") { + doc["unit_of_meas"] = unit_of_measure; + } + if (icon != "") { + doc["ic"] = icon; + } + if (device_class != DEVICE_CLS_NONE) { + doc["dev_cla"] = deviceClass_name[device_class]; + } + if (state_class != STATE_CLS_NONE) { + doc["stat_cla"] = stateClass_name[state_class];; + } + if (category != CATEGORY_NONE) { + doc["ent_cat"] = category_name[category]; + } +} + +void MqttHandleHassClass::publishBinarySensor( + JsonDocument& doc, + const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + String sensor_id = name; + sensor_id.toLowerCase(); + sensor_id.replace(" ", "_"); + + doc["name"] = name; + doc["uniq_id"] = unique_id_prefix + "_" + sensor_id; + doc["stat_t"] = MqttSettings.getPrefix() + state_topic; + doc["pl_on"] = payload_on; + doc["pl_off"] = payload_off; + + addCommonMetadata(doc, "", "", device_class, state_class, category); + + const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config"; + publish(configTopic, doc); +} + +void MqttHandleHassClass::publishDtuBinarySensor( + const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, state_class, category); +} + +void MqttHandleHassClass::publishInverterBinarySensor( + std::shared_ptr inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String serial = inv->serialString(); + + JsonDocument root; + createInverterInfo(root, inv); + publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, state_class, category); +} + +void MqttHandleHassClass::publishSensor( + JsonDocument& doc, + const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + String sensor_id = name; + sensor_id.toLowerCase(); + sensor_id.replace(" ", "_"); + + doc["name"] = name; + doc["uniq_id"] = unique_id_prefix + "_" + sensor_id; + doc["stat_t"] = MqttSettings.getPrefix() + state_topic; + + addCommonMetadata(doc, unit_of_measure, icon, device_class, state_class, category); + + const CONFIG_T& config = Configuration.get(); + doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic; + doc["pl_avail"] = config.Mqtt.Lwt.Value_Online; + doc["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline; + + const String configTopic = "sensor/" + root_device + "/" + sensor_id + "/config"; + publish(configTopic, doc); +} + +void MqttHandleHassClass::publishDtuSensor( + const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category); +} + +void MqttHandleHassClass::publishInverterSensor( + std::shared_ptr inv, const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String serial = inv->serialString(); + + JsonDocument root; + createInverterInfo(root, inv); + publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, state_class, category); } diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index d099b4443..70a7222d2 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -7,13 +7,6 @@ #include "MqttSettings.h" #include -#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative" -#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" -#define TOPIC_SUB_POWER "power" -#define TOPIC_SUB_RESTART "restart" - #define PUBLISH_MAX_INTERVAL 60000 MqttHandleInverterClass MqttHandleInverter; @@ -50,6 +43,15 @@ void MqttHandleInverterClass::loop() // Name MqttSettings.publish(subtopic + "/name", inv->name()); + // Radio Statistics + MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData)); + MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment)); + MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess)); + MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData)); + MqttSettings.publish(subtopic + "/radio/rssi", String(inv->getLastRssi())); + if (inv->DevInfo()->getLastUpdate() > 0) { // Bootloader Version MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); @@ -146,7 +148,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, const size_t len, const size_t index, const size_t total) +void MqttHandleInverterClass::onMqttMessage(Topic t, 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(); @@ -154,15 +156,11 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* char* serial_str; - char* subtopic; - char* setting; char* rest = &token_topic[strlen(config.Mqtt.Topic)]; serial_str = strtok_r(rest, "/", &rest); - subtopic = strtok_r(rest, "/", &rest); - setting = strtok_r(rest, "/", &rest); - if (serial_str == NULL || subtopic == NULL || setting == NULL) { + if (serial_str == NULL) { return; } @@ -175,33 +173,30 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro return; } - // check if subtopic is unequal cmd - if (strcmp(subtopic, "cmd")) { + std::string strValue(reinterpret_cast(payload), len); + float payload_val = -1; + try { + payload_val = std::stof(strValue); + } catch (std::invalid_argument const& e) { + MessageOutput.printf("MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", + topic, strValue.c_str()); return; } - char* strlimit = new char[len + 1]; - memcpy(strlimit, payload, len); - strlimit[len] = '\0'; - const float payload_val = strtof(strlimit, NULL); - delete[] strlimit; - - if (payload_val < 0) { - MessageOutput.printf("MQTT payload < 0 received --> ignoring\r\n"); - return; - } - - if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { + switch (t) { + case Topic::LimitPersistentRelative: // Set inverter limit relative persistent MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { + case Topic::LimitPersistentAbsolute: // Set inverter limit absolute persistent MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { + case Topic::LimitNonPersistentRelative: // Set inverter limit relative non persistent MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val); if (!properties.retain) { @@ -209,8 +204,9 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained"); } + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { + case Topic::LimitNonPersistentAbsolute: // Set inverter limit absolute non persistent MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val); if (!properties.retain) { @@ -218,13 +214,15 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained"); } + break; - } else if (!strcmp(setting, TOPIC_SUB_POWER)) { + case Topic::Power: // Turn inverter on or off - MessageOutput.printf("Set inverter power to: %d\r\n", static_cast(payload_val)); + MessageOutput.printf("Set inverter power to: %" PRId32 "\r\n", static_cast(payload_val)); inv->sendPowerControlRequest(static_cast(payload_val) > 0); + break; - } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { + case Topic::Restart: // Restart inverter MessageOutput.printf("Restart inverter\r\n"); if (!properties.retain && payload_val == 1) { @@ -232,34 +230,41 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained or numeric value not '1'"); } + break; + + case Topic::ResetRfStats: + // Reset RF Stats + MessageOutput.printf("Reset RF stats\r\n"); + if (!properties.retain && payload_val == 1) { + inv->resetRadioStats(); + } else { + MessageOutput.println("Ignored because retained or numeric value not '1'"); + } } } void MqttHandleInverterClass::subscribeTopics() { - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - const String topic = MqttSettings.getPrefix(); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + String const& prefix = MqttSettings.getPrefix(); + + auto subscribe = [&prefix, this](char const* subTopic, Topic t) { + String fullTopic(prefix + _cmdtopic.data() + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandleInverterClass::onMqttMessage, this, t, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + for (auto const& s : _subscriptions) { + subscribe(s.first.data(), s.second); + } } void MqttHandleInverterClass::unsubscribeTopics() { - const String topic = MqttSettings.getPrefix(); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART)); + String const& prefix = MqttSettings.getPrefix() + _cmdtopic.data(); + for (auto const& s : _subscriptions) { + MqttSettings.unsubscribe(prefix + s.first.data()); + } } diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index b86c51f54..c547e0db0 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -91,8 +91,7 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re 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) { - MessageOutput.print("Received MQTT message on topic: "); - MessageOutput.println(topic); + MessageOutput.printf("Received MQTT message on topic: %s\r\n", topic); _mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total); } diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 55ea428e5..cb3af62ef 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -7,10 +7,10 @@ #include "MessageOutput.h" #include "PinMapping.h" #include "Utils.h" +#include "__compiled_constants.h" #include "defaults.h" #include #include -#include "__compiled_constants.h" NetworkSettingsClass::NetworkSettingsClass() : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this)) @@ -23,20 +23,41 @@ NetworkSettingsClass::NetworkSettingsClass() void NetworkSettingsClass::init(Scheduler& scheduler) { using std::placeholders::_1; + using std::placeholders::_2; WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.disconnect(true, true); - WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); + WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2)); + + if (PinMapping.isValidW5500Config()) { + PinMapping_t& pin = PinMapping.get(); + _w5500 = W5500::setup(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst); + if (_w5500) + MessageOutput.println("W5500: Connection successful"); + else + MessageOutput.println("W5500: Connection error!!"); + } +#if CONFIG_ETH_USE_ESP32_EMAC + else if (PinMapping.isValidEthConfig()) { + PinMapping_t& pin = PinMapping.get(); +#if ESP_ARDUINO_VERSION_MAJOR < 3 + ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode); +#else + ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode); +#endif + } +#endif + setupMode(); scheduler.addTask(_loopTask); _loopTask.enable(); } -void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) +void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info) { switch (event) { case ARDUINO_EVENT_ETH_START: @@ -76,7 +97,8 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) } break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - MessageOutput.println("WiFi disconnected"); + // Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141 + MessageOutput.printf("WiFi disconnected: %" PRIu8 "\r\n", info.wifi_sta_disconnected.reason); if (_networkMode == network_mode::WiFi) { MessageOutput.println("Try reconnecting"); WiFi.disconnect(true, false); @@ -95,12 +117,12 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) } } -bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event) +bool NetworkSettingsClass::onEvent(DtuNetworkEventCb cbEvent, const network_event event) { if (!cbEvent) { return pdFALSE; } - NetworkEventCbList_t newEventHandler; + DtuNetworkEventCbList_t newEventHandler; newEventHandler.cb = cbEvent; newEventHandler.event = event; _cbEventList.push_back(newEventHandler); @@ -109,8 +131,7 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event e void NetworkSettingsClass::raiseEvent(const network_event event) { - for (uint32_t i = 0; i < _cbEventList.size(); i++) { - const NetworkEventCbList_t entry = _cbEventList[i]; + for (auto& entry : _cbEventList) { if (entry.cb) { if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) { entry.cb(event); @@ -167,11 +188,6 @@ void NetworkSettingsClass::setupMode() WiFi.mode(WIFI_MODE_NULL); } } - - if (PinMapping.isValidEthConfig()) { - PinMapping_t& pin = PinMapping.get(); - ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode); - } } void NetworkSettingsClass::enableAdminMode() @@ -210,7 +226,7 @@ void NetworkSettingsClass::loop() if (_adminEnabled && _adminTimeoutCounterMax > 0) { _adminTimeoutCounter++; if (_adminTimeoutCounter % 10 == 0) { - MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); + MessageOutput.printf("Admin AP remaining seconds: %" PRId32 " / %" PRId32 "\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); } } _connectTimeoutTimer++; @@ -399,6 +415,9 @@ String NetworkSettingsClass::macAddress() const { switch (_networkMode) { case network_mode::Ethernet: + if (_w5500) { + return _w5500->macAddress(); + } return ETH.macAddress(); break; case network_mode::WiFi: diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index e37f0dc1b..8aa0a8d9a 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -4,6 +4,7 @@ */ #include "PinMapping.h" #include "MessageOutput.h" +#include "Utils.h" #include #include #include @@ -92,6 +93,57 @@ #define SERIAL_MODBUS_RX -1 #endif +#ifndef W5500_MOSI +#define W5500_MOSI -1 +#endif + +#ifndef W5500_MISO +#define W5500_MISO -1 +#endif + +#ifndef W5500_SCLK +#define W5500_SCLK -1 +#endif + +#ifndef W5500_CS +#define W5500_CS -1 +#endif + +#ifndef W5500_INT +#define W5500_INT -1 +#endif + +#ifndef W5500_RST +#define W5500_RST -1 +#endif + +#if CONFIG_ETH_USE_ESP32_EMAC + +#ifndef ETH_PHY_ADDR +#define ETH_PHY_ADDR -1 +#endif + +#ifndef ETH_PHY_POWER +#define ETH_PHY_POWER -1 +#endif + +#ifndef ETH_PHY_MDC +#define ETH_PHY_MDC -1 +#endif + +#ifndef ETH_PHY_MDIO +#define ETH_PHY_MDIO -1 +#endif + +#ifndef ETH_PHY_TYPE +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#endif + +#ifndef ETH_CLK_MODE +#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN +#endif +#endif + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -111,18 +163,26 @@ PinMappingClass::PinMappingClass() _pinMapping.cmt_gpio3 = CMT_GPIO3; _pinMapping.cmt_sdio = CMT_SDIO; + _pinMapping.w5500_mosi = W5500_MOSI; + _pinMapping.w5500_miso = W5500_MISO; + _pinMapping.w5500_sclk = W5500_SCLK; + _pinMapping.w5500_cs = W5500_CS; + _pinMapping.w5500_int = W5500_INT; + _pinMapping.w5500_rst = W5500_RST; + +#if CONFIG_ETH_USE_ESP32_EMAC #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = true; #else _pinMapping.eth_enabled = false; #endif - _pinMapping.eth_phy_addr = ETH_PHY_ADDR; _pinMapping.eth_power = ETH_PHY_POWER; _pinMapping.eth_mdc = ETH_PHY_MDC; _pinMapping.eth_mdio = ETH_PHY_MDIO; _pinMapping.eth_type = ETH_PHY_TYPE; _pinMapping.eth_clk_mode = ETH_CLK_MODE; +#endif _pinMapping.display_type = DISPLAY_TYPE; _pinMapping.display_data = DISPLAY_DATA; @@ -150,6 +210,8 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } + Utils::skipBom(f); + JsonDocument doc; // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); @@ -175,18 +237,26 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3; _pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO; + _pinMapping.w5500_mosi = doc[i]["w5500"]["mosi"] | W5500_MOSI; + _pinMapping.w5500_miso = doc[i]["w5500"]["miso"] | W5500_MISO; + _pinMapping.w5500_sclk = doc[i]["w5500"]["sclk"] | W5500_SCLK; + _pinMapping.w5500_cs = doc[i]["w5500"]["cs"] | W5500_CS; + _pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT; + _pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST; + +#if CONFIG_ETH_USE_ESP32_EMAC #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; #else _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false; #endif - _pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR; _pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER; _pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC; _pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO; _pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE; _pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE; +#endif _pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE; _pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA; @@ -225,10 +295,24 @@ bool PinMappingClass::isValidCmt2300Config() const && _pinMapping.cmt_sdio >= 0; } +bool PinMappingClass::isValidW5500Config() const +{ + return _pinMapping.w5500_mosi >= 0 + && _pinMapping.w5500_miso >= 0 + && _pinMapping.w5500_sclk >= 0 + && _pinMapping.w5500_cs >= 0 + && _pinMapping.w5500_int >= 0 + && _pinMapping.w5500_rst >= 0; +} + +#if CONFIG_ETH_USE_ESP32_EMAC bool PinMappingClass::isValidEthConfig() const { - return _pinMapping.eth_enabled; + return _pinMapping.eth_enabled + && _pinMapping.eth_mdc >= 0 + && _pinMapping.eth_mdio >= 0; } +#endif bool PinMappingClass::isValidSerialModbusConfig() const { diff --git a/src/RestartHelper.cpp b/src/RestartHelper.cpp new file mode 100644 index 000000000..ab385ef6b --- /dev/null +++ b/src/RestartHelper.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "RestartHelper.h" +#include "Display_Graphic.h" +#include "Led_Single.h" +#include + +RestartHelperClass RestartHelper; + +RestartHelperClass::RestartHelperClass() + : _rebootTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&RestartHelperClass::loop, this)) +{ +} + +void RestartHelperClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_rebootTask); +} + +void RestartHelperClass::triggerRestart() +{ + _rebootTask.enable(); + _rebootTask.restart(); +} + +void RestartHelperClass::loop() +{ + if (_rebootTask.isFirstIteration()) { + LedSingle.turnAllOff(); + Display.setStatus(false); + } else { + ESP.restart(); + } +} diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp index f1e1bee4a..5a2f27a2a 100644 --- a/src/SunPosition.cpp +++ b/src/SunPosition.cpp @@ -7,6 +7,8 @@ #include "Utils.h" #include +#define CALC_UNIQUE_ID (((timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday) << 1 | timeinfo.tm_isdst) + SunPositionClass SunPosition; SunPositionClass::SunPositionClass() @@ -57,7 +59,7 @@ bool SunPositionClass::checkRecalcDayChanged() const time(&now); localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms - const uint32_t ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; + const uint32_t ymd = CALC_UNIQUE_ID; return _lastSunPositionCalculatedYMD != ymd; } @@ -67,7 +69,7 @@ void SunPositionClass::updateSunData() struct tm timeinfo; const bool gotLocalTime = getLocalTime(&timeinfo, 5); - _lastSunPositionCalculatedYMD = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; + _lastSunPositionCalculatedYMD = CALC_UNIQUE_ID; setDoRecalc(false); if (!gotLocalTime) { diff --git a/src/Utils.cpp b/src/Utils.cpp index 6abe4dd19..c567ec116 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -4,11 +4,10 @@ */ #include "Utils.h" -#include "Display_Graphic.h" -#include "Led_Single.h" #include "MessageOutput.h" -#include +#include "PinMapping.h" #include +#include uint32_t Utils::getChipId() { @@ -59,20 +58,10 @@ int Utils::getTimezoneOffset() return static_cast(difftime(rawtime, gmt)); } -void Utils::restartDtu() -{ - LedSingle.turnAllOff(); - Display.setStatus(false); - yield(); - delay(1000); - yield(); - ESP.restart(); -} - bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) { if (doc.overflowed()) { - MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line); + MessageOutput.printf("Alloc failed: %s, %" PRId16 "\r\n", function, line); return false; } @@ -92,3 +81,45 @@ void Utils::removeAllFiles() file = root.getNextFileName(); } } + +String Utils::generateMd5FromFile(String file) +{ + if (!LittleFS.exists(file)) { + return String(); + } + + File f = LittleFS.open(file, "r"); + if (!file) { + return String(); + } + + MD5Builder md5; + md5.begin(); + + // Read the file in chunks to avoid using too much memory + uint8_t buffer[512]; + + while (f.available()) { + size_t bytesRead = f.read(buffer, sizeof(buffer) / sizeof(buffer[0])); + md5.add(buffer, bytesRead); + } + + // Finalize and calculate the MD5 hash + md5.calculate(); + + f.close(); + + return md5.toString(); +} + +void Utils::skipBom(File& f) +{ + // skip Byte Order Mask (BOM). valid JSON docs always start with '{' or '['. + while (f.available() > 0) { + int c = f.peek(); + if (c == '{' || c == '[') { + break; + } + f.read(); + } +} diff --git a/src/W5500.cpp b/src/W5500.cpp new file mode 100644 index 000000000..bf5394340 --- /dev/null +++ b/src/W5500.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ + +#include "W5500.h" + +#include +#include + +// Internal Arduino functions from WiFiGeneric +void tcpipInit(); +void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif); + +W5500::W5500(spi_device_handle_t spi, gpio_num_t pin_int) + : eth_handle(nullptr) + , eth_netif(nullptr) +{ + // Arduino function to start networking stack if not already started + tcpipInit(); + + ESP_ERROR_CHECK(tcpip_adapter_set_default_eth_handlers()); + + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi); + w5500_config.int_gpio_num = pin_int; + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.rx_task_stack_size = 4096; + esp_eth_mac_t* mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.reset_gpio_num = -1; + esp_eth_phy_t* phy = esp_eth_phy_new_w5500(&phy_config); + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + ESP_ERROR_CHECK(esp_eth_driver_install(ð_config, ð_handle)); + + // Configure MAC address + uint8_t mac_addr[6]; + ESP_ERROR_CHECK(esp_read_mac(mac_addr, ESP_MAC_ETH)); + ESP_ERROR_CHECK(esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr)); + + esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH(); + eth_netif = esp_netif_new(&netif_config); + + ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle))); + + // Add to Arduino + add_esp_interface_netif(ESP_IF_ETH, eth_netif); + + ESP_ERROR_CHECK(esp_eth_start(eth_handle)); +} + +W5500::~W5500() +{ + // TODO(LennartF22): support cleanup at some point? +} + +std::unique_ptr W5500::setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) +{ + gpio_reset_pin(static_cast(pin_rst)); + gpio_set_level(static_cast(pin_rst), 0); + gpio_set_direction(static_cast(pin_rst), GPIO_MODE_OUTPUT); + + gpio_reset_pin(static_cast(pin_cs)); + gpio_reset_pin(static_cast(pin_int)); + + auto bus_config = std::make_shared( + static_cast(pin_mosi), + static_cast(pin_miso), + static_cast(pin_sclk)); + + spi_device_interface_config_t device_config { + .command_bits = 16, // actually address phase + .address_bits = 8, // actually command phase + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, // only 0 supported + .cs_ena_posttrans = 0, // only 0 supported + .clock_speed_hz = 20000000, // stable with OpenDTU Fusion shield + .input_delay_ns = 0, + .spics_io_num = pin_cs, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + + spi_device_handle_t spi = SpiManagerInst.alloc_device("", bus_config, device_config); + if (!spi) + return nullptr; + + // Reset sequence + delayMicroseconds(500); + gpio_set_level(static_cast(pin_rst), 1); + delayMicroseconds(1000); + + if (!connection_check_spi(spi)) + return nullptr; + if (!connection_check_interrupt(static_cast(pin_int))) + return nullptr; + + // Use Arduino functions to temporarily attach interrupt to enable the GPIO ISR service + // (if we used ESP-IDF functions, a warning would be printed the first time anyone uses attachInterrupt) + attachInterrupt(pin_int, nullptr, FALLING); + detachInterrupt(pin_int); + + // Return to default state once again after connection check and temporary interrupt registration + gpio_reset_pin(static_cast(pin_int)); + + return std::unique_ptr(new W5500(spi, static_cast(pin_int))); +} + +String W5500::macAddress() +{ + uint8_t mac_addr[6] = {}; + esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); + + char mac_addr_str[18]; + snprintf( + mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", + mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + return String(mac_addr_str); +} + +bool W5500::connection_check_spi(spi_device_handle_t spi) +{ + spi_transaction_t trans = { + .flags = SPI_TRANS_USE_RXDATA, + .cmd = 0x0039, // actually address (VERSIONR) + .addr = (0b00000 << 3) | (0 << 2) | (0b00 < 0), // actually command (common register, read, VDM) + .length = 8, + .rxlength = 8, + .user = nullptr, + .tx_buffer = nullptr, + .rx_data = {}, + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + + // Version number (VERSIONR) is always 0x04 + return *reinterpret_cast(&trans.rx_data) == 0x04; +} + +bool W5500::connection_check_interrupt(gpio_num_t pin_int) +{ + gpio_set_direction(pin_int, GPIO_MODE_INPUT); + gpio_set_pull_mode(pin_int, GPIO_PULLDOWN_ONLY); + int level = gpio_get_level(pin_int); + + // Interrupt line must be high + return level == 1; +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 1a5b28709..835a98dca 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -15,13 +15,14 @@ WebApiClass::WebApiClass() void WebApiClass::init(Scheduler& scheduler) { - _webApiConfig.init(_server, scheduler); _webApiDevice.init(_server, scheduler); _webApiDevInfo.init(_server, scheduler); _webApiDtu.init(_server, scheduler); _webApiEventlog.init(_server, scheduler); + _webApiFile.init(_server, scheduler); _webApiFirmware.init(_server, scheduler); _webApiGridprofile.init(_server, scheduler); + _webApiI18n.init(_server, scheduler); _webApiInverter.init(_server, scheduler); _webApiLimit.init(_server, scheduler); _webApiMaintenance.init(_server, scheduler); @@ -39,9 +40,15 @@ void WebApiClass::init(Scheduler& scheduler) _server.begin(); } +void WebApiClass::reload() +{ + _webApiWsConsole.reload(); + _webApiWsLive.reload(); +} + bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) { - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); if (request->authenticate(AUTH_USERNAME, config.Security.Password)) { return true; } @@ -59,7 +66,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) { - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); if (config.Security.AllowReadonly) { return true; } else { @@ -131,7 +138,7 @@ bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResp root["code"] = WebApiError::GenericInternalServerError; root["type"] = "danger"; response->setCode(500); - MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); + MessageOutput.printf("WebResponse failed: %s, %" PRId16 "\r\n", function, line); ret_val = false; } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 3b821618a..3a7d4e65c 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -6,7 +6,7 @@ #include "Configuration.h" #include "Display_Graphic.h" #include "PinMapping.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -50,6 +50,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio3"] = pin.cmt_gpio3; + auto w5500PinObj = curPin["w5500"].to(); + w5500PinObj["sclk"] = pin.w5500_sclk; + w5500PinObj["mosi"] = pin.w5500_mosi; + w5500PinObj["miso"] = pin.w5500_miso; + w5500PinObj["cs"] = pin.w5500_cs; + w5500PinObj["int"] = pin.w5500_int; + w5500PinObj["rst"] = pin.w5500_rst; + +#if CONFIG_ETH_USE_ESP32_EMAC auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; @@ -58,6 +67,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) ethPinObj["mdio"] = pin.eth_mdio; ethPinObj["type"] = pin.eth_type; ethPinObj["clk_mode"] = pin.eth_clk_mode; +#endif auto displayPinObj = curPin["display"].to(); displayPinObj["type"] = pin.display_type; @@ -76,7 +86,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) display["power_safe"] = config.Display.PowerSafe; display["screensaver"] = config.Display.ScreenSaver; display["contrast"] = config.Display.Contrast; - display["language"] = config.Display.Language; + display["locale"] = config.Display.Locale; display["diagramduration"] = config.Display.Diagram.Duration; display["diagrammode"] = config.Display.Diagram.Mode; @@ -107,8 +117,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("curPin") - || root.containsKey("display"))) { + if (!(root["curPin"].is() + || root["display"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -123,29 +133,34 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } - CONFIG_T& config = Configuration.get(); - 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(); - config.Display.Diagram.Duration = root["display"]["diagramduration"].as(); - config.Display.Diagram.Mode = root["display"]["diagrammode"].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); + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + 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(); + strlcpy(config.Display.Locale, root["display"]["locale"].as().c_str(), sizeof(config.Display.Locale)); + config.Display.Diagram.Duration = root["display"]["diagramduration"].as(); + config.Display.Diagram.Mode = root["display"]["diagrammode"].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); + } } + auto const& config = Configuration.get(); + bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping; + Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.setOrientation(config.Display.Rotation); Display.enablePowerSafe = config.Display.PowerSafe; Display.enableScreensaver = config.Display.ScreenSaver; Display.setContrast(config.Display.Contrast); - Display.setLanguage(config.Display.Language); + Display.setLocale(config.Display.Locale); Display.Diagram().updatePeriod(); WebApi.writeConfig(retMsg); @@ -153,6 +168,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { - Utils::restartDtu(); + RestartHelper.triggerRestart(); } } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 9b67ec39f..5efa1a90f 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -27,7 +27,7 @@ void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiDtuClass::applyDataTaskCb() { // Execute stuff in main thread to avoid busy SPI bus - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel); Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial); @@ -49,9 +49,9 @@ 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))); + snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32, + static_cast((config.Dtu.Serial >> 32) & 0xFFFFFFFF), + static_cast(config.Dtu.Serial & 0xFFFFFFFF)); root["serial"] = buffer; root["pollinterval"] = config.Dtu.PollInterval; root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized(); @@ -90,12 +90,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("pollinterval") - && root.containsKey("nrf_palevel") - && root.containsKey("cmt_palevel") - && root.containsKey("cmt_frequency") - && root.containsKey("cmt_country"))) { + if (!(root["serial"].is() + && root["pollinterval"].is() + && root["nrf_palevel"].is() + && root["cmt_palevel"].is() + && root["cmt_frequency"].is() + && root["cmt_country"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -153,14 +153,16 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - CONFIG_T& config = Configuration.get(); - - config.Dtu.Serial = serial; - config.Dtu.PollInterval = root["pollinterval"].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(); - config.Dtu.Cmt.CountryMode = root["cmt_country"].as(); + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + config.Dtu.Serial = serial; + config.Dtu.PollInterval = root["pollinterval"].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(); + config.Dtu.Cmt.CountryMode = root["cmt_country"].as(); + } WebApi.writeConfig(retMsg); diff --git a/src/WebApi_config.cpp b/src/WebApi_file.cpp similarity index 59% rename from src/WebApi_config.cpp rename to src/WebApi_file.cpp index 759b6b243..60c0bea02 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_file.cpp @@ -2,15 +2,16 @@ /* * Copyright (C) 2022-2024 Thomas Basler and others */ -#include "WebApi_config.h" +#include "WebApi_file.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" #include #include -void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler) +void WebApiFileClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; using std::placeholders::_2; @@ -19,15 +20,43 @@ void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - 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)); - server.on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1)); - server.on("/api/config/upload", HTTP_POST, - std::bind(&WebApiConfigClass::onConfigUploadFinish, this, _1), - std::bind(&WebApiConfigClass::onConfigUpload, this, _1, _2, _3, _4, _5, _6)); + server.on("/api/file/get", HTTP_GET, std::bind(&WebApiFileClass::onFileGet, this, _1)); + server.on("/api/file/delete", HTTP_POST, std::bind(&WebApiFileClass::onFileDelete, this, _1)); + server.on("/api/file/delete_all", HTTP_POST, std::bind(&WebApiFileClass::onFileDeleteAll, this, _1)); + server.on("/api/file/list", HTTP_GET, std::bind(&WebApiFileClass::onFileListGet, this, _1)); + server.on("/api/file/upload", HTTP_POST, + std::bind(&WebApiFileClass::onFileUploadFinish, this, _1), + std::bind(&WebApiFileClass::onFileUpload, this, _1, _2, _3, _4, _5, _6)); } -void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) +void WebApiFileClass::onFileListGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + auto data = root.to(); + + File rootfs = LittleFS.open("/"); + File file = rootfs.openNextFile(); + while (file) { + if (file.isDirectory()) { + continue; + } + JsonObject obj = data.add(); + obj["name"] = String(file.name()); + obj["size"] = file.size(); + + file = rootfs.openNextFile(); + } + file.close(); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiFileClass::onFileGet(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -47,7 +76,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) request->send(LittleFS, requestFile, String(), true); } -void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) +void WebApiFileClass::onFileDelete(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -61,73 +90,67 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("delete"))) { + if (!(root["file"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["delete"].as() == false) { - retMsg["message"] = "Not deleted anything!"; - retMsg["code"] = WebApiError::ConfigNotDeleted; - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + String name = "/" + root["file"].as(); + if (!LittleFS.exists(name)) { + request->send(404); return; } + LittleFS.remove(name); + retMsg["type"] = "success"; - retMsg["message"] = "Configuration resettet. Rebooting now..."; - retMsg["code"] = WebApiError::ConfigSuccess; + retMsg["message"] = "File deleted"; + retMsg["code"] = WebApiError::FileDeleteSuccess; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - - Utils::removeAllFiles(); - Utils::restartDtu(); } -void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) +void WebApiFileClass::onFileDeleteAll(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - auto data = root["configs"].to(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } - File rootfs = LittleFS.open("/"); - File file = rootfs.openNextFile(); - while (file) { - if (file.isDirectory()) { - continue; - } - JsonObject obj = data.add(); - obj["name"] = String(file.name()); + auto& retMsg = response->getRoot(); - file = rootfs.openNextFile(); + if (!(root["delete"].is())) { + retMsg["message"] = "Values are missing!"; + retMsg["code"] = WebApiError::GenericValueMissing; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; } - file.close(); - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); -} - -void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { + if (root["delete"].as() == false) { + retMsg["message"] = "Not deleted anything!"; + retMsg["code"] = WebApiError::FileNotDeleted; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - // the request handler is triggered after the upload has finished... - // create the response, add header, and send response + retMsg["type"] = "success"; + retMsg["message"] = "Configuration resettet. Rebooting now..."; + retMsg["code"] = WebApiError::FileSuccess; - AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", "OK"); - response->addHeader("Connection", "close"); - response->addHeader("Access-Control-Allow-Origin", "*"); - request->send(response); - Utils::restartDtu(); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + Utils::removeAllFiles(); + RestartHelper.triggerRestart(); } -void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) +void WebApiFileClass::onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { if (!WebApi.checkCredentials(request)) { return; @@ -153,3 +176,19 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi request->_tempFile.close(); } } + +void WebApiFileClass::onFileUploadFinish(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + // the request handler is triggered after the upload has finished... + // create the response, add header, and send response + + AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", "OK"); + response->addHeader("Connection", "close"); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + RestartHelper.triggerRestart(); +} diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 9491f935d..b179eb106 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_firmware.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Update.h" #include "Utils.h" #include "WebApi.h" @@ -37,7 +38,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_i18n.cpp b/src/WebApi_i18n.cpp new file mode 100644 index 000000000..6a43072fa --- /dev/null +++ b/src/WebApi_i18n.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "WebApi_i18n.h" +#include "I18n.h" +#include "Utils.h" +#include "WebApi.h" +#include +#include + +void WebApiI18nClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + server.on("/api/i18n/languages", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguages, this, _1)); + server.on("/api/i18n/language", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguage, this, _1)); +} + +void WebApiI18nClass::onI18nLanguages(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(true); + auto& root = response->getRoot(); + const auto& languages = I18n.getAvailableLanguages(); + + for (auto& language : languages) { + auto jsonLang = root.add(); + + jsonLang["code"] = language.code; + jsonLang["name"] = language.name; + } + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiI18nClass::onI18nLanguage(AsyncWebServerRequest* request) +{ + if (request->hasParam("code")) { + String code = request->getParam("code")->value(); + + String filename = I18n.getFilenameByLocale(code); + + if (filename != "") { + String md5 = Utils::generateMd5FromFile(filename); + + String expectedEtag; + expectedEtag = "\""; + expectedEtag += md5; + expectedEtag += "\""; + + bool eTagMatch = false; + if (request->hasHeader("If-None-Match")) { + const AsyncWebHeader* h = request->getHeader("If-None-Match"); + eTagMatch = h->value().equals(expectedEtag); + } + + // begin response 200 or 304 + AsyncWebServerResponse* response; + if (eTagMatch) { + response = request->beginResponse(304); + } else { + response = request->beginResponse(LittleFS, filename, asyncsrv::T_application_json); + } + + // HTTP requires cache headers in 200 and 304 to be identical + response->addHeader("Cache-Control", "public, must-revalidate"); + response->addHeader("ETag", expectedEtag); + + request->send(response); + return; + } + } + + request->send(404); + return; +} diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 5a8585f70..a67e65416 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -21,6 +21,7 @@ void WebApiInverterClass::init(AsyncWebServer& server, Scheduler& scheduler) server.on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1)); server.on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1)); server.on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1)); + server.on("/api/inverter/stats_reset", HTTP_GET, std::bind(&WebApiInverterClass::onInverterStatReset, this, _1)); } void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) @@ -44,9 +45,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0x%08x", - ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); + snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32, + static_cast((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF), + static_cast(config.Inverter[i].Serial & 0xFFFFFFFF)); obj["serial"] = buffer; obj["poll_enable"] = config.Inverter[i].Poll_Enable; obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; @@ -95,8 +96,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("name"))) { + if (!(root["serial"].is() + && root["name"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -165,7 +166,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { + if (!(root["id"].is() + && root["serial"].is() + && root["name"].is() + && root["channel"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -180,9 +184,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } // Interpret the string as a hex value and convert it to uint64_t - const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + const uint64_t new_serial = strtoll(root["serial"].as().c_str(), NULL, 16); - if (serial == 0) { + if (new_serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -205,37 +209,42 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as()]; - - uint64_t new_serial = serial; - uint64_t old_serial = inverter.Serial; - - // Interpret the string as a hex value and convert it to uint64_t - inverter.Serial = new_serial; - strncpy(inverter.Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); - - inverter.Poll_Enable = root["poll_enable"] | true; - inverter.Poll_Enable_Night = root["poll_enable_night"] | true; - inverter.Command_Enable = root["command_enable"] | true; - inverter.Command_Enable_Night = root["command_enable_night"] | true; - inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; - inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; - inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; - inverter.ClearEventlogOnMidnight = root["clear_eventlog"] | false; - inverter.YieldDayCorrection = root["yieldday_correction"] | false; - - uint8_t arrayCount = 0; - for (JsonVariant channel : channelArray) { - inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as(); - inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as(); - strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name)); - arrayCount++; + uint64_t old_serial = 0; + + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + INVERTER_CONFIG_T& inverter = config.Inverter[root["id"].as()]; + + old_serial = inverter.Serial; + inverter.Serial = new_serial; + strncpy(inverter.Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); + + inverter.Poll_Enable = root["poll_enable"] | true; + inverter.Poll_Enable_Night = root["poll_enable_night"] | true; + inverter.Command_Enable = root["command_enable"] | true; + inverter.Command_Enable_Night = root["command_enable_night"] | true; + inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; + inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; + inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; + inverter.ClearEventlogOnMidnight = root["clear_eventlog"] | false; + inverter.YieldDayCorrection = root["yieldday_correction"] | false; + + uint8_t arrayCount = 0; + for (JsonVariant channel : channelArray) { + inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as(); + inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as(); + strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name)); + arrayCount++; + } } WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[root["id"].as()]; std::shared_ptr inv = Hoymiles.getInverterBySerial(old_serial); if (inv != nullptr && new_serial != old_serial) { @@ -281,7 +290,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id"))) { + if (!(root["id"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -296,7 +305,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } uint8_t inverter_id = root["id"].as(); - INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; + INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[inverter_id]; Hoymiles.removeInverterBySerial(inverter.Serial); @@ -323,7 +332,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("order"))) { + if (!(root["order"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -333,16 +342,42 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) // The order array contains list or id in the right order JsonArray orderArray = root["order"].as(); uint8_t order = 0; - for (JsonVariant id : orderArray) { - uint8_t inverter_id = id.as(); - if (inverter_id < INV_MAX_COUNT) { - INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; - inverter.Order = order; + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + for (JsonVariant id : orderArray) { + uint8_t inverter_id = id.as(); + if (inverter_id < INV_MAX_COUNT) { + INVERTER_CONFIG_T& inverter = config.Inverter[inverter_id]; + inverter.Order = order; + } + order++; } - order++; } WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } + +void WebApiInverterClass::onInverterStatReset(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto retMsg = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv != nullptr) { + inv->resetRadioStats(); + retMsg["type"] = "success"; + retMsg["message"] = "Stats resetted"; + retMsg["code"] = WebApiError::InverterStatsResetted; + } + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 6a6c90ca4..e3f53ae05 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -64,9 +64,9 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("limit_value") - && root.containsKey("limit_type"))) { + if (!(root["serial"].is() + && root["limit_value"].is() + && root["limit_type"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 1504f9d75..1835138f5 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -4,7 +4,7 @@ */ #include "WebApi_maintenance.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include @@ -30,7 +30,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("reboot"))) { + if (!(root["reboot"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -43,7 +43,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MaintenanceRebootTriggered; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index af40643ee..7558110e3 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -107,29 +107,29 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("mqtt_enabled") - && root.containsKey("mqtt_hostname") - && root.containsKey("mqtt_port") - && root.containsKey("mqtt_clientid") - && root.containsKey("mqtt_username") - && root.containsKey("mqtt_password") - && root.containsKey("mqtt_topic") - && root.containsKey("mqtt_retain") - && root.containsKey("mqtt_tls") - && root.containsKey("mqtt_tls_cert_login") - && root.containsKey("mqtt_client_cert") - && root.containsKey("mqtt_client_key") - && root.containsKey("mqtt_lwt_topic") - && root.containsKey("mqtt_lwt_online") - && root.containsKey("mqtt_lwt_offline") - && root.containsKey("mqtt_lwt_qos") - && root.containsKey("mqtt_publish_interval") - && root.containsKey("mqtt_clean_session") - && root.containsKey("mqtt_hass_enabled") - && root.containsKey("mqtt_hass_expire") - && root.containsKey("mqtt_hass_retain") - && root.containsKey("mqtt_hass_topic") - && root.containsKey("mqtt_hass_individualpanels"))) { + if (!(root["mqtt_enabled"].is() + && root["mqtt_hostname"].is() + && root["mqtt_port"].is() + && root["mqtt_clientid"].is() + && root["mqtt_username"].is() + && root["mqtt_password"].is() + && root["mqtt_topic"].is() + && root["mqtt_retain"].is() + && root["mqtt_tls"].is() + && root["mqtt_tls_cert_login"].is() + && root["mqtt_client_cert"].is() + && root["mqtt_client_key"].is() + && root["mqtt_lwt_topic"].is() + && root["mqtt_lwt_online"].is() + && root["mqtt_lwt_offline"].is() + && root["mqtt_lwt_qos"].is() + && root["mqtt_publish_interval"].is() + && root["mqtt_clean_session"].is() + && root["mqtt_hass_enabled"].is() + && root["mqtt_hass_expire"].is() + && root["mqtt_hass_retain"].is() + && root["mqtt_hass_topic"].is() + && root["mqtt_hass_individualpanels"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -271,36 +271,40 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) } } - CONFIG_T& config = Configuration.get(); - config.Mqtt.Enabled = root["mqtt_enabled"].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.ClientId, root["mqtt_clientid"].as().c_str(), sizeof(config.Mqtt.ClientId)); - 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.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)); - - // Check if base topic was changed - if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) { - MqttHandleInverter.unsubscribeTopics(); - strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); - MqttHandleInverter.subscribeTopics(); + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + config.Mqtt.Enabled = root["mqtt_enabled"].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.ClientId, root["mqtt_clientid"].as().c_str(), sizeof(config.Mqtt.ClientId)); + 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.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)); + + // Check if base topic was changed + if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) { + MqttHandleInverter.unsubscribeTopics(); + strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); + MqttHandleInverter.subscribeTopics(); + } } WebApi.writeConfig(retMsg); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 7fec44b2a..51db32e43 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -88,16 +88,16 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("ssid") - && root.containsKey("password") - && root.containsKey("hostname") - && root.containsKey("dhcp") - && root.containsKey("ipaddress") - && root.containsKey("netmask") - && root.containsKey("gateway") - && root.containsKey("dns1") - && root.containsKey("dns2") - && root.containsKey("aptimeout"))) { + if (!(root["ssid"].is() + && root["password"].is() + && root["hostname"].is() + && root["dhcp"].is() + && root["ipaddress"].is() + && root["netmask"].is() + && root["gateway"].is() + && root["dns1"].is() + && root["dns2"].is() + && root["aptimeout"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -164,37 +164,41 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) return; } - 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)); - if (root["dhcp"].as()) { - config.WiFi.Dhcp = true; - } else { - config.WiFi.Dhcp = false; + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + 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; + } else { + 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(); WebApi.writeConfig(retMsg); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index d50e0f02f..f58a2bbd6 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -100,11 +100,11 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("ntp_server") - && root.containsKey("ntp_timezone") - && root.containsKey("longitude") - && root.containsKey("latitude") - && root.containsKey("sunsettype"))) { + if (!(root["ntp_server"].is() + && root["ntp_timezone"].is() + && root["longitude"].is() + && root["latitude"].is() + && root["sunsettype"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -135,13 +135,17 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) return; } - 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(); + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + 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(); + } WebApi.writeConfig(retMsg); @@ -193,12 +197,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("year") - && root.containsKey("month") - && root.containsKey("day") - && root.containsKey("hour") - && root.containsKey("minute") - && root.containsKey("second"))) { + if (!(root["year"].is() + && root["month"].is() + && root["day"].is() + && root["hour"].is() + && root["minute"].is() + && root["second"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index b2b2ce42e..83e7fac6e 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -57,9 +57,9 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && (root.containsKey("power") - || root.containsKey("restart")))) { + if (!(root["serial"].is() + && (root["power"].is() + || root["restart"].is()))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -84,8 +84,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - if (root.containsKey("power")) { - uint16_t power = root["power"].as(); + if (root["power"].is()) { + bool power = root["power"].as(); inv->sendPowerControlRequest(power); } else { if (root["restart"].as()) { diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index ad95aacbf..9a79466ad 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -42,23 +42,23 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_heap_size System memory size\n"); stream->print("# TYPE opendtu_heap_size gauge\n"); - stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); + stream->printf("opendtu_heap_size %" PRId32 "\n", ESP.getHeapSize()); stream->print("# HELP opendtu_free_heap_size System free memory\n"); stream->print("# TYPE opendtu_free_heap_size gauge\n"); - stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); + stream->printf("opendtu_free_heap_size %" PRId32 "\n", ESP.getFreeHeap()); stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n"); stream->print("# TYPE opendtu_biggest_heap_block gauge\n"); - stream->printf("opendtu_biggest_heap_block %zu\n", ESP.getMaxAllocHeap()); + stream->printf("opendtu_biggest_heap_block %" PRId32 "\n", ESP.getMaxAllocHeap()); stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n"); stream->print("# TYPE opendtu_heap_min_free gauge\n"); - stream->printf("opendtu_heap_min_free %zu\n", ESP.getMinFreeHeap()); + stream->printf("opendtu_heap_min_free %" PRId32 "\n", ESP.getMinFreeHeap()); stream->print("# HELP wifi_rssi WiFi RSSI\n"); stream->print("# TYPE wifi_rssi gauge\n"); - stream->printf("wifi_rssi %d\n", WiFi.RSSI()); + stream->printf("wifi_rssi %" PRId8 "\n", WiFi.RSSI()); stream->print("# HELP wifi_station WiFi Station info\n"); stream->print("# TYPE wifi_station gauge\n"); @@ -73,14 +73,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_last_update last update from inverter in s\n"); stream->print("# TYPE opendtu_last_update gauge\n"); } - stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", + stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %" PRId32 "\n", serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); if (i == 0) { stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n"); stream->print("# TYPE opendtu_inverter_limit_relative gauge\n"); } - stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", + stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n", serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0); if (inv->DevInfo()->getMaxPower() > 0) { @@ -88,7 +88,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n"); stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n"); } - stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", + stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n", serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0); } @@ -126,7 +126,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String& stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); } - stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", chanName, serial.c_str(), idx, @@ -150,7 +150,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_PanelInfo panel information\n"); stream->print("# TYPE opendtu_PanelInfo gauge\n"); } - stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", + stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", serial.c_str(), idx, inv->name(), @@ -161,7 +161,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_MaxPower panel maximum output power\n"); stream->print("# TYPE opendtu_MaxPower gauge\n"); } - stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %d\n", + stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\"} %d\n", serial.c_str(), idx, inv->name(), @@ -172,7 +172,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"); stream->print("# TYPE opendtu_YieldTotalOffset gauge\n"); } - stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", + stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%" PRId16 "\"} %f\n", serial.c_str(), idx, inv->name(), diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index eb0f27d20..8ebd6fb6a 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -48,8 +48,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!root.containsKey("password") - && root.containsKey("allow_readonly")) { + if (!root["password"].is() + && root["allow_readonly"].is()) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -64,13 +64,19 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) return; } - 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(); + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + strlcpy(config.Security.Password, root["password"].as().c_str(), sizeof(config.Security.Password)); + config.Security.AllowReadonly = root["allow_readonly"].as(); + } WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + WebApi.reload(); } void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index e7a174314..5054b4d56 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -53,6 +53,20 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["chipcores"] = ESP.getChipCores(); root["flashsize"] = ESP.getFlashChipSize(); + JsonArray taskDetails = root["task_details"].to(); + static std::array constexpr task_names = { + "IDLE0", "IDLE1", "wifi", "tiT", "loopTask", "async_tcp", "mqttclient", + "HUAWEI_CAN_0", "PM:SDM", "PM:HTTP+JSON", "PM:SML", "PM:HTTP+SML" + }; + for (char const* task_name : task_names) { + TaskHandle_t const handle = xTaskGetHandle(task_name); + if (!handle) { continue; } + JsonObject task = taskDetails.add(); + task["name"] = task_name; + task["stack_watermark"] = uxTaskGetStackHighWaterMark(handle); + task["priority"] = uxTaskPriorityGet(handle); + } + String reason; reason = ResetReason::get_reset_reason_verbose(0); root["resetreason_0"] = reason; diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp index 1f1efcb20..51035f6fa 100644 --- a/src/WebApi_ws_console.cpp +++ b/src/WebApi_ws_console.cpp @@ -21,16 +21,30 @@ void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler) scheduler.addTask(_wsCleanupTask); _wsCleanupTask.enable(); + + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("console websocket"); + + reload(); +} + +void WebApiWsConsoleClass::reload() +{ + _ws.removeMiddleware(&_simpleDigestAuth); + + auto const& config = Configuration.get(); + + if (config.Security.AllowReadonly) { return; } + + _ws.enable(false); + _simpleDigestAuth.setPassword(config.Security.Password); + _ws.addMiddleware(&_simpleDigestAuth); + _ws.closeAll(); + _ws.enable(true); } void WebApiWsConsoleClass::wsCleanupTaskCb() { // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients _ws.cleanupClients(); - - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index eac4e144c..220f5dde5 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -37,18 +37,31 @@ void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) scheduler.addTask(_sendDataTask); _sendDataTask.enable(); + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("live websocket"); + + reload(); +} + +void WebApiWsLiveClass::reload() +{ + _ws.removeMiddleware(&_simpleDigestAuth); + + auto const& config = Configuration.get(); + + if (config.Security.AllowReadonly) { return; } + + _ws.enable(false); + _simpleDigestAuth.setPassword(config.Security.Password); + _ws.addMiddleware(&_simpleDigestAuth); + _ws.closeAll(); + _ws.enable(true); } void WebApiWsLiveClass::wsCleanupTaskCb() { // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients _ws.cleanupClients(); - - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } } void WebApiWsLiveClass::sendDataTaskCb() @@ -138,6 +151,13 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std } else { root["limit_absolute"] = -1; } + root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData; + root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment; + root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess; + root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer; + root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer; + root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData; + root["radio_stats"]["rssi"] = inv->getLastRssi(); } void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) diff --git a/src/main.cpp b/src/main.cpp index dabdc7313..7b09e7a41 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "Configuration.h" #include "Datastore.h" #include "Display_Graphic.h" +#include "I18n.h" #include "InverterSettings.h" #include "JsyMk.h" #include "Led_Single.h" @@ -18,6 +19,7 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "RestartHelper.h" #include "Scheduler.h" #include "SunPosition.h" #include "Utils.h" @@ -25,6 +27,7 @@ #include "defaults.h" #include #include +#include #include #include @@ -33,12 +36,16 @@ void setup() // Move all dynamic allocations >512byte to psram (if available) heap_caps_malloc_extmem_enable(512); + // Initialize SpiManager + SpiManagerInst.register_bus(SPI2_HOST); +#if SOC_SPI_PERIPH_NUM > 2 + SpiManagerInst.register_bus(SPI3_HOST); +#endif + // Initialize serial output Serial.begin(SERIAL_BAUDRATE); -#if ARDUINO_USB_CDC_ON_BOOT - Serial.setTxTimeoutMs(0); - delay(100); -#else +#if !ARDUINO_USB_CDC_ON_BOOT + // Only wait for serial interface to be set up when not using CDC while (!Serial) yield(); #endif @@ -60,10 +67,9 @@ void setup() } // Read configuration values + Configuration.init(scheduler); MessageOutput.print("Reading configuration... "); if (!Configuration.read()) { - MessageOutput.print("initializing... "); - Configuration.init(); if (Configuration.write()) { MessageOutput.print("written... "); } else { @@ -77,6 +83,11 @@ void setup() auto& config = Configuration.get(); MessageOutput.println("done"); + // Read languate pack + MessageOutput.print("Reading language pack... "); + I18n.init(scheduler); + MessageOutput.println("done"); + // Load PinMapping MessageOutput.print("Reading PinMapping... "); if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { @@ -132,7 +143,7 @@ void setup() Display.enablePowerSafe = config.Display.PowerSafe; Display.enableScreensaver = config.Display.ScreenSaver; Display.setContrast(config.Display.Contrast); - Display.setLanguage(config.Display.Language); + Display.setLocale(config.Display.Locale); Display.setStartupDisplay(); MessageOutput.println("done"); @@ -146,22 +157,10 @@ void setup() JsyMk.init(scheduler); MessageOutput.println("done"); - // Check for default DTU serial - MessageOutput.print("Check for default DTU serial... "); - if (config.Dtu.Serial == DTU_SERIAL) { - MessageOutput.print("generate serial based on ESP chip id: "); - const uint64_t dtuId = Utils::generateDtuSerial(); - MessageOutput.printf("%0x%08x... ", - ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), - ((uint32_t)(dtuId & 0xFFFFFFFF))); - config.Dtu.Serial = dtuId; - Configuration.write(); - } - MessageOutput.println("done"); - InverterSettings.init(scheduler); Datastore.init(scheduler); + RestartHelper.init(scheduler); } void loop() diff --git a/webapp/env.d.ts b/webapp/env.d.ts index 038f29277..c264efc8d 100644 --- a/webapp/env.d.ts +++ b/webapp/env.d.ts @@ -1,7 +1,7 @@ /// import { Router, Route } from 'vue-router' -declare module '@vue/runtime-core' { +declare module 'vue' { interface ComponentCustomProperties { $router: Router $route: Route diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index 9a2aaecb7..94f27454c 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -1,22 +1,12 @@ /* eslint-env node */ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { FlatCompat } from "@eslint/eslintrc"; import js from "@eslint/js"; import pluginVue from 'eslint-plugin-vue' - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, -}); +import vueTsEslintConfig from '@vue/eslint-config-typescript' export default [ js.configs.recommended, ...pluginVue.configs['flat/essential'], - ...compat.extends("@vue/eslint-config-typescript/recommended"), + ...vueTsEslintConfig(), { files: [ "**/*.vue", diff --git a/webapp/package.json b/webapp/package.json index a427ce2c7..a646f6c0f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,34 +17,35 @@ "bootstrap": "^5.3.3", "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", - "sortablejs": "^1.15.2", + "sortablejs": "^1.15.3", "spark-md5": "^3.0.2", - "vue": "^3.4.35", - "vue-i18n": "^9.13.1", - "vue-router": "^4.4.2" + "vue": "^3.5.12", + "vue-i18n": "10.0.4", + "vue-router": "^4.4.5" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^4.0.0", - "@tsconfig/node18": "^18.2.4", + "@intlify/unplugin-vue-i18n": "^5.2.0", + "@tsconfig/node22": "^22.0.0", "@types/bootstrap": "^5.2.10", - "@types/node": "^22.1.0", + "@types/node": "^22.9.0", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", - "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^5.1.2", - "@vue/eslint-config-typescript": "^13.0.0", + "@types/spark-md5": "^3.0.5", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/eslint-config-typescript": "^14.1.3", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.8.0", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^9.14.0", + "eslint-plugin-vue": "^9.30.0", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "pulltorefreshjs": "^0.1.22", - "sass": "^1.77.6", - "terser": "^5.31.3", - "typescript": "^5.5.4", - "vite": "^5.3.5", + "sass": "=1.77.6", + "terser": "^5.36.0", + "typescript": "^5.6.3", + "vite": "^5.4.10", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.5.1", - "vue-tsc": "^2.0.29" - } + "vite-plugin-css-injected-by-js": "^3.5.2", + "vue-tsc": "^2.1.10" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 200ce6695..8c5f2f98a 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -18,7 +18,7 @@ export default defineComponent({ diff --git a/webapp/src/components/CardElement.vue b/webapp/src/components/CardElement.vue index 0b067953d..b7d7322cd 100644 --- a/webapp/src/components/CardElement.vue +++ b/webapp/src/components/CardElement.vue @@ -1,5 +1,5 @@