diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..559f9bfd2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# http://editorconfig.org +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 112e2b22e..544684f46 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -5,11 +5,18 @@ body: - type: markdown attributes: value: > - ### ✋ **This is bug tracker, not a support forum** + ### ⚠️ Please remember: issues are for *bugs* + That is, something you believe affects every single user of OpenDTU, not just you. If you're not sure, start with one of the other options below. + - type: markdown + attributes: + value: | + #### Have a question? 👉 [Start a new discussion](https://github.com/tbnobody/OpenDTU/discussions/new) or [ask in chat](https://discord.gg/WzhxEY62mB). - If something isn't working right, you have questions or need help, [**get in touch on the Discussions**](https://github.com/tbnobody/OpenDTU/discussions). + #### Before opening an issue, please double check: - Please quickly search existing issues first before submitting a bug. + - [Documentation](https://www.opendtu.solar). + - [The FAQs](https://www.opendtu.solar/firmware/faq/). + - [Existing issues and discussions](https://github.com/tbnobody/OpenDTU/search?q=&type=issues). - type: textarea id: what-happened attributes: @@ -65,4 +72,15 @@ body: Links? References? Anything that will give us more context about the issue you are encountering! Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: - required: false \ No newline at end of file + required: false + - type: checkboxes + id: required-checks + attributes: + label: Please confirm the following + options: + - label: I believe this issue is a bug that affects all users of OpenDTU, not something specific to my installation. + required: true + - label: I have already searched for relevant existing issues and discussions before opening this report. + required: true + - label: I have updated the title field above with a concise description. + required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bb23363f..32bbad64e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,14 +18,14 @@ jobs: - uses: actions/checkout@v4 - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" @@ -56,7 +56,7 @@ jobs: run: git fetch --force --tags origin - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} @@ -64,13 +64,13 @@ jobs: ${{ runner.os }}-pip- - name: Cache PlatformIO - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" @@ -101,7 +101,7 @@ jobs: - name: Rename Factory Firmware run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: opendtu-${{ matrix.environment }} path: | @@ -119,7 +119,7 @@ jobs: - name: Build Changelog id: github_release - uses: mikepenz/release-changelog-builder-action@v3 + uses: mikepenz/release-changelog-builder-action@v4 with: failOnError: true commitMode: true @@ -127,7 +127,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: artifacts/ @@ -138,7 +138,7 @@ jobs: for i in */; do cp ${i}opendtu-*.bin ./; done - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body: ${{steps.github_release.outputs.changelog}} draft: False diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index af5e8b79f..4ee4b4a82 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install dependencies diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml new file mode 100644 index 000000000..f7290c2e5 --- /dev/null +++ b/.github/workflows/repo-maintenance.yml @@ -0,0 +1,54 @@ +name: 'Repository Maintenance' + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + discussions: write + +concurrency: + group: lock + +jobs: + stale: + name: 'Stale' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 14 + days-before-close: 60 + any-of-labels: 'cant-reproduce,not a bug' + stale-issue-label: stale + stale-pr-label: stale + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + + lock-threads: + name: 'Lock Old Threads' + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + issue-inactive-days: '30' + pr-inactive-days: '30' + discussion-inactive-days: '30' + log-output: true + issue-comment: > + This issue has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + pr-comment: > + This pull request has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + discussion-comment: > + This discussion has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion for related concerns. diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d14c6bfee..d18910d31 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,8 +3,8 @@ // for the documentation about the extensions.json format "recommendations": [ "DavidAnson.vscode-markdownlint", + "EditorConfig.EditorConfig", "Vue.volar", - "Vue.vscode-typescript-vue-plugin", "platformio.platformio-ide" ], "unwantedRecommendations": [ diff --git a/README.md b/README.md index d27d23f45..cb9f4ef48 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,28 @@ It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with th ## Documentation -Currently the documentation is separated into different locations (this README.md, the `doc` folder and the [Wiki](https://github.com/tbnobody/OpenDTU/wiki)). -This is not very nice and it's planned to move everything into the [Wiki](https://github.com/tbnobody/OpenDTU/wiki). +The documentation can be found [here](https://tbnobody.github.io/OpenDTU-docs/). +Please feel free to support and create a PR in [this](https://github.com/tbnobody/OpenDTU-docs) repository to make the documentation even better. -## Screenshots - -Several screenshots of the frontend can be found here: [Screenshots](docs/screenshots/README.md) +## Breaking changes -## Builds +Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING` -Different builds from existing installations can be found here [Builds](docs/builds/README.md) -Like to show your own build? Just send me a Pull Request. +```code +* 1b637f08 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics +* e1564780 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics +* f0b5542c 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics +* c27ecc36 2024-01-29 BREAKING CHANGE: Web API Endpoint /api/livedata/status +* 71d1b3b 2023-11-07 BREAKING CHANGE: Home Assistant Auto Discovery to new naming scheme +* 04f62e0 2023-04-20 BREAKING CHANGE: Web API Endpoint /api/eventlog/status no nested serial object +* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv= +* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device! +* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API! +* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API! +* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method +* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed +* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1 +``` ## Currently supported Inverters @@ -53,9 +64,9 @@ Like to show your own build? Just send me a Pull Request. | 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 | 4 | 3 | -| Hoymiles HMT-1800-4T | CMT2300A | 4 | 4 | 3 | -| Hoymiles HMT-2000-4T | CMT2300A | 4 | 4 | 3 | +| 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 | @@ -64,282 +75,6 @@ Like to show your own build? Just send me a Pull Request. | TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | | TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | | TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | - -**TSUN compatibility remark:** -Compatibility with OpenDTU is most likly related to the serial number of the inverter. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not. - -## Features for end users - -* Read live data from inverter -* Show inverters internal event log -* Show inverter information like firmware version, firmware build date, hardware revision and hardware version -* Show and set the current inverter limit -* Function to turn the inverter off and on -* Uses ESP32 microcontroller and NRF24L01+ -* Multi-Inverter support -* MQTT support (with TLS) -* Home Assistant MQTT Auto Discovery support -* Nice and fancy WebApp with visualization of current data -* Firmware upgrade using the web UI -* Default source supports up to 10 inverters -* Time zone support -* Ethernet support -* Prometheus API endpoint (/api/prometheus/metrics) -* English, german and french web interface -* Displays (SSD1306, SH1106, PCD8544) -* Status LEDs -* Configuration management (export / import configurations) -* Dark Theme - -## Features for developers - -* The microcontroller part - * Build with Arduino PlatformIO Framework for the ESP32 - * Uses a fork of [ESPAsyncWebserver](https://github.com/yubox-node-org/ESPAsyncWebServer) and [espMqttClient](https://github.com/bertmelis/espMqttClient) - -* The WebApp part - * Build with [Vue.js](https://vuejs.org) - * Source is written in TypeScript - -## Breaking changes - -Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING` - -```code -* 71d1b3b 2023-11-07 BREAKING CHANGE: Home Assistant Auto Discovery to new naming scheme -* 04f62e0 2023-04-20 BREAKING CHANGE: Web API Endpoint /api/eventlog/status no nested serial object -* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv= -* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device! -* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API! -* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API! -* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method -* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed -* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1 -``` - -## Hardware you need - -### ESP32 board - -For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it. - -Sample Picture: - -![NodeMCU-ESP32](docs/nodemcu-esp32.png) - -Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware) - -### NRF24L01+ radio board (See inverter table above for supported inverters) - -The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna. - -Sample picture: - -![nrf24l01plus](docs/nrf24l01plus.png) - -Buy your hardware from a trusted source, at best from a dealer/online shop in your country where you have support and the right to return non-functional hardware. -When you want to buy from Amazon, AliExpress, eBay etc., take note that there is a lot of low-quality or fake hardware offered. Read customer comments and ratings carefully! - -A heavily incomplete list of trusted hardware shops in germany is: - -* [AZ-Delivery](https://www.az-delivery.de/) -* [Makershop](https://www.makershop.de/) -* [Berrybase](https://www.berrybase.de/) - -This list is for your convenience only, the project is not related to any of these shops. - -### CMT2300A radio board (See inverter table above for supported inverters) - -It is important to get a module which supports SPI communicatiton. The following modules are currently supported: - -* EBYTE E49-900M20S - -The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future. - -### Power supply - -Use a power suppy with 5 V and 1 A. The USB cable connected to your PC/Notebook may be powerful enough or may be not. - -## Wiring up the NRF24L01+ module - -### Schematic - -![Schematic](docs/Wiring_ESP32_Schematic.png) - -### Symbolic view - -![Symbolic](docs/Wiring_ESP32_Symbol.png) - -### Change pin assignment - -Its possible to change all the pins of the NRF24L01+ module, the Display, the LED etc. -The recommend way to change the pin assignment is by creating a custom [device profile](docs/DeviceProfiles.md). -It is also possible to create a custom environment and compile the source yourself. This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter: - -```makefile --DHOYMILES_PIN_MISO=19 --DHOYMILES_PIN_MOSI=23 --DHOYMILES_PIN_SCLK=18 --DHOYMILES_PIN_IRQ=16 --DHOYMILES_PIN_CE=4 --DHOYMILES_PIN_CS=5 -``` - -It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy. - -## Flashing and starting up - -### with Visual Studio Code - -* Install [Visual Studio Code](https://code.visualstudio.com/download) (from now named "vscode") -* In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) -* Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/) -* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory. -* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" and "platformio_override.ini" file) -* Adjust the COM port in the file "platformio_override.ini" for your USB-to-serial-converter. It occurs twice: - * upload_port - * monitor_port -* Select the arrow button in the blue bottom status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically. -* Under Linux, if the upload fails with error messages "Could not open /dev/ttyUSB0, the port doesn't exist", you can check via ```ls -la /dev/tty*``` to which group your port belongs to, and then add your user this group via ```sudo adduser dialout``` (if you are using ```arch-linux``` use: ```sudo gpasswd -a uucp```, this method requires a logout/login of the affected user). -* There are two videos showing these steps: - * [Git Clone and compilation](https://youtu.be/9cA_esv3zeA) - * [Full installation and compilation](https://youtu.be/xs6TqHn7QWM) - -### on the commandline with PlatformIO Core - -* Install [PlatformIO Core](https://platformio.org/install/cli) -* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur) -* Adjust the COM port in the file "platformio_override.ini". It occurs twice: - * upload_port - * monitor_port -* build: `platformio run -e generic` -* upload to esp module: `platformio run -e generic -t upload` -* other options: - * clean the sources: `platformio run -e generic -t clean` - * erase flash: `platformio run -e generic -t erase` - -### using the pre-compiled .bin files - -The pre-compiled binary files can be found here on the [github page behind "Releases"](https://github.com/tbnobody/OpenDTU/releases) (look at the right column). For a first installation on an ESP32, download `opendtu-generic.factory.bin` and use a ESP32 flash tool of your choice to flash the `.bin` file to the address `0x0`. (The previous method with different .bin files is no more necessary.) - -For further updates download `opendtu-generic.bin` and use the over-the-air firmware update in OpenDTU's web interface. - -#### Flash with esptool.py (Linux) - -```bash -esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset \ - write_flash --flash_mode dout --flash_freq 40m --flash_size detect \ - 0x0 opendtu-generic.factory.bin -``` - -#### Flash with Espressif Flash Download Tool (Windows) - -[Download link](https://www.espressif.com/en/support/download/other-tools) - -* On startup, select Chip Type -> "ESP32" / WorkMode -> "Develop" -* Prepare all settings (see picture). Make sure to uncheck the `DoNotChgBin` option. Otherwise you may get errors like "invalid header". -* ![flash tool image](docs/esp32_flash_download_tool.png) -* Press "Erase" button on screen. Look into the terminal window, you should see dots appear. Then press the "Boot" button on the ESP32 board. Wait for "FINISH" to see if flashing/erasing is done. -* To program, press "Start" on screen, then the "Boot" button. -* When flashing is complete (FINISH appears) then press the Reset button on the ESP32 board (or powercycle ) to start the OpenDTU application. - -#### Flash with ESP_Flasher (Windows) - -Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/releases/) is suitable for flashing OpenDTU on Windows. - -#### Flash with [ESP_Flasher](https://espressif.github.io/esptool-js/) - web version - -It is also possible to flash it via the web tools which might be more convenient and is platform independent. - -## First configuration - -* After the initial flashing of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42". -* Use a web browser to open the address [http://192.168.4.1](http://192.168.4.1) -* Navigate to Settings --> Network Settings and enter your WiFi credentials. The username to access the config menu is "admin" and the password the same as for accessing the Access Point (default: "openDTU42"). -* OpenDTU then simultaneously connects to your WiFi AP with these credentials. Navigate to Info --> Network and look into section "Network Interface (Station)" for the IP address received via DHCP. -* If your WiFi AP uses an allow-list for MAC-addresses, please be aware that the ESP32 has two different MAC addresses for its AP and client modes, they are also listed at Info --> Network. -* When OpenDTU is connected to a configured WiFI AP, the "OpenDTU-*" Access Point is closed after 3 minutes. -* OpenDTU needs access to a working NTP server to get the current date & time. Both are sent to the inverter with each request. Default NTP server is pool.ntp.org. If your network has different requirements please change accordingly (Settings --> NTP Settings). -* Add your inverter in the inverter settings (Settings --> Inverter Settings) - -## Flashing an Update using "Over The Air" OTA Update - -Once you have your OpenDTU running and connected to WLAN, you can do further updates through the web interface. -Navigate to Settings --> Firmware upgrade and press the browse button. Select the firmware file from your local computer. - -You'll find the firmware file (after a successful build process) under `.pio/build/generic/firmware.bin`. - -If you downloaded a precompiled zip archive, unpack it and choose `opendtu-generic.bin`. - -After the successful upload, the OpenDTU immediately restarts into the new firmware. - -## MQTT Topic Documentation - -A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md) - -## Web API Documentation - -A documentation of the Web API can be found here: [Web-API Documentation](docs/Web-API.md) - -## OpenDTU Breakoutboard - -We sat down together and designed a PCB. This is 100% compatible with openDTU and has space for all extensions such as display and LEDs. You can find the PCB design here: - -A ready to solder kit can be found here: - -OpenDTU Breakout Board with CaseOpenDTU Breakout Board with Case - -## Available cases - -* -* -* -* -* -* -* - -## Available layouts for printed circuit boards - -* [BreakoutBoard - sample printed circuit board for OpenDTU and Ahoy](https://github.com/dokuhn/openDTU-BreakoutBoard) -* [Board for OpenDTU with Display](https://github.com/SteffMUC/openDTU_wDisplay2) -* [OpenDTU PCB mit Display](https://github.com/turrican944/OpenDTU-PCB) -* [PCB for OpenDTU in Cable Branchbox](https://github.com/plewka/ESP-Solar_OpenDTU) - -## Building - -* Building the WebApp - * The WebApp can be build using yarn - - ```bash - cd webapp - yarn install - yarn build - ``` - - * The updated output is placed in the 'webapp_dist' directory - * It is only necessary to build the webapp when you made changes to it -* Building the microcontroller firmware - * Visual Studio Code with the PlatformIO Extension is required for building - -## Troubleshooting - -* First: When there is no light on the solar panels, the inverter completely turns off and does not answer to OpenDTU! So if you assembled your OpenDTU in the evening, wait until tomorrow. -* When there is no data received from the inverter(s) - try to reduce the distance between the openDTU and the inverter (e.g. move it to the window towards the roof) -* Under Settings -> DTU Settings you can increase the transmit power "PA level". Default is "minimum". -* The NRF24L01+ needs relatively much current. With bad power supply (and especially bad cables!) a 10 µF capacitor soldered directly to the NRF24L01+ board connector brings more stability (pin 1+2 are the power supply). Note the polarity of the capacitor… -* You can try to use an USB power supply with 1 A or more instead of connecting the ESP32 to the computer. -* Try a different USB cable. Once again, a stable power source is important. Some USB cables are made of much plastic and very little copper inside. -* Double check that you have a radio module NRF24L01+ with a plus sign at the end. NRF24L01 module without the plus are not compatible with this project. -* There is no possibility of auto-discovering the inverters. Double check you have entered the serial numbers of the inverters correctly. -* OpenDTU needs access to a working NTP server to get the current date & time. -* If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information. -* Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/) -* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards. -* Make sure to connect one inverter only to one DTU (Original, Ahoy, OpenDTU doesn't make a difference). If you query a inverter by multiple DTUs you will get strange peaks in your values. - -## Related Projects - -* [Ahoy](https://github.com/grindylow/ahoy) -* [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) -* [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect) +| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 | +| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 | +| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 | diff --git a/docs/DeviceProfiles.md b/docs/DeviceProfiles.md index 5e4a708a0..fd19199e1 100644 --- a/docs/DeviceProfiles.md +++ b/docs/DeviceProfiles.md @@ -1,113 +1,3 @@ # Device Profiles -It is possible to change hardware settings like pin assignments or ethernet support using a json file. The json file can be uploaded using the configuration management in the web interface. Just select "Pin Mapping (pin_mapping.json)" in the recovery section. - -When the file is uploaded the ESP performs a reboot. This is required as the pin settings could have changed within the file. By default all the pin assignments are used as compiled into the firmware. - -To change the device profile, navigate to the "Device Manager" and selected the appropriate profile. You can see the current (Active) and the new (Selected) in assignment in the table below the combobox. - -## Structure of the json file - -```json -[ - { - "name": "Generic NodeMCU 38 pin", - "nrf24": { - "miso": 19, - "mosi": 23, - "clk": 18, - "irq": 16, - "en": 4, - "cs": 5 - }, - "eth": { - "enabled": false, - "phy_addr": -1, - "power": -1, - "mdc": -1, - "mdio": -1, - "type": -1, - "clk_mode": -1 - } - }, - { - "name": "Generic NodeMCU 38 pin with SSD1306", - "nrf24": { - "miso": 19, - "mosi": 23, - "clk": 18, - "irq": 16, - "en": 4, - "cs": 5 - }, - "eth": { - "enabled": false, - "phy_addr": -1, - "power": -1, - "mdc": -1, - "mdio": -1, - "type": -1, - "clk_mode": -1 - }, - "display": { - "type": 2, - "data": 21, - "clk": 22 - } - }, - { - "name": "Olimex ESP32-POE", - "nrf24": { - "miso": 15, - "mosi": 2, - "clk": 14, - "irq": 13, - "en": 16, - "cs": 5 - }, - "eth": { - "enabled": true, - "phy_addr": 0, - "power": 12, - "mdc": 23, - "mdio": 18, - "type": 0, - "clk_mode": 3 - } - } -] -``` - -The json file can contain multiple profiles. Each profile requires a name and different parameters. If one parameter is not set, the default value, as compiled into the firmware is used. The example above shows all the currently supported values. Others may follow. Sample files for some boards can be found [here](DeviceProfiles/). This means you can just flash the generic bin file and upload the json file. Then you select your board and everything works hopyfully as expected. - -## Implemented configuration values - -| Parameter | Data Type | Description | -| ------------- | --------- | ----------- | -| name | string | Unique name of the profile (max 63 characters) | -| nrf24.miso | number | MISO Pin | -| nrf24.mosi | number | MOSI Pin | -| nrf24.clk | number | Clock Pin | -| nrf24.irq | number | Interrupt Pin | -| nrf24.en | number | Enable Pin | -| nrf24.cs | number | Chip Select Pin | -| cmt.sdio | number | SDIO Pin | -| cmt.clk | number | CLK Pin | -| cmt.cs | number | CS Pin | -| cmt.fcs | number | FCS Pin | -| cmt.gpio2 | number | GPIO2 Pin (optional) | -| cmt.gpio3 | number | GPIO3 Pin (optional) | -| eth.enabled | boolean | Enable/Disable the ethernet stack | -| eth.phy_addr | number | Unique PHY addr | -| eth.power | number | Power Pin (if available). Use -1 for not assigned pins. | -| eth.mdc | number | Serial Management Interface MDC Pin. Use -1 for not assigned pins. | -| eth.mdio | number | Serial Management Interface MDIO Pin. Use -1 for not assigned pins. | -| eth.type | number | Possible values:
* 0 = ETH_PHY_LAN8720
* 1 = ETH_PHY_TLK110
* 2 = ETH_PHY_RTL8201
* 3 = ETH_PHY_DP83848
* 4 = ETH_PHY_DM9051
* 5 = ETH_PHY_KSZ8041
* 6 = ETH_PHY_KSZ8081 | -| eth.clk_mode | number | Possible values:
* 0 = ETH_CLOCK_GPIO0_IN
* 1 = ETH_CLOCK_GPIO0_OUT
* 2 = ETH_CLOCK_GPIO16_OUT
* 3 = ETH_CLOCK_GPIO17_OUT | -| display.type | number | Specify type of display. Possible values:
* 0 = None (default)
* 1 = PCD8544
* 2 = SSD1306
* 3 = SH1106 | -| display.data | number | Data Pin (e.g. SDA for i2c displays) required for all displays. Use 255 for not assigned pins. | -| display.clk | number | Clock Pin (e.g. SCL for i2c displays) required for SSD1306 and SH1106. Use 255 for not assigned pins. | -| display.cs | number | Chip Select Pin required for PCD8544. Use 255 for not assigned pins. | -| display.reset | number | Reset Pin required for PCD8544, optional for all other displays. Use 255 for not assigned pins. | -| led.led0 | number | LED pin for network indication. Blinking = WLAN connected but NTP & MQTT (if enabled) disconnected. On = WLAN, NTP, MQTT connected. Off = Network not connected | -| led.led1 | number | LED pin for inverter indication. On = All inverters reachable & producing. Blinking = All inverters reachable but not producing. Off = At least one inverter is not reachable. Only inverters with polling enabled are considered. | +This documentation will has been moved and can be found here: diff --git a/docs/DeviceProfiles/AhoyDTU-ESP32.json b/docs/DeviceProfiles/AhoyDTU-ESP32.json new file mode 100644 index 000000000..5de694463 --- /dev/null +++ b/docs/DeviceProfiles/AhoyDTU-ESP32.json @@ -0,0 +1,76 @@ +[ + { + "name": "AhoyDTU ESP32 Display LED", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + }, + "display": { + "type": 2, + "data": 21, + "clk": 22 + } + }, + { + "name": "AhoyDTU ESP32 Display", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "display": { + "type": 2, + "data": 21, + "clk": 22 + } + }, + { + "name": "AhoyDTU ESP32 LED", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + } + }, + { + "name": "AhoyDTU ESP32", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + } + } +] \ No newline at end of file diff --git a/docs/DeviceProfiles/CASmo-DTU.json b/docs/DeviceProfiles/CASmo-DTU.json new file mode 100644 index 000000000..0de52e1dc --- /dev/null +++ b/docs/DeviceProfiles/CASmo-DTU.json @@ -0,0 +1,20 @@ +[ + { + "name": "CASmo-DTU", + "links": [ + {"name": "Information", "url": "https://casmo.info/product-details/?product=2"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + } + } +] \ No newline at end of file diff --git a/docs/DeviceProfiles/blinkyparts_esp32.json b/docs/DeviceProfiles/blinkyparts_esp32.json index 0ee922bfe..617777f5c 100644 --- a/docs/DeviceProfiles/blinkyparts_esp32.json +++ b/docs/DeviceProfiles/blinkyparts_esp32.json @@ -1,6 +1,12 @@ [ { "name": "NRF, LEDs, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -21,6 +27,12 @@ }, { "name": "CMT, LEDs, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -49,6 +61,12 @@ }, { "name": "NRF, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -65,6 +83,12 @@ }, { "name": "CMT, Display", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -89,6 +113,12 @@ }, { "name": "NRF, LEDs", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -104,6 +134,12 @@ }, { "name": "CMT, LEDs", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, @@ -127,6 +163,12 @@ }, { "name": "NRF", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-NRF-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HM-Serie-NRF-Modul/blink237542"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": 19, "mosi": 23, @@ -138,6 +180,12 @@ }, { "name": "CMT", + "links": [ + {"name": "Information", "url": "https://shop.blinkyparts.com/de/OpenDTU-CMT-Deine-Auswertung-fuer-deine-Balkonsolaranlage-kompatibel-zu-Hoymiles-HMS-und-HMT-Serie-CMT-Modul/blink238342"}, + {"name": "Manual DE", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_de.pdf"}, + {"name": "Manual EN", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/manual/OpenDTU_Breakout_en.pdf"}, + {"name": "Schematic", "url": "https://binary-kitchen.github.io/SolderingTutorial/OpenDTU_Breakout/ibom.html"} + ], "nrf24": { "miso": -1, "mosi": -1, diff --git a/docs/DeviceProfiles/esp32_stick_poe_a.json b/docs/DeviceProfiles/esp32_stick_poe_a.json new file mode 100644 index 000000000..aca95d6b9 --- /dev/null +++ b/docs/DeviceProfiles/esp32_stick_poe_a.json @@ -0,0 +1,25 @@ +[ + { + "name": "Esp32-Stick-PoE-A", + "links": [ + {"name": "Information", "url": "https://github.com/allexoK/Esp32-Stick-Boards-Docs"} + ], + "nrf24": { + "miso": 2, + "mosi": 15, + "clk": 14, + "irq": 34, + "en": 12, + "cs": 4 + }, + "eth": { + "enabled": true, + "phy_addr": 1, + "power": -1, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + } + } +] diff --git a/docs/DeviceProfiles/liligo_t-eth-lite_poe.json b/docs/DeviceProfiles/liligo_t-eth-lite_poe.json new file mode 100644 index 000000000..be91f95ba --- /dev/null +++ b/docs/DeviceProfiles/liligo_t-eth-lite_poe.json @@ -0,0 +1,74 @@ +[ + { + "name": "LILYGO T-ETH-Lite-POE CMT", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "cmt": { + "clk": 15, + "cs": 32, + "fcs": 33, + "sdio": 4 + } + }, + { + "name": "LILYGO T-ETH-Lite-POE NRF24", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "nrf24": { + "miso": 34, + "mosi": 13, + "clk": 14, + "irq": 35, + "en": 4, + "cs": 2 + } + }, + { + "name": "LILYGO T-ETH-Lite-POE NRF24 + Display", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "nrf24": { + "miso": 34, + "mosi": 13, + "clk": 14, + "irq": 35, + "en": 4, + "cs": 2 + }, + "display": { + "type": 3, + "data": 32, + "clk": 33 + } + } +] diff --git a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json index 538f3000b..b5bb4ace4 100644 --- a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json +++ b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json @@ -1,6 +1,9 @@ [ { "name": "LILYGO TTGO T-Internet-POE", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 2, "mosi": 15, @@ -21,6 +24,9 @@ }, { "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 12, "mosi": 4, @@ -41,6 +47,9 @@ }, { "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder, SSD1306", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-internet-poe"} + ], "nrf24": { "miso": 12, "mosi": 4, diff --git a/docs/DeviceProfiles/nodemcu_esp32.json b/docs/DeviceProfiles/nodemcu_esp32.json index d7f6a6141..0587dd88c 100644 --- a/docs/DeviceProfiles/nodemcu_esp32.json +++ b/docs/DeviceProfiles/nodemcu_esp32.json @@ -73,6 +73,25 @@ "clk": 22 } }, + { + "name": "NRF24 with SSD1309", + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "eth": { + "enabled": false + }, + "display": { + "type": 4, + "data": 21, + "clk": 22 + } + }, { "name": "CMT2300A with SSD1306", "nrf24": { @@ -127,6 +146,33 @@ "clk": 22 } }, + { + "name": "CMT2300A with SSD1309", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + }, + "eth": { + "enabled": false + }, + "display": { + "type": 4, + "data": 21, + "clk": 22 + } + }, { "name": "NRF24 + CMT2300A", "nrf24": { diff --git a/docs/DeviceProfiles/olimex_esp32_evb.json b/docs/DeviceProfiles/olimex_esp32_evb.json index 9b66926dd..ea0a8065d 100644 --- a/docs/DeviceProfiles/olimex_esp32_evb.json +++ b/docs/DeviceProfiles/olimex_esp32_evb.json @@ -1,6 +1,9 @@ [ { "name": "Olimex ESP32-EVB", + "links": [ + { "name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware" } + ], "nrf24": { "miso": 15, "mosi": 2, diff --git a/docs/DeviceProfiles/olimex_esp32_gateway.json b/docs/DeviceProfiles/olimex_esp32_gateway.json new file mode 100644 index 000000000..a359573c6 --- /dev/null +++ b/docs/DeviceProfiles/olimex_esp32_gateway.json @@ -0,0 +1,47 @@ +[ + { + "name": "Olimex ESP32-Gateway", + "nrf24": { + "miso": 14, + "mosi": 13, + "clk": 12, + "irq": 15, + "en": 2, + "cs": 4 + }, + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + } + }, + { + "name": "Olimex ESP32-Gateway with SSH1106", + "nrf24": { + "miso": 14, + "mosi": 13, + "clk": 12, + "irq": 15, + "en": 2, + "cs": 4 + }, + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + }, + "display": { + "type": 3, + "data": 32, + "clk": 16 + } + } +] diff --git a/docs/DeviceProfiles/olimex_esp32_poe.json b/docs/DeviceProfiles/olimex_esp32_poe.json index e43dff245..e0a81a010 100644 --- a/docs/DeviceProfiles/olimex_esp32_poe.json +++ b/docs/DeviceProfiles/olimex_esp32_poe.json @@ -1,6 +1,9 @@ [ { "name": "Olimex ESP32-POE", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, @@ -21,6 +24,9 @@ }, { "name": "Olimex ESP32-POE with SSD1306", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, @@ -46,6 +52,9 @@ }, { "name": "Olimex ESP32-POE with SH1106", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], "nrf24": { "miso": 15, "mosi": 2, @@ -68,5 +77,33 @@ "data": 33, "clk": 32 } + }, + { + "name": "Olimex ESP32-POE with SSD1309", + "links": [ + {"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"} + ], + "nrf24": { + "miso": 15, + "mosi": 2, + "clk": 14, + "irq": 13, + "en": 16, + "cs": 5 + }, + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + }, + "display": { + "type": 4, + "data": 33, + "clk": 32 + } } ] \ No newline at end of file diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index e8aee2312..8af112832 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -1,6 +1,9 @@ [ { "name": "WT32-ETH01", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], "nrf24": { "miso": 4, "mosi": 2, @@ -21,6 +24,9 @@ }, { "name": "WT32-ETH01 with SSD1306", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], "nrf24": { "miso": 4, "mosi": 2, @@ -43,5 +49,33 @@ "data": 5, "clk": 17 } + }, + { + "name": "WT32-ETH01 with SSD1309", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], + "nrf24": { + "miso": 4, + "mosi": 2, + "clk": 32, + "irq": 33, + "en": 14, + "cs": 15 + }, + "eth": { + "enabled": true, + "phy_addr": 1, + "power": 16, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 0 + }, + "display": { + "type": 4, + "data": 5, + "clk": 17 + } } ] \ No newline at end of file diff --git a/docs/Display.md b/docs/Display.md index 154aa05ad..c914e4a8d 100644 --- a/docs/Display.md +++ b/docs/Display.md @@ -1,20 +1,3 @@ # Display integration -OpenDTU currently supports 3 types of displays (SSD1306, SH1106 and PCD8544). Currently only displays with a resolution of 128x64 pixel are supported. To activate a display you have to specify it's type and pin assignment either in the `platformio_override.ini` or in a device profile. Due to the fact that device profiles work with the pre-compiled binary the following documentation will only cover the device profile method. - -You can either create your own device profile as described [here](DeviceProfiles.md) or use some pre-defined. The pre-defined profiles can be found [here](DeviceProfiles/). You can simply open the json file with a text editor of your choice to view/edit the pin assignment. - -## Uploading Device Profiles - -Use the "Config Management" site to upload (Restore) the json file. Make sure to choose "Pin Mapping (pin_mapping.json)" in the combo box. After you click on restore the ESP will restart. At this point, the profile is not yet active. Please read the next chapter. -![Config Management](screenshots/14_ConfigManagement.png) - -## Selecting a Device Profile - -After you uploaded the device profile you can select the profile in the "Device Manager" view. After a click on "Save" the ESP will be restarted and the pin assignment is active. At this point the display should already show something. Please see the next chapter for display settings. -![Device Manager](screenshots/20_DeviceManager_Pin.png) - -## Display Settings - -Display settings can also be found in the "Device Manager". -![Device Manager Display](screenshots/21_DeviceManager_Display.png) \ No newline at end of file +This documentation will has been moved and can be found here: diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 204916f7d..e9925f874 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -1,88 +1,3 @@ # MQTT Topics -The base topic, as configured in the web GUI is prepended to all follwing topics. - -## General topics - -| Topic | R / W | Description | Value / Unit | -| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| dtu/ip | R | IP address of OpenDTU | IP address | -| dtu/hostname | R | Current hostname of the dtu (as set in web GUI) | | -| dtu/rssi | R | WiFi network quality | db value | -| dtu/status | R | Indicates whether OpenDTU network is reachable | online / offline | -| dtu/uptime | R | Time in seconds since startup | seconds | - -## Inverter total topics - -Enabled inverter means, that only inverters with "Poll inverter data" enabled are considered. - -| Topic | R / W | Description | Value / Unit | -| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| ac/power | R | Sum of AC active power of all enabled inverters | W | -| ac/yieldtotal | R | Sum of energy converted to AC since reset watt hours of all enabled inverters | Kilo watt hours (kWh) | -| ac/yieldday | R | Sum of energy converted to AC per day in watt hours of all enabled inverters | Watt hours (Wh) -| ac/is_valid | R | Indicator whether all enabled inverters where reachable | 0 or 1 | -| dc/power | R | Sum of DC power of all enabled inverters | Watt (W) | -| dc/irradiation | R | Produced power of all enabled inverter stripes with defined irradiation settings divided by sum of all enabled inverters irradiation | % | -| dc/is_valid | R | Indicator whether all enabled inverters where reachable | 0 or 1 | - -## Inverter specific topics - -serial will be replaced with the serial number of the inverter. - -| Topic | R / W | Description | Value / Unit | -| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| [serial]/name | R | Name of the inverter as configured in web GUI | | -| [serial]/device/bootloaderversion | R | Bootloader version of the inverter | | -| [serial]/device/fwbuildversion | R | Firmware version of the inverter | | -| [serial]/device/fwbuilddatetime | R | Build date / time of inverter firmware | | -| [serial]/device/hwpartnumber | R | Hardware part number of the inverter | | -| [serial]/device/hwversion | R | Hardware version of the inverter | | -| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 | -| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 | -| [serial]/status/last_update | R | Unix timestamp of last inverter statistics udpate | seconds since JAN 01 1970 (UTC) | - -### AC channel / global specific topics - -| Topic | R / W | Description | Value / Unit | -| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| [serial]/0/current | R | AC current in ampere | Ampere (A) | -| [serial]/0/efficiency | R | Ratio AC Power over DC Power in percent | % | -| [serial]/0/frequency | R | AC frequency in hertz | Hertz (Hz) | -| [serial]/0/power | R | AC active power in watts | Watt (W) | -| [serial]/0/powerdc | R | DC power in watts | Watt (W) | -| [serial]/0/powerfactor | R | Power factor in percent | % | -| [serial]/0/reactivepower | R | AC reactive power in VAr | VAr | -| [serial]/0/temperature | R | Temperature of inverter in degree celsius | Degree Celsius (°C) | -| [serial]/0/voltage | R | AC voltage in volt | Volt (V) | -| [serial]/0/yieldday | R | Energy converted to AC per day in watt hours | Watt hours (Wh) | -| [serial]/0/yieldtotal | R | Energy converted to AC since reset watt hours | Kilo watt hours (kWh) | - -### DC input channel topics - -[1-4] represents the different inputs. The amount depends on the inverter model. - -| Topic | R / W | Description | Value / Unit | -| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| [serial]/[1-4]/current | R | DC current of specific input in ampere | Ampere (A) | -| [serial]/[1-4]/name | R | Name of the DC input channel as configured in web GUI| | -| [serial]/[1-4]/irradiation | R | Ratio DC Power over set maximum power (in web GUI) | % | -| [serial]/[1-4]/power | R | DC power of specific input in watt | Watt (W) | -| [serial]/[1-4]/voltage | R | DC voltage of specific input in volt | Volt (V) | -| [serial]/[1-4]/yieldday | R | Energy converted to AC per day on specific input | Watt hours (Wh) | -| [serial]/[1-4]/yieldtotal | R | Energy converted to AC since reset on specific input | Kilo watt hours (kWh) | - -### Inverter limit specific topics - -cmd topics are used to set values. Status topics are updated from values set in the inverter. - -| Topic | R / W | Description | Value / Unit | -| ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | -| [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output | -| [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) | -| [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | -| [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. | Watt (W) | -| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. The value must be published non-retained, otherwise it will be ignored! | % | -| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. The value must be published non-retained, otherwise it will be ignored! | Watt (W) | -| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 | -| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 | +This documentation will has been moved and can be found here: diff --git a/docs/UpgradePartition.md b/docs/UpgradePartition.md index f2cd8bf9a..782463f56 100644 --- a/docs/UpgradePartition.md +++ b/docs/UpgradePartition.md @@ -1,21 +1,3 @@ # Upgrade Partition -To be able to install further updates you have to update the partition table of the ESP32. Doing so will **erase** all configuration data. Over The Air update using the web interface is **NOT** possible! - -**So make sure you export a backup of your configuration files before continuing.** - -There are several possibilities to update the partition table: - -- Using Visual Studio Code or PlatformIO CLI - - If you have already used Visual Studio Code or the `platformio` command you can use it again to install the latest version. The partition table is upgraded automatically. - -- Any kind of flash interface - - If you like to use any kind of flash interface like `esptool.py`, Espressif Flash Download Tool, ESP_Flasher or esptool-js you have to make sure to upload the provided .factory.bin file. It is important to enter the correct target address. - - | Address | File | - | ---------| ---------------------- | - | 0x0 | opendtu-*.factory.bin | - -After upgrading the ESP32 will open the intergrated access point (AP) again. Just connect to it using the default password ("openDTU42"). If you are connected, just visit and enter the "Configuration Management". Recover the previously backuped config files. +This documentation will has been moved and can be found here: diff --git a/docs/Web-API.md b/docs/Web-API.md index 3e3eeb939..baa648974 100644 --- a/docs/Web-API.md +++ b/docs/Web-API.md @@ -1,564 +1,3 @@ # Web API -Information in JSON format can be obtained through the web API - -## List of URLs - -may be incomplete - -| GET/POST | Auth required | URL | -| -------- | --- | -- | -| Get | yes | /api/config/get | -| Post | yes | /api/config/delete | -| Get | yes | /api/config/list | -| Post | yes | /api/config/upload | -| Get+Post | yes | /api/device/config | -| Get | no | /api/devinfo/status | -| Get+Post | yes | /api/dtu/config | -| Get | no | /api/eventlog/status?inv=inverter-serialnumber | -| Post | yes | /api/firmware/update | -| Get | yes | /api/inverter/list | -| Post | yes | /api/inverter/add | -| Post | yes | /api/inverter/del | -| Post | yes | /api/inverter/edit | -| Post | yes | /api/limit/config | -| Get | no | /api/limit/status | -| Get | no | /api/livedata/status | -| Post | yes | /api/maintenance/reboot | -| Get+Post | yes | /api/mqtt/config | -| Get | no | /api/mqtt/status | -| Get+Post | yes | /api/network/config | -| Get | no | /api/network/status | -| Get+Post | yes | /api/ntp/config | -| Get | no | /api/ntp/status | -| Get+Post | yes | /api/ntp/time | -| Get | no | /api/power/status | -| Post | yes | /api/power/config | -| Get | no | /api/prometheus/metrics | -| Get+Post | yes | /api/security/config | -| Get | yes | /api/security/authenticate | -| Get | no | /api/system/status | - -## Examples of Use - -### Important notes - -- IP addresses and serial numbers in this examples are anonymized. Adjust to your own needs. -- The output from curl is without a linefeed at the end, so please be careful when copying the output - do not accidentally add the shell prompt directly after it. -- When POSTing config data to OpenDTU, always send all settings back, even if only one setting was changed. Sending single settings is not supported and you will receive a response `{"type":"warning","message":"Values are missing!"}` -- When POSTing, always put single quotes around the data part. Do not confuse the single quote `'` with the backtick `` ` ``. You have been warned. -- Some API calls have a single URL for GET and POST - e.g. `/api/ntp/config` -- Other API calls use e.g. `/api/limit/status` to GET data and a different URL `/api/limit/config` to POST data. -- If you want to investigate the web api communication, a good tool is [Postman](https://www.postman.com/) -- Settings API require username and password provided with Basic Authentication credentials -- If you disable the readonly access to the web API, every endpoint requires authentication - -### Get information - -You can "talk" to the OpenDTU with a command line tool like `curl`. The output is in plain JSON, without carriage return/linefeed and is therefore not very human readable. - -#### Get current livedata - -```bash -$ curl http://192.168.10.10/api/livedata/status -{"inverters":[{"serial":"11617160xxxx","name":"Meine Solaranlage","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"2":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"3":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0},{"serial":"11417160xxxx","name":"test","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":"test 1"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":"test 2"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0}],"total":{"Power":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":2}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}} -``` - -To enhance readability (and filter information) use the JSON command line processor `jq`. - -```bash -$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq -{ - "inverters": [ - { - "serial": "116171603546", - "name": "Meine Solaranlage", - "data_age": 7038, - "reachable": false, - "producing": false, - "limit_relative": 0, - "limit_absolute": -1, - "AC": { - "0": { - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "Power DC": { - "v": 0, - "u": "W", - "d": 1 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Frequency": { - "v": 0, - "u": "Hz", - "d": 2 - }, - "PowerFactor": { - "v": 0, - "u": "", - "d": 3 - }, - "ReactivePower": { - "v": 0, - "u": "var", - "d": 1 - }, - "Efficiency": { - "v": 0, - "u": "%", - "d": 3 - } - } - }, - "DC": { - "0": { - "name": { - "u": "" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 3 - } - }, - "1": { - "name": { - "u": "" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 3 - } - }, - "2": { - "name": { - "u": "" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 3 - } - }, - "3": { - "name": { - "u": "" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - } - } - }, - "INV": { - "0": { - "Temperature": { - "v": 0, - "u": "°C", - "d": 1 - } - } - }, - "events": 0 - }, - { - "serial": "114171603548", - "name": "test", - "data_age": 7038, - "reachable": false, - "producing": false, - "limit_relative": 0, - "limit_absolute": -1, - "AC": { - "0": { - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "Power DC": { - "v": 0, - "u": "W", - "d": 1 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Frequency": { - "v": 0, - "u": "Hz", - "d": 2 - }, - "PowerFactor": { - "v": 0, - "u": "", - "d": 3 - }, - "ReactivePower": { - "v": 0, - "u": "var", - "d": 1 - }, - "Efficiency": { - "v": 0, - "u": "%", - "d": 3 - } - } - }, - "DC": { - "0": { - "name": { - "u": "test 1" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 3 - } - }, - "1": { - "name": { - "u": "test 2" - }, - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 0, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 3 - } - } - }, - "INV": { - "0": { - "Temperature": { - "v": 0, - "u": "°C", - "d": 1 - } - } - }, - "events": 0 - } - ], - "total": { - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 0, - "u": "kWh", - "d": 2 - } - }, - "hints": { - "time_sync": false, - "radio_problem": false, - "default_password": false - } -} -``` - -The eventlog can be fetched with the inverter serial number as parameter: - -```bash -$ curl --no-progress-meter http://192.168.10.10/api/eventlog/status?inv=11418186xxxx | jq -{ - "11418186xxxx": { - "count": 4, - "events": [ - { - "message_id": 1, - "message": "Inverter start", - "start_time": 28028, - "end_time": 28028 - }, - { - "message_id": 209, - "message": "PV-1: No input", - "start_time": 28036, - "end_time": 0 - }, - { - "message_id": 2, - "message": "DTU command failed", - "start_time": 28092, - "end_time": 28092 - }, - { - "message_id": 207, - "message": "MPPT-A: Input undervoltage", - "start_time": 28336, - "end_time": 0 - } - ] - } -} -``` - -#### combine curl and jq - -`jq` can filter specific fields from json output. - -For example, filter out the current total power: - -```bash -$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq '.total | .Power.v' -140.7999878 -``` - -#### Get information where login is required - -When config data is requested, username and password have to be provided to `curl` -Username is always `admin`, the default password is `openDTU42`. The password is used for both the admin login and the Admin-mode Access Point. - -```bash -$ curl --u admin:openDTU42 http://192.168.10.10/api/ntp/config -{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"} -``` - -### Post information - -With HTTP POST commands information can be written to the OpenDTU. - -The Web API is designed to allow the web frontend in the web browser to communicate with the OpenDTU software running on the ESP32. It is not designed to be intuitive or user-friendly, so please follow the instructions here. - -#### Example 1: change ntp settings - -If you want to configure the ntp server setting, first fetch the information from the web API: - -```bash -$ curl -u "admin:password" http://192.168.10.10/api/ntp/config -{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"} -``` - -Then, second step, send your new settings. Use the text output from curl in the first step, add `data=` and enclose the whole data with single quotes. - -```bash -$ curl -u "admin:password" http://192.168.10.10/api/ntp/config -d 'data={"ntp_server":"my.own.ntp.server.home","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"}' -{"type":"success","message":"Settings saved!"} -``` - -You will receive a json formatted response. - -#### Example 2: change power limit - -In the second example, I want to change the non persistent power limit of an inverter. Again, first fetch current data: - -```bash -$ curl http://192.168.10.10/api/limit/status -{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Ok"}} -``` - -I see data from two configured inverters. - -Now I set the relative power limit of inverter with serialnumber `11418180xxxx` to 50%. - -```bash -$ curl -u "admin:password" http://192.168.10.10/api/limit/config -d 'data={"serial":"11418180xxxx", "limit_type":1, "limit_value":50}' -{"type":"success","message":"Settings saved!"} -``` - -Then I read again the limit status. In the first answer the status is `pending`, some seconds later it changed to `OK`. - -```bash -$ curl http://192.168.10.10/api/limit/status -{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Pending"}} - -... - -$ curl http://192.168.10.10/api/limit/status -{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":50,"max_power":800,"limit_set_status":"Ok"}} -``` +This documentation will has been moved and can be found here: diff --git a/include/Configuration.h b/include/Configuration.h index 39284cb8e..4a802e4e1 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "PinMapping.h" #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011900 // 0.1.24 // make sure to clean all after change +#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -15,6 +16,7 @@ #define NTP_MAX_TIMEZONEDESCR_STRLEN 50 #define MQTT_MAX_HOSTNAME_STRLEN 128 +#define MQTT_MAX_CLIENTID_STRLEN 64 #define MQTT_MAX_USERNAME_STRLEN 64 #define MQTT_MAX_PASSWORD_STRLEN 64 #define MQTT_MAX_TOPIC_STRLEN 32 @@ -29,8 +31,6 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define JSON_BUFFER_SIZE 12288 - struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; @@ -48,76 +48,115 @@ struct INVERTER_CONFIG_T { uint8_t ReachableThreshold; bool ZeroRuntimeDataIfUnrechable; bool ZeroYieldDayOnMidnight; + bool ClearEventlogOnMidnight; + bool YieldDayCorrection; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; struct CONFIG_T { - uint32_t Cfg_Version; - uint32_t Cfg_SaveCount; - - char WiFi_Ssid[WIFI_MAX_SSID_STRLEN + 1]; - char WiFi_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; - uint8_t WiFi_Ip[4]; - uint8_t WiFi_Netmask[4]; - uint8_t WiFi_Gateway[4]; - uint8_t WiFi_Dns1[4]; - uint8_t WiFi_Dns2[4]; - bool WiFi_Dhcp; - char WiFi_Hostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; - uint32_t WiFi_ApTimeout; - - bool Mdns_Enabled; - - char Ntp_Server[NTP_MAX_SERVER_STRLEN + 1]; - char Ntp_Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; - char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; - double Ntp_Longitude; - double Ntp_Latitude; - uint8_t Ntp_SunsetType; - - bool Mqtt_Enabled; - char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; - uint32_t Mqtt_Port; - char Mqtt_Username[MQTT_MAX_USERNAME_STRLEN + 1]; - char Mqtt_Password[MQTT_MAX_PASSWORD_STRLEN + 1]; - char Mqtt_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - bool Mqtt_Retain; - char Mqtt_LwtTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char Mqtt_LwtValue_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; - char Mqtt_LwtValue_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; - uint32_t Mqtt_PublishInterval; - bool Mqtt_CleanSession; - - bool Mqtt_Hass_Enabled; - bool Mqtt_Hass_Retain; - char Mqtt_Hass_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - bool Mqtt_Hass_IndividualPanels; - bool Mqtt_Hass_Expire; - - bool Mqtt_Tls; - char Mqtt_RootCaCert[MQTT_MAX_CERT_STRLEN + 1]; - bool Mqtt_TlsCertLogin; - char Mqtt_ClientCert[MQTT_MAX_CERT_STRLEN + 1]; - char Mqtt_ClientKey[MQTT_MAX_CERT_STRLEN + 1]; + struct { + uint32_t Version; + uint32_t SaveCount; + } Cfg; + + struct { + char Ssid[WIFI_MAX_SSID_STRLEN + 1]; + char Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + uint8_t Ip[4]; + uint8_t Netmask[4]; + uint8_t Gateway[4]; + uint8_t Dns1[4]; + uint8_t Dns2[4]; + bool Dhcp; + char Hostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; + uint32_t ApTimeout; + } WiFi; + + struct { + bool Enabled; + } Mdns; + + struct { + char Server[NTP_MAX_SERVER_STRLEN + 1]; + char Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; + char TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; + double Longitude; + double Latitude; + uint8_t SunsetType; + } Ntp; + + struct { + bool Enabled; + char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; + uint32_t Port; + char ClientId[MQTT_MAX_CLIENTID_STRLEN + 1]; + char Username[MQTT_MAX_USERNAME_STRLEN + 1]; + char Password[MQTT_MAX_PASSWORD_STRLEN + 1]; + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + bool Retain; + uint32_t PublishInterval; + bool CleanSession; + + struct { + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + char Value_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; + char Value_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; + uint8_t Qos; + } Lwt; + + struct { + bool Enabled; + bool Retain; + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + bool IndividualPanels; + bool Expire; + } Hass; + + struct { + bool Enabled; + char RootCaCert[MQTT_MAX_CERT_STRLEN + 1]; + bool CertLogin; + char ClientCert[MQTT_MAX_CERT_STRLEN + 1]; + char ClientKey[MQTT_MAX_CERT_STRLEN + 1]; + } Tls; + } Mqtt; + + struct { + uint64_t Serial; + uint32_t PollInterval; + struct { + uint8_t PaLevel; + } Nrf; + struct { + int8_t PaLevel; + uint32_t Frequency; + uint8_t CountryMode; + } Cmt; + } Dtu; + + struct { + char Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + bool AllowReadonly; + } Security; + + struct { + bool PowerSafe; + bool ScreenSaver; + uint8_t Rotation; + uint8_t Contrast; + uint8_t Language; + struct { + uint32_t Duration; + uint8_t Mode; + } Diagram; + } Display; + + struct { + uint8_t Brightness; + } Led_Single[PINMAPPING_LED_COUNT]; INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; - - uint64_t Dtu_Serial; - uint32_t Dtu_PollInterval; - uint8_t Dtu_NrfPaLevel; - int8_t Dtu_CmtPaLevel; - uint32_t Dtu_CmtFrequency; - - char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; - bool Security_AllowReadonly; - char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; - - bool Display_PowerSafe; - bool Display_ScreenSaver; - uint8_t Display_Rotation; - uint8_t Display_Contrast; - uint8_t Display_Language; }; class ConfigurationClass { @@ -129,7 +168,8 @@ class ConfigurationClass { CONFIG_T& get(); INVERTER_CONFIG_T* getFreeInverterSlot(); - INVERTER_CONFIG_T* getInverterConfig(uint64_t serial); + INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); + void deleteInverterById(const uint8_t id); }; -extern ConfigurationClass Configuration; \ No newline at end of file +extern ConfigurationClass Configuration; diff --git a/include/Datastore.h b/include/Datastore.h index 6e4c03964..b4610bfc8 100644 --- a/include/Datastore.h +++ b/include/Datastore.h @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include #include class DatastoreClass { public: - void init(); - void loop(); + DatastoreClass(); + void init(Scheduler& scheduler); // Sum of yield total of all enabled inverters, a inverter which is just disabled at night is also included float getTotalAcYieldTotalEnabled(); @@ -58,7 +58,10 @@ class DatastoreClass { bool getIsAllEnabledReachable(); private: - TimeoutHelper _updateTimeout; + void loop(); + + Task _loopTask; + std::mutex _mutex; float _totalAcYieldTotalEnabled = 0; @@ -79,4 +82,4 @@ class DatastoreClass { bool _isAtLeastOnePollEnabled = false; }; -extern DatastoreClass Datastore; \ No newline at end of file +extern DatastoreClass Datastore; diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 9fe202c4b..e49bf9f61 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -1,14 +1,34 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Display_Graphic_Diagram.h" #include "defaults.h" +#include #include +#define CHART_HEIGHT 20 // chart area hight in pixels +#define CHART_WIDTH 47 // chart area width in pixels + +// Left-Upper position of diagram is drawn +// (text of Y-axis is display left of that pos) +#define CHART_POSX 80 +#define CHART_POSY 0 + enum DisplayType_t { None, PCD8544, SSD1306, SH1106, + SSD1309, + ST7567_GM12864I_59N, + DisplayType_Max, +}; + +enum DiagramMode_t { + Off, + Small, + Fullscreen, + DisplayMode_Max, }; class DisplayGraphicClass { @@ -16,36 +36,43 @@ class DisplayGraphicClass { DisplayGraphicClass(); ~DisplayGraphicClass(); - void init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset); - void loop(); - void setContrast(uint8_t contrast); - void setStatus(bool turnOn); - void setOrientation(uint8_t rotation = DISPLAY_ROTATION); - void setLanguage(uint8_t language); + void init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset); + void setContrast(const uint8_t contrast); + void setStatus(const bool turnOn); + void setOrientation(const uint8_t rotation = DISPLAY_ROTATION); + void setLanguage(const uint8_t language); + void setDiagramMode(DiagramMode_t mode); void setStartupDisplay(); + DisplayGraphicDiagramClass& Diagram(); + bool enablePowerSafe = true; bool enableScreensaver = true; private: - void printText(const char* text, uint8_t line); + void loop(); + void printText(const char* text, const uint8_t line); void calcLineHeights(); - void setFont(uint8_t line); + void setFont(const uint8_t line); + bool isValidDisplay(); + + Task _loopTask; U8G2* _display; + DisplayGraphicDiagramClass _diagram; bool _displayTurnedOn; DisplayType_t _display_type = DisplayType_t::None; + DiagramMode_t _diagram_mode = DiagramMode_t::Off; uint8_t _display_language = DISPLAY_LANGUAGE; uint8_t _mExtra; - uint16_t _period = 1000; - uint16_t _interval = 60000; // interval at which to power save (milliseconds) - uint32_t _lastDisplayUpdate = 0; + const uint16_t _period = 1000; + const uint16_t _interval = 60000; // interval at which to power save (milliseconds) uint32_t _previousMillis = 0; char _fmtText[32]; bool _isLarge = false; uint8_t _lineOffsets[5]; }; -extern DisplayGraphicClass Display; \ No newline at end of file +extern DisplayGraphicClass Display; diff --git a/include/Display_Graphic_Diagram.h b/include/Display_Graphic_Diagram.h new file mode 100644 index 000000000..26cacc372 --- /dev/null +++ b/include/Display_Graphic_Diagram.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +#define MAX_DATAPOINTS 128 + +class DisplayGraphicDiagramClass { +public: + DisplayGraphicDiagramClass(); + + void init(Scheduler& scheduler, U8G2* display); + void redraw(uint8_t screenSaverOffsetX, uint8_t xPos, uint8_t yPos, uint8_t width, uint8_t height, bool isFullscreen); + + void updatePeriod(); + +private: + void averageLoop(); + void dataPointLoop(); + + uint32_t getSecondsPerDot(); + + Task _averageTask; + Task _dataPointTask; + + U8G2* _display = nullptr; + std::array _graphValues = {}; + uint8_t _graphValuesCount = 0; + + uint8_t _chartWidth = MAX_DATAPOINTS; + + float _iRunningAverage = 0; + uint16_t _iRunningAverageCnt = 0; +}; diff --git a/include/InverterSettings.h b/include/InverterSettings.h index 6375dfcfa..6a36a7053 100644 --- a/include/InverterSettings.h +++ b/include/InverterSettings.h @@ -1,17 +1,22 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include #define INVERTER_UPDATE_SETTINGS_INTERVAL 60000l class InverterSettingsClass { public: - void init(); - void loop(); + InverterSettingsClass(); + void init(Scheduler& scheduler); private: - uint32_t _lastUpdate = 0; + void settingsLoop(); + void hoyLoop(); + + Task _settingsTask; + Task _hoyTask; }; extern InverterSettingsClass InverterSettings; diff --git a/include/Led_Single.h b/include/Led_Single.h index a5c601bda..9404152be 100644 --- a/include/Led_Single.h +++ b/include/Led_Single.h @@ -2,38 +2,38 @@ #pragma once #include "PinMapping.h" +#include #include #define LEDSINGLE_UPDATE_INTERVAL 2000 -enum eLedFunction { - CONNECTED_NETWORK, - CONNECTED_MQTT, - INV_REACHABLE, - INV_PRODUCING, -}; - class LedSingleClass { public: LedSingleClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); void turnAllOff(); void turnAllOn(); private: + void setLoop(); + void outputLoop(); + + void setLed(const uint8_t ledNo, const bool ledState); + + Task _setTask; + Task _outputTask; + enum class LedState_t { On, Off, Blink, }; - LedState_t _ledState[PINMAPPING_LED_COUNT]; - LedState_t _allState; - TimeoutHelper _updateTimeout; + LedState_t _ledMode[PINMAPPING_LED_COUNT]; + LedState_t _allMode; + bool _ledStateCurrent[PINMAPPING_LED_COUNT]; TimeoutHelper _blinkTimeout; - uint8_t _ledActive = 0; }; extern LedSingleClass LedSingle; \ No newline at end of file diff --git a/include/MessageOutput.h b/include/MessageOutput.h index 7c56a6f47..40489752e 100644 --- a/include/MessageOutput.h +++ b/include/MessageOutput.h @@ -1,28 +1,34 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include -#include - -#define BUFFER_SIZE 500 - -class MessageOutputClass : public Print { -public: - void loop(); - size_t write(uint8_t c) override; - size_t write(const uint8_t *buffer, size_t size) override; - void register_ws_output(AsyncWebSocket* output); - -private: - AsyncWebSocket* _ws = NULL; - char _buffer[BUFFER_SIZE]; - uint16_t _buff_pos = 0; - uint32_t _lastSend = 0; - bool _forceSend = false; - - std::mutex _msgLock; -}; - -extern MessageOutputClass MessageOutput; \ No newline at end of file +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 500 + +class MessageOutputClass : public Print { +public: + MessageOutputClass(); + void init(Scheduler& scheduler); + size_t write(uint8_t c) override; + size_t write(const uint8_t* buffer, size_t size) override; + void register_ws_output(AsyncWebSocket* output); + +private: + void loop(); + + Task _loopTask; + + AsyncWebSocket* _ws = nullptr; + char _buffer[BUFFER_SIZE]; + uint16_t _buff_pos = 0; + uint32_t _lastSend = 0; + bool _forceSend = false; + + std::mutex _msgLock; +}; + +extern MessageOutputClass MessageOutput; diff --git a/include/MqttHandleDtu.h b/include/MqttHandleDtu.h index fb5663450..5afc87715 100644 --- a/include/MqttHandleDtu.h +++ b/include/MqttHandleDtu.h @@ -1,15 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include class MqttHandleDtuClass { public: - void init(); - void loop(); + MqttHandleDtuClass(); + void init(Scheduler& scheduler); private: - uint32_t _lastPublish = 0; + void loop(); + + Task _loopTask; }; -extern MqttHandleDtuClass MqttHandleDtu; \ No newline at end of file +extern MqttHandleDtuClass MqttHandleDtu; diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 5bf7e71eb..a76cb0c7b 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -3,6 +3,7 @@ #include #include +#include // mqtt discovery device classes enum { @@ -50,21 +51,33 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { class MqttHandleHassClass { public: - void init(); - void loop(); + MqttHandleHassClass(); + void init(Scheduler& scheduler); void publishConfig(); void forceUpdate(); private: + void loop(); void publish(const String& subtopic, const String& payload); - void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); + void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic); + void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = ""); + void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); - void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, int16_t min = 1, int16_t max = 100); + void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); - void createDeviceInfo(JsonObject& object, std::shared_ptr inv); + + static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); + static void createDtuInfo(JsonDocument& doc); + + static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); + + static String getDtuUniqueId(); + static String getDtuUrl(); + + Task _loopTask; bool _wasConnected = false; bool _updateForced = false; }; -extern MqttHandleHassClass MqttHandleHass; \ No newline at end of file +extern MqttHandleHassClass MqttHandleHass; diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index 0194bacf4..7c86a8098 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -3,22 +3,27 @@ #include "Configuration.h" #include -#include +#include #include class MqttHandleInverterClass { public: - void init(); - void loop(); + MqttHandleInverterClass(); + void init(Scheduler& scheduler); + + static String getTopic(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); - static String getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + void subscribeTopics(); + void unsubscribeTopics(); private: - void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + void loop(); + void publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); + + Task _loopTask; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; - uint32_t _lastPublish = 0; FieldId_t _publishFields[14] = { FLD_UDC, @@ -38,4 +43,4 @@ class MqttHandleInverterClass { }; }; -extern MqttHandleInverterClass MqttHandleInverter; \ No newline at end of file +extern MqttHandleInverterClass MqttHandleInverter; diff --git a/include/MqttHandleInverterTotal.h b/include/MqttHandleInverterTotal.h index fa4ce4b63..ae4d66c47 100644 --- a/include/MqttHandleInverterTotal.h +++ b/include/MqttHandleInverterTotal.h @@ -1,15 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include class MqttHandleInverterTotalClass { public: - void init(); - void loop(); + MqttHandleInverterTotalClass(); + void init(Scheduler& scheduler); private: - TimeoutHelper _lastPublish; + void loop(); + + Task _loopTask; }; -extern MqttHandleInverterTotalClass MqttHandleInverterTotal; \ No newline at end of file +extern MqttHandleInverterTotalClass MqttHandleInverterTotal; diff --git a/include/MqttSettings.h b/include/MqttSettings.h index 68b12c9c5..41b0e6ec7 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -14,31 +14,30 @@ class MqttSettingsClass { void performReconnect(); bool getConnected(); void publish(const String& subtopic, const String& payload); - void publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos = 0); + void publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos = 0); - void subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); + void subscribe(const String& topic, const uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb); void unsubscribe(const String& topic); - String getPrefix(); + String getPrefix() const; + String getClientId(); private: void NetworkEvent(network_event event); void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason); - void onMqttConnect(bool sessionPresent); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + void onMqttConnect(const bool sessionPresent); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); void performConnect(); void performDisconnect(); void createMqttClientObject(); - MqttClient* mqttClient = nullptr; - String clientId; - String willTopic; - Ticker mqttReconnectTimer; + MqttClient* _mqttClient = nullptr; + Ticker _mqttReconnectTimer; MqttSubscribeParser _mqttSubscribeParser; std::mutex _clientLock; }; -extern MqttSettingsClass MqttSettings; \ No newline at end of file +extern MqttSettingsClass MqttSettings; diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index fa94c8b3a..40ddc914d 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include @@ -29,7 +30,7 @@ typedef struct NetworkEventCbList { network_event event; NetworkEventCbList() - : cb(NULL) + : cb(nullptr) , event(network_event::NETWORK_UNKNOWN) { } @@ -38,46 +39,50 @@ typedef struct NetworkEventCbList { class NetworkSettingsClass { public: NetworkSettingsClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); void applyConfig(); void enableAdminMode(); - String getApName(); + String getApName() const; - IPAddress localIP(); - IPAddress subnetMask(); - IPAddress gatewayIP(); - IPAddress dnsIP(uint8_t dns_no = 0); - String macAddress(); + IPAddress localIP() const; + IPAddress subnetMask() const; + IPAddress gatewayIP() const; + IPAddress dnsIP(const uint8_t dns_no = 0) const; + String macAddress() const; static String getHostname(); - bool isConnected(); - network_mode NetworkMode(); + bool isConnected() const; + network_mode NetworkMode() const; - bool onEvent(NetworkEventCb cbEvent, network_event event = network_event::NETWORK_EVENT_MAX); - void raiseEvent(network_event event); + bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); + void raiseEvent(const network_event event); private: + void loop(); void setHostname(); void setStaticIp(); void handleMDNS(); void setupMode(); - void NetworkEvent(WiFiEvent_t event); - bool adminEnabled = true; - bool forceDisconnection = false; - uint32_t adminTimeoutCounter = 0; - uint32_t adminTimeoutCounterMax = 0; - uint32_t connectTimeoutTimer = 0; - uint32_t connectRedoTimer = 0; - uint32_t lastTimerCall = 0; - const byte DNS_PORT = 53; - IPAddress apIp; - IPAddress apNetmask; - std::unique_ptr dnsServer; - bool dnsServerStatus = false; + void NetworkEvent(const WiFiEvent_t event); + + Task _loopTask; + + static constexpr byte DNS_PORT = 53; + + bool _adminEnabled = true; + bool _forceDisconnection = false; + uint32_t _adminTimeoutCounter = 0; + uint32_t _adminTimeoutCounterMax = 0; + uint32_t _connectTimeoutTimer = 0; + uint32_t _connectRedoTimer = 0; + uint32_t _lastTimerCall = 0; + IPAddress _apIp; + IPAddress _apNetmask; + std::unique_ptr _dnsServer; + bool _dnsServerStatus = false; network_mode _networkMode = network_mode::Undefined; bool _ethConnected = false; std::vector _cbEventList; - bool lastMdnsEnabled = false; + bool _lastMdnsEnabled = false; }; extern NetworkSettingsClass NetworkSettings; \ No newline at end of file diff --git a/include/PinMapping.h b/include/PinMapping.h index 2360a6741..e0db88b6f 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -47,9 +47,9 @@ class PinMappingClass { bool init(const String& deviceMapping); PinMapping_t& get(); - bool isValidNrf24Config(); - bool isValidCmt2300Config(); - bool isValidEthConfig(); + bool isValidNrf24Config() const; + bool isValidCmt2300Config() const; + bool isValidEthConfig() const; private: PinMapping_t _pinMapping; diff --git a/include/Scheduler.h b/include/Scheduler.h new file mode 100644 index 000000000..44f1b51d7 --- /dev/null +++ b/include/Scheduler.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +extern Scheduler scheduler; \ No newline at end of file diff --git a/include/SunPosition.h b/include/SunPosition.h index 26caab7b7..49c9be4f4 100644 --- a/include/SunPosition.h +++ b/include/SunPosition.h @@ -1,34 +1,35 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include +#include #include class SunPositionClass { public: SunPositionClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); - bool isDayPeriod(); - bool isSunsetAvailable(); - bool sunsetTime(struct tm* info); - bool sunriseTime(struct tm* info); - void setDoRecalc(bool doRecalc); + bool isDayPeriod() const; + bool isSunsetAvailable() const; + bool sunsetTime(struct tm* info) const; + bool sunriseTime(struct tm* info) const; + void setDoRecalc(const bool doRecalc); private: + void loop(); void updateSunData(); - bool checkRecalcDayChanged(); - bool getDoRecalc(); + bool checkRecalcDayChanged() const; + bool getSunTime(struct tm* info, const uint32_t offset) const; + + Task _loopTask; - SunSet _sun; bool _isSunsetAvailable = true; uint32_t _sunriseMinutes = 0; uint32_t _sunsetMinutes = 0; bool _isValidInfo = false; - bool _doRecalc = true; - std::mutex _recalcLock; + std::atomic_bool _doRecalc = true; uint32_t _lastSunPositionCalculatedYMD = 0; }; diff --git a/include/Utils.h b/include/Utils.h index 6de962b02..f81e73180 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include class Utils { @@ -9,4 +10,6 @@ class Utils { static uint64_t generateDtuSerial(); static int getTimezoneOffset(); static void restartDtu(); + static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); + static void removeAllFiles(); }; diff --git a/include/WebApi.h b/include/WebApi.h index b61091de6..b6fdbd089 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -5,6 +5,7 @@ #include "WebApi_device.h" #include "WebApi_devinfo.h" #include "WebApi_dtu.h" +#include "WebApi_errors.h" #include "WebApi_eventlog.h" #include "WebApi_firmware.h" #include "WebApi_gridprofile.h" @@ -21,22 +22,28 @@ #include "WebApi_webapp.h" #include "WebApi_ws_console.h" #include "WebApi_ws_live.h" +#include #include +#include class WebApiClass { public: WebApiClass(); - void init(); - void loop(); + void init(Scheduler& scheduler); static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); static void sendTooManyRequests(AsyncWebServerRequest* request); + static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); + + static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document); + static uint64_t parseSerialFromRequest(AsyncWebServerRequest* request, String param_name = "inv"); + static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line); + private: AsyncWebServer _server; - AsyncEventSource _events; WebApiConfigClass _webApiConfig; WebApiDeviceClass _webApiDevice; @@ -60,4 +67,4 @@ class WebApiClass { WebApiWsLiveClass _webApiWsLive; }; -extern WebApiClass WebApi; \ No newline at end of file +extern WebApiClass WebApi; diff --git a/include/WebApi_config.h b/include/WebApi_config.h index e022af654..f29dc8fcf 100644 --- a/include/WebApi_config.h +++ b/include/WebApi_config.h @@ -2,11 +2,11 @@ #pragma once #include +#include class WebApiConfigClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onConfigGet(AsyncWebServerRequest* request); @@ -14,6 +14,4 @@ class WebApiConfigClass { 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); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_device.h b/include/WebApi_device.h index ae76edd03..48976bce6 100644 --- a/include/WebApi_device.h +++ b/include/WebApi_device.h @@ -2,15 +2,13 @@ #pragma once #include +#include class WebApiDeviceClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onDeviceAdminGet(AsyncWebServerRequest* request); void onDeviceAdminPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_devinfo.h b/include/WebApi_devinfo.h index 0b8471e77..e312ecdf0 100644 --- a/include/WebApi_devinfo.h +++ b/include/WebApi_devinfo.h @@ -2,14 +2,12 @@ #pragma once #include +#include class WebApiDevInfoClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onDevInfoStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_dtu.h b/include/WebApi_dtu.h index 4dcc235ec..20f5274e6 100644 --- a/include/WebApi_dtu.h +++ b/include/WebApi_dtu.h @@ -2,15 +2,17 @@ #pragma once #include +#include class WebApiDtuClass { public: - void init(AsyncWebServer* server); - void loop(); + WebApiDtuClass(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onDtuAdminGet(AsyncWebServerRequest* request); void onDtuAdminPost(AsyncWebServerRequest* request); - AsyncWebServer* _server; -}; \ No newline at end of file + Task _applyDataTask; + void applyDataTaskCb(); +}; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index ac91941ef..0da8d3d9f 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -5,15 +5,18 @@ enum WebApiError { GenericBase = 1000, GenericSuccess, GenericNoValueFound, - GenericDataTooLarge, + GenericDataTooLarge, // not used anymore GenericParseError, GenericValueMissing, + GenericWriteFailed, + GenericInternalServerError, DtuBase = 2000, DtuSerialZero, DtuPollZero, DtuInvalidPowerLevel, DtuInvalidCmtFrequency, + DtuInvalidCmtCountry, ConfigBase = 3000, ConfigNotDeleted, @@ -56,6 +59,8 @@ enum WebApiError { MqttPublishInterval, MqttHassTopicLength, MqttHassTopicCharacter, + MqttLwtQos, + MqttClientIdLength, NetworkBase = 8000, NetworkIpInvalid, @@ -87,4 +92,4 @@ enum WebApiError { HardwareBase = 12000, HardwarePinMappingLength, -}; \ No newline at end of file +}; diff --git a/include/WebApi_eventlog.h b/include/WebApi_eventlog.h index 311b52894..e7fe9874a 100644 --- a/include/WebApi_eventlog.h +++ b/include/WebApi_eventlog.h @@ -2,14 +2,12 @@ #pragma once #include +#include class WebApiEventlogClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onEventlogStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h index f99b248dc..990a5e064 100644 --- a/include/WebApi_firmware.h +++ b/include/WebApi_firmware.h @@ -2,15 +2,13 @@ #pragma once #include +#include class WebApiFirmwareClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onFirmwareUpdateFinish(AsyncWebServerRequest* request); void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_gridprofile.h b/include/WebApi_gridprofile.h index cf78cf647..cff4ddb86 100644 --- a/include/WebApi_gridprofile.h +++ b/include/WebApi_gridprofile.h @@ -2,14 +2,13 @@ #pragma once #include +#include class WebApiGridProfileClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onGridProfileStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file + void onGridProfileRawdata(AsyncWebServerRequest* request); +}; diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index 9f2b06731..c316622e5 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -2,11 +2,11 @@ #pragma once #include +#include class WebApiInverterClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onInverterList(AsyncWebServerRequest* request); @@ -14,6 +14,4 @@ class WebApiInverterClass { void onInverterEdit(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request); void onInverterOrder(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_limit.h b/include/WebApi_limit.h index 026f7ef88..285be27cc 100644 --- a/include/WebApi_limit.h +++ b/include/WebApi_limit.h @@ -2,15 +2,13 @@ #pragma once #include +#include class WebApiLimitClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onLimitStatus(AsyncWebServerRequest* request); void onLimitPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_maintenance.h b/include/WebApi_maintenance.h index dd7915375..5a00bbab8 100644 --- a/include/WebApi_maintenance.h +++ b/include/WebApi_maintenance.h @@ -2,14 +2,12 @@ #pragma once #include +#include class WebApiMaintenanceClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onRebootPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index 91f736798..6e428249e 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -2,19 +2,15 @@ #pragma once #include - -#define MQTT_JSON_DOC_SIZE 10240 +#include class WebApiMqttClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onMqttStatus(AsyncWebServerRequest* request); void onMqttAdminGet(AsyncWebServerRequest* request); void onMqttAdminPost(AsyncWebServerRequest* request); String getTlsCertInfo(const char* cert); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_network.h b/include/WebApi_network.h index 693bf583f..179fa4920 100644 --- a/include/WebApi_network.h +++ b/include/WebApi_network.h @@ -2,16 +2,14 @@ #pragma once #include +#include class WebApiNetworkClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onNetworkStatus(AsyncWebServerRequest* request); void onNetworkAdminGet(AsyncWebServerRequest* request); void onNetworkAdminPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_ntp.h b/include/WebApi_ntp.h index fae87811f..5ce040ede 100644 --- a/include/WebApi_ntp.h +++ b/include/WebApi_ntp.h @@ -2,11 +2,11 @@ #pragma once #include +#include class WebApiNtpClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onNtpStatus(AsyncWebServerRequest* request); @@ -14,6 +14,4 @@ class WebApiNtpClass { void onNtpAdminPost(AsyncWebServerRequest* request); void onNtpTimeGet(AsyncWebServerRequest* request); void onNtpTimePost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_power.h b/include/WebApi_power.h index f8912c0fb..aed11b0ef 100644 --- a/include/WebApi_power.h +++ b/include/WebApi_power.h @@ -2,15 +2,13 @@ #pragma once #include +#include class WebApiPowerClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onPowerStatus(AsyncWebServerRequest* request); void onPowerPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index b03f81786..b3ee6a18c 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -3,21 +3,19 @@ #include #include +#include #include class WebApiPrometheusClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onPrometheusMetricsGet(AsyncWebServerRequest* request); - void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName = NULL); + void addField(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const char* metricName, const char* channelName = nullptr); - void addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel); - - AsyncWebServer* _server; + void addPanelInfo(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel); enum MetricType_t { NONE = 0, @@ -47,4 +45,4 @@ class WebApiPrometheusClass { { FLD_EFF, MetricType_t::GAUGE }, { FLD_IRR, MetricType_t::GAUGE }, }; -}; \ No newline at end of file +}; diff --git a/include/WebApi_security.h b/include/WebApi_security.h index 37c56fae8..ac76522a5 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -2,17 +2,15 @@ #pragma once #include +#include class WebApiSecurityClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onSecurityGet(AsyncWebServerRequest* request); void onSecurityPost(AsyncWebServerRequest* request); void onAuthenticateGet(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_sysstatus.h b/include/WebApi_sysstatus.h index 9b22a835a..c754ac0df 100644 --- a/include/WebApi_sysstatus.h +++ b/include/WebApi_sysstatus.h @@ -2,14 +2,12 @@ #pragma once #include +#include class WebApiSysstatusClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: void onSystemStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_webapp.h b/include/WebApi_webapp.h index ad1614382..5330fbdf7 100644 --- a/include/WebApi_webapp.h +++ b/include/WebApi_webapp.h @@ -2,12 +2,12 @@ #pragma once #include +#include class WebApiWebappClass { public: - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: - AsyncWebServer* _server; -}; \ No newline at end of file + void responseBinaryDataWithETagCache(AsyncWebServerRequest* request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len); +}; diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index 81df81e90..cf7beecce 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -1,19 +1,17 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include - -class WebApiWsConsoleClass { -public: - WebApiWsConsoleClass(); - void init(AsyncWebServer* server); - void loop(); - -private: - void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - - AsyncWebServer* _server; - AsyncWebSocket _ws; - - uint32_t _lastWsCleanup = 0; -}; \ No newline at end of file +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiWsConsoleClass { +public: + WebApiWsConsoleClass(); + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + AsyncWebSocket _ws; + + Task _wsCleanupTask; + void wsCleanupTaskCb(); +}; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 3cb564487..8b33fad86 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -1,32 +1,45 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Configuration.h" #include #include #include +#include class WebApiWsLiveClass { public: WebApiWsLiveClass(); - void init(AsyncWebServer* server); - void loop(); + void init(AsyncWebServer& server, Scheduler& scheduler); private: +<<<<<<< HEAD void generateJsonResponse(JsonVariant& root); void generateJsonResponseTotals(JsonVariant& root); void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic = ""); void addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits); +======= + static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateCommonJsonResponse(JsonVariant& root); + + static void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); + static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); + +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 void onLivedataStatus(AsyncWebServerRequest* request); void onLivedataTotals(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - AsyncWebServer* _server; AsyncWebSocket _ws; - uint32_t _lastWsPublish = 0; - uint32_t _lastInvUpdateCheck = 0; - uint32_t _lastWsCleanup = 0; - uint32_t _newestInverterTimestamp = 0; + uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; std::mutex _mutex; -}; \ No newline at end of file + + Task _wsCleanupTask; + void wsCleanupTaskCb(); + + Task _sendDataTask; + void sendDataTaskCb(); +}; diff --git a/include/__compiled_constants.h b/include/__compiled_constants.h new file mode 100644 index 000000000..ac8991e9f --- /dev/null +++ b/include/__compiled_constants.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +// The referenced values are generated by pio-scripts/auto_firmware_version.py + + +extern const char *__COMPILED_GIT_HASH__; +// extern const char *__COMPILED_DATE_TIME_UTC_STR__; diff --git a/include/defaults.h b/include/defaults.h index 2ee00630d..ee3f7b2fe 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -9,11 +9,11 @@ #define ACCESS_POINT_NAME "OpenDTU-" #define ACCESS_POINT_PASSWORD "openDTU42" -#define ACCESS_POINT_TIMEOUT 3; +#define ACCESS_POINT_TIMEOUT 3 #define AUTH_USERNAME "admin" #define SECURITY_ALLOW_READONLY true -#define WIFI_RECONNECT_TIMEOUT 15 +#define WIFI_RECONNECT_TIMEOUT 30 #define WIFI_RECONNECT_REDO_TIMEOUT 600 #define WIFI_SSID "" @@ -22,7 +22,8 @@ #define MDNS_ENABLED false -#define NTP_SERVER "pool.ntp.org" +#define NTP_SERVER_OLD "pool.ntp.org" +#define NTP_SERVER "opendtu.pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" #define NTP_TIMEZONEDESCR "Europe/Berlin" #define NTP_LONGITUDE 10.4515f @@ -75,6 +76,7 @@ #define MQTT_LWT_TOPIC "dtu/status" #define MQTT_LWT_ONLINE "online" #define MQTT_LWT_OFFLINE "offline" +#define MQTT_LWT_QOS 2U #define MQTT_PUBLISH_INTERVAL 5U #define MQTT_CLEAN_SESSION true @@ -82,7 +84,8 @@ #define DTU_POLL_INTERVAL 5U #define DTU_NRF_PA_LEVEL 0U #define DTU_CMT_PA_LEVEL 0 -#define DTU_CMT_FREQUENCY 865000U +#define DTU_CMT_FREQUENCY 865000000U +#define DTU_CMT_COUNTRY_MODE 0U #define MQTT_HASS_ENABLED false #define MQTT_HASS_EXPIRE true @@ -97,5 +100,11 @@ #define DISPLAY_ROTATION 2U #define DISPLAY_CONTRAST 60U #define DISPLAY_LANGUAGE 0U +#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) +#define DISPLAY_DIAGRAM_MODE 1U #define REACHABLE_THRESHOLD 2U + +#define LED_BRIGHTNESS 100U + +#define MAX_INVERTER_LIMIT 2250 diff --git a/lib/CMT2300a/cmt2300a.c b/lib/CMT2300a/cmt2300a.c index 67e3d99d7..31817ccc7 100644 --- a/lib/CMT2300a/cmt2300a.c +++ b/lib/CMT2300a/cmt2300a.c @@ -628,7 +628,11 @@ int CMT2300A_GetRssiDBm(void) * for fast frequency hopping operation. * @param nChann: the frequency channel * *********************************************************/ +<<<<<<< HEAD void CMT2300A_SetFrequencyChannel(uint8_t nChann) +======= +void CMT2300A_SetFrequencyChannel(const uint8_t nChann) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { CMT2300A_WriteReg(CMT2300A_CUS_FREQ_CHNL, nChann); } diff --git a/lib/CMT2300a/cmt2300a.h b/lib/CMT2300a/cmt2300a.h index 7826caa06..0b8ae5573 100644 --- a/lib/CMT2300a/cmt2300a.h +++ b/lib/CMT2300a/cmt2300a.h @@ -75,7 +75,11 @@ void CMT2300A_EnableTxDinInvert(bool bEnable); bool CMT2300A_IsExist(void); uint8_t CMT2300A_GetRssiCode(void); int CMT2300A_GetRssiDBm(void); +<<<<<<< HEAD void CMT2300A_SetFrequencyChannel(uint8_t nChann); +======= +void CMT2300A_SetFrequencyChannel(const uint8_t nChann); +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 void CMT2300A_SetFrequencyStep(uint8_t nOffset); void CMT2300A_SetPayloadLength(uint16_t nLength); void CMT2300A_EnableLfosc(bool bEnable); diff --git a/lib/CMT2300a/cmt2300a_hal.c b/lib/CMT2300a/cmt2300a_hal.c index 486418fcc..bff4ee145 100644 --- a/lib/CMT2300a/cmt2300a_hal.c +++ b/lib/CMT2300a/cmt2300a_hal.c @@ -26,7 +26,11 @@ * @name CMT2300A_InitSpi * @desc Initializes the CMT2300A SPI interface. * *********************************************************/ +<<<<<<< HEAD void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed) +======= +void CMT2300A_InitSpi(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) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed); } @@ -37,7 +41,11 @@ void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin * @param addr: register address * @return Register value * *********************************************************/ +<<<<<<< HEAD uint8_t CMT2300A_ReadReg(uint8_t addr) +======= +uint8_t CMT2300A_ReadReg(const uint8_t addr) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { return cmt_spi3_read(addr); } @@ -48,7 +56,11 @@ uint8_t CMT2300A_ReadReg(uint8_t addr) * @param addr: register address * dat: register value * *********************************************************/ +<<<<<<< HEAD void CMT2300A_WriteReg(uint8_t addr, uint8_t dat) +======= +void CMT2300A_WriteReg(const uint8_t addr, const uint8_t dat) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { cmt_spi3_write(addr, dat); } @@ -59,7 +71,11 @@ void CMT2300A_WriteReg(uint8_t addr, uint8_t dat) * @param buf: buffer where to copy the FIFO read data * len: number of bytes to be read from the FIFO * *********************************************************/ +<<<<<<< HEAD void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len) +======= +void CMT2300A_ReadFifo(uint8_t buf[], const uint16_t len) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { cmt_spi3_read_fifo(buf, len); } @@ -70,7 +86,11 @@ void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len) * @param buf: buffer containing data to be put on the FIFO * len: number of bytes to be written to the FIFO * *********************************************************/ +<<<<<<< HEAD void CMT2300A_WriteFifo(const uint8_t buf[], uint16_t len) +======= +void CMT2300A_WriteFifo(const uint8_t buf[], const uint16_t len) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { cmt_spi3_write_fifo(buf, len); } diff --git a/lib/CMT2300a/cmt2300a_hal.h b/lib/CMT2300a/cmt2300a_hal.h index 67037d2c2..371fcab18 100644 --- a/lib/CMT2300a/cmt2300a_hal.h +++ b/lib/CMT2300a/cmt2300a_hal.h @@ -36,6 +36,7 @@ extern "C" { #define CMT2300A_GetTickCount() millis() /* ************************************************************************ */ +<<<<<<< HEAD void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed); uint8_t CMT2300A_ReadReg(uint8_t addr); @@ -43,6 +44,15 @@ void CMT2300A_WriteReg(uint8_t addr, uint8_t dat); void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len); void CMT2300A_WriteFifo(const uint8_t buf[], uint16_t len); +======= +void CMT2300A_InitSpi(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); + +uint8_t CMT2300A_ReadReg(const uint8_t addr); +void CMT2300A_WriteReg(const uint8_t addr, const uint8_t dat); + +void CMT2300A_ReadFifo(uint8_t buf[], const uint16_t len); +void CMT2300A_WriteFifo(const uint8_t buf[], const uint16_t len); +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 #ifdef __cplusplus } diff --git a/lib/CMT2300a/cmt2300a_params.h b/lib/CMT2300a/cmt2300a_params_860.h similarity index 89% rename from lib/CMT2300a/cmt2300a_params.h rename to lib/CMT2300a/cmt2300a_params_860.h index 4e10f6a25..94f155269 100644 --- a/lib/CMT2300a/cmt2300a_params.h +++ b/lib/CMT2300a/cmt2300a_params_860.h @@ -85,14 +85,14 @@ ; RSSI Offset = 0 ; RSSI Offset Sign = 0 */ -#ifndef __CMT2300A_PARAMS_H -#define __CMT2300A_PARAMS_H +#ifndef __CMT2300A_PARAMS_860_H +#define __CMT2300A_PARAMS_860_H #include "cmt2300a_defs.h" #include /* [CMT Bank] with RSSI offset of +- 0 (and Tx power double bit not set) */ -static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = { +static uint8_t g_cmt2300aCmtBank_860[CMT2300A_CMT_BANK_SIZE] = { 0x00, 0x66, 0xEC, @@ -108,7 +108,7 @@ static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = { }; /* [System Bank] */ -static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = { +static uint8_t g_cmt2300aSystemBank_860[CMT2300A_SYSTEM_BANK_SIZE] = { 0xAE, 0xE0, 0x35, @@ -124,7 +124,7 @@ static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = { }; /* [Frequency Bank] 860 MHz */ -static uint8_t g_cmt2300aFrequencyBank[CMT2300A_FREQUENCY_BANK_SIZE] = { +static uint8_t g_cmt2300aFrequencyBank_860[CMT2300A_FREQUENCY_BANK_SIZE] = { 0x42, 0x32, 0xCF, @@ -136,7 +136,7 @@ static uint8_t g_cmt2300aFrequencyBank[CMT2300A_FREQUENCY_BANK_SIZE] = { }; /* [Data Rate Bank] */ -static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = { +static uint8_t g_cmt2300aDataRateBank_860[CMT2300A_DATA_RATE_BANK_SIZE] = { 0xA6, 0xC9, 0x20, @@ -164,7 +164,7 @@ static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = { }; /* [Baseband Bank] - EU */ -static uint8_t g_cmt2300aBasebandBank[CMT2300A_BASEBAND_BANK_SIZE] = { +static uint8_t g_cmt2300aBasebandBank_860[CMT2300A_BASEBAND_BANK_SIZE] = { 0x12, 0x1E, 0x00, @@ -197,7 +197,7 @@ static uint8_t g_cmt2300aBasebandBank[CMT2300A_BASEBAND_BANK_SIZE] = { }; /* [Tx Bank] 13 dBm */ -static uint8_t g_cmt2300aTxBank[CMT2300A_TX_BANK_SIZE] = { +static uint8_t g_cmt2300aTxBank_860[CMT2300A_TX_BANK_SIZE] = { 0x70, 0x4D, 0x06, diff --git a/lib/CMT2300a/cmt2300a_params_900.h b/lib/CMT2300a/cmt2300a_params_900.h new file mode 100644 index 000000000..19b8eeac1 --- /dev/null +++ b/lib/CMT2300a/cmt2300a_params_900.h @@ -0,0 +1,214 @@ +/* +;--------------------------------------- +; CMT2300A Configuration File +; Generated by CMOSTEK RFPDK 1.46 +; 2023.03.17 23:16 +;--------------------------------------- +; Mode = Advanced +; Part Number = CMT2300A +; Frequency = 900.000 MHz +; Xtal Frequency = 26.0000 MHz +; Demodulation = GFSK +; AGC = On +; Data Rate = 20.0 kbps +; Deviation = 20.0 kHz +; Tx Xtal Tol. = 20 ppm +; Rx Xtal Tol. = 20 ppm +; TRx Matching Network Type = 20 dBm +; Tx Power = +13 dBm +; Gaussian BT = 0.5 +; Bandwidth = Auto-Select kHz +; CDR Type = Counting +; CDR DR Range = NA +; AFC = On +; AFC Method = Auto-Select +; Data Representation = 0:F-low 1:F-high +; Rx Duty-Cycle = Off +; Tx Duty-Cycle = Off +; Sleep Timer = Off +; Sleep Time = NA +; Rx Timer = Off +; Rx Time T1 = NA +; Rx Time T2 = NA +; Rx Exit State = STBY +; Tx Exit State = STBY +; SLP Mode = Disable +; RSSI Valid Source = PJD +; PJD Window = 8 Jumps +; LFOSC Calibration = On +; Xtal Stable Time = 155 us +; RSSI Compare TH = NA +; Data Mode = Packet +; Whitening = Disable +; Whiten Type = NA +; Whiten Seed Type = NA +; Whiten Seed = NA +; Manchester = Disable +; Manchester Type = NA +; FEC = Enable +; FEC Type = x^3+x^2+1 +; Tx Prefix Type = 0 +; Tx Packet Number = 1 +; Tx Packet Gap = 32 +; Packet Type = Variable Length +; Node-Length Position = First Node, then Length +; Payload Bit Order = Start from msb +; Preamble Rx Size = 2 +; Preamble Tx Size = 30 +; Preamble Value = 170 +; Preamble Unit = 8-bit +; Sync Size = 4-byte +; Sync Value = 1296587336 +; Sync Tolerance = None +; Sync Manchester = Disable +; Node ID Size = NA +; Node ID Value = NA +; Node ID Mode = None +; Node ID Err Mask = Disable +; Node ID Free = Disable +; Payload Length = 32 +; CRC Options = IBM-16 +; CRC Seed = 0 crc_seed +; CRC Range = Entire Payload +; CRC Swap = Start from MSB +; CRC Bit Invert = Normal +; CRC Bit Order = Start from bit 15 +; Dout Mute = Off +; Dout Adjust Mode = Disable +; Dout Adjust Percentage = NA +; Collision Detect = Off +; Collision Detect Offset = NA +; RSSI Detect Mode = At PREAM_OK +; RSSI Filter Setting = 32-tap +; RF Performance = High +; LBD Threshold = 2.4 V +; RSSI Offset = 0 +; RSSI Offset Sign = 0 +*/ +#ifndef __CMT2300A_PARAMS_900_H +#define __CMT2300A_PARAMS_900_H + +#include "cmt2300a_defs.h" +#include + +/* [CMT Bank] with RSSI offset of +- 0 (and Tx power double bit not set) */ +static uint8_t g_cmt2300aCmtBank_900[CMT2300A_CMT_BANK_SIZE] = { +0x00, +0x66, +0xEC, +0x1C, +0x70, +0x80, +0x14, +0x08, +0x11, +0x02, +0x02, +0x00, +}; + +/* [System Bank] */ +static uint8_t g_cmt2300aSystemBank_900[CMT2300A_SYSTEM_BANK_SIZE] = { +0xAE, +0xE0, +0x35, +0x00, +0x00, +0xF4, +0x10, +0xE2, +0x42, +0x20, +0x0C, +0x81, +}; + +/* [Frequency Bank] 900 MHz */ +static uint8_t g_cmt2300aFrequencyBank_900[CMT2300A_FREQUENCY_BANK_SIZE] = { +0x45, +0x46, +0x0A, +0x84, +0x45, +0x3B, +0xB1, +0x13, +}; + +/* [Data Rate Bank] */ +static uint8_t g_cmt2300aDataRateBank_900[CMT2300A_DATA_RATE_BANK_SIZE] = { +0xA6, +0xC9, +0x20, +0x20, +0xD2, +0x35, +0x0C, +0x0B, +0x9F, +0x4B, +0x29, +0x29, +0xC0, +0x14, +0x05, +0x53, +0x10, +0x00, +0xB4, +0x00, +0x00, +0x01, +0x00, +0x00, +}; + +/* [Baseband Bank] - EU */ +static uint8_t g_cmt2300aBasebandBank_900[CMT2300A_BASEBAND_BANK_SIZE] = { +0x12, +0x1E, +0x00, +0xAA, +0x06, +0x00, +0x00, +0x00, +0x00, +0x48, +0x5A, +0x48, +0x4D, +0x01, +0x1F, +0x00, +0x00, +0x00, +0x00, +0x00, +0xC3, +0x00, +0x00, +0x60, +0xFF, +0x00, +0x00, +0x1F, +0x10, +}; + +/* [Tx Bank] 13 dBm */ +static uint8_t g_cmt2300aTxBank_900[CMT2300A_TX_BANK_SIZE] = { +0x70, +0x4D, +0x06, +0x00, +0x07, +0x50, +0x00, +0x53, +0x09, +0x3F, +0x7F, +}; + +#endif diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index 21265f50a..016ef56fd 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -1,12 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "cmt2300wrapper.h" #include "cmt2300a.h" -#include "cmt2300a_params.h" +#include "cmt2300a_params_860.h" +#include "cmt2300a_params_900.h" -CMT2300A::CMT2300A(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t spi_speed) +CMT2300A::CMT2300A(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t spi_speed) { _pin_sdio = pin_sdio; _pin_clk = pin_clk; @@ -57,7 +58,7 @@ bool CMT2300A::available(void) ) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); } -void CMT2300A::read(void* buf, uint8_t len) +void CMT2300A::read(void* buf, const uint8_t len) { // Fetch the payload CMT2300A_ReadFifo(static_cast(buf), len); @@ -65,7 +66,7 @@ void CMT2300A::read(void* buf, uint8_t len) CMT2300A_ClearInterruptFlags(); } -bool CMT2300A::write(const uint8_t* buf, uint8_t len) +bool CMT2300A::write(const uint8_t* buf, const uint8_t len) { CMT2300A_GoStby(); CMT2300A_ClearInterruptFlags(); @@ -100,7 +101,7 @@ bool CMT2300A::write(const uint8_t* buf, uint8_t len) return true; } -void CMT2300A::setChannel(uint8_t channel) +void CMT2300A::setChannel(const uint8_t channel) { CMT2300A_SetFrequencyChannel(channel); } @@ -122,7 +123,7 @@ int CMT2300A::getRssiDBm() return CMT2300A_GetRssiDBm(); } -bool CMT2300A::setPALevel(int8_t level) +bool CMT2300A::setPALevel(const int8_t level) { uint16_t Tx_dBm_word; switch (level) { @@ -242,6 +243,22 @@ bool CMT2300A::rxFifoAvailable() ) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); } +uint32_t CMT2300A::getBaseFrequency() const +{ + return getBaseFrequency(_frequencyBand); +} + +FrequencyBand_t CMT2300A::getFrequencyBand() const +{ + return _frequencyBand; +} + +void CMT2300A::setFrequencyBand(const FrequencyBand_t mode) +{ + _frequencyBand = mode; + _init_radio(); +} + void CMT2300A::flush_rx(void) { CMT2300A_ClearRxFifo(); @@ -261,12 +278,24 @@ bool CMT2300A::_init_radio() } /* config registers */ - CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank, CMT2300A_CMT_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank, CMT2300A_SYSTEM_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank, CMT2300A_FREQUENCY_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank, CMT2300A_DATA_RATE_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank, CMT2300A_BASEBAND_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank, CMT2300A_TX_BANK_SIZE); + switch (_frequencyBand) { + case FrequencyBand_t::BAND_900: + CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank_900, CMT2300A_CMT_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank_900, CMT2300A_SYSTEM_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank_900, CMT2300A_FREQUENCY_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank_900, CMT2300A_DATA_RATE_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank_900, CMT2300A_BASEBAND_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank_900, CMT2300A_TX_BANK_SIZE); + break; + default: + CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank_860, CMT2300A_CMT_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank_860, CMT2300A_SYSTEM_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank_860, CMT2300A_FREQUENCY_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank_860, CMT2300A_DATA_RATE_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank_860, CMT2300A_BASEBAND_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank_860, CMT2300A_TX_BANK_SIZE); + break; + } // xosc_aac_code[2:0] = 2 uint8_t tmp; diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index 5f76b06f2..d1639fe9b 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -4,13 +4,21 @@ #include #define CMT2300A_ONE_STEP_SIZE 2500 // frequency channel step size for fast frequency hopping operation: One step size is 2.5 kHz. -#define CMT_BASE_FREQ 860000000 // from Frequency Bank in cmt2300a_params.h #define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset #define CMT_SPI_SPEED 4000000 // 4 MHz +#define CMT_BASE_FREQ_900 900000000 +#define CMT_BASE_FREQ_860 860000000 + +enum FrequencyBand_t { + BAND_860, + BAND_900, + FrequencyBand_Max, +}; + class CMT2300A { public: - CMT2300A(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t _spi_speed = CMT_SPI_SPEED); + CMT2300A(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t _spi_speed = CMT_SPI_SPEED); bool begin(void); @@ -54,15 +62,15 @@ class CMT2300A { * in one call is 32 (for dynamic payload lengths) or whatever number was * previously passed to setPayloadSize() (for static payload lengths). */ - void read(void* buf, uint8_t len); + void read(void* buf, const uint8_t len); - bool write(const uint8_t* buf, uint8_t len); + bool write(const uint8_t* buf, const uint8_t len); /** * Set RF communication channel. The frequency used by a channel is * @param channel Which RF channel to communicate on, 0-254 */ - void setChannel(uint8_t channel); + void setChannel(const uint8_t channel); /** * Get RF communication channel @@ -82,10 +90,26 @@ class CMT2300A { int getRssiDBm(); - bool setPALevel(int8_t level); + bool setPALevel(const int8_t level); bool rxFifoAvailable(); + uint32_t getBaseFrequency() const; + static constexpr uint32_t getBaseFrequency(FrequencyBand_t band) + { + switch (band) { + case FrequencyBand_t::BAND_900: + return CMT_BASE_FREQ_900; + break; + default: + return CMT_BASE_FREQ_860; + break; + } + } + + FrequencyBand_t getFrequencyBand() const; + void setFrequencyBand(const FrequencyBand_t mode); + /** * Empty the RX (receive) FIFO buffers. */ @@ -109,4 +133,6 @@ class CMT2300A { int8_t _pin_cs; int8_t _pin_fcs; uint32_t _spi_speed; -}; \ No newline at end of file + + FrequencyBand_t _frequencyBand = FrequencyBand_t::BAND_860; +}; diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c index 13afc1092..16801cbbe 100644 --- a/lib/CMT2300a/cmt_spi3.c +++ b/lib/CMT2300a/cmt_spi3.c @@ -16,7 +16,11 @@ SemaphoreHandle_t paramLock = NULL; spi_device_handle_t spi_reg, spi_fifo; +<<<<<<< HEAD void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed) +======= +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) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { paramLock = xSemaphoreCreateMutex(); @@ -67,7 +71,11 @@ void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fc delay(100); } +<<<<<<< HEAD void cmt_spi3_write(uint8_t addr, uint8_t dat) +======= +void cmt_spi3_write(const uint8_t addr, const uint8_t dat) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { uint8_t tx_data; tx_data = ~dat; @@ -84,7 +92,11 @@ void cmt_spi3_write(uint8_t addr, uint8_t dat) delayMicroseconds(100); } +<<<<<<< HEAD uint8_t cmt_spi3_read(uint8_t addr) +======= +uint8_t cmt_spi3_read(const uint8_t addr) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { uint8_t rx_data; spi_transaction_t t = { @@ -102,7 +114,11 @@ uint8_t cmt_spi3_read(uint8_t addr) return rx_data; } +<<<<<<< HEAD void cmt_spi3_write_fifo(const uint8_t* buf, uint16_t len) +======= +void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { uint8_t tx_data; @@ -121,7 +137,11 @@ void cmt_spi3_write_fifo(const uint8_t* buf, uint16_t len) SPI_PARAM_UNLOCK(); } +<<<<<<< HEAD void cmt_spi3_read_fifo(uint8_t* buf, uint16_t len) +======= +void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { uint8_t rx_data; diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index 7831e9321..b71a60250 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -3,6 +3,7 @@ #include +<<<<<<< HEAD void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed); void cmt_spi3_write(uint8_t addr, uint8_t dat); @@ -10,5 +11,14 @@ uint8_t cmt_spi3_read(uint8_t addr); void cmt_spi3_write_fifo(const uint8_t* p_buf, uint16_t len); void cmt_spi3_read_fifo(uint8_t* p_buf, uint16_t len); +======= +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); + +void cmt_spi3_write(const uint8_t addr, const uint8_t dat); +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); +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 #endif diff --git a/lib/CpuTemperature/src/CpuTemperature.cpp b/lib/CpuTemperature/src/CpuTemperature.cpp new file mode 100644 index 000000000..60e3fc7b4 --- /dev/null +++ b/lib/CpuTemperature/src/CpuTemperature.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ + +#include "CpuTemperature.h" +#include + +#if defined(CONFIG_IDF_TARGET_ESP32) +// there is no official API available on the original ESP32 +extern "C" { +uint8_t temprature_sens_read(); +} +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) +#include "driver/temp_sensor.h" +#endif + +CpuTemperatureClass CpuTemperature; + +float CpuTemperatureClass::read() +{ + std::lock_guard lock(_mutex); + + float temperature = NAN; + bool success = false; + +#if defined(CONFIG_IDF_TARGET_ESP32) + uint8_t raw = temprature_sens_read(); + ESP_LOGV(TAG, "Raw temperature value: %d", raw); + temperature = (raw - 32) / 1.8f; + success = (raw != 128); +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); + temp_sensor_set_config(tsens); + temp_sensor_start(); +#if defined(CONFIG_IDF_TARGET_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3)) +#error \ + "ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271" +#endif + esp_err_t result = temp_sensor_read_celsius(&temperature); + temp_sensor_stop(); + success = (result == ESP_OK); +#endif + + if (success && std::isfinite(temperature)) { + return temperature; + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + return NAN; + } +} diff --git a/lib/CpuTemperature/src/CpuTemperature.h b/lib/CpuTemperature/src/CpuTemperature.h new file mode 100644 index 000000000..06199c825 --- /dev/null +++ b/lib/CpuTemperature/src/CpuTemperature.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class CpuTemperatureClass { +public: + float read(); + +private: + std::mutex _mutex; +}; + +extern CpuTemperatureClass CpuTemperature; diff --git a/lib/Frozen/AUTHORS b/lib/Frozen/AUTHORS new file mode 100644 index 000000000..d83d0f86e --- /dev/null +++ b/lib/Frozen/AUTHORS @@ -0,0 +1,3 @@ +serge-sans-paille +Jérôme Dumesnil +Chris Beck diff --git a/lib/Frozen/LICENSE b/lib/Frozen/LICENSE new file mode 100644 index 000000000..5b4b9bdc6 --- /dev/null +++ b/lib/Frozen/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Quarkslab + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/Frozen/README.rst b/lib/Frozen/README.rst new file mode 100644 index 000000000..5a1ceb9f2 --- /dev/null +++ b/lib/Frozen/README.rst @@ -0,0 +1,245 @@ +Frozen +###### + +.. image:: https://travis-ci.org/serge-sans-paille/frozen.svg?branch=master + :target: https://travis-ci.org/serge-sans-paille/frozen + +Header-only library that provides 0 cost initialization for immutable containers, fixed-size containers, and various algorithms. + +Frozen provides: + +- immutable (a.k.a. frozen), ``constexpr``-compatible versions of ``std::set``, + ``std::unordered_set``, ``std::map`` and ``std::unordered_map``. + +- fixed-capacity, ``constinit``-compatible versions of ``std::map`` and + ``std::unordered_map`` with immutable, compile-time selected keys mapped + to mutable values. + +- 0-cost initialization version of ``std::search`` for frozen needles using + Boyer-Moore or Knuth-Morris-Pratt algorithms. + + +The ``unordered_*`` containers are guaranteed *perfect* (a.k.a. no hash +collision) and the extra storage is linear with respect to the number of keys. + +Once initialized, the container keys cannot be updated, and in exchange, lookups +are faster. And initialization is free when ``constexpr`` or ``constinit`` is +used :-). + + +Installation +------------ + +Just copy the ``include/frozen`` directory somewhere and points to it using the ``-I`` flag. Alternatively, using CMake: + +.. code:: sh + + > mkdir build + > cd build + > cmake -D CMAKE_BUILD_TYPE=Release .. + > make install + + +Installation via CMake populates configuration files into the ``/usr/local/share`` +directory which can be consumed by CMake's ``find_package`` instrinsic function. + +Requirements +------------ + +A C++ compiler that supports C++14. Clang version 5 is a good pick, GCC version +6 lags behind in terms of ``constexpr`` compilation time (At least on my +setup), but compiles correctly. Visual Studio 2017 also works correctly! + +Note that gcc 5 isn't supported. (Here's an `old compat branch`_ where a small amount of stuff was ported.) + +.. _old compat branch: https://github.com/cbeck88/frozen/tree/gcc5-support + +Usage +----- + +Compiled with ``-std=c++14`` flag: + +.. code:: C++ + + #include + + constexpr frozen::set some_ints = {1,2,3,5}; + + constexpr bool letitgo = some_ints.count(8); + + extern int n; + bool letitgoooooo = some_ints.count(n); + + +As the constructor and some methods are ``constexpr``, it's also possible to write weird stuff like: + +.. code:: C++ + + #include + + template + std::enable_if_t< frozen::set{{1,11,111}}.count(N), int> foo(); + +String support is built-in: + +.. code:: C++ + + #include + #include + + constexpr frozen::unordered_map olaf = { + {"19", 19}, + {"31", 31}, + }; + constexpr auto val = olaf.at("19"); + +The associative containers have different functionality with and without ``constexpr``. +With ``constexpr``, frozen maps have immutable keys and values. Without ``constexpr``, the +values can be updated in runtime (the keys, however, remain immutable): + +.. code:: C++ + + + #include + #include + + static constinit frozen::unordered_map voice = { + {"Anna", "???"}, + {"Elsa", "???"} + }; + + int main() { + voice.at("Anna") = "Kristen"; + voice.at("Elsa") = "Idina"; + } + +You may also prefer a slightly more DRY initialization syntax: + +.. code:: C++ + + #include + + constexpr auto some_ints = frozen::make_set({1,2,3,5}); + +There are similar ``make_X`` functions for all frozen containers. + +Exception Handling +------------------ + +For compatibility with STL's API, Frozen may eventually throw exceptions, as in +``frozen::map::at``. If you build your code without exception support, or +define the ``FROZEN_NO_EXCEPTIONS`` macro variable, they will be turned into an +``std::abort``. + +Extending +--------- + +Just like the regular C++14 container, you can specialize the hash function, +the key equality comparator for ``unordered_*`` containers, and the comparison +functions for the ordered version. + +It's also possible to specialize the ``frozen::elsa`` structure used for +hashing. Note that unlike `std::hash`, the hasher also takes a seed in addition +to the value being hashed. + +.. code:: C++ + + template struct elsa { + // in case of collisions, different seeds are tried + constexpr std::size_t operator()(T const &value, std::size_t seed) const; + }; + +Ideally, the hash function should have nice statistical properties like *pairwise-independence*: + +If ``x`` and ``y`` are different values, the chance that ``elsa{}(x, seed) == elsa{}(y, seed)`` +should be very low for a random value of ``seed``. + +Note that frozen always ultimately produces a perfect hash function, and you will always have ``O(1)`` +lookup with frozen. It's just that if the input hasher performs poorly, the search will take longer and +your project will take longer to compile. + +Troubleshooting +--------------- + +If you hit a message like this: + +.. code:: none + + [...] + note: constexpr evaluation hit maximum step limit; possible infinite loop? + +Then either you've got a very big container and you should increase Clang's +thresholds, using ``-fconstexpr-steps=1000000000`` for instance, or the hash +functions used by frozen do not suit your data, and you should change them, as +in the following: + +.. code:: c++ + + struct olaf { + constexpr std::size_t operator()(frozen::string const &value, std::size_t seed) const { return seed ^ value[0];} + }; + + constexpr frozen::unordered_set hans = { "a", "b" }; + +Tests and Benchmarks +-------------------- + +Using hand-written Makefiles crafted with love and care: + +.. code:: sh + + > # running tests + > make -C tests check + > # running benchmarks + > make -C benchmarks GOOGLE_BENCHMARK_PREFIX= + +Using CMake to generate a static configuration build system: + +.. code:: sh + + > mkdir build + > cd build + > cmake -D CMAKE_BUILD_TYPE=Release \ + -D frozen.benchmark=ON \ + -G <"Unix Makefiles" or "Ninja"> .. + > # building the tests and benchmarks... + > make # ... with make + > ninja # ... with ninja + > cmake --build . # ... with cmake + > # running the tests... + > make test # ... with make + > ninja test # ... with ninja + > cmake --build . --target test # ... with cmake + > ctest # ... with ctest + > # running the benchmarks... + > make benchmark # ... with make + > ninja benchmark # ... with ninja + > cmake --build . --target benchmark # ... with cmake + +Using CMake to generate an IDE build system with test and benchmark targets + +.. code:: sh + + > mkdir build + > cd build + > cmake -D frozen.benchmark=ON -G <"Xcode" or "Visual Studio 15 2017"> .. + > # using cmake to drive the IDE build, test, and benchmark + > cmake --build . --config Release + > cmake --build . --target test + > cmake --build . --target benchmark + + +Credits +------- + +The perfect hashing is strongly inspired by the blog post `Throw away the keys: +Easy, Minimal Perfect Hashing `_. + +Thanks a lot to Jérôme Dumesnil for his high-quality reviews, and to Chris Beck +for his contributions on perfect hashing. + +Contact +------- + +Serge sans Paille ```` + diff --git a/lib/Frozen/frozen/CMakeLists.txt b/lib/Frozen/frozen/CMakeLists.txt new file mode 100644 index 000000000..185378d5c --- /dev/null +++ b/lib/Frozen/frozen/CMakeLists.txt @@ -0,0 +1,12 @@ +target_sources(frozen-headers INTERFACE + "${prefix}/frozen/algorithm.h" + "${prefix}/frozen/map.h" + "${prefix}/frozen/random.h" + "${prefix}/frozen/set.h" + "${prefix}/frozen/string.h" + "${prefix}/frozen/unordered_map.h" + "${prefix}/frozen/unordered_set.h" + "${prefix}/frozen/bits/algorithms.h" + "${prefix}/frozen/bits/basic_types.h" + "${prefix}/frozen/bits/elsa.h" + "${prefix}/frozen/bits/pmh.h") diff --git a/lib/Frozen/frozen/algorithm.h b/lib/Frozen/frozen/algorithm.h new file mode 100644 index 000000000..3abd529b6 --- /dev/null +++ b/lib/Frozen/frozen/algorithm.h @@ -0,0 +1,198 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_ALGORITHM_H +#define FROZEN_LETITGO_ALGORITHM_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/version.h" +#include "frozen/string.h" + +namespace frozen { + +// 'search' implementation if C++17 is not available +// https://en.cppreference.com/w/cpp/algorithm/search +template +ForwardIterator search(ForwardIterator first, ForwardIterator last, const Searcher & searcher) +{ + return searcher(first, last).first; +} + +// text book implementation from +// https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + +template class knuth_morris_pratt_searcher { + bits::carray step_; + bits::carray needle_; + + static constexpr bits::carray + build_kmp_cache(char const (&needle)[size + 1]) { + std::ptrdiff_t cnd = 0; + bits::carray cache(-1); + for (std::size_t pos = 1; pos < size; ++pos) { + if (needle[pos] == needle[cnd]) { + cache[pos] = cache[cnd]; + cnd += 1; + } else { + cache[pos] = cnd; + cnd = cache[cnd]; + while (cnd >= 0 && needle[pos] != needle[cnd]) + cnd = cache[cnd]; + cnd += 1; + } + } + return cache; + } + +public: + constexpr knuth_morris_pratt_searcher(char const (&needle)[size + 1]) + : step_{build_kmp_cache(needle)}, needle_(needle) {} + + template + constexpr std::pair operator()(ForwardIterator first, ForwardIterator last) const { + std::size_t i = 0; + ForwardIterator iter = first; + while (iter != last) { + if (needle_[i] == *iter) { + if (i == (size - 1)) + return { iter - i, iter - i + size }; + ++i; + ++iter; + } else { + if (step_[i] > -1) { + i = step_[i]; + } else { + ++iter; + i = 0; + } + } + } + return { last, last }; + } +}; + +template +constexpr knuth_morris_pratt_searcher make_knuth_morris_pratt_searcher(char const (&needle)[N]) { + return {needle}; +} + +// text book implementation from +// https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm + +template class boyer_moore_searcher { + using skip_table_type = bits::carray; + using suffix_table_type = bits::carray; + + skip_table_type skip_table_; + suffix_table_type suffix_table_; + bits::carray needle_; + + constexpr auto build_skip_table(char const (&needle)[size + 1]) { + skip_table_type skip_table(size); + for (std::size_t i = 0; i < size - 1; ++i) + skip_table[needle[i]] -= i + 1; + return skip_table; + } + + constexpr bool is_prefix(char const (&needle)[size + 1], std::size_t pos) { + std::size_t suffixlen = size - pos; + + for (std::size_t i = 0; i < suffixlen; i++) { + if (needle[i] != needle[pos + i]) + return false; + } + return true; + } + + constexpr std::size_t suffix_length(char const (&needle)[size + 1], + std::size_t pos) { + // increment suffix length slen to the first mismatch or beginning + // of the word + for (std::size_t slen = 0; slen < pos ; slen++) + if (needle[pos - slen] != needle[size - 1 - slen]) + return slen; + + return pos; + } + + constexpr auto build_suffix_table(char const (&needle)[size + 1]) { + suffix_table_type suffix; + std::ptrdiff_t last_prefix_index = size - 1; + + // first loop + for (std::ptrdiff_t p = size - 1; p >= 0; p--) { + if (is_prefix(needle, p + 1)) + last_prefix_index = p + 1; + + suffix[p] = last_prefix_index + (size - 1 - p); + } + + // second loop + for (std::size_t p = 0; p < size - 1; p++) { + auto slen = suffix_length(needle, p); + if (needle[p - slen] != needle[size - 1 - slen]) + suffix[size - 1 - slen] = size - 1 - p + slen; + + } + return suffix; + } + +public: + constexpr boyer_moore_searcher(char const (&needle)[size + 1]) + : skip_table_{build_skip_table(needle)}, + suffix_table_{build_suffix_table(needle)}, + needle_(needle) {} + + template + constexpr std::pair operator()(RandomAccessIterator first, RandomAccessIterator last) const { + if (size == 0) + return { first, first }; + + if (size > size_t(last - first)) + return { last, last }; + + RandomAccessIterator iter = first + size - 1; + while (true) { + std::ptrdiff_t j = size - 1; + while (j > 0 && (*iter == needle_[j])) { + --iter; + --j; + } + if (j == 0 && *iter == needle_[0]) + return { iter, iter + size}; + + std::ptrdiff_t jump = std::max(skip_table_[*iter], suffix_table_[j]); + if (jump >= last - iter) + return { last, last }; + iter += jump; + } + } +}; + +template +constexpr boyer_moore_searcher make_boyer_moore_searcher(char const (&needle)[N]) { + return {needle}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/algorithms.h b/lib/Frozen/frozen/bits/algorithms.h new file mode 100644 index 000000000..4efa61b21 --- /dev/null +++ b/lib/Frozen/frozen/bits/algorithms.h @@ -0,0 +1,235 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BITS_ALGORITHMS_H +#define FROZEN_LETITGO_BITS_ALGORITHMS_H + +#include "frozen/bits/basic_types.h" + +#include +#include + +namespace frozen { + +namespace bits { + +auto constexpr next_highest_power_of_two(std::size_t v) { + // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + constexpr auto trip_count = std::numeric_limits::digits; + v--; + for(std::size_t i = 1; i < trip_count; i <<= 1) + v |= v >> i; + v++; + return v; +} + +template +auto constexpr log(T v) { + std::size_t n = 0; + while (v > 1) { + n += 1; + v >>= 1; + } + return n; +} + +constexpr std::size_t bit_weight(std::size_t n) { + return (n <= 8*sizeof(unsigned int)) + + (n <= 8*sizeof(unsigned long)) + + (n <= 8*sizeof(unsigned long long)) + + (n <= 128); +} + +unsigned int select_uint_least(std::integral_constant); +unsigned long select_uint_least(std::integral_constant); +unsigned long long select_uint_least(std::integral_constant); +template +unsigned long long select_uint_least(std::integral_constant) { + static_assert(N < 2, "unsupported type size"); + return {}; +} + + +template +using select_uint_least_t = decltype(select_uint_least(std::integral_constant())); + +template +constexpr auto min_element(Iter begin, const Iter end, + Compare const &compare) { + auto result = begin; + while (begin != end) { + if (compare(*begin, *result)) { + result = begin; + } + ++begin; + } + return result; +} + +template +constexpr void cswap(T &a, T &b) { + auto tmp = a; + a = b; + b = tmp; +} + +template +constexpr void cswap(std::pair & a, std::pair & b) { + cswap(a.first, b.first); + cswap(a.second, b.second); +} + +template +constexpr void cswap(std::tuple &a, std::tuple &b, std::index_sequence) { + using swallow = int[]; + (void) swallow{(cswap(std::get(a), std::get(b)), 0)...}; +} + +template +constexpr void cswap(std::tuple &a, std::tuple &b) { + cswap(a, b, std::make_index_sequence()); +} + +template +constexpr void iter_swap(Iter a, Iter b) { + cswap(*a, *b); +} + +template +constexpr Iterator partition(Iterator left, Iterator right, Compare const &compare) { + auto pivot = left + (right - left) / 2; + iter_swap(right, pivot); + pivot = right; + for (auto it = left; 0 < right - it; ++it) { + if (compare(*it, *pivot)) { + iter_swap(it, left); + left++; + } + } + iter_swap(pivot, left); + pivot = left; + return pivot; +} + +template +constexpr void quicksort(Iterator left, Iterator right, Compare const &compare) { + while (0 < right - left) { + auto new_pivot = bits::partition(left, right, compare); + quicksort(left, new_pivot, compare); + left = new_pivot + 1; + } +} + +template +constexpr Container quicksort(Container const &array, + Compare const &compare) { + Container res = array; + quicksort(res.begin(), res.end() - 1, compare); + return res; +} + +template struct LowerBound { + T const &value_; + Compare const &compare_; + constexpr LowerBound(T const &value, Compare const &compare) + : value_(value), compare_(compare) {} + + template + inline constexpr ForwardIt doit_fast(ForwardIt first, + std::integral_constant) { + return first; + } + + template + inline constexpr ForwardIt doit_fast(ForwardIt first, + std::integral_constant) { + auto constexpr step = N / 2; + static_assert(N/2 == N - N / 2 - 1, "power of two minus 1"); + auto it = first + step; + auto next_it = compare_(*it, value_) ? it + 1 : first; + return doit_fast(next_it, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + return doit_fast(first, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + auto constexpr next_power = next_highest_power_of_two(N); + auto constexpr next_start = next_power / 2 - 1; + auto it = first + next_start; + if (compare_(*it, value_)) { + auto constexpr next = N - next_start - 1; + return doitfirst(it + 1, std::integral_constant{}, std::integral_constant{}); + } + else + return doit_fast(first, std::integral_constant{}); + } + + template + inline constexpr ForwardIt doitfirst(ForwardIt first, std::integral_constant, std::integral_constant) { + return doit_fast(first, std::integral_constant{}); + } +}; + +template +constexpr ForwardIt lower_bound(ForwardIt first, const T &value, Compare const &compare) { + return LowerBound{value, compare}.doitfirst(first, std::integral_constant{}, std::integral_constant{}); +} + +template +constexpr bool binary_search(ForwardIt first, const T &value, + Compare const &compare) { + ForwardIt where = lower_bound(first, value, compare); + return (!(where == first + N) && !(compare(value, *where))); +} + + +template +constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2) +{ + for (; first1 != last1; ++first1, ++first2) { + if (!(*first1 == *first2)) { + return false; + } + } + return true; +} + +template +constexpr bool lexicographical_compare(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2) +{ + for (; (first1 != last1) && (first2 != last2); ++first1, ++first2) { + if (*first1 < *first2) + return true; + if (*first2 < *first1) + return false; + } + return (first1 == last1) && (first2 != last2); +} + +} // namespace bits +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/basic_types.h b/lib/Frozen/frozen/bits/basic_types.h new file mode 100644 index 000000000..239270afc --- /dev/null +++ b/lib/Frozen/frozen/bits/basic_types.h @@ -0,0 +1,198 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BASIC_TYPES_H +#define FROZEN_LETITGO_BASIC_TYPES_H + +#include "frozen/bits/exceptions.h" + +#include +#include +#include +#include + +namespace frozen { + +namespace bits { + +// used as a fake argument for frozen::make_set and frozen::make_map in the case of N=0 +struct ignored_arg {}; + +template +class cvector { + T data [N] = {}; // zero-initialization for scalar type T, default-initialized otherwise + std::size_t dsize = 0; + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr cvector(void) = default; + constexpr cvector(size_type count, const T& value) : dsize(count) { + for (std::size_t i = 0; i < N; ++i) + data[i] = value; + } + + // Iterators + constexpr iterator begin() noexcept { return data; } + constexpr iterator end() noexcept { return data + dsize; } + constexpr const_iterator begin() const noexcept { return data; } + constexpr const_iterator end() const noexcept { return data + dsize; } + + // Capacity + constexpr size_type size() const { return dsize; } + + // Element access + constexpr reference operator[](std::size_t index) { return data[index]; } + constexpr const_reference operator[](std::size_t index) const { return data[index]; } + + constexpr reference back() { return data[dsize - 1]; } + constexpr const_reference back() const { return data[dsize - 1]; } + + // Modifiers + constexpr void push_back(const T & a) { data[dsize++] = a; } + constexpr void push_back(T && a) { data[dsize++] = std::move(a); } + constexpr void pop_back() { --dsize; } + + constexpr void clear() { dsize = 0; } +}; + +template +class carray { + T data_ [N] = {}; // zero-initialization for scalar type T, default-initialized otherwise + + template + constexpr carray(Iter iter, std::index_sequence) + : data_{((void)I, *iter++)...} {} + template + constexpr carray(const T& value, std::index_sequence) + : data_{((void)I, value)...} {} + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr carray() = default; + constexpr carray(const value_type& val) + : carray(val, std::make_index_sequence()) {} + template ::value, std::size_t> M> + constexpr carray(U const (&init)[M]) + : carray(init, std::make_index_sequence()) + { + static_assert(M >= N, "Cannot initialize a carray with an smaller array"); + } + template ::value, std::size_t> M> + constexpr carray(std::array const &init) + : carray(init.begin(), std::make_index_sequence()) + { + static_assert(M >= N, "Cannot initialize a carray with an smaller array"); + } + template ::value>* = nullptr> + constexpr carray(std::initializer_list init) + : carray(init.begin(), std::make_index_sequence()) + { + // clang & gcc doesn't recognize init.size() as a constexpr + // static_assert(init.size() >= N, "Cannot initialize a carray with an smaller initializer list"); + } + template ::value>* = nullptr> + constexpr carray(const carray& rhs) + : carray(rhs.begin(), std::make_index_sequence()) + { + } + + // Iterators + constexpr iterator begin() noexcept { return data_; } + constexpr const_iterator begin() const noexcept { return data_; } + constexpr iterator end() noexcept { return data_ + N; } + constexpr const_iterator end() const noexcept { return data_ + N; } + + // Capacity + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + // Element access + constexpr reference operator[](std::size_t index) { return data_[index]; } + constexpr const_reference operator[](std::size_t index) const { return data_[index]; } + + constexpr reference at(std::size_t index) { + if (index > N) + FROZEN_THROW_OR_ABORT(std::out_of_range("Index (" + std::to_string(index) + ") out of bound (" + std::to_string(N) + ')')); + return data_[index]; + } + constexpr const_reference at(std::size_t index) const { + if (index > N) + FROZEN_THROW_OR_ABORT(std::out_of_range("Index (" + std::to_string(index) + ") out of bound (" + std::to_string(N) + ')')); + return data_[index]; + } + + constexpr reference front() { return data_[0]; } + constexpr const_reference front() const { return data_[0]; } + + constexpr reference back() { return data_[N - 1]; } + constexpr const_reference back() const { return data_[N - 1]; } + + constexpr value_type* data() noexcept { return data_; } + constexpr const value_type* data() const noexcept { return data_; } +}; +template +class carray { + +public: + // Container typdefs + using value_type = T; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + using iterator = pointer; + using const_iterator = const_pointer; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // Constructors + constexpr carray(void) = default; + +}; + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/constexpr_assert.h b/lib/Frozen/frozen/bits/constexpr_assert.h new file mode 100644 index 000000000..912210dc2 --- /dev/null +++ b/lib/Frozen/frozen/bits/constexpr_assert.h @@ -0,0 +1,40 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_CONSTEXPR_ASSERT_H +#define FROZEN_LETITGO_CONSTEXPR_ASSERT_H + +#include + +#ifdef _MSC_VER + +// FIXME: find a way to implement that correctly for msvc +#define constexpr_assert(cond, msg) + +#else + +#define constexpr_assert(cond, msg)\ + assert(cond && msg); +#endif + +#endif + diff --git a/lib/Frozen/frozen/bits/defines.h b/lib/Frozen/frozen/bits/defines.h new file mode 100644 index 000000000..e20f6d0ce --- /dev/null +++ b/lib/Frozen/frozen/bits/defines.h @@ -0,0 +1,66 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_DEFINES_H +#define FROZEN_LETITGO_DEFINES_H + +#if defined(_MSVC_LANG) && !(defined(__EDG__) && defined(__clang__)) // TRANSITION, VSO#273681 + #define FROZEN_LETITGO_IS_MSVC +#endif + +// Code taken from https://stackoverflow.com/questions/43639122/which-values-can-msvc-lang-have +#if defined(FROZEN_LETITGO_IS_MSVC) + #if _MSVC_LANG > 201402 + #define FROZEN_LETITGO_HAS_CXX17 1 + #else /* _MSVC_LANG > 201402 */ + #define FROZEN_LETITGO_HAS_CXX17 0 + #endif /* _MSVC_LANG > 201402 */ +#else /* _MSVC_LANG etc. */ + #if __cplusplus > 201402 + #define FROZEN_LETITGO_HAS_CXX17 1 + #else /* __cplusplus > 201402 */ + #define FROZEN_LETITGO_HAS_CXX17 0 + #endif /* __cplusplus > 201402 */ +#endif /* _MSVC_LANG etc. */ +// End if taken code + +#if FROZEN_LETITGO_HAS_CXX17 == 1 && defined(FROZEN_LETITGO_IS_MSVC) + #define FROZEN_LETITGO_HAS_STRING_VIEW // We assume Visual Studio always has string_view in C++17 +#else + #if FROZEN_LETITGO_HAS_CXX17 == 1 && __has_include() + #define FROZEN_LETITGO_HAS_STRING_VIEW + #endif +#endif + +#ifdef __cpp_char8_t + #define FROZEN_LETITGO_HAS_CHAR8T +#endif + +#if __cpp_deduction_guides >= 201703L + #define FROZEN_LETITGO_HAS_DEDUCTION_GUIDES +#endif + +#if __cpp_lib_constexpr_string >= 201907L + #define FROZEN_LETITGO_HAS_CONSTEXPR_STRING +#endif + +#endif // FROZEN_LETITGO_DEFINES_H diff --git a/lib/Frozen/frozen/bits/elsa.h b/lib/Frozen/frozen/bits/elsa.h new file mode 100644 index 000000000..6c9ecb78f --- /dev/null +++ b/lib/Frozen/frozen/bits/elsa.h @@ -0,0 +1,57 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_ELSA_H +#define FROZEN_LETITGO_ELSA_H + +#include + +namespace frozen { + +template struct elsa { + static_assert(std::is_integral::value || std::is_enum::value, + "only supports integral types, specialize for other types"); + + constexpr std::size_t operator()(T const &value, std::size_t seed) const { + std::size_t key = seed ^ static_cast(value); + key = (~key) + (key << 21); // key = (key << 21) - key - 1; + key = key ^ (key >> 24); + key = (key + (key << 3)) + (key << 8); // key * 265 + key = key ^ (key >> 14); + key = (key + (key << 2)) + (key << 4); // key * 21 + key = key ^ (key >> 28); + key = key + (key << 31); + return key; + } +}; + +template <> struct elsa { + template + constexpr std::size_t operator()(T const &value, std::size_t seed) const { + return elsa{}(value, seed); + } +}; + +template using anna = elsa; +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/elsa_std.h b/lib/Frozen/frozen/bits/elsa_std.h new file mode 100644 index 000000000..df1a9cfc3 --- /dev/null +++ b/lib/Frozen/frozen/bits/elsa_std.h @@ -0,0 +1,41 @@ +#ifndef FROZEN_LETITGO_BITS_ELSA_STD_H +#define FROZEN_LETITGO_BITS_ELSA_STD_H + +#include "defines.h" +#include "elsa.h" +#include "hash_string.h" + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW +#include +#endif +#include + +namespace frozen { + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW + +template struct elsa> +{ + constexpr std::size_t operator()(const std::basic_string_view& value) const { + return hash_string(value); + } + constexpr std::size_t operator()(const std::basic_string_view& value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +#endif + +template struct elsa> +{ + constexpr std::size_t operator()(const std::basic_string& value) const { + return hash_string(value); + } + constexpr std::size_t operator()(const std::basic_string& value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +} // namespace frozen + +#endif // FROZEN_LETITGO_BITS_ELSA_STD_H diff --git a/lib/Frozen/frozen/bits/exceptions.h b/lib/Frozen/frozen/bits/exceptions.h new file mode 100644 index 000000000..b43e3e6b9 --- /dev/null +++ b/lib/Frozen/frozen/bits/exceptions.h @@ -0,0 +1,39 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_EXCEPTIONS_H +#define FROZEN_LETITGO_EXCEPTIONS_H + +#if defined(FROZEN_NO_EXCEPTIONS) || (defined(_MSC_VER) && !defined(_CPPUNWIND)) || (!defined(_MSC_VER) && !defined(__cpp_exceptions)) + +#include +#define FROZEN_THROW_OR_ABORT(_) std::abort() + +#else + +#include +#define FROZEN_THROW_OR_ABORT(err) throw err + + +#endif + +#endif diff --git a/lib/Frozen/frozen/bits/hash_string.h b/lib/Frozen/frozen/bits/hash_string.h new file mode 100644 index 000000000..b2f7e90e6 --- /dev/null +++ b/lib/Frozen/frozen/bits/hash_string.h @@ -0,0 +1,28 @@ +#ifndef FROZEN_LETITGO_BITS_HASH_STRING_H +#define FROZEN_LETITGO_BITS_HASH_STRING_H + +#include + +namespace frozen { + +template +constexpr std::size_t hash_string(const String& value) { + std::size_t d = 5381; + for (const auto& c : value) + d = d * 33 + static_cast(c); + return d; +} + +// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function +// With the lowest bits removed, based on experimental setup. +template +constexpr std::size_t hash_string(const String& value, std::size_t seed) { + std::size_t d = (0x811c9dc5 ^ seed) * static_cast(0x01000193); + for (const auto& c : value) + d = (d ^ static_cast(c)) * static_cast(0x01000193); + return d >> 8 ; +} + +} // namespace frozen + +#endif // FROZEN_LETITGO_BITS_HASH_STRING_H \ No newline at end of file diff --git a/lib/Frozen/frozen/bits/mpl.h b/lib/Frozen/frozen/bits/mpl.h new file mode 100644 index 000000000..8f87f99c8 --- /dev/null +++ b/lib/Frozen/frozen/bits/mpl.h @@ -0,0 +1,56 @@ +/* + * Frozen + * Copyright 2022 Giel van Schijndel + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_BITS_MPL_H +#define FROZEN_LETITGO_BITS_MPL_H + +#include + +namespace frozen { + +namespace bits { + +// Forward declarations +template +class carray; + +template +struct remove_cv : std::remove_cv {}; + +template +struct remove_cv> { + using type = std::pair::type...>; +}; + +template +struct remove_cv> { + using type = carray::type, N>; +}; + +template +using remove_cv_t = typename remove_cv::type; + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/pmh.h b/lib/Frozen/frozen/bits/pmh.h new file mode 100644 index 000000000..1bb402163 --- /dev/null +++ b/lib/Frozen/frozen/bits/pmh.h @@ -0,0 +1,254 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// inspired from http://stevehanov.ca/blog/index.php?id=119 +#ifndef FROZEN_LETITGO_PMH_H +#define FROZEN_LETITGO_PMH_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" + +#include +#include +#include +#include + +namespace frozen { + +namespace bits { + +// Function object for sorting buckets in decreasing order of size +struct bucket_size_compare { + template + bool constexpr operator()(B const &b0, + B const &b1) const { + return b0.size() > b1.size(); + } +}; + +// Step One in pmh routine is to take all items and hash them into buckets, +// with some collisions. Then process those buckets further to build a perfect +// hash function. +// pmh_buckets represents the initial placement into buckets. + +template +struct pmh_buckets { + // Step 0: Bucket max is 2 * sqrt M + // TODO: Come up with justification for this, should it not be O(log M)? + static constexpr auto bucket_max = 2 * (1u << (log(M) / 2)); + + using bucket_t = cvector; + carray buckets; + std::uint64_t seed; + + // Represents a reference to a bucket. This is used because the buckets + // have to be sorted, but buckets are big, making it slower than sorting refs + struct bucket_ref { + unsigned hash; + const bucket_t * ptr; + + // Forward some interface of bucket + using value_type = typename bucket_t::value_type; + using const_iterator = typename bucket_t::const_iterator; + + constexpr auto size() const { return ptr->size(); } + constexpr const auto & operator[](std::size_t idx) const { return (*ptr)[idx]; } + constexpr auto begin() const { return ptr->begin(); } + constexpr auto end() const { return ptr->end(); } + }; + + // Make a bucket_ref for each bucket + template + carray constexpr make_bucket_refs(std::index_sequence) const { + return {{ bucket_ref{Is, &buckets[Is]}... }}; + } + + // Makes a bucket_ref for each bucket and sorts them by size + carray constexpr get_sorted_buckets() const { + carray result{this->make_bucket_refs(std::make_index_sequence())}; + bits::quicksort(result.begin(), result.end() - 1, bucket_size_compare{}); + return result; + } +}; + +template +pmh_buckets constexpr make_pmh_buckets(const carray & items, + Hash const & hash, + Key const & key, + PRG & prg) { + using result_t = pmh_buckets; + // Continue until all items are placed without exceeding bucket_max + while (1) { + result_t result{}; + result.seed = prg(); + bool rejected = false; + for (std::size_t i = 0; i < items.size(); ++i) { + auto & bucket = result.buckets[hash(key(items[i]), static_cast(result.seed)) % M]; + if (bucket.size() >= result_t::bucket_max) { + rejected = true; + break; + } + bucket.push_back(i); + } + if (!rejected) { return result; } + } +} + +// Check if an item appears in a cvector +template +constexpr bool all_different_from(cvector & data, T & a) { + for (std::size_t i = 0; i < data.size(); ++i) + if (data[i] == a) + return false; + + return true; +} + +// Represents either an index to a data item array, or a seed to be used with +// a hasher. Seed must have high bit of 1, value has high bit of zero. +struct seed_or_index { + using value_type = std::uint64_t; + +private: + static constexpr value_type MINUS_ONE = std::numeric_limits::max(); + static constexpr value_type HIGH_BIT = ~(MINUS_ONE >> 1); + + value_type value_ = 0; + +public: + constexpr value_type value() const { return value_; } + constexpr bool is_seed() const { return value_ & HIGH_BIT; } + + constexpr seed_or_index(bool is_seed, value_type value) + : value_(is_seed ? (value | HIGH_BIT) : (value & ~HIGH_BIT)) {} + + constexpr seed_or_index() = default; + constexpr seed_or_index(const seed_or_index &) = default; + constexpr seed_or_index & operator =(const seed_or_index &) = default; +}; + +// Represents the perfect hash function created by pmh algorithm +template +struct pmh_tables : private Hasher { + std::uint64_t first_seed_; + carray first_table_; + carray second_table_; + + constexpr pmh_tables( + std::uint64_t first_seed, + carray first_table, + carray second_table, + Hasher hash) noexcept + : Hasher(hash) + , first_seed_(first_seed) + , first_table_(first_table) + , second_table_(second_table) + {} + + constexpr Hasher const& hash_function() const noexcept { + return static_cast(*this); + } + + template + constexpr std::size_t lookup(const KeyType & key) const { + return lookup(key, hash_function()); + } + + // Looks up a given key, to find its expected index in carray + // Always returns a valid index, must use KeyEqual test after to confirm. + template + constexpr std::size_t lookup(const KeyType & key, const HasherType& hasher) const { + auto const d = first_table_[hasher(key, static_cast(first_seed_)) % M]; + if (!d.is_seed()) { return static_cast(d.value()); } // this is narrowing std::uint64 -> std::size_t but should be fine + else { return second_table_[hasher(key, static_cast(d.value())) % M]; } + } +}; + +// Make pmh tables for given items, hash function, prg, etc. +template +pmh_tables constexpr make_pmh_tables(const carray & + items, + Hash const &hash, + Key const &key, + PRG prg) { + // Step 1: Place all of the keys into buckets + auto step_one = make_pmh_buckets(items, hash, key, prg); + + // Step 2: Sort the buckets to process the ones with the most items first. + auto buckets = step_one.get_sorted_buckets(); + + // Special value for unused slots. This is purposefully the index + // one-past-the-end of 'items' to function as a sentinel value. Both to avoid + // the need to apply the KeyEqual predicate and to be easily convertible to + // end(). + // Unused entries in both hash tables (G and H) have to contain this value. + const auto UNUSED = items.size(); + + // G becomes the first hash table in the resulting pmh function + carray G({false, UNUSED}); + + // H becomes the second hash table in the resulting pmh function + carray H(UNUSED); + + // Step 3: Map the items in buckets into hash tables. + for (const auto & bucket : buckets) { + auto const bsize = bucket.size(); + + if (bsize == 1) { + // Store index to the (single) item in G + // assert(bucket.hash == hash(key(items[bucket[0]]), step_one.seed) % M); + G[bucket.hash] = {false, static_cast(bucket[0])}; + } else if (bsize > 1) { + + // Repeatedly try different H of d until we find a hash function + // that places all items in the bucket into free slots + seed_or_index d{true, prg()}; + cvector bucket_slots; + + while (bucket_slots.size() < bsize) { + auto slot = hash(key(items[bucket[bucket_slots.size()]]), static_cast(d.value())) % M; + + if (H[slot] != UNUSED || !all_different_from(bucket_slots, slot)) { + bucket_slots.clear(); + d = {true, prg()}; + continue; + } + + bucket_slots.push_back(slot); + } + + // Put successful seed in G, and put indices to items in their slots + // assert(bucket.hash == hash(key(items[bucket[0]]), step_one.seed) % M); + G[bucket.hash] = d; + for (std::size_t i = 0; i < bsize; ++i) + H[bucket_slots[i]] = bucket[i]; + } + } + + return {step_one.seed, G, H, hash}; +} + +} // namespace bits + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/bits/version.h b/lib/Frozen/frozen/bits/version.h new file mode 100644 index 000000000..7e57d707e --- /dev/null +++ b/lib/Frozen/frozen/bits/version.h @@ -0,0 +1,30 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_VERSION_H +#define FROZEN_LETITGO_VERSION_H + +#define FROZEN_MAJOR_VERSION 1 +#define FROZEN_MINOR_VERSION 1 +#define FROZEN_PATCH_VERSION 1 + +#endif diff --git a/lib/Frozen/frozen/map.h b/lib/Frozen/frozen/map.h new file mode 100644 index 000000000..d54128a6c --- /dev/null +++ b/lib/Frozen/frozen/map.h @@ -0,0 +1,357 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_MAP_H +#define FROZEN_LETITGO_MAP_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/exceptions.h" +#include "frozen/bits/mpl.h" +#include "frozen/bits/version.h" + +#include +#include + +namespace frozen { + +namespace impl { + +template class CompareKey : private Comparator { +public: + constexpr Comparator const& key_comp() const noexcept { + return static_cast(*this); + } + + constexpr CompareKey(Comparator const &comparator) + : Comparator(comparator) {} + + template + constexpr int operator()(std::pair const &self, + std::pair const &other) const { + return key_comp()(std::get<0>(self), std::get<0>(other)); + } + + template + constexpr int operator()(Key1 const &self_key, + std::pair const &other) const { + return key_comp()(self_key, std::get<0>(other)); + } + + template + constexpr int operator()(std::pair const &self, + Key2 const &other_key) const { + return key_comp()(std::get<0>(self), other_key); + } + + template + constexpr int operator()(Key1 const &self_key, Key2 const &other_key) const { + return key_comp()(self_key, other_key); + } +}; + +} // namespace impl + +template > +class map : private impl::CompareKey { + using container_type = bits::carray, N>; + container_type items_; + +public: + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using key_compare = Compare; + using value_compare = impl::CompareKey; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = typename container_type::iterator; + using const_iterator = typename container_type::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + +public: + /* constructors */ + constexpr map(container_type items, Compare const &compare) + : impl::CompareKey{compare} + , items_{bits::quicksort(bits::remove_cv_t(items), value_comp())} {} + + explicit constexpr map(container_type items) + : map{items, Compare{}} {} + + constexpr map(std::initializer_list items, Compare const &compare) + : map{container_type {items}, compare} { + constexpr_assert(items.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr map(std::initializer_list items) + : map{items, Compare{}} {} + + /* element access */ + constexpr Value const& at(Key const &key) const { + return at_impl(*this, key); + } + constexpr Value& at(Key const &key) { + return at_impl(*this, key); + } + + /* iterators */ + constexpr iterator begin() { return items_.begin(); } + constexpr const_iterator begin() const { return items_.begin(); } + constexpr const_iterator cbegin() const { return items_.begin(); } + constexpr iterator end() { return items_.end(); } + constexpr const_iterator end() const { return items_.end(); } + constexpr const_iterator cend() const { return items_.end(); } + + constexpr reverse_iterator rbegin() { return reverse_iterator{items_.end()}; } + constexpr const_reverse_iterator rbegin() const { return const_reverse_iterator{items_.end()}; } + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{items_.end()}; } + constexpr reverse_iterator rend() { return reverse_iterator{items_.begin()}; } + constexpr const_reverse_iterator rend() const { return const_reverse_iterator{items_.begin()}; } + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{items_.begin()}; } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + + template + constexpr std::size_t count(KeyType const &key) const { + return bits::binary_search(items_.begin(), key, value_comp()); + } + + template + constexpr const_iterator find(KeyType const &key) const { + return map::find_impl(*this, key); + } + template + constexpr iterator find(KeyType const &key) { + return map::find_impl(*this, key); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != this->end(); + } + + template + constexpr std::pair + equal_range(KeyType const &key) const { + return equal_range_impl(*this, key); + } + template + constexpr std::pair equal_range(KeyType const &key) { + return equal_range_impl(*this, key); + } + + template + constexpr const_iterator lower_bound(KeyType const &key) const { + return lower_bound_impl(*this, key); + } + template + constexpr iterator lower_bound(KeyType const &key) { + return lower_bound_impl(*this, key); + } + + template + constexpr const_iterator upper_bound(KeyType const &key) const { + return upper_bound_impl(*this, key); + } + template + constexpr iterator upper_bound(KeyType const &key) { + return upper_bound_impl(*this, key); + } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp().key_comp(); } + constexpr const value_compare& value_comp() const { return static_cast const&>(*this); } + + private: + template + static inline constexpr auto& at_impl(This&& self, KeyType const &key) { + auto where = self.find(key); + if (where != self.end()) + return where->second; + else + FROZEN_THROW_OR_ABORT(std::out_of_range("unknown key")); + } + + template + static inline constexpr auto find_impl(This&& self, KeyType const &key) { + auto where = self.lower_bound(key); + if (where != self.end() && !self.value_comp()(key, *where)) + return where; + else + return self.end(); + } + + template + static inline constexpr auto equal_range_impl(This&& self, KeyType const &key) { + auto lower = self.lower_bound(key); + using lower_t = decltype(lower); + if (lower != self.end() && !self.value_comp()(key, *lower)) + return std::pair{lower, lower + 1}; + else + return std::pair{lower, lower}; + } + + template + static inline constexpr auto lower_bound_impl(This&& self, KeyType const &key) -> decltype(self.end()) { + return bits::lower_bound(self.items_.begin(), key, self.value_comp()); + } + + template + static inline constexpr auto upper_bound_impl(This&& self, KeyType const &key) { + auto lower = self.lower_bound(key); + if (lower != self.end() && !self.value_comp()(key, *lower)) + return lower + 1; + else + return lower; + } +}; + +template +class map : private impl::CompareKey { + using container_type = bits::carray, 0>; + +public: + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using key_compare = Compare; + using value_compare = impl::CompareKey; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = pointer; + using const_reverse_iterator = const_pointer; + +public: + /* constructors */ + constexpr map(const map &other) = default; + constexpr map(std::initializer_list, Compare const &compare) + : impl::CompareKey{compare} {} + constexpr map(std::initializer_list items) + : map{items, Compare{}} {} + + /* element access */ + template + constexpr mapped_type at(KeyType const &) const { + FROZEN_THROW_OR_ABORT(std::out_of_range("invalid key")); + } + template + constexpr mapped_type at(KeyType const &) { + FROZEN_THROW_OR_ABORT(std::out_of_range("invalid key")); + } + + /* iterators */ + constexpr iterator begin() { return nullptr; } + constexpr const_iterator begin() const { return nullptr; } + constexpr const_iterator cbegin() const { return nullptr; } + constexpr iterator end() { return nullptr; } + constexpr const_iterator end() const { return nullptr; } + constexpr const_iterator cend() const { return nullptr; } + + constexpr reverse_iterator rbegin() { return nullptr; } + constexpr const_reverse_iterator rbegin() const { return nullptr; } + constexpr const_reverse_iterator crbegin() const { return nullptr; } + constexpr reverse_iterator rend() { return nullptr; } + constexpr const_reverse_iterator rend() const { return nullptr; } + constexpr const_reverse_iterator crend() const { return nullptr; } + + /* capacity */ + constexpr bool empty() const { return true; } + constexpr size_type size() const { return 0; } + constexpr size_type max_size() const { return 0; } + + /* lookup */ + + template + constexpr std::size_t count(KeyType const &) const { return 0; } + + template + constexpr const_iterator find(KeyType const &) const { return end(); } + template + constexpr iterator find(KeyType const &) { return end(); } + + template + constexpr std::pair + equal_range(KeyType const &) const { return {end(), end()}; } + template + constexpr std::pair + equal_range(KeyType const &) { return {end(), end()}; } + + template + constexpr const_iterator lower_bound(KeyType const &) const { return end(); } + template + constexpr iterator lower_bound(KeyType const &) { return end(); } + + template + constexpr const_iterator upper_bound(KeyType const &) const { return end(); } + template + constexpr iterator upper_bound(KeyType const &) { return end(); } + +/* observers */ + constexpr key_compare const& key_comp() const { return value_comp().key_comp(); } + constexpr value_compare const& value_comp() const { return static_cast const&>(*this); } +}; + +template > +constexpr auto make_map(bits::ignored_arg = {}/* for consistency with the initializer below for N = 0*/) { + return map{}; +} + +template +constexpr auto make_map(std::pair const (&items)[N]) { + return map{items}; +} + +template +constexpr auto make_map(std::array, N> const &items) { + return map{items}; +} + +template +constexpr auto make_map(std::pair const (&items)[N], Compare const& compare = Compare{}) { + return map{items, compare}; +} + +template +constexpr auto make_map(std::array, N> const &items, Compare const& compare = Compare{}) { + return map{items, compare}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/random.h b/lib/Frozen/frozen/random.h new file mode 100644 index 000000000..727133bb1 --- /dev/null +++ b/lib/Frozen/frozen/random.h @@ -0,0 +1,97 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_RANDOM_H +#define FROZEN_LETITGO_RANDOM_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/version.h" + +#include +#include + +namespace frozen { +template +class linear_congruential_engine { + + static_assert(std::is_unsigned::value, + "UIntType must be an unsigned integral type"); + + template + static constexpr UIntType modulo(T val, std::integral_constant) { + return static_cast(val); + } + + template + static constexpr UIntType modulo(T val, std::integral_constant) { + // the static cast below may end up doing a truncation + return static_cast(val % M); + } + +public: + using result_type = UIntType; + static constexpr result_type multiplier = a; + static constexpr result_type increment = c; + static constexpr result_type modulus = m; + static constexpr result_type default_seed = 1u; + + linear_congruential_engine() = default; + constexpr linear_congruential_engine(result_type s) { seed(s); } + + void seed(result_type s = default_seed) { state_ = s; } + constexpr result_type operator()() { + using uint_least_t = bits::select_uint_least_t; + uint_least_t tmp = static_cast(multiplier) * state_ + increment; + + state_ = modulo(tmp, std::integral_constant()); + return state_; + } + constexpr void discard(unsigned long long n) { + while (n--) + operator()(); + } + static constexpr result_type min() { return increment == 0u ? 1u : 0u; } + static constexpr result_type max() { return modulus - 1u; } + friend constexpr bool operator==(linear_congruential_engine const &self, + linear_congruential_engine const &other) { + return self.state_ == other.state_; + } + friend constexpr bool operator!=(linear_congruential_engine const &self, + linear_congruential_engine const &other) { + return !(self == other); + } + +private: + result_type state_ = default_seed; +}; + +using minstd_rand0 = + linear_congruential_engine; +using minstd_rand = + linear_congruential_engine; + +// This generator is used by default in unordered frozen containers +using default_prg_t = minstd_rand; + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/set.h b/lib/Frozen/frozen/set.h new file mode 100644 index 000000000..430d4a54c --- /dev/null +++ b/lib/Frozen/frozen/set.h @@ -0,0 +1,260 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_SET_H +#define FROZEN_SET_H + +#include "frozen/bits/algorithms.h" +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/version.h" +#include "frozen/bits/defines.h" + +#include +#include + +namespace frozen { + +template > class set : private Compare { + using container_type = bits::carray; + container_type keys_; + +public: + /* container typedefs*/ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::size_type; + using key_compare = Compare; + using value_compare = Compare; + using reference = typename container_type::const_reference; + using const_reference = reference; + using pointer = typename container_type::const_pointer; + using const_pointer = pointer; + using iterator = typename container_type::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_iterator = iterator; + using const_reverse_iterator = std::reverse_iterator; + +public: + /* constructors */ + constexpr set(const set &other) = default; + + constexpr set(container_type keys, Compare const & comp) + : Compare{comp} + , keys_(bits::quicksort(keys, value_comp())) { + } + + explicit constexpr set(container_type keys) + : set{keys, Compare{}} {} + + constexpr set(std::initializer_list keys, Compare const & comp) + : set{container_type{keys}, comp} { + constexpr_assert(keys.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr set(std::initializer_list keys) + : set{keys, Compare{}} {} + + constexpr set& operator=(const set &other) = default; + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return bits::binary_search(keys_.begin(), key, value_comp()); + } + + template + constexpr const_iterator find(KeyType const &key) const { + const_iterator where = lower_bound(key); + if ((where != end()) && !value_comp()(key, *where)) + return where; + else + return end(); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != keys_.end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + auto const lower = lower_bound(key); + if (lower == end()) + return {lower, lower}; + else + return {lower, lower + 1}; + } + + template + constexpr const_iterator lower_bound(KeyType const &key) const { + auto const where = bits::lower_bound(keys_.begin(), key, value_comp()); + if ((where != end()) && !value_comp()(key, *where)) + return where; + else + return end(); + } + + template + constexpr const_iterator upper_bound(KeyType const &key) const { + auto const where = bits::lower_bound(keys_.begin(), key, value_comp()); + if ((where != end()) && !value_comp()(key, *where)) + return where + 1; + else + return end(); + } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp(); } + constexpr const key_compare& value_comp() const { return static_cast(*this); } + + /* iterators */ + constexpr const_iterator begin() const { return keys_.begin(); } + constexpr const_iterator cbegin() const { return keys_.begin(); } + constexpr const_iterator end() const { return keys_.end(); } + constexpr const_iterator cend() const { return keys_.end(); } + + constexpr const_reverse_iterator rbegin() const { return const_reverse_iterator{keys_.end()}; } + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{keys_.end()}; } + constexpr const_reverse_iterator rend() const { return const_reverse_iterator{keys_.begin()}; } + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{keys_.begin()}; } + + /* comparison */ + constexpr bool operator==(set const& rhs) const { return bits::equal(begin(), end(), rhs.begin()); } + constexpr bool operator!=(set const& rhs) const { return !(*this == rhs); } + constexpr bool operator<(set const& rhs) const { return bits::lexicographical_compare(begin(), end(), rhs.begin(), rhs.end()); } + constexpr bool operator<=(set const& rhs) const { return (*this < rhs) || (*this == rhs); } + constexpr bool operator>(set const& rhs) const { return bits::lexicographical_compare(rhs.begin(), rhs.end(), begin(), end()); } + constexpr bool operator>=(set const& rhs) const { return (*this > rhs) || (*this == rhs); } +}; + +template class set : private Compare { + using container_type = bits::carray; // just for the type definitions + +public: + /* container typedefs*/ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::size_type; + using key_compare = Compare; + using value_compare = Compare; + using reference = typename container_type::const_reference; + using const_reference = reference; + using pointer = typename container_type::const_pointer; + using const_pointer = pointer; + using iterator = pointer; + using reverse_iterator = pointer; + using const_iterator = const_pointer; + using const_reverse_iterator = const_pointer; + +public: + /* constructors */ + constexpr set(const set &other) = default; + constexpr set(bits::carray, Compare const &) {} + explicit constexpr set(bits::carray) {} + + constexpr set(std::initializer_list, Compare const &comp) + : Compare{comp} {} + constexpr set(std::initializer_list keys) : set{keys, Compare{}} {} + + constexpr set& operator=(const set &other) = default; + + /* capacity */ + constexpr bool empty() const { return true; } + constexpr size_type size() const { return 0; } + constexpr size_type max_size() const { return 0; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &) const { return 0; } + + template + constexpr const_iterator find(KeyType const &) const { return end(); } + + template + constexpr std::pair + equal_range(KeyType const &) const { return {end(), end()}; } + + template + constexpr const_iterator lower_bound(KeyType const &) const { return end(); } + + template + constexpr const_iterator upper_bound(KeyType const &) const { return end(); } + + /* observers */ + constexpr const key_compare& key_comp() const { return value_comp(); } + constexpr const key_compare& value_comp() const { return static_cast(*this); } + + /* iterators */ + constexpr const_iterator begin() const { return nullptr; } + constexpr const_iterator cbegin() const { return nullptr; } + constexpr const_iterator end() const { return nullptr; } + constexpr const_iterator cend() const { return nullptr; } + + constexpr const_reverse_iterator rbegin() const { return nullptr; } + constexpr const_reverse_iterator crbegin() const { return nullptr; } + constexpr const_reverse_iterator rend() const { return nullptr; } + constexpr const_reverse_iterator crend() const { return nullptr; } +}; + +template +constexpr auto make_set(bits::ignored_arg = {}/* for consistency with the initializer below for N = 0*/) { + return set{}; +} + +template +constexpr auto make_set(const T (&args)[N]) { + return set(args); +} + +template +constexpr auto make_set(std::array const &args) { + return set(args); +} + +template +constexpr auto make_set(const T (&args)[N], Compare const& compare = Compare{}) { + return set(args, compare); +} + +template +constexpr auto make_set(std::array const &args, Compare const& compare = Compare{}) { + return set(args, compare); +} + +#ifdef FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +template +set(T, Args...) -> set; + +#endif // FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/string.h b/lib/Frozen/frozen/string.h new file mode 100644 index 000000000..354ed9c15 --- /dev/null +++ b/lib/Frozen/frozen/string.h @@ -0,0 +1,152 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_STRING_H +#define FROZEN_LETITGO_STRING_H + +#include "frozen/bits/elsa.h" +#include "frozen/bits/hash_string.h" +#include "frozen/bits/version.h" +#include "frozen/bits/defines.h" + +#include +#include + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW +#include +#endif + +namespace frozen { + +template +class basic_string { + using chr_t = _CharT; + + chr_t const *data_; + std::size_t size_; + +public: + template + constexpr basic_string(chr_t const (&data)[N]) + : data_(data), size_(N - 1) {} + constexpr basic_string(chr_t const *data, std::size_t size) + : data_(data), size_(size) {} + +#ifdef FROZEN_LETITGO_HAS_STRING_VIEW + constexpr basic_string(std::basic_string_view data) + : data_(data.data()), size_(data.size()) {} +#endif + + constexpr basic_string(const basic_string &) noexcept = default; + constexpr basic_string &operator=(const basic_string &) noexcept = default; + + constexpr std::size_t size() const { return size_; } + + constexpr chr_t operator[](std::size_t i) const { return data_[i]; } + + constexpr bool operator==(basic_string other) const { + if (size_ != other.size_) + return false; + for (std::size_t i = 0; i < size_; ++i) + if (data_[i] != other.data_[i]) + return false; + return true; + } + + constexpr bool operator<(const basic_string &other) const { + unsigned i = 0; + while (i < size() && i < other.size()) { + if ((*this)[i] < other[i]) { + return true; + } + if ((*this)[i] > other[i]) { + return false; + } + ++i; + } + return size() < other.size(); + } + + friend constexpr bool operator>(const basic_string& lhs, const basic_string& rhs) { + return rhs < lhs; + } + + constexpr const chr_t *data() const { return data_; } + constexpr const chr_t *begin() const { return data(); } + constexpr const chr_t *end() const { return data() + size(); } +}; + +template struct elsa> { + constexpr std::size_t operator()(basic_string<_CharT> value) const { + return hash_string(value); + } + constexpr std::size_t operator()(basic_string<_CharT> value, std::size_t seed) const { + return hash_string(value, seed); + } +}; + +using string = basic_string; +using wstring = basic_string; +using u16string = basic_string; +using u32string = basic_string; + +#ifdef FROZEN_LETITGO_HAS_CHAR8T +using u8string = basic_string; +#endif + +namespace string_literals { + +constexpr string operator"" _s(const char *data, std::size_t size) { + return {data, size}; +} + +constexpr wstring operator"" _s(const wchar_t *data, std::size_t size) { + return {data, size}; +} + +constexpr u16string operator"" _s(const char16_t *data, std::size_t size) { + return {data, size}; +} + +constexpr u32string operator"" _s(const char32_t *data, std::size_t size) { + return {data, size}; +} + +#ifdef FROZEN_LETITGO_HAS_CHAR8T +constexpr u8string operator"" _s(const char8_t *data, std::size_t size) { + return {data, size}; +} +#endif + +} // namespace string_literals + +} // namespace frozen + +namespace std { +template struct hash> { + std::size_t operator()(frozen::basic_string<_CharT> s) const { + return frozen::elsa>{}(s); + } +}; +} // namespace std + +#endif diff --git a/lib/Frozen/frozen/unordered_map.h b/lib/Frozen/frozen/unordered_map.h new file mode 100644 index 000000000..6f7b4a009 --- /dev/null +++ b/lib/Frozen/frozen/unordered_map.h @@ -0,0 +1,217 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_UNORDERED_MAP_H +#define FROZEN_LETITGO_UNORDERED_MAP_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/elsa.h" +#include "frozen/bits/exceptions.h" +#include "frozen/bits/pmh.h" +#include "frozen/bits/version.h" +#include "frozen/random.h" + +#include +#include +#include + +namespace frozen { + +namespace bits { + +struct GetKey { + template constexpr auto const &operator()(KV const &kv) const { + return kv.first; + } +}; + +} // namespace bits + +template , + class KeyEqual = std::equal_to> +class unordered_map : private KeyEqual { + static constexpr std::size_t storage_size = + bits::next_highest_power_of_two(N) * (N < 32 ? 2 : 1); // size adjustment to prevent high collision rate for small sets + using container_type = bits::carray, N>; + using tables_type = bits::pmh_tables; + + container_type items_; + tables_type tables_; + +public: + /* typedefs */ + using Self = unordered_map; + using key_type = Key; + using mapped_type = Value; + using value_type = typename container_type::value_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using hasher = Hash; + using key_equal = KeyEqual; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = typename container_type::iterator; + using const_iterator = typename container_type::const_iterator; + +public: + /* constructors */ + unordered_map(unordered_map const &) = default; + constexpr unordered_map(container_type items, + Hash const &hash, KeyEqual const &equal) + : KeyEqual{equal} + , items_{items} + , tables_{ + bits::make_pmh_tables( + items_, hash, bits::GetKey{}, default_prg_t{})} {} + explicit constexpr unordered_map(container_type items) + : unordered_map{items, Hash{}, KeyEqual{}} {} + + constexpr unordered_map(std::initializer_list items, + Hash const & hash, KeyEqual const & equal) + : unordered_map{container_type{items}, hash, equal} { + constexpr_assert(items.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + constexpr unordered_map(std::initializer_list items) + : unordered_map{items, Hash{}, KeyEqual{}} {} + + /* iterators */ + constexpr iterator begin() { return items_.begin(); } + constexpr iterator end() { return items_.end(); } + constexpr const_iterator begin() const { return items_.begin(); } + constexpr const_iterator end() const { return items_.end(); } + constexpr const_iterator cbegin() const { return items_.begin(); } + constexpr const_iterator cend() const { return items_.end(); } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return find(key) != end(); + } + + template + constexpr Value const &at(KeyType const &key) const { + return at_impl(*this, key); + } + template + constexpr Value &at(KeyType const &key) { + return at_impl(*this, key); + } + + template + constexpr const_iterator find(KeyType const &key) const { + return find_impl(*this, key, hash_function(), key_eq()); + } + template + constexpr iterator find(KeyType const &key) { + return find_impl(*this, key, hash_function(), key_eq()); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != this->end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + return equal_range_impl(*this, key); + } + template + constexpr std::pair equal_range(KeyType const &key) { + return equal_range_impl(*this, key); + } + + /* bucket interface */ + constexpr std::size_t bucket_count() const { return storage_size; } + constexpr std::size_t max_bucket_count() const { return storage_size; } + + /* observers*/ + constexpr const hasher& hash_function() const { return tables_.hash_function(); } + constexpr const key_equal& key_eq() const { return static_cast(*this); } + +private: + template + static inline constexpr auto& at_impl(This&& self, KeyType const &key) { + auto it = self.find(key); + if (it != self.end()) + return it->second; + else + FROZEN_THROW_OR_ABORT(std::out_of_range("unknown key")); + } + + template + static inline constexpr auto find_impl(This&& self, KeyType const &key, Hasher const &hash, Equal const &equal) { + auto const pos = self.tables_.lookup(key, hash); + auto it = self.items_.begin() + pos; + if (it != self.items_.end() && equal(it->first, key)) + return it; + else + return self.items_.end(); + } + + template + static inline constexpr auto equal_range_impl(This&& self, KeyType const &key) { + auto const it = self.find(key); + if (it != self.end()) + return std::make_pair(it, it + 1); + else + return std::make_pair(self.end(), self.end()); + } +}; + +template +constexpr auto make_unordered_map(std::pair const (&items)[N]) { + return unordered_map{items}; +} + +template +constexpr auto make_unordered_map( + std::pair const (&items)[N], + Hasher const &hash = elsa{}, + Equal const &equal = std::equal_to{}) { + return unordered_map{items, hash, equal}; +} + +template +constexpr auto make_unordered_map(std::array, N> const &items) { + return unordered_map{items}; +} + +template +constexpr auto make_unordered_map( + std::array, N> const &items, + Hasher const &hash = elsa{}, + Equal const &equal = std::equal_to{}) { + return unordered_map{items, hash, equal}; +} + +} // namespace frozen + +#endif diff --git a/lib/Frozen/frozen/unordered_set.h b/lib/Frozen/frozen/unordered_set.h new file mode 100644 index 000000000..81bca6c5f --- /dev/null +++ b/lib/Frozen/frozen/unordered_set.h @@ -0,0 +1,181 @@ +/* + * Frozen + * Copyright 2016 QuarksLab + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef FROZEN_LETITGO_UNORDERED_SET_H +#define FROZEN_LETITGO_UNORDERED_SET_H + +#include "frozen/bits/basic_types.h" +#include "frozen/bits/constexpr_assert.h" +#include "frozen/bits/elsa.h" +#include "frozen/bits/pmh.h" +#include "frozen/bits/version.h" +#include "frozen/random.h" + +#include + +namespace frozen { + +namespace bits { + +struct Get { + template constexpr T const &operator()(T const &key) const { + return key; + } +}; + +} // namespace bits + +template , + class KeyEqual = std::equal_to> +class unordered_set : private KeyEqual { + static constexpr std::size_t storage_size = + bits::next_highest_power_of_two(N) * (N < 32 ? 2 : 1); // size adjustment to prevent high collision rate for small sets + using container_type = bits::carray; + using tables_type = bits::pmh_tables; + + container_type keys_; + tables_type tables_; + +public: + /* typedefs */ + using key_type = Key; + using value_type = Key; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using hasher = Hash; + using key_equal = KeyEqual; + using const_reference = typename container_type::const_reference; + using reference = const_reference; + using const_pointer = typename container_type::const_pointer; + using pointer = const_pointer; + using const_iterator = typename container_type::const_iterator; + using iterator = const_iterator; + +public: + /* constructors */ + unordered_set(unordered_set const &) = default; + constexpr unordered_set(container_type keys, Hash const &hash, + KeyEqual const &equal) + : KeyEqual{equal} + , keys_{keys} + , tables_{bits::make_pmh_tables( + keys_, hash, bits::Get{}, default_prg_t{})} {} + explicit constexpr unordered_set(container_type keys) + : unordered_set{keys, Hash{}, KeyEqual{}} {} + + constexpr unordered_set(std::initializer_list keys) + : unordered_set{keys, Hash{}, KeyEqual{}} {} + + constexpr unordered_set(std::initializer_list keys, Hash const & hash, KeyEqual const & equal) + : unordered_set{container_type{keys}, hash, equal} { + constexpr_assert(keys.size() == N, "Inconsistent initializer_list size and type size argument"); + } + + /* iterators */ + constexpr const_iterator begin() const { return keys_.begin(); } + constexpr const_iterator end() const { return keys_.end(); } + constexpr const_iterator cbegin() const { return keys_.begin(); } + constexpr const_iterator cend() const { return keys_.end(); } + + /* capacity */ + constexpr bool empty() const { return !N; } + constexpr size_type size() const { return N; } + constexpr size_type max_size() const { return N; } + + /* lookup */ + template + constexpr std::size_t count(KeyType const &key) const { + return find(key, hash_function(), key_eq()) != end(); + } + + template + constexpr const_iterator find(KeyType const &key, Hasher const &hash, Equal const &equal) const { + auto const pos = tables_.lookup(key, hash); + auto it = keys_.begin() + pos; + if (it != keys_.end() && equal(*it, key)) + return it; + else + return keys_.end(); + } + template + constexpr const_iterator find(KeyType const &key) const { + auto const pos = tables_.lookup(key, hash_function()); + auto it = keys_.begin() + pos; + if (it != keys_.end() && key_eq()(*it, key)) + return it; + else + return keys_.end(); + } + + template + constexpr bool contains(KeyType const &key) const { + return this->find(key) != keys_.end(); + } + + template + constexpr std::pair equal_range(KeyType const &key) const { + auto const it = find(key); + if (it != end()) + return {it, it + 1}; + else + return {keys_.end(), keys_.end()}; + } + + /* bucket interface */ + constexpr std::size_t bucket_count() const { return storage_size; } + constexpr std::size_t max_bucket_count() const { return storage_size; } + + /* observers*/ + constexpr const hasher& hash_function() const { return tables_.hash_function(); } + constexpr const key_equal& key_eq() const { return static_cast(*this); } +}; + +template +constexpr auto make_unordered_set(T const (&keys)[N]) { + return unordered_set{keys}; +} + +template +constexpr auto make_unordered_set(T const (&keys)[N], Hasher const& hash, Equal const& equal) { + return unordered_set{keys, hash, equal}; +} + +template +constexpr auto make_unordered_set(std::array const &keys) { + return unordered_set{keys}; +} + +template +constexpr auto make_unordered_set(std::array const &keys, Hasher const& hash, Equal const& equal) { + return unordered_set{keys, hash, equal}; +} + +#ifdef FROZEN_LETITGO_HAS_DEDUCTION_GUIDES + +template +unordered_set(T, Args...) -> unordered_set; + +#endif + +} // namespace frozen + +#endif diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index d138e4f16..1416a73ab 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_2CH.h" +#include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_1CHv2.h" #include "inverters/HMS_2CH.h" @@ -24,12 +26,12 @@ void HoymilesClass::init() _radioCmt.reset(new HoymilesRadio_CMT()); } -void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ) { _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); } -void HoymilesClass::initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3) +void HoymilesClass::initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { _radioCmt->init(pin_sdio, pin_clk, pin_cs, pin_fcs, pin_gpio2, pin_gpio3); } @@ -40,110 +42,117 @@ void HoymilesClass::loop() _radioNrf->loop(); _radioCmt->loop(); - if (getNumInverters() > 0) { - if (millis() - _lastPoll > (_pollInterval * 1000)) { - static uint8_t inverterPos = 0; + if (getNumInverters() == 0) { + return; + } - std::shared_ptr iv = getInverterByPos(inverterPos); - if ((iv == nullptr) || ((iv != nullptr) && (!iv->getRadio()->isInitialized()))) { - if (++inverterPos >= getNumInverters()) { - inverterPos = 0; - } + if (millis() - _lastPoll > (_pollInterval * 1000)) { + static uint8_t inverterPos = 0; + + std::shared_ptr iv = getInverterByPos(inverterPos); + if ((iv == nullptr) || ((iv != nullptr) && (!iv->getRadio()->isInitialized()))) { + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; } + } - if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { + if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { - if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { - Hoymiles.getMessageOutput()->println("Set runtime data to zero"); - iv->Statistics()->zeroRuntimeData(); - } + if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { + iv->Statistics()->zeroRuntimeData(); + } - if (iv->getEnablePolling() || iv->getEnableCommands()) { - _messageOutput->print("Fetch inverter: "); - _messageOutput->println(iv->serial(), HEX); + if (iv->getEnablePolling() || iv->getEnableCommands()) { + _messageOutput->print("Fetch inverter: "); + _messageOutput->println(iv->serial(), HEX); - if (!iv->isReachable()) { - iv->sendChangeChannelRequest(); - } + if (!iv->isReachable()) { + iv->sendChangeChannelRequest(); + } - iv->sendStatsRequest(); + iv->sendStatsRequest(); - // Fetch event log - bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(force); + // Fetch event log + const bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; + iv->sendAlarmLogRequest(force); - // Fetch limit - if (((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) - && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - _messageOutput->println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(); - } + // Fetch limit + if (((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) + && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { + _messageOutput->println("Request SystemConfigPara"); + iv->sendSystemConfigParaRequest(); + } - // Set limit if required - if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend ActivePowerControl"); - iv->resendActivePowerControlRequest(); - } + // Set limit if required + if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend ActivePowerControl"); + iv->resendActivePowerControlRequest(); + } - // Set power status if required - if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend PowerCommand"); - iv->resendPowerControlRequest(); - } + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend PowerCommand"); + iv->resendPowerControlRequest(); + } - // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0) { - bool invalidDevInfo = !iv->DevInfo()->containsValidData() - && iv->DevInfo()->getLastUpdateAll() > 0 - && iv->DevInfo()->getLastUpdateSimple() > 0; - - if (invalidDevInfo) { - _messageOutput->println("DevInfo: No Valid Data"); - } - - if ((iv->DevInfo()->getLastUpdateAll() == 0) - || (iv->DevInfo()->getLastUpdateSimple() == 0) - || invalidDevInfo) { - _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(); - } - } + // Fetch dev info (but first fetch stats) + if (iv->Statistics()->getLastUpdate() > 0) { + const bool invalidDevInfo = !iv->DevInfo()->containsValidData() + && iv->DevInfo()->getLastUpdateAll() > 0 + && iv->DevInfo()->getLastUpdateSimple() > 0; - // Fetch grid profile - if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { - iv->sendGridOnProFileParaRequest(); + if (invalidDevInfo) { + _messageOutput->println("DevInfo: No Valid Data"); } - _lastPoll = millis(); + if ((iv->DevInfo()->getLastUpdateAll() == 0) + || (iv->DevInfo()->getLastUpdateSimple() == 0) + || invalidDevInfo) { + _messageOutput->println("Request device info"); + iv->sendDevInfoRequest(); + } } - if (++inverterPos >= getNumInverters()) { - inverterPos = 0; + // Fetch grid profile + if (iv->Statistics()->getLastUpdate() > 0 && (iv->GridProfile()->getLastUpdate() == 0 || !iv->GridProfile()->containsValidData())) { + iv->sendGridOnProFileParaRequest(); } + + _lastPoll = millis(); } - // Perform housekeeping of all inverters on day change - int8_t currentWeekDay = Utils::getWeekDay(); - static int8_t lastWeekDay = -1; - if (lastWeekDay == -1) { - lastWeekDay = currentWeekDay; - } else { - if (currentWeekDay != lastWeekDay) { + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; + } + } - for (auto& inv : _inverters) { - if (inv->getZeroYieldDayOnMidnight()) { - inv->Statistics()->zeroDailyData(); - } + // Perform housekeeping of all inverters on day change + const int8_t currentWeekDay = Utils::getWeekDay(); + static int8_t lastWeekDay = -1; + if (lastWeekDay == -1) { + lastWeekDay = currentWeekDay; + } else { + if (currentWeekDay != lastWeekDay) { + + for (auto& inv : _inverters) { + // Have to reset the offets first, otherwise it will + // Substract the offset from zero which leads to a high value + inv->Statistics()->resetYieldDayCorrection(); + if (inv->getZeroYieldDayOnMidnight()) { + inv->Statistics()->zeroDailyData(); + } + if (inv->getClearEventlogOnMidnight()) { + inv->EventLog()->clearBuffer(); } - - lastWeekDay = currentWeekDay; } + + lastWeekDay = currentWeekDay; } } } } -std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) +std::shared_ptr HoymilesClass::addInverter(const char* name, const uint64_t serial) { std::shared_ptr i = nullptr; if (HMT_4CH::isValidSerial(serial)) { @@ -164,6 +173,10 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u i = std::make_shared(_radioNrf.get(), serial); } else if (HM_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)) { + i = std::make_shared(_radioNrf.get(), serial); } if (i) { @@ -176,7 +189,7 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u return nullptr; } -std::shared_ptr HoymilesClass::getInverterByPos(uint8_t pos) +std::shared_ptr HoymilesClass::getInverterByPos(const uint8_t pos) { if (pos >= _inverters.size()) { return nullptr; @@ -185,7 +198,7 @@ std::shared_ptr HoymilesClass::getInverterByPos(uint8_t pos) } } -std::shared_ptr HoymilesClass::getInverterBySerial(uint64_t serial) +std::shared_ptr HoymilesClass::getInverterBySerial(const uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { @@ -195,9 +208,9 @@ std::shared_ptr HoymilesClass::getInverterBySerial(uint64_t se return nullptr; } -std::shared_ptr HoymilesClass::getInverterByFragment(fragment_t* fragment) +std::shared_ptr HoymilesClass::getInverterByFragment(const fragment_t& fragment) { - if (fragment->len <= 4) { + if (fragment.len <= 4) { return nullptr; } @@ -207,10 +220,10 @@ std::shared_ptr HoymilesClass::getInverterByFragment(fragment_ serial_u p; p.u64 = inv->serial(); - if ((p.b[3] == fragment->fragment[1]) - && (p.b[2] == fragment->fragment[2]) - && (p.b[1] == fragment->fragment[3]) - && (p.b[0] == fragment->fragment[4])) { + if ((p.b[3] == fragment.fragment[1]) + && (p.b[2] == fragment.fragment[2]) + && (p.b[1] == fragment.fragment[3]) + && (p.b[0] == fragment.fragment[4])) { return inv; } @@ -218,7 +231,7 @@ std::shared_ptr HoymilesClass::getInverterByFragment(fragment_ return nullptr; } -void HoymilesClass::removeInverterBySerial(uint64_t serial) +void HoymilesClass::removeInverterBySerial(const uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { @@ -229,7 +242,7 @@ void HoymilesClass::removeInverterBySerial(uint64_t serial) } } -size_t HoymilesClass::getNumInverters() +size_t HoymilesClass::getNumInverters() const { return _inverters.size(); } @@ -244,17 +257,17 @@ HoymilesRadio_CMT* HoymilesClass::getRadioCmt() return _radioCmt.get(); } -bool HoymilesClass::isAllRadioIdle() +bool HoymilesClass::isAllRadioIdle() const { return _radioNrf.get()->isIdle() && _radioCmt.get()->isIdle(); } -uint32_t HoymilesClass::PollInterval() +uint32_t HoymilesClass::PollInterval() const { return _pollInterval; } -void HoymilesClass::setPollInterval(uint32_t interval) +void HoymilesClass::setPollInterval(const uint32_t interval) { _pollInterval = interval; } @@ -267,4 +280,4 @@ void HoymilesClass::setMessageOutput(Print* output) Print* HoymilesClass::getMessageOutput() { return _messageOutput; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 11b847632..42724e066 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -16,27 +16,27 @@ class HoymilesClass { public: void init(); - void initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); - void initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3); + void initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ); + void initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); void setMessageOutput(Print* output); Print* getMessageOutput(); - std::shared_ptr addInverter(const char* name, uint64_t serial); - std::shared_ptr getInverterByPos(uint8_t pos); - std::shared_ptr getInverterBySerial(uint64_t serial); - std::shared_ptr getInverterByFragment(fragment_t* fragment); - void removeInverterBySerial(uint64_t serial); - size_t getNumInverters(); + std::shared_ptr addInverter(const char* name, const uint64_t serial); + std::shared_ptr getInverterByPos(const uint8_t pos); + std::shared_ptr getInverterBySerial(const uint64_t serial); + std::shared_ptr getInverterByFragment(const fragment_t& fragment); + void removeInverterBySerial(const uint64_t serial); + size_t getNumInverters() const; HoymilesRadio_NRF* getRadioNrf(); HoymilesRadio_CMT* getRadioCmt(); - uint32_t PollInterval(); - void setPollInterval(uint32_t interval); + uint32_t PollInterval() const; + void setPollInterval(const uint32_t interval); - bool isAllRadioIdle(); + bool isAllRadioIdle() const; private: std::vector> _inverters; diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 4afc447a7..9d288554e 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -6,17 +6,17 @@ #include "Hoymiles.h" #include "crc.h" -serial_u HoymilesRadio::DtuSerial() +serial_u HoymilesRadio::DtuSerial() const { return _dtuSerial; } -void HoymilesRadio::setDtuSerial(uint64_t serial) +void HoymilesRadio::setDtuSerial(const uint64_t serial) { _dtuSerial.u64 = serial; } -serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) +serial_u HoymilesRadio::convertSerialToRadioId(const serial_u serial) { serial_u radioId; radioId.u64 = 0; @@ -28,27 +28,27 @@ serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) return radioId; } -bool HoymilesRadio::checkFragmentCrc(fragment_t* fragment) +bool HoymilesRadio::checkFragmentCrc(const fragment_t& fragment) const { - uint8_t crc = crc8(fragment->fragment, fragment->len - 1); - return (crc == fragment->fragment[fragment->len - 1]); + const uint8_t crc = crc8(fragment.fragment, fragment.len - 1); + return (crc == fragment.fragment[fragment.len - 1]); } -void HoymilesRadio::sendRetransmitPacket(uint8_t fragment_id) +void HoymilesRadio::sendRetransmitPacket(const uint8_t fragment_id) { CommandAbstract* cmd = _commandQueue.front().get(); CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); if (requestCmd != nullptr) { - sendEsbPacket(requestCmd); + sendEsbPacket(*requestCmd); } } void HoymilesRadio::sendLastPacketAgain() { CommandAbstract* cmd = _commandQueue.front().get(); - sendEsbPacket(cmd); + sendEsbPacket(*cmd); } void HoymilesRadio::handleReceivedPackage() @@ -59,7 +59,7 @@ void HoymilesRadio::handleReceivedPackage() if (nullptr != inv) { CommandAbstract* cmd = _commandQueue.front().get(); - uint8_t verifyResult = inv->verifyAllFragments(cmd); + uint8_t verifyResult = inv->verifyAllFragments(*cmd); if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { Hoymiles.getMessageOutput()->println("Nothing received, resend whole request"); sendLastPacketAgain(); @@ -105,7 +105,7 @@ void HoymilesRadio::handleReceivedPackage() auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); if (nullptr != inv) { inv->clearRxFragmentBuffer(); - sendEsbPacket(cmd); + sendEsbPacket(*cmd); } else { Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); _commandQueue.pop(); @@ -114,7 +114,7 @@ void HoymilesRadio::handleReceivedPackage() } } -void HoymilesRadio::dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline) +void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline) { for (uint8_t i = 0; i < len; i++) { Hoymiles.getMessageOutput()->printf("%02X ", buf[i]); @@ -124,17 +124,17 @@ void HoymilesRadio::dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline } } -bool HoymilesRadio::isInitialized() +bool HoymilesRadio::isInitialized() const { return _isInitialized; } -bool HoymilesRadio::isIdle() +bool HoymilesRadio::isIdle() const { return !_busyFlag; } -bool HoymilesRadio::isQueueEmpty() +bool HoymilesRadio::isQueueEmpty() const { return _commandQueue.size() == 0; } diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index fa2f6945d..296b479bb 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,20 +1,20 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "TimeoutHelper.h" #include "commands/CommandAbstract.h" #include "types.h" -#include #include +#include +#include class HoymilesRadio { public: - serial_u DtuSerial(); - virtual void setDtuSerial(uint64_t serial); + serial_u DtuSerial() const; + virtual void setDtuSerial(const uint64_t serial); - bool isIdle(); - bool isQueueEmpty(); - bool isInitialized(); + bool isIdle() const; + bool isQueueEmpty() const; + bool isInitialized() const; void enqueCommand(std::shared_ptr cmd) { @@ -22,18 +22,18 @@ class HoymilesRadio { } template - std::shared_ptr prepareCommand() + std::shared_ptr prepareCommand(InverterAbstract* inv) { - return std::make_shared(); + return std::make_shared(inv); } protected: - static serial_u convertSerialToRadioId(serial_u serial); - void dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline = true); + static serial_u convertSerialToRadioId(const serial_u serial); + static void dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline = true); - bool checkFragmentCrc(fragment_t* fragment); - virtual void sendEsbPacket(CommandAbstract* cmd) = 0; - void sendRetransmitPacket(uint8_t fragment_id); + bool checkFragmentCrc(const fragment_t& fragment) const; + virtual void sendEsbPacket(CommandAbstract& cmd) = 0; + void sendRetransmitPacket(const uint8_t fragment_id); void sendLastPacketAgain(); void handleReceivedPackage(); @@ -43,4 +43,4 @@ class HoymilesRadio { bool _busyFlag = false; TimeoutHelper _rxTimeout; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index c7dc5a266..bbd31f212 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -1,49 +1,79 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HoymilesRadio_CMT.h" #include "Hoymiles.h" #include "crc.h" #include +#include -#define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min -#define HOY_BASE_FREQ 860000000 -// offset from initalized CMT base frequency to Hoy base frequency in channels -#define CMT_BASE_CH_OFFSET860 ((CMT_BASE_FREQ - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) +constexpr CountryFrequencyDefinition_t make_value(FrequencyBand_t Band, uint32_t Freq_Legal_Min, uint32_t Freq_Legal_Max, uint32_t Freq_Default, uint32_t Freq_StartUp) +{ + // frequency can not be lower than actual initailized base freq + 250000 + uint32_t minFrequency = CMT2300A::getBaseFrequency(Band) + HoymilesRadio_CMT::getChannelWidth(); + + // =923500, 0xFF does not work + uint32_t maxFrequency = CMT2300A::getBaseFrequency(Band) + 0xFE * HoymilesRadio_CMT::getChannelWidth(); -// frequency can not be lower than actual initailized base freq -#define MIN_FREQ_KHZ ((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) + CountryFrequencyDefinition_t v = { Band, minFrequency, maxFrequency, Freq_Legal_Min, Freq_Legal_Max, Freq_Default, Freq_StartUp }; + return v; +} -// =923500, 0xFF does not work -#define MAX_FREQ_KHZ ((HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) +constexpr frozen::map countryDefinition = { + { CountryModeId_t::MODE_EU, make_value(FrequencyBand_t::BAND_860, 863e6, 870e6, 865e6, 868e6) }, + { CountryModeId_t::MODE_US, make_value(FrequencyBand_t::BAND_900, 905e6, 925e6, 918e6, 915e6) }, + { CountryModeId_t::MODE_BR, make_value(FrequencyBand_t::BAND_900, 915e6, 928e6, 918e6, 915e6) }, +}; -float HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) +uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const { - return (CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0; + return (_radio->getBaseFrequency() + channel * getChannelWidth()); } -uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t freq_kHz) +uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const { - if ((freq_kHz % 250) != 0) { - Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by 250 kHz!\r\n", freq_kHz / 1000.0); + if ((frequency % getChannelWidth()) != 0) { + Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth()); return 0xFF; // ERROR } - if (freq_kHz < MIN_FREQ_KHZ || freq_kHz > MAX_FREQ_KHZ) { + if (frequency < getMinFrequency() || frequency > getMaxFrequency()) { Hoymiles.getMessageOutput()->printf("%.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", - freq_kHz / 1000.0, MIN_FREQ_KHZ / 1000.0, MAX_FREQ_KHZ / 1000.0); + frequency / 1000000.0, getMinFrequency() / 1000000.0, getMaxFrequency() / 1000000.0); return 0xFF; // ERROR } - if (freq_kHz < 863000 || freq_kHz > 870000) { - Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n", - freq_kHz / 1000.0); + 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", + frequency / 1000000.0, + static_cast(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6), + static_cast(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6)); + } + + return (frequency - _radio->getBaseFrequency()) / getChannelWidth(); // frequency to channel +} + +std::vector HoymilesRadio_CMT::getCountryFrequencyList() const +{ + std::vector v; + for (const auto& [key, value] : countryDefinition) { + CountryFrequencyList_t s; + s.mode = key; + s.definition.Band = value.Band; + s.definition.Freq_Default = value.Freq_Default; + s.definition.Freq_StartUp = value.Freq_StartUp; + s.definition.Freq_Min = value.Freq_Min; + s.definition.Freq_Max = value.Freq_Max; + s.definition.Freq_Legal_Max = value.Freq_Legal_Max; + s.definition.Freq_Legal_Min = value.Freq_Legal_Min; + + v.push_back(s); } - return (freq_kHz * 1000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - CMT_BASE_CH_OFFSET860; // frequency to channel + return v; } -bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) +bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_frequency) { - const uint8_t toChannel = getChannelFromFrequency(to_freq_kHz); + const uint8_t toChannel = getChannelFromFrequency(to_frequency); if (toChannel == 0xFF) { return false; } @@ -53,7 +83,7 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) return true; } -void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3) +void HoymilesRadio_CMT::init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { _dtuSerial.u64 = 0; @@ -61,6 +91,7 @@ void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int _radio->begin(); + setCountryMode(CountryModeId_t::MODE_EU); cmtSwitchDtuFreq(_inverterTargetFrequency); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched if (!_radio->isChipConnected()) { @@ -122,19 +153,19 @@ void HoymilesRadio_CMT::loop() // Perform package parsing only if no packages are received if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); - if (checkFragmentCrc(&f)) { + if (checkFragmentCrc(f)) { - serial_u dtuId = convertSerialToRadioId(_dtuSerial); + const serial_u dtuId = convertSerialToRadioId(_dtuSerial); // The CMT RF module does not filter foreign packages by itself. // Has to be done manually here. if (memcmp(&f.fragment[5], &dtuId.b[1], 4) == 0) { - std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + std::shared_ptr inv = Hoymiles.getInverterByFragment(f); if (nullptr != inv) { // Save packet in inverter rx buffer - Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel)); + 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); @@ -156,7 +187,7 @@ void HoymilesRadio_CMT::loop() handleReceivedPackage(); } -void HoymilesRadio_CMT::setPALevel(int8_t paLevel) +void HoymilesRadio_CMT::setPALevel(const int8_t paLevel) { if (!_isInitialized) { return; @@ -169,7 +200,7 @@ void HoymilesRadio_CMT::setPALevel(int8_t paLevel) } } -void HoymilesRadio_CMT::setInverterTargetFrequency(uint32_t frequency) +void HoymilesRadio_CMT::setInverterTargetFrequency(const uint32_t frequency) { _inverterTargetFrequency = frequency; if (!_isInitialized) { @@ -178,12 +209,12 @@ void HoymilesRadio_CMT::setInverterTargetFrequency(uint32_t frequency) cmtSwitchDtuFreq(_inverterTargetFrequency); } -uint32_t HoymilesRadio_CMT::getInverterTargetFrequency() +uint32_t HoymilesRadio_CMT::getInverterTargetFrequency() const { return _inverterTargetFrequency; } -bool HoymilesRadio_CMT::isConnected() +bool HoymilesRadio_CMT::isConnected() const { if (!_isInitialized) { return false; @@ -191,14 +222,34 @@ bool HoymilesRadio_CMT::isConnected() return _radio->isChipConnected(); } -uint32_t HoymilesRadio_CMT::getMinFrequency() +uint32_t HoymilesRadio_CMT::getMinFrequency() const +{ + return countryDefinition.at(_countryMode).Freq_Min; +} + +uint32_t HoymilesRadio_CMT::getMaxFrequency() const +{ + return countryDefinition.at(_countryMode).Freq_Max; +} + +CountryModeId_t HoymilesRadio_CMT::getCountryMode() const +{ + return _countryMode; +} + +void HoymilesRadio_CMT::setCountryMode(const CountryModeId_t mode) { - return MIN_FREQ_KHZ; + _countryMode = mode; + if (!_isInitialized) { + return; + } + _radio->setFrequencyBand(countryDefinition.at(mode).Band); } -uint32_t HoymilesRadio_CMT::getMaxFrequency() +uint32_t HoymilesRadio_CMT::getInvBootFrequency() const { - return MAX_FREQ_KHZ; + // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min + return countryDefinition.at(_countryMode).Freq_StartUp; } void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt1() @@ -211,27 +262,27 @@ void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt2() _packetReceived = true; } -void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) +void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd) { - cmd->incrementSendCount(); + cmd.incrementSendCount(); - cmd->setRouterAddress(DtuSerial().u64); + cmd.setRouterAddress(DtuSerial().u64); _radio->stopListening(); - if (cmd->getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command - cmtSwitchDtuFreq(HOY_BOOT_FREQ / 1000); + if (cmd.getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command + cmtSwitchDtuFreq(getInvBootFrequency()); } Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ", - cmd->getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); - cmd->dumpDataPayload(Hoymiles.getMessageOutput()); + cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0); + cmd.dumpDataPayload(Hoymiles.getMessageOutput()); - if (!_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { + if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) { Hoymiles.getMessageOutput()->println("TX SPI Timeout"); } cmtSwitchDtuFreq(_inverterTargetFrequency); _radio->startListening(); _busyFlag = true; - _rxTimeout.set(cmd->getTimeout()); + _rxTimeout.set(cmd.getTimeout()); } diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 66314b3d4..770617fe3 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -8,35 +8,69 @@ #include #include #include +#include // number of fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 #ifndef HOYMILES_CMT_WORK_FREQ -#define HOYMILES_CMT_WORK_FREQ 865000 +#define HOYMILES_CMT_WORK_FREQ 865000000 #endif +enum CountryModeId_t { + MODE_EU, + MODE_US, + MODE_BR, + CountryModeId_Max +}; + +struct CountryFrequencyDefinition_t { + FrequencyBand_t Band; + uint32_t Freq_Min; + uint32_t Freq_Max; + uint32_t Freq_Legal_Min; + uint32_t Freq_Legal_Max; + uint32_t Freq_Default; + uint32_t Freq_StartUp; +}; + +struct CountryFrequencyList_t { + CountryModeId_t mode; + CountryFrequencyDefinition_t definition; +}; + class HoymilesRadio_CMT : public HoymilesRadio { public: - void init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3); + void init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); - void setPALevel(int8_t paLevel); - void setInverterTargetFrequency(uint32_t frequency); - uint32_t getInverterTargetFrequency(); + void setPALevel(const int8_t paLevel); + void setInverterTargetFrequency(const uint32_t frequency); + uint32_t getInverterTargetFrequency() const; + + bool isConnected() const; - bool isConnected(); + uint32_t getMinFrequency() const; + uint32_t getMaxFrequency() const; + static constexpr uint32_t getChannelWidth() + { + return FH_OFFSET * CMT2300A_ONE_STEP_SIZE; + } - static uint32_t getMinFrequency(); - static uint32_t getMaxFrequency(); + CountryModeId_t getCountryMode() const; + void setCountryMode(const CountryModeId_t mode); - static float getFrequencyFromChannel(const uint8_t channel); - static uint8_t getChannelFromFrequency(const uint32_t freq_kHz); + uint32_t getInvBootFrequency() const; + + uint32_t getFrequencyFromChannel(const uint8_t channel) const; + uint8_t getChannelFromFrequency(const uint32_t frequency) const; + + std::vector getCountryFrequencyList() const; private: void ARDUINO_ISR_ATTR handleInt1(); void ARDUINO_ISR_ATTR handleInt2(); - void sendEsbPacket(CommandAbstract* cmd); + void sendEsbPacket(CommandAbstract& cmd); std::unique_ptr _radio; @@ -51,5 +85,7 @@ class HoymilesRadio_CMT : public HoymilesRadio { uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ; - bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); -}; \ No newline at end of file + bool cmtSwitchDtuFreq(const uint32_t to_frequency); + + CountryModeId_t _countryMode; +}; diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index ecb9501e1..4bf104ade 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -8,7 +8,7 @@ #include #include -void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ) { _dtuSerial.u64 = 0; @@ -71,8 +71,8 @@ void HoymilesRadio_NRF::loop() // Perform package parsing only if no packages are received if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); - if (checkFragmentCrc(&f)) { - std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + if (checkFragmentCrc(f)) { + std::shared_ptr inv = Hoymiles.getInverterByFragment(f); if (nullptr != inv) { // Save packet in inverter rx buffer @@ -97,7 +97,7 @@ void HoymilesRadio_NRF::loop() handleReceivedPackage(); } -void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) +void HoymilesRadio_NRF::setPALevel(const rf24_pa_dbm_e paLevel) { if (!_isInitialized) { return; @@ -105,7 +105,7 @@ void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) _radio->setPALevel(paLevel); } -void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) +void HoymilesRadio_NRF::setDtuSerial(const uint64_t serial) { HoymilesRadio::setDtuSerial(serial); @@ -115,7 +115,7 @@ void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) openReadingPipe(); } -bool HoymilesRadio_NRF::isConnected() +bool HoymilesRadio_NRF::isConnected() const { if (!_isInitialized) { return false; @@ -123,7 +123,7 @@ bool HoymilesRadio_NRF::isConnected() return _radio->isChipConnected(); } -bool HoymilesRadio_NRF::isPVariant() +bool HoymilesRadio_NRF::isPVariant() const { if (!_isInitialized) { return false; @@ -133,15 +133,13 @@ bool HoymilesRadio_NRF::isPVariant() void HoymilesRadio_NRF::openReadingPipe() { - serial_u s; - s = convertSerialToRadioId(_dtuSerial); + const serial_u s = convertSerialToRadioId(_dtuSerial); _radio->openReadingPipe(1, s.u64); } -void HoymilesRadio_NRF::openWritingPipe(serial_u serial) +void HoymilesRadio_NRF::openWritingPipe(const serial_u serial) { - serial_u s; - s = convertSerialToRadioId(serial); + const serial_u s = convertSerialToRadioId(serial); _radio->openWritingPipe(s.u64); } @@ -171,29 +169,29 @@ void HoymilesRadio_NRF::switchRxCh() _radio->startListening(); } -void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) +void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd) { - cmd->incrementSendCount(); + cmd.incrementSendCount(); - cmd->setRouterAddress(DtuSerial().u64); + cmd.setRouterAddress(DtuSerial().u64); _radio->stopListening(); _radio->setChannel(getTxNxtChannel()); serial_u s; - s.u64 = cmd->getTargetAddress(); + s.u64 = cmd.getTargetAddress(); openWritingPipe(s); _radio->setRetries(3, 15); Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", - cmd->getCommandName().c_str(), _radio->getChannel()); - cmd->dumpDataPayload(Hoymiles.getMessageOutput()); - _radio->write(cmd->getDataPayload(), cmd->getDataSize()); + cmd.getCommandName().c_str(), _radio->getChannel()); + cmd.dumpDataPayload(Hoymiles.getMessageOutput()); + _radio->write(cmd.getDataPayload(), cmd.getDataSize()); _radio->setRetries(0, 0); openReadingPipe(); _radio->setChannel(getRxNxtChannel()); _radio->startListening(); _busyFlag = true; - _rxTimeout.set(cmd->getTimeout()); + _rxTimeout.set(cmd.getTimeout()); } diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.h b/lib/Hoymiles/src/HoymilesRadio_NRF.h index 8530a0e34..a6777ce52 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.h +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.h @@ -13,14 +13,14 @@ class HoymilesRadio_NRF : public HoymilesRadio { public: - void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); + void init(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ); void loop(); - void setPALevel(rf24_pa_dbm_e paLevel); + void setPALevel(const rf24_pa_dbm_e paLevel); - virtual void setDtuSerial(uint64_t serial); + virtual void setDtuSerial(const uint64_t serial); - bool isConnected(); - bool isPVariant(); + bool isConnected() const; + bool isPVariant() const; private: void ARDUINO_ISR_ATTR handleIntr(); @@ -28,9 +28,9 @@ class HoymilesRadio_NRF : public HoymilesRadio { uint8_t getTxNxtChannel(); void switchRxCh(); void openReadingPipe(); - void openWritingPipe(serial_u serial); + void openWritingPipe(const serial_u serial); - void sendEsbPacket(CommandAbstract* cmd); + void sendEsbPacket(CommandAbstract& cmd); std::unique_ptr _spiPtr; std::unique_ptr _radio; diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 78bcd55eb..b9e8eea26 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -1,14 +1,32 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to send a limit to the inverter. + +Derives from DevControlCommand. + +Command structure: +SCmd: Sub-Command ID. Is always 0x0b +Limit: limit to be set in the inverter +Type: absolute / relative and persistant/non-persistant + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +------------------------------------------------------------------------------------------------------------------- + |<------ CRC16 ------>| +51 71 60 35 46 80 12 23 04 81 0b 00 00 00 00 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Cmd SCmd ? Limit Type CRC16 CRC8 +*/ #include "ActivePowerControlCommand.h" #include "inverters/InverterAbstract.h" #define CRC_SIZE 6 -ActivePowerControlCommand::ActivePowerControlCommand(uint64_t target_address, uint64_t router_address) - : DevControlCommand(target_address, router_address) +ActivePowerControlCommand::ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address) + : DevControlCommand(inv, router_address) { _payload[10] = 0x0b; _payload[11] = 0x00; @@ -17,21 +35,21 @@ ActivePowerControlCommand::ActivePowerControlCommand(uint64_t target_address, ui _payload[14] = 0x00; _payload[15] = 0x00; - udpateCRC(CRC_SIZE); // 2 byte crc + udpateCRC(CRC_SIZE); // 6 byte crc _payload_size = 18; setTimeout(2000); } -String ActivePowerControlCommand::getCommandName() +String ActivePowerControlCommand::getCommandName() const { return "ActivePowerControl"; } -void ActivePowerControlCommand::setActivePowerLimit(float limit, PowerLimitControlType type) +void ActivePowerControlCommand::setActivePowerLimit(const float limit, const PowerLimitControlType type) { - uint16_t l = limit * 10; + const uint16_t l = limit * 10; // limit _payload[12] = (l >> 8) & 0xff; @@ -44,30 +62,30 @@ void ActivePowerControlCommand::setActivePowerLimit(float limit, PowerLimitContr udpateCRC(CRC_SIZE); } -bool ActivePowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { - if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) { return false; } if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) { - inverter->SystemConfigPara()->setLimitPercent(getLimit()); + _inv->SystemConfigPara()->setLimitPercent(getLimit()); } else { - uint16_t max_power = inverter->DevInfo()->getMaxPower(); + const uint16_t max_power = _inv->DevInfo()->getMaxPower(); if (max_power > 0) { - inverter->SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); + _inv->SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); } else { // TODO(tbnobody): Not implemented yet because we only can publish the percentage value } } - inverter->SystemConfigPara()->setLastUpdateCommand(millis()); - inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); + _inv->SystemConfigPara()->setLastUpdateCommand(millis()); + _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); return true; } -float ActivePowerControlCommand::getLimit() +float ActivePowerControlCommand::getLimit() const { - uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); + const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); return l / 10; } @@ -76,7 +94,7 @@ PowerLimitControlType ActivePowerControlCommand::getType() return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]); } -void ActivePowerControlCommand::gotTimeout(InverterAbstract* inverter) +void ActivePowerControlCommand::gotTimeout() { - inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); -} \ No newline at end of file + _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h index f3a359ead..375b278bb 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h @@ -12,14 +12,14 @@ typedef enum { // ToDo: to be verified by field tests class ActivePowerControlCommand : public DevControlCommand { public: - explicit ActivePowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); - void setActivePowerLimit(float limit, PowerLimitControlType type = RelativNonPersistent); - float getLimit(); + void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent); + float getLimit() const; PowerLimitControlType getType(); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index 574e0be2b..98a97d0b1 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -1,45 +1,63 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch the eventlog from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x11 +* AlarmId: The last event id received from the inverter or zero in case that no events + has been received yet. --> Not Implemented yet + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 11 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap AlarmId Password CRC16 CRC8 +*/ #include "AlarmDataCommand.h" #include "inverters/InverterAbstract.h" -AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +AlarmDataCommand::AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x11); setTimeout(750); } -String AlarmDataCommand::getCommandName() +String AlarmDataCommand::getCommandName() const { return "AlarmData"; } -bool AlarmDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool AlarmDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter->EventLog()->beginAppendFragment(); - inverter->EventLog()->clearBuffer(); + _inv->EventLog()->beginAppendFragment(); + _inv->EventLog()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->EventLog()->endAppendFragment(); - inverter->EventLog()->setLastAlarmRequestSuccess(CMD_OK); - inverter->EventLog()->setLastUpdate(millis()); + _inv->EventLog()->endAppendFragment(); + _inv->EventLog()->setLastAlarmRequestSuccess(CMD_OK); + _inv->EventLog()->setLastUpdate(millis()); return true; } -void AlarmDataCommand::gotTimeout(InverterAbstract* inverter) +void AlarmDataCommand::gotTimeout() { - inverter->EventLog()->setLastAlarmRequestSuccess(CMD_NOK); -} \ No newline at end of file + _inv->EventLog()->setLastAlarmRequestSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.h b/lib/Hoymiles/src/commands/AlarmDataCommand.h index 1c34a826c..ef8404c30 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.h +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.h @@ -5,10 +5,10 @@ class AlarmDataCommand : public MultiDataCommand { public: - explicit AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp index 139bbea34..ad89f2d5f 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp @@ -1,39 +1,73 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Special command to set frequency channel on HMS/HMT inverters. + +Command structure: +* ID: fixed identifier and everytime 0x56 +* CH: Channel to which the inverter will be switched to + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------- +56 71 60 35 46 80 12 23 04 02 15 21 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (860 MHz band) +56 71 60 35 46 80 12 23 04 03 17 3c 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (900 MHz band) +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^ +ID Target Addr Source Addr ? ? ? CH ? CRC8 +*/ #include "ChannelChangeCommand.h" -ChannelChangeCommand::ChannelChangeCommand(uint64_t target_address, uint64_t router_address, uint8_t channel) - : CommandAbstract(target_address, router_address) +ChannelChangeCommand::ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t channel) + : CommandAbstract(inv, router_address) { _payload[0] = 0x56; - _payload[9] = 0x02; - _payload[10] = 0x15; - _payload[11] = 0x21; _payload[13] = 0x14; _payload_size = 14; + setCountryMode(CountryModeId_t::MODE_EU); setChannel(channel); setTimeout(10); } -String ChannelChangeCommand::getCommandName() +String ChannelChangeCommand::getCommandName() const { return "ChannelChangeCommand"; } -void ChannelChangeCommand::setChannel(uint8_t channel) +void ChannelChangeCommand::setChannel(const uint8_t channel) { _payload[12] = channel; } -uint8_t ChannelChangeCommand::getChannel() +uint8_t ChannelChangeCommand::getChannel() const { return _payload[12]; } -bool ChannelChangeCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +void ChannelChangeCommand::setCountryMode(const CountryModeId_t mode) +{ + switch (mode) { + case CountryModeId_t::MODE_US: + _payload[9] = 0x03; + _payload[10] = 0x17; + _payload[11] = 0x3c; + break; + case CountryModeId_t::MODE_BR: + _payload[9] = 0x03; + _payload[10] = 0x17; + _payload[11] = 0x3c; + break; + default: + _payload[9] = 0x02; + _payload[10] = 0x15; + _payload[11] = 0x21; + break; + } +} + +bool ChannelChangeCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { return true; } @@ -42,4 +76,4 @@ uint8_t ChannelChangeCommand::getMaxResendCount() { // This command will never retrieve an answer. Therefor it's not required to repeat it return 0; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.h b/lib/Hoymiles/src/commands/ChannelChangeCommand.h index b646217c2..70b5f64c7 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.h +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.h @@ -2,17 +2,20 @@ #pragma once #include "CommandAbstract.h" +#include "../HoymilesRadio_CMT.h" class ChannelChangeCommand : public CommandAbstract { public: - explicit ChannelChangeCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t channel = 0); + explicit ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t channel = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - void setChannel(uint8_t channel); - uint8_t getChannel(); + void setChannel(const uint8_t channel); + uint8_t getChannel() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + void setCountryMode(const CountryModeId_t mode); + + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual uint8_t getMaxResendCount(); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index 78d8d07d7..16a7857e1 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -1,17 +1,44 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +Command structure: +* Each package has a maximum of 32 bytes +* Target Address: the address of the inverter. Has to be read as hex value +* Source Address the address of the dtu itself. Has to be read as hex value +* CRC8: a crc8 checksum added to the end of the payload containing all valid data. + Each sub-commmand has to set it's own payload size. + +Conversion of Target Addr: +Inverter Serial Number: (0x)116171603546 +Target Address: 71 60 35 46 + +Conversion of Source Addr: +DTU Serial Number: (0x)199980122304 +Source Address: 80 12 23 04 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------- +|<------------- CRC8 ------------>| +00 71 60 35 46 80 12 23 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ + Target Addr Source Addr CRC8 +*/ #include "CommandAbstract.h" #include "crc.h" #include +#include "../inverters/InverterAbstract.h" -CommandAbstract::CommandAbstract(uint64_t target_address, uint64_t router_address) +CommandAbstract::CommandAbstract(InverterAbstract* inv, const uint64_t router_address) { memset(_payload, 0, RF_LEN); _payload_size = 0; - setTargetAddress(target_address); + _inv = inv; + + setTargetAddress(_inv->serial()); setRouterAddress(router_address); setSendCount(0); setTimeout(0); @@ -32,48 +59,48 @@ void CommandAbstract::dumpDataPayload(Print* stream) stream->println(""); } -uint8_t CommandAbstract::getDataSize() +uint8_t CommandAbstract::getDataSize() const { return _payload_size + 1; // Original payload plus crc8 } -void CommandAbstract::setTargetAddress(uint64_t address) +void CommandAbstract::setTargetAddress(const uint64_t address) { convertSerialToPacketId(&_payload[1], address); _targetAddress = address; } -uint64_t CommandAbstract::getTargetAddress() +uint64_t CommandAbstract::getTargetAddress() const { return _targetAddress; } -void CommandAbstract::setRouterAddress(uint64_t address) +void CommandAbstract::setRouterAddress(const uint64_t address) { convertSerialToPacketId(&_payload[5], address); _routerAddress = address; } -uint64_t CommandAbstract::getRouterAddress() +uint64_t CommandAbstract::getRouterAddress() const { return _routerAddress; } -void CommandAbstract::setTimeout(uint32_t timeout) +void CommandAbstract::setTimeout(const uint32_t timeout) { _timeout = timeout; } -uint32_t CommandAbstract::getTimeout() +uint32_t CommandAbstract::getTimeout() const { return _timeout; } -void CommandAbstract::setSendCount(uint8_t count) +void CommandAbstract::setSendCount(const uint8_t count) { _sendCount = count; } -uint8_t CommandAbstract::getSendCount() +uint8_t CommandAbstract::getSendCount() const { return _sendCount; } @@ -83,12 +110,12 @@ uint8_t CommandAbstract::incrementSendCount() return _sendCount++; } -CommandAbstract* CommandAbstract::getRequestFrameCommand(uint8_t frame_no) +CommandAbstract* CommandAbstract::getRequestFrameCommand(const uint8_t frame_no) { return nullptr; } -void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial) +void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], const uint64_t serial) { serial_u s; s.u64 = serial; @@ -98,16 +125,16 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial) buffer[0] = s.b[3]; } -void CommandAbstract::gotTimeout(InverterAbstract* inverter) +void CommandAbstract::gotTimeout() { } -uint8_t CommandAbstract::getMaxResendCount() +uint8_t CommandAbstract::getMaxResendCount() const { return MAX_RESEND_COUNT; } -uint8_t CommandAbstract::getMaxRetransmitCount() +uint8_t CommandAbstract::getMaxRetransmitCount() const { return MAX_RETRANSMIT_COUNT; } diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index e6abc686c..c93cb3416 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -13,39 +13,38 @@ class InverterAbstract; class CommandAbstract { public: - explicit CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0); + explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0); virtual ~CommandAbstract() {}; const uint8_t* getDataPayload(); void dumpDataPayload(Print* stream); - uint8_t getDataSize(); + uint8_t getDataSize() const; - void setTargetAddress(uint64_t address); - uint64_t getTargetAddress(); + uint64_t getTargetAddress() const; - void setRouterAddress(uint64_t address); - uint64_t getRouterAddress(); + void setRouterAddress(const uint64_t address); + uint64_t getRouterAddress() const; - void setTimeout(uint32_t timeout); - uint32_t getTimeout(); + void setTimeout(const uint32_t timeout); + uint32_t getTimeout() const; - virtual String getCommandName() = 0; + virtual String getCommandName() const = 0; - void setSendCount(uint8_t count); - uint8_t getSendCount(); + void setSendCount(const uint8_t count); + uint8_t getSendCount() const; uint8_t incrementSendCount(); - virtual CommandAbstract* getRequestFrameCommand(uint8_t frame_no); + virtual CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) = 0; - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) = 0; + virtual void gotTimeout(); // Sets the amount how often the specific command is resent if all fragments where missing - virtual uint8_t getMaxResendCount(); + virtual uint8_t getMaxResendCount() const; // Sets the amount how often a missing fragment is re-requested if it was not available - virtual uint8_t getMaxRetransmitCount(); + virtual uint8_t getMaxRetransmitCount() const; protected: uint8_t _payload[RF_LEN]; @@ -56,6 +55,9 @@ class CommandAbstract { uint64_t _targetAddress; uint64_t _routerAddress; + InverterAbstract* _inv; + private: - static void convertSerialToPacketId(uint8_t buffer[], uint64_t serial); -}; \ No newline at end of file + void setTargetAddress(const uint64_t address); + static void convertSerialToPacketId(uint8_t buffer[], const uint64_t serial); +}; diff --git a/lib/Hoymiles/src/commands/DevControlCommand.cpp b/lib/Hoymiles/src/commands/DevControlCommand.cpp index fce935b82..b73f74f0c 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.cpp +++ b/lib/Hoymiles/src/commands/DevControlCommand.cpp @@ -1,13 +1,30 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Has a variable length. + +Command structure: +* ID: fixed identifier and everytime 0x51 +* Cmd: Fixed at 0x81 for these types of commands +* Payload: dynamic amount of bytes +* CRC16: calcuclated over the highlighted amount of bytes + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +------------------------------------------------------------------------------------------------------------- + |<->| CRC16 +51 71 60 35 46 80 12 23 04 81 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^ ^^ +ID Target Addr Source Addr Cmd Payload CRC16 CRC8 +*/ #include "DevControlCommand.h" #include "crc.h" -DevControlCommand::DevControlCommand(uint64_t target_address, uint64_t router_address) - : CommandAbstract(target_address, router_address) +DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x51; _payload[9] = 0x81; @@ -15,14 +32,14 @@ DevControlCommand::DevControlCommand(uint64_t target_address, uint64_t router_ad setTimeout(1000); } -void DevControlCommand::udpateCRC(uint8_t len) +void DevControlCommand::udpateCRC(const uint8_t len) { - uint16_t crc = crc16(&_payload[10], len); + const uint16_t crc = crc16(&_payload[10], len); _payload[10 + len] = (uint8_t)(crc >> 8); _payload[10 + len + 1] = (uint8_t)(crc); } -bool DevControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { for (uint8_t i = 0; i < max_fragment_id; i++) { if (fragment[i].mainCmd != (_payload[0] | 0x80)) { @@ -31,4 +48,4 @@ bool DevControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fr } return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevControlCommand.h b/lib/Hoymiles/src/commands/DevControlCommand.h index f4d0a0495..7e7637edc 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.h +++ b/lib/Hoymiles/src/commands/DevControlCommand.h @@ -5,10 +5,10 @@ class DevControlCommand : public CommandAbstract { public: - explicit DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit DevControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); protected: - void udpateCRC(uint8_t len); -}; \ No newline at end of file + void udpateCRC(const uint8_t len); +}; diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp index b175822e5..8a258ac20 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp @@ -1,39 +1,55 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch firmware information from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x01 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 01 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "DevInfoAllCommand.h" #include "inverters/InverterAbstract.h" -DevInfoAllCommand::DevInfoAllCommand(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +DevInfoAllCommand::DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x01); setTimeout(200); } -String DevInfoAllCommand::getCommandName() +String DevInfoAllCommand::getCommandName() const { return "DevInfoAll"; } -bool DevInfoAllCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevInfoAllCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter->DevInfo()->beginAppendFragment(); - inverter->DevInfo()->clearBufferAll(); + _inv->DevInfo()->beginAppendFragment(); + _inv->DevInfo()->clearBufferAll(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); + _inv->DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->DevInfo()->endAppendFragment(); - inverter->DevInfo()->setLastUpdateAll(millis()); + _inv->DevInfo()->endAppendFragment(); + _inv->DevInfo()->setLastUpdateAll(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.h b/lib/Hoymiles/src/commands/DevInfoAllCommand.h index 165563846..8ddfd8341 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.h @@ -5,9 +5,9 @@ class DevInfoAllCommand : public MultiDataCommand { public: - explicit DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp index 09d5a4675..d134a0ac1 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp @@ -1,39 +1,55 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch hardware information from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x00 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 00 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "DevInfoSimpleCommand.h" #include "inverters/InverterAbstract.h" -DevInfoSimpleCommand::DevInfoSimpleCommand(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +DevInfoSimpleCommand::DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x00); setTimeout(200); } -String DevInfoSimpleCommand::getCommandName() +String DevInfoSimpleCommand::getCommandName() const { return "DevInfoSimple"; } -bool DevInfoSimpleCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool DevInfoSimpleCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter->DevInfo()->beginAppendFragment(); - inverter->DevInfo()->clearBufferSimple(); + _inv->DevInfo()->beginAppendFragment(); + _inv->DevInfo()->clearBufferSimple(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); + _inv->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->DevInfo()->endAppendFragment(); - inverter->DevInfo()->setLastUpdateSimple(millis()); + _inv->DevInfo()->endAppendFragment(); + _inv->DevInfo()->setLastUpdateSimple(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h index 99b7f503b..927f1eab9 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h @@ -5,9 +5,9 @@ class DevInfoSimpleCommand : public MultiDataCommand { public: - explicit DevInfoSimpleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp index e9171672d..779303773 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp @@ -1,40 +1,56 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch the grid profile from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x02 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 02 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "GridOnProFilePara.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -GridOnProFilePara::GridOnProFilePara(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +GridOnProFilePara::GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x02); setTimeout(500); } -String GridOnProFilePara::getCommandName() +String GridOnProFilePara::getCommandName() const { return "GridOnProFilePara"; } -bool GridOnProFilePara::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool GridOnProFilePara::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter->GridProfile()->beginAppendFragment(); - inverter->GridProfile()->clearBuffer(); + _inv->GridProfile()->beginAppendFragment(); + _inv->GridProfile()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->GridProfile()->endAppendFragment(); - inverter->GridProfile()->setLastUpdate(millis()); + _inv->GridProfile()->endAppendFragment(); + _inv->GridProfile()->setLastUpdate(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.h b/lib/Hoymiles/src/commands/GridOnProFilePara.h index 41ee57ece..b2380c75e 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.h +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.h @@ -5,9 +5,9 @@ class GridOnProFilePara : public MultiDataCommand { public: - explicit GridOnProFilePara(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool 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 39a0d4c64..0e7bf51f1 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -1,12 +1,36 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +Derives from CommandAbstract. Has a fixed length of 26 bytes. + +Command structure: +* ID: fixed identifier and everytime 0x15 +* Idx: the counter of sequencial packages to send. Currently it's only 0x80 + because all request requests only consist of one package. +* DT: repressents the data type and specifies which sub-command to be fetched +* Time: represents the current unix timestamp as hex format. The time on the inverter is synced to the sent time. + Can be calculated e.g. using the following command + echo "obase=16; $(date --date='2023-12-07 18:54:00' +%s)" | bc +* Gap: always 0x0 +* Password: currently always 0x0 +* CRC16: calcuclated over the highlighted amount of bytes + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 00 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "MultiDataCommand.h" #include "crc.h" -MultiDataCommand::MultiDataCommand(uint64_t target_address, uint64_t router_address, uint8_t data_type, time_t time) - : CommandAbstract(target_address, router_address) +MultiDataCommand::MultiDataCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t data_type, const time_t time) + : CommandAbstract(inv, router_address) + , _cmdRequestFrame(inv) { _payload[0] = 0x15; _payload[9] = 0x80; @@ -27,17 +51,17 @@ MultiDataCommand::MultiDataCommand(uint64_t target_address, uint64_t router_addr _payload_size = 26; } -void MultiDataCommand::setDataType(uint8_t data_type) +void MultiDataCommand::setDataType(const uint8_t data_type) { _payload[10] = data_type; udpateCRC(); } -uint8_t MultiDataCommand::getDataType() +uint8_t MultiDataCommand::getDataType() const { return _payload[10]; } -void MultiDataCommand::setTime(time_t time) +void MultiDataCommand::setTime(const time_t time) { _payload[12] = (uint8_t)(time >> 24); _payload[13] = (uint8_t)(time >> 16); @@ -46,7 +70,7 @@ void MultiDataCommand::setTime(time_t time) udpateCRC(); } -time_t MultiDataCommand::getTime() +time_t MultiDataCommand::getTime() const { return (time_t)(_payload[12] << 24) | (time_t)(_payload[13] << 16) @@ -54,15 +78,14 @@ time_t MultiDataCommand::getTime() | (time_t)(_payload[15]); } -CommandAbstract* MultiDataCommand::getRequestFrameCommand(uint8_t frame_no) +CommandAbstract* MultiDataCommand::getRequestFrameCommand(const uint8_t frame_no) { - _cmdRequestFrame.setTargetAddress(getTargetAddress()); _cmdRequestFrame.setFrameNo(frame_no); return &_cmdRequestFrame; } -bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // All fragments are available --> Check CRC uint16_t crc = 0xffff, crcRcv = 0; @@ -88,12 +111,12 @@ bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fra void MultiDataCommand::udpateCRC() { - uint16_t crc = crc16(&_payload[10], 14); // From data_type till password + const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password _payload[24] = (uint8_t)(crc >> 8); _payload[25] = (uint8_t)(crc); } -uint8_t MultiDataCommand::getTotalFragmentSize(fragment_t fragment[], uint8_t max_fragment_id) +uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id) { uint8_t fragmentSize = 0; for (uint8_t i = 0; i < max_fragment_id; i++) { diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.h b/lib/Hoymiles/src/commands/MultiDataCommand.h index 4d2adfde4..5693287fa 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.h +++ b/lib/Hoymiles/src/commands/MultiDataCommand.h @@ -7,20 +7,20 @@ class MultiDataCommand : public CommandAbstract { public: - explicit MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0); + explicit MultiDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0); - void setTime(time_t time); - time_t getTime(); + void setTime(const time_t time); + time_t getTime() const; - CommandAbstract* getRequestFrameCommand(uint8_t frame_no); + CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); protected: - void setDataType(uint8_t data_type); - uint8_t getDataType(); + void setDataType(const uint8_t data_type); + uint8_t getDataType() const; void udpateCRC(); - static uint8_t getTotalFragmentSize(fragment_t fragment[], uint8_t max_fragment_id); + static uint8_t getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id); RequestFrameCommand _cmdRequestFrame; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.cpp b/lib/Hoymiles/src/commands/ParaSetCommand.cpp index 4a48cbea9..8b71867bd 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.cpp +++ b/lib/Hoymiles/src/commands/ParaSetCommand.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "ParaSetCommand.h" -ParaSetCommand::ParaSetCommand(uint64_t target_address, uint64_t router_address) - : CommandAbstract(target_address, router_address) +ParaSetCommand::ParaSetCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x52; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.h b/lib/Hoymiles/src/commands/ParaSetCommand.h index 9ca4e8a97..224aba390 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.h +++ b/lib/Hoymiles/src/commands/ParaSetCommand.h @@ -5,5 +5,5 @@ class ParaSetCommand : public CommandAbstract { public: - explicit ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0); -}; \ No newline at end of file + explicit ParaSetCommand(InverterAbstract* inv, const uint64_t router_address = 0); +}; diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.cpp b/lib/Hoymiles/src/commands/PowerControlCommand.cpp index 522ad5f2d..927c33303 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/PowerControlCommand.cpp @@ -1,14 +1,33 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to power cycle the inverter. + +Derives from DevControlCommand. + +Command structure: +SCmd: Sub-Command ID + 00 --> Turn On + 01 --> Turn Off + 02 --> Restart + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------------- + |<--->| CRC16 +51 71 60 35 46 80 12 23 04 81 00 00 00 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^ ^^ +ID Target Addr Source Addr Cmd SCmd ? CRC16 CRC8 +*/ #include "PowerControlCommand.h" #include "inverters/InverterAbstract.h" #define CRC_SIZE 2 -PowerControlCommand::PowerControlCommand(uint64_t target_address, uint64_t router_address) - : DevControlCommand(target_address, router_address) +PowerControlCommand::PowerControlCommand(InverterAbstract* inv, const uint64_t router_address) + : DevControlCommand(inv, router_address) { _payload[10] = 0x00; // TurnOn _payload[11] = 0x00; @@ -20,28 +39,28 @@ PowerControlCommand::PowerControlCommand(uint64_t target_address, uint64_t route setTimeout(2000); } -String PowerControlCommand::getCommandName() +String PowerControlCommand::getCommandName() const { return "PowerControl"; } -bool PowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool PowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { - if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) { return false; } - inverter->PowerCommand()->setLastUpdateCommand(millis()); - inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_OK); + _inv->PowerCommand()->setLastUpdateCommand(millis()); + _inv->PowerCommand()->setLastPowerCommandSuccess(CMD_OK); return true; } -void PowerControlCommand::gotTimeout(InverterAbstract* inverter) +void PowerControlCommand::gotTimeout() { - inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); + _inv->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); } -void PowerControlCommand::setPowerOn(bool state) +void PowerControlCommand::setPowerOn(const bool state) { if (state) { _payload[10] = 0x00; // TurnOn @@ -57,4 +76,4 @@ void PowerControlCommand::setRestart() _payload[10] = 0x02; // Restart udpateCRC(CRC_SIZE); // 2 byte crc -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h index 376d201e9..d40c356db 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.h +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -5,13 +5,13 @@ class PowerControlCommand : public DevControlCommand { public: - explicit PowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); - void setPowerOn(bool state); + void setPowerOn(const bool state); void setRestart(); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 3f0aed36b..b1396a4dd 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -1,36 +1,52 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch live run time data from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x0b + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 0b 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "RealTimeRunDataCommand.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -RealTimeRunDataCommand::RealTimeRunDataCommand(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +RealTimeRunDataCommand::RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x0b); setTimeout(500); } -String RealTimeRunDataCommand::getCommandName() +String RealTimeRunDataCommand::getCommandName() const { return "RealTimeRunData"; } -bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Check if at least all required bytes are received // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. - uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - uint8_t expectedSize = inverter->Statistics()->getExpectedByteCount(); + const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); + const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -40,19 +56,19 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment // Move all fragments into target buffer uint8_t offs = 0; - inverter->Statistics()->beginAppendFragment(); - inverter->Statistics()->clearBuffer(); + _inv->Statistics()->beginAppendFragment(); + _inv->Statistics()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->Statistics()->endAppendFragment(); - inverter->Statistics()->resetRxFailureCount(); - inverter->Statistics()->setLastUpdate(millis()); + _inv->Statistics()->endAppendFragment(); + _inv->Statistics()->resetRxFailureCount(); + _inv->Statistics()->setLastUpdate(millis()); return true; } -void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter) +void RealTimeRunDataCommand::gotTimeout() { - inverter->Statistics()->incrementRxFailureCount(); -} \ No newline at end of file + _inv->Statistics()->incrementRxFailureCount(); +} diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h index 8cb5be39b..9341247f6 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h @@ -5,10 +5,10 @@ class RealTimeRunDataCommand : public MultiDataCommand { public: - explicit RealTimeRunDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp index e2bfb7668..0abb52356 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp @@ -1,11 +1,29 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to re-request a specific fragment returned by a MultiDataCommand from the inverter. + +Derives from SingleDataCommand. Has a fixed length of 10 bytes. + +Command structure: +* ID: fixed identifier and everytime 0x15 +* Idx: the counter of sequencial packages to send. Currently it's only 0x80 + because all request requests only consist of one package. +* Frm: is set to the fragment id to re-request. "Or" operation with 0x80 is applied to the frame. + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------- +15 71 60 35 46 80 12 23 04 85 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ +ID Target Addr Source Addr Frm CRC8 +*/ #include "RequestFrameCommand.h" -RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t router_address, uint8_t frame_no) - : SingleDataCommand(target_address, router_address) +RequestFrameCommand::RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address, uint8_t frame_no) + : SingleDataCommand(inv, router_address) { if (frame_no > 127) { frame_no = 0; @@ -14,22 +32,22 @@ RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t route _payload_size = 10; } -String RequestFrameCommand::getCommandName() +String RequestFrameCommand::getCommandName() const { return "RequestFrame"; } -void RequestFrameCommand::setFrameNo(uint8_t frame_no) +void RequestFrameCommand::setFrameNo(const uint8_t frame_no) { _payload[9] = frame_no | 0x80; } -uint8_t RequestFrameCommand::getFrameNo() +uint8_t RequestFrameCommand::getFrameNo() const { return _payload[9] & (~0x80); } -bool RequestFrameCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool RequestFrameCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.h b/lib/Hoymiles/src/commands/RequestFrameCommand.h index 5d5e9da15..2924e69bb 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.h +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.h @@ -5,12 +5,12 @@ class RequestFrameCommand : public SingleDataCommand { public: - explicit RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0); + explicit RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address = 0, uint8_t frame_no = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - void setFrameNo(uint8_t frame_no); - uint8_t getFrameNo(); + void setFrameNo(const uint8_t frame_no); + uint8_t getFrameNo() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.cpp b/lib/Hoymiles/src/commands/SingleDataCommand.cpp index 636ee87ac..3b648814a 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.cpp +++ b/lib/Hoymiles/src/commands/SingleDataCommand.cpp @@ -1,11 +1,26 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to send simple commands, containing only one payload, to the inverter. + +Derives from CommandAbstract. + +Command structure: +* ID: fixed identifier and everytime 0x15 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +--------------------------------------------------------------------------------------------------------- +15 71 60 35 46 80 12 23 04 00 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ +ID Target Addr Source Addr CRC8 +*/ #include "SingleDataCommand.h" -SingleDataCommand::SingleDataCommand(uint64_t target_address, uint64_t router_address) - : CommandAbstract(target_address, router_address) +SingleDataCommand::SingleDataCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x15; setTimeout(100); diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.h b/lib/Hoymiles/src/commands/SingleDataCommand.h index c891bda96..39f3c480c 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.h +++ b/lib/Hoymiles/src/commands/SingleDataCommand.h @@ -5,5 +5,5 @@ class SingleDataCommand : public CommandAbstract { public: - explicit SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0); -}; \ No newline at end of file + explicit SingleDataCommand(InverterAbstract* inv, const uint64_t router_address = 0); +}; diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 5e238a59b..0c142afc8 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -1,36 +1,52 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This command is used to fetch current set limits from the inverter. + +Derives from MultiDataCommand + +Command structure: +* DT: this specific command uses 0x05 + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 +----------------------------------------------------------------------------------------------------------------------- + |<------------------- CRC16 --------------------->| +15 71 60 35 46 80 12 23 04 80 05 00 65 72 06 B8 00 00 00 00 00 00 00 00 00 00 00 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^ +ID Target Addr Source Addr Idx DT ? Time Gap Password CRC16 CRC8 +*/ #include "SystemConfigParaCommand.h" #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64_t router_address, time_t time) - : MultiDataCommand(target_address, router_address) +SystemConfigParaCommand::SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x05); setTimeout(200); } -String SystemConfigParaCommand::getCommandName() +String SystemConfigParaCommand::getCommandName() const { return "SystemConfigPara"; } -bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Check if at least all required bytes are received // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. - uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - uint8_t expectedSize = inverter->SystemConfigPara()->getExpectedByteCount(); + const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); + const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -40,19 +56,19 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragmen // Move all fragments into target buffer uint8_t offs = 0; - inverter->SystemConfigPara()->beginAppendFragment(); - inverter->SystemConfigPara()->clearBuffer(); + _inv->SystemConfigPara()->beginAppendFragment(); + _inv->SystemConfigPara()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->SystemConfigPara()->endAppendFragment(); - inverter->SystemConfigPara()->setLastUpdateRequest(millis()); - inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); + _inv->SystemConfigPara()->endAppendFragment(); + _inv->SystemConfigPara()->setLastUpdateRequest(millis()); + _inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); return true; } -void SystemConfigParaCommand::gotTimeout(InverterAbstract* inverter) +void SystemConfigParaCommand::gotTimeout() { - inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); -} \ No newline at end of file + _inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h index ef266fffd..147f18dae 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h @@ -5,10 +5,10 @@ class SystemConfigParaCommand : public MultiDataCommand { public: - explicit SystemConfigParaCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + explicit SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); - virtual String getCommandName(); + virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract* inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/crc.cpp b/lib/Hoymiles/src/crc.cpp index 531971328..582f4ed60 100644 --- a/lib/Hoymiles/src/crc.cpp +++ b/lib/Hoymiles/src/crc.cpp @@ -4,7 +4,7 @@ */ #include "crc.h" -uint8_t crc8(const uint8_t buf[], uint8_t len) +uint8_t crc8(const uint8_t buf[], const uint8_t len) { uint8_t crc = CRC8_INIT; for (uint8_t i = 0; i < len; i++) { @@ -16,7 +16,7 @@ uint8_t crc8(const uint8_t buf[], uint8_t len) return crc; } -uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start) +uint16_t crc16(const uint8_t buf[], const uint8_t len, const uint16_t start) { uint16_t crc = start; uint8_t shift = 0; @@ -33,7 +33,7 @@ uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start) return crc; } -uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit, uint16_t crcIn) +uint16_t crc16nrf24(const uint8_t buf[], const uint16_t lenBits, const uint16_t startBit, const uint16_t crcIn) { uint16_t crc = crcIn; uint8_t idx, val = buf[(startBit >> 3)]; diff --git a/lib/Hoymiles/src/crc.h b/lib/Hoymiles/src/crc.h index a1b01febf..e0fad8890 100644 --- a/lib/Hoymiles/src/crc.h +++ b/lib/Hoymiles/src/crc.h @@ -9,6 +9,6 @@ #define CRC16_MODBUS_POLYNOM 0xA001 #define CRC16_NRF24_POLYNOM 0x1021 -uint8_t crc8(const uint8_t buf[], uint8_t len); -uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start = 0xffff); -uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff); +uint8_t crc8(const uint8_t buf[], const uint8_t len); +uint16_t crc16(const uint8_t buf[], const uint8_t len, const uint16_t start = 0xffff); +uint16_t crc16nrf24(const uint8_t buf[], const uint16_t lenBits, const uint16_t startBit = 0, const uint16_t crcIn = 0xffff); diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.cpp b/lib/Hoymiles/src/inverters/HERF_2CH.cpp new file mode 100644 index 000000000..f0216a643 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.cpp @@ -0,0 +1,62 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_2CH.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_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, 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, 32, 2, 10, false, 1 }, + { 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 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + + { 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_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_2CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x282100000000 && serial <= 0x2821ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2821; +} + +String HERF_2CH::typeName() const +{ + return "HERF-800-2T"; +} + +const byteAssign_t* HERF_2CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_2CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.h b/lib/Hoymiles/src/inverters/HERF_2CH.h new file mode 100644 index 000000000..048ccb618 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_2CH : public HM_Abstract { +public: + explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.cpp b/lib/Hoymiles/src/inverters/HERF_4CH.cpp new file mode 100644 index 000000000..dcd01b6d5 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_4CH.h" + +HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial) + : HM_4CH(radio, serial) {}; + +bool HERF_4CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x280100000000 && serial <= 0x2801ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2801; +} + +String HERF_4CH::typeName() const +{ + return "HERF-1600/1800-4T"; +} diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.h b/lib/Hoymiles/src/inverters/HERF_4CH.h new file mode 100644 index 000000000..70c1ad216 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_4CH.h" + +class HERF_4CH : public HM_4CH { +public: + explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index c659794c6..2c7e3857b 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_1CH.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, @@ -22,33 +22,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMS_1CH::HMS_1CH(HoymilesRadio* radio, uint64_t serial) +HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_1CH::isValidSerial(uint64_t serial) +bool HMS_1CH::isValidSerial(const uint64_t serial) { - // serial >= 0x112400000000 && serial <= 0x112499999999 + // serial >= 0x112400000000 && serial <= 0x1124ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1124; } -String HMS_1CH::typeName() +String HMS_1CH::typeName() const { return "HMS-300/350/400/450/500-1T"; } -const byteAssign_t* HMS_1CH::getByteAssignment() +const byteAssign_t* HMS_1CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_1CH::getByteAssignmentSize() +uint8_t HMS_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.h b/lib/Hoymiles/src/inverters/HMS_1CH.h index 437f3d332..a5a64c177 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.h +++ b/lib/Hoymiles/src/inverters/HMS_1CH.h @@ -6,9 +6,9 @@ class HMS_1CH : public HMS_Abstract { public: - explicit HMS_1CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_1CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp index 08de0a354..d79d2c1d2 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_1CHv2.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { 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_IRR_CH, CH0, CMD_CALC, 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 }, @@ -22,33 +22,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 18, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, uint64_t serial) +HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_1CHv2::isValidSerial(uint64_t serial) +bool HMS_1CHv2::isValidSerial(const uint64_t serial) { - // serial >= 0x112500000000 && serial <= 0x112599999999 + // serial >= 0x112500000000 && serial <= 0x1125ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1125; } -String HMS_1CHv2::typeName() +String HMS_1CHv2::typeName() const { return "HMS-500-1T v2"; } -const byteAssign_t* HMS_1CHv2::getByteAssignment() +const byteAssign_t* HMS_1CHv2::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_1CHv2::getByteAssignmentSize() +uint8_t HMS_1CHv2::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.h b/lib/Hoymiles/src/inverters/HMS_1CHv2.h index 5f4981185..c831d1204 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.h +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.h @@ -6,9 +6,9 @@ class HMS_1CHv2 : public HMS_Abstract { public: - explicit HMS_1CHv2(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index e33de9944..4a700a9a9 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_2CH.h" @@ -10,14 +10,14 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, 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 }, @@ -29,33 +29,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMS_2CH::HMS_2CH(HoymilesRadio* radio, uint64_t serial) +HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_2CH::isValidSerial(uint64_t serial) +bool HMS_2CH::isValidSerial(const uint64_t serial) { - // serial >= 0x114400000000 && serial <= 0x114499999999 + // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1144; } -String HMS_2CH::typeName() +String HMS_2CH::typeName() const { return "HMS-600/700/800/900/1000-2T"; } -const byteAssign_t* HMS_2CH::getByteAssignment() +const byteAssign_t* HMS_2CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_2CH::getByteAssignmentSize() +uint8_t HMS_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.h b/lib/Hoymiles/src/inverters/HMS_2CH.h index dff704ec2..9f1ed91f6 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.h +++ b/lib/Hoymiles/src/inverters/HMS_2CH.h @@ -6,9 +6,9 @@ class HMS_2CH : public HMS_Abstract { public: - explicit HMS_2CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index ffdc20559..b3cf1f380 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { 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_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 26, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 30, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 34, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 46, 2, 1, false, 0 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 28, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 32, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 36, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 48, 2, 1, false, 0 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 42, 4, 1000, false, 3 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 50, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, @@ -43,33 +43,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 62, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 64, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMS_4CH::HMS_4CH(HoymilesRadio* radio, uint64_t serial) +HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) : HMS_Abstract(radio, serial) {}; -bool HMS_4CH::isValidSerial(uint64_t serial) +bool HMS_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x116400000000 && serial <= 0x116499999999 + // serial >= 0x116400000000 && serial <= 0x1164ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1164; } -String HMS_4CH::typeName() +String HMS_4CH::typeName() const { - return "HMS-1600/1800/2000"; + return "HMS-1600/1800/2000-4T"; } -const byteAssign_t* HMS_4CH::getByteAssignment() +const byteAssign_t* HMS_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMS_4CH::getByteAssignmentSize() +uint8_t HMS_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.h b/lib/Hoymiles/src/inverters/HMS_4CH.h index 6a2e2b144..9d49de07a 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.h +++ b/lib/Hoymiles/src/inverters/HMS_4CH.h @@ -5,9 +5,9 @@ class HMS_4CH : public HMS_Abstract { public: - explicit HMS_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMS_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp index f67ff11bb..4fc64b036 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_Abstract.h" #include "Hoymiles.h" #include "HoymilesRadio_CMT.h" #include "commands/ChannelChangeCommand.h" -HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, uint64_t serial) +HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) { } @@ -18,9 +18,9 @@ bool HMS_Abstract::sendChangeChannelRequest() return false; } - auto cmdChannel = _radio->prepareCommand(); - cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); - cmdChannel->setTargetAddress(serial()); + auto cmdChannel = _radio->prepareCommand(this); + cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode()); + cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); _radio->enqueCommand(cmdChannel); return true; diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.h b/lib/Hoymiles/src/inverters/HMS_Abstract.h index 6d363f6ec..c4026a536 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.h @@ -5,7 +5,7 @@ class HMS_Abstract : public HM_Abstract { public: - explicit HMS_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HMS_Abstract(HoymilesRadio* radio, const uint64_t serial); virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index d30a404ba..c84eff478 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMT_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 74, 2, 10, false, 1 }, // dummy { TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, @@ -52,33 +52,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 96, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMT_4CH::HMT_4CH(HoymilesRadio* radio, uint64_t serial) +HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) : HMT_Abstract(radio, serial) {}; -bool HMT_4CH::isValidSerial(uint64_t serial) +bool HMT_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x136100000000 && serial <= 0x136199999999 + // serial >= 0x136100000000 && serial <= 0x1361ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1361; } -String HMT_4CH::typeName() +String HMT_4CH::typeName() const { - return F("HMT-1600/1800/2000-4T"); + return "HMT-1600/1800/2000-4T"; } -const byteAssign_t* HMT_4CH::getByteAssignment() +const byteAssign_t* HMT_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMT_4CH::getByteAssignmentSize() +uint8_t HMT_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.h b/lib/Hoymiles/src/inverters/HMT_4CH.h index 7358dd45d..01d328938 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.h +++ b/lib/Hoymiles/src/inverters/HMT_4CH.h @@ -5,9 +5,9 @@ class HMT_4CH : public HMT_Abstract { public: - explicit HMT_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMT_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 69b3a60bb..2c3dd5f3a 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMT_6CH.h" @@ -10,42 +10,42 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_DC, CH4, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_DC, CH4, FLD_IDC, UNIT_A, 48, 2, 100, false, 2 }, { TYPE_DC, CH4, FLD_PDC, UNIT_W, 52, 2, 10, false, 1 }, { TYPE_DC, CH4, FLD_YT, UNIT_KWH, 56, 4, 1000, false, 3 }, { TYPE_DC, CH4, FLD_YD, UNIT_WH, 64, 2, 1, false, 0 }, - { TYPE_DC, CH4, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH4, CMD_CALC, false, 3 }, + { TYPE_DC, CH4, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH4, CMD_CALC, false, 3 }, { TYPE_DC, CH5, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_DC, CH5, FLD_IDC, UNIT_A, 50, 2, 100, false, 2 }, { TYPE_DC, CH5, FLD_PDC, UNIT_W, 54, 2, 10, false, 1 }, { TYPE_DC, CH5, FLD_YT, UNIT_KWH, 60, 4, 1000, false, 3 }, { TYPE_DC, CH5, FLD_YD, UNIT_WH, 66, 2, 1, false, 0 }, - { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH5, CMD_CALC, false, 3 }, + { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH5, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 74, 2, 10, false, 1 }, // dummy { TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, @@ -57,7 +57,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_F, UNIT_HZ, 80, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PAC, UNIT_W, 82, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 84, 2, 10, true, 1 }, - { TYPE_AC, CH0, FLD_IAC, UNIT_A, 86, 2, 100, false, 2 }, // dummy + { TYPE_AC, CH0, FLD_IAC, UNIT_A, CALC_TOTAL_IAC, 0, CMD_CALC, false, 2 }, { TYPE_AC, CH0, FLD_IAC_1, UNIT_A, 86, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_IAC_2, UNIT_A, 88, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_IAC_3, UNIT_A, 90, 2, 100, false, 2 }, @@ -66,33 +66,33 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 96, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HMT_6CH::HMT_6CH(HoymilesRadio* radio, uint64_t serial) +HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) : HMT_Abstract(radio, serial) {}; -bool HMT_6CH::isValidSerial(uint64_t serial) +bool HMT_6CH::isValidSerial(const uint64_t serial) { - // serial >= 0x138200000000 && serial <= 0x138299999999 + // serial >= 0x138200000000 && serial <= 0x1382ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1382; } -String HMT_6CH::typeName() +String HMT_6CH::typeName() const { - return F("HMT-1800/2250-6T"); + return "HMT-1800/2250-6T"; } -const byteAssign_t* HMT_6CH::getByteAssignment() +const byteAssign_t* HMT_6CH::getByteAssignment() const { return byteAssignment; } -uint8_t HMT_6CH::getByteAssignmentSize() +uint8_t HMT_6CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.h b/lib/Hoymiles/src/inverters/HMT_6CH.h index ea4be7153..6b7280068 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.h +++ b/lib/Hoymiles/src/inverters/HMT_6CH.h @@ -5,9 +5,9 @@ class HMT_6CH : public HMT_Abstract { public: - explicit HMT_6CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HMT_6CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp index c345be977..50c895cc6 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMT_Abstract.h" #include "Hoymiles.h" @@ -8,7 +8,7 @@ #include "commands/ChannelChangeCommand.h" #include "parser/AlarmLogParser.h" -HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, uint64_t serial) +HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) { EventLog()->setMessageType(AlarmMessageType_t::HMT); @@ -20,10 +20,10 @@ bool HMT_Abstract::sendChangeChannelRequest() return false; } - auto cmdChannel = _radio->prepareCommand(); - cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); - cmdChannel->setTargetAddress(serial()); + auto cmdChannel = _radio->prepareCommand(this); + cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode()); + cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); _radio->enqueCommand(cmdChannel); return true; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.h b/lib/Hoymiles/src/inverters/HMT_Abstract.h index 9e10a2c39..c913683c5 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.h @@ -5,7 +5,7 @@ class HMT_Abstract : public HM_Abstract { public: - explicit HMT_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HMT_Abstract(HoymilesRadio* radio, const uint64_t serial); virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index a7c39f4c0..0f0c64c23 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_1CH.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, @@ -22,18 +22,18 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HM_1CH::HM_1CH(HoymilesRadio* radio, uint64_t serial) +HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_1CH::isValidSerial(uint64_t serial) +bool HM_1CH::isValidSerial(const uint64_t serial) { - // serial >= 0x112100000000 && serial <= 0x112199999999 + // serial >= 0x112100000000 && serial <= 0x1121ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); @@ -51,17 +51,17 @@ bool HM_1CH::isValidSerial(uint64_t serial) return false; } -String HM_1CH::typeName() +String HM_1CH::typeName() const { return "HM-300/350/400-1T"; } -const byteAssign_t* HM_1CH::getByteAssignment() +const byteAssign_t* HM_1CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_1CH::getByteAssignmentSize() +uint8_t HM_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index cb18dcf51..a35b4e568 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -6,9 +6,9 @@ class HM_1CH : public HM_Abstract { public: - explicit HM_1CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_1CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 2dc674b26..02dd8ae4f 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_2CH.h" @@ -11,14 +11,14 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 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_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 8, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 10, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, 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 }, @@ -30,18 +30,18 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HM_2CH::HM_2CH(HoymilesRadio* radio, uint64_t serial) +HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_2CH::isValidSerial(uint64_t serial) +bool HM_2CH::isValidSerial(const uint64_t serial) { - // serial >= 0x114100000000 && serial <= 0x114199999999 + // serial >= 0x114100000000 && serial <= 0x1141ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); @@ -59,17 +59,17 @@ bool HM_2CH::isValidSerial(uint64_t serial) return false; } -String HM_2CH::typeName() +String HM_2CH::typeName() const { return "HM-600/700/800-2T"; } -const byteAssign_t* HM_2CH::getByteAssignment() +const byteAssign_t* HM_2CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_2CH::getByteAssignmentSize() +uint8_t HM_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index 06ac509da..1fd54496a 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -5,9 +5,9 @@ class HM_2CH : public HM_Abstract { public: - explicit HM_2CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index f5920491a..586248b59 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, - { TYPE_DC, CH1, FLD_UDC, UNIT_V, CALC_UDC_CH, CH0, CMD_CALC, false, 1 }, + { TYPE_DC, CH1, FLD_UDC, UNIT_V, CALC_CH_UDC, CH0, CMD_CALC, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, - { TYPE_DC, CH3, FLD_UDC, UNIT_V, CALC_UDC_CH, CH2, CMD_CALC, false, 1 }, + { TYPE_DC, CH3, FLD_UDC, UNIT_V, CALC_CH_UDC, CH2, CMD_CALC, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 54, 2, 100, false, 2 }, @@ -43,18 +43,18 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 58, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 60, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; -HM_4CH::HM_4CH(HoymilesRadio* radio, uint64_t serial) +HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) {}; -bool HM_4CH::isValidSerial(uint64_t serial) +bool HM_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x116100000000 && serial <= 0x116199999999 + // serial >= 0x116100000000 && serial <= 0x1161ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); @@ -72,17 +72,17 @@ bool HM_4CH::isValidSerial(uint64_t serial) return false; } -String HM_4CH::typeName() +String HM_4CH::typeName() const { return "HM-1000/1200/1500-4T"; } -const byteAssign_t* HM_4CH::getByteAssignment() +const byteAssign_t* HM_4CH::getByteAssignment() const { return byteAssignment; } -uint8_t HM_4CH::getByteAssignmentSize() +uint8_t HM_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index 44d341aeb..e54f33234 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -5,9 +5,9 @@ class HM_4CH : public HM_Abstract { public: - explicit HM_4CH(HoymilesRadio* radio, uint64_t serial); - static bool isValidSerial(uint64_t serial); - String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getByteAssignmentSize(); + explicit HM_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 7997b2eab..45efc99db 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_Abstract.h" #include "HoymilesRadio.h" @@ -13,7 +13,7 @@ #include "commands/RealTimeRunDataCommand.h" #include "commands/SystemConfigParaCommand.h" -HM_Abstract::HM_Abstract(HoymilesRadio* radio, uint64_t serial) +HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial) : InverterAbstract(radio, serial) {}; bool HM_Abstract::sendStatsRequest() @@ -30,15 +30,14 @@ bool HM_Abstract::sendStatsRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); _radio->enqueCommand(cmd); return true; } -bool HM_Abstract::sendAlarmLogRequest(bool force) +bool HM_Abstract::sendAlarmLogRequest(const bool force) { if (!getEnablePolling()) { return false; @@ -62,9 +61,8 @@ bool HM_Abstract::sendAlarmLogRequest(bool force) time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); EventLog()->setLastAlarmRequestSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -85,14 +83,12 @@ bool HM_Abstract::sendDevInfoRequest() time_t now; time(&now); - auto cmdAll = _radio->prepareCommand(); + auto cmdAll = _radio->prepareCommand(this); cmdAll->setTime(now); - cmdAll->setTargetAddress(serial()); _radio->enqueCommand(cmdAll); - auto cmdSimple = _radio->prepareCommand(); + auto cmdSimple = _radio->prepareCommand(this); cmdSimple->setTime(now); - cmdSimple->setTargetAddress(serial()); _radio->enqueCommand(cmdSimple); return true; @@ -112,16 +108,15 @@ bool HM_Abstract::sendSystemConfigParaRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitRequestSuccess(CMD_PENDING); _radio->enqueCommand(cmd); return true; } -bool HM_Abstract::sendActivePowerControlRequest(float limit, PowerLimitControlType type) +bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitControlType type) { if (!getEnableCommands()) { return false; @@ -134,9 +129,8 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, PowerLimitControlTy _activePowerControlLimit = limit; _activePowerControlType = type; - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setActivePowerLimit(limit, type); - cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -148,7 +142,7 @@ bool HM_Abstract::resendActivePowerControlRequest() return sendActivePowerControlRequest(_activePowerControlLimit, _activePowerControlType); } -bool HM_Abstract::sendPowerControlRequest(bool turnOn) +bool HM_Abstract::sendPowerControlRequest(const bool turnOn) { if (!getEnableCommands()) { return false; @@ -160,9 +154,8 @@ bool HM_Abstract::sendPowerControlRequest(bool turnOn) _powerState = 0; } - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setPowerOn(turnOn); - cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -177,9 +170,8 @@ bool HM_Abstract::sendRestartControlRequest() _powerState = 2; - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setRestart(); - cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -219,9 +211,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); _radio->enqueCommand(cmd); return true; diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index 3a5cc637b..491149dc2 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -5,14 +5,14 @@ class HM_Abstract : public InverterAbstract { public: - explicit HM_Abstract(HoymilesRadio* radio, uint64_t serial); + explicit HM_Abstract(HoymilesRadio* radio, const uint64_t serial); bool sendStatsRequest(); - bool sendAlarmLogRequest(bool force = false); + bool sendAlarmLogRequest(const bool force = false); bool sendDevInfoRequest(); bool sendSystemConfigParaRequest(); - bool sendActivePowerControlRequest(float limit, PowerLimitControlType type); + bool sendActivePowerControlRequest(float limit, const PowerLimitControlType type); bool resendActivePowerControlRequest(); - bool sendPowerControlRequest(bool turnOn); + bool sendPowerControlRequest(const bool turnOn); bool sendRestartControlRequest(); bool resendPowerControlRequest(); bool sendGridOnProFileParaRequest(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 4c5aa422d..68d611836 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -7,7 +7,7 @@ #include "crc.h" #include -InverterAbstract::InverterAbstract(HoymilesRadio* radio, uint64_t serial) +InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial) { _serial.u64 = serial; _radio = radio; @@ -35,12 +35,12 @@ void InverterAbstract::init() _statisticsParser.get()->setByteAssignment(getByteAssignment(), getByteAssignmentSize()); } -uint64_t InverterAbstract::serial() +uint64_t InverterAbstract::serial() const { return _serial.u64; } -const String& InverterAbstract::serialString() +const String& InverterAbstract::serialString() const { return _serialString; } @@ -55,7 +55,7 @@ void InverterAbstract::setName(const char* name) _name[len] = '\0'; } -const char* InverterAbstract::name() +const char* InverterAbstract::name() const { return _name; } @@ -77,56 +77,66 @@ bool InverterAbstract::isReachable() return _enablePolling && Statistics()->getRxFailureCount() <= _reachableThreshold; } -void InverterAbstract::setEnablePolling(bool enabled) +void InverterAbstract::setEnablePolling(const bool enabled) { _enablePolling = enabled; } -bool InverterAbstract::getEnablePolling() +bool InverterAbstract::getEnablePolling() const { return _enablePolling; } -void InverterAbstract::setEnableCommands(bool enabled) +void InverterAbstract::setEnableCommands(const bool enabled) { _enableCommands = enabled; } -bool InverterAbstract::getEnableCommands() +bool InverterAbstract::getEnableCommands() const { return _enableCommands; } -void InverterAbstract::setReachableThreshold(uint8_t threshold) +void InverterAbstract::setReachableThreshold(const uint8_t threshold) { _reachableThreshold = threshold; } -uint8_t InverterAbstract::getReachableThreshold() +uint8_t InverterAbstract::getReachableThreshold() const { return _reachableThreshold; } -void InverterAbstract::setZeroValuesIfUnreachable(bool enabled) +void InverterAbstract::setZeroValuesIfUnreachable(const bool enabled) { _zeroValuesIfUnreachable = enabled; } -bool InverterAbstract::getZeroValuesIfUnreachable() +bool InverterAbstract::getZeroValuesIfUnreachable() const { return _zeroValuesIfUnreachable; } -void InverterAbstract::setZeroYieldDayOnMidnight(bool enabled) +void InverterAbstract::setZeroYieldDayOnMidnight(const bool enabled) { _zeroYieldDayOnMidnight = enabled; } -bool InverterAbstract::getZeroYieldDayOnMidnight() +bool InverterAbstract::getZeroYieldDayOnMidnight() const { return _zeroYieldDayOnMidnight; } +void InverterAbstract::setClearEventlogOnMidnight(const bool enabled) +{ + _clearEventlogOnMidnight = enabled; +} + +bool InverterAbstract::getClearEventlogOnMidnight() const +{ + return _clearEventlogOnMidnight; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; @@ -175,7 +185,7 @@ void InverterAbstract::clearRxFragmentBuffer() _rxFragmentRetransmitCnt = 0; } -void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) +void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) { if (len < 11) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); @@ -187,10 +197,10 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) return; } - uint8_t fragmentCount = fragment[9]; + const uint8_t fragmentCount = fragment[9]; // Packets with 0x81 will be seen as 1 - uint8_t fragmentId = fragmentCount & 0b01111111; // fragmentId is 1 based + const uint8_t fragmentId = fragmentCount & 0b01111111; // fragmentId is 1 based if (fragmentId == 0) { Hoymiles.getMessageOutput()->println("ERROR: fragment id zero received and ignored"); @@ -218,15 +228,15 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) } // Returns Zero on Success or the Fragment ID for retransmit or error code -uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) +uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) { // All missing if (_rxFragmentLastPacketId == 0) { Hoymiles.getMessageOutput()->println("All missing"); - if (cmd->getSendCount() <= cmd->getMaxResendCount()) { + if (cmd.getSendCount() <= cmd.getMaxResendCount()) { return FRAGMENT_ALL_MISSING_RESEND; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(); return FRAGMENT_ALL_MISSING_TIMEOUT; } } @@ -234,10 +244,10 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // Last fragment is missing (the one with 0x80) if (_rxFragmentMaxPacketId == 0) { Hoymiles.getMessageOutput()->println("Last missing"); - if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { + if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return _rxFragmentLastPacketId + 1; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(); return FRAGMENT_RETRANSMIT_TIMEOUT; } } @@ -246,19 +256,19 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) { if (!_rxFragmentBuffer[i].wasReceived) { Hoymiles.getMessageOutput()->println("Middle missing"); - if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { + if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return i + 1; } else { - cmd->gotTimeout(this); + cmd.gotTimeout(); return FRAGMENT_RETRANSMIT_TIMEOUT; } } } - if (!cmd->handleResponse(this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) { - cmd->gotTimeout(this); + if (!cmd.handleResponse(_rxFragmentBuffer, _rxFragmentMaxPacketId)) { + cmd.gotTimeout(); return FRAGMENT_HANDLE_ERROR; } return FRAGMENT_OK; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index e6f70f070..2a51079ba 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -30,45 +30,48 @@ class CommandAbstract; class InverterAbstract { public: - explicit InverterAbstract(HoymilesRadio* radio, uint64_t serial); + explicit InverterAbstract(HoymilesRadio* radio, const uint64_t serial); void init(); - uint64_t serial(); - const String& serialString(); + uint64_t serial() const; + const String& serialString() const; void setName(const char* name); - const char* name(); - virtual String typeName() = 0; - virtual const byteAssign_t* getByteAssignment() = 0; - virtual uint8_t getByteAssignmentSize() = 0; + const char* name() const; + virtual String typeName() const = 0; + virtual const byteAssign_t* getByteAssignment() const = 0; + virtual uint8_t getByteAssignmentSize() const = 0; bool isProducing(); bool isReachable(); - void setEnablePolling(bool enabled); - bool getEnablePolling(); + void setEnablePolling(const bool enabled); + bool getEnablePolling() const; - void setEnableCommands(bool enabled); - bool getEnableCommands(); + void setEnableCommands(const bool enabled); + bool getEnableCommands() const; - void setReachableThreshold(uint8_t threshold); - uint8_t getReachableThreshold(); + void setReachableThreshold(const uint8_t threshold); + uint8_t getReachableThreshold() const; - void setZeroValuesIfUnreachable(bool enabled); - bool getZeroValuesIfUnreachable(); + void setZeroValuesIfUnreachable(const bool enabled); + bool getZeroValuesIfUnreachable() const; - void setZeroYieldDayOnMidnight(bool enabled); - bool getZeroYieldDayOnMidnight(); + void setZeroYieldDayOnMidnight(const bool enabled); + bool getZeroYieldDayOnMidnight() const; + + void setClearEventlogOnMidnight(const bool enabled); + bool getClearEventlogOnMidnight() const; void clearRxFragmentBuffer(); - void addRxFragment(uint8_t fragment[], uint8_t len); - uint8_t verifyAllFragments(CommandAbstract* cmd); + void addRxFragment(const uint8_t fragment[], const uint8_t len); + uint8_t verifyAllFragments(CommandAbstract& cmd); virtual bool sendStatsRequest() = 0; - virtual bool sendAlarmLogRequest(bool force = false) = 0; + virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendDevInfoRequest() = 0; virtual bool sendSystemConfigParaRequest() = 0; - virtual bool sendActivePowerControlRequest(float limit, PowerLimitControlType type) = 0; + virtual bool sendActivePowerControlRequest(float limit, const PowerLimitControlType type) = 0; virtual bool resendActivePowerControlRequest() = 0; - virtual bool sendPowerControlRequest(bool turnOn) = 0; + virtual bool sendPowerControlRequest(const bool turnOn) = 0; virtual bool sendRestartControlRequest() = 0; virtual bool resendPowerControlRequest() = 0; virtual bool sendChangeChannelRequest(); @@ -102,6 +105,7 @@ class InverterAbstract { bool _zeroValuesIfUnreachable = false; bool _zeroYieldDayOnMidnight = false; + bool _clearEventlogOnMidnight = false; std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; @@ -109,4 +113,4 @@ class InverterAbstract { std::unique_ptr _powerCommandParser; std::unique_ptr _statisticsParser; std::unique_ptr _systemConfigParaParser; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index c080a7351..6d6104a20 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -11,3 +11,5 @@ | 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 | diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index fe2d2bab4..e08baf052 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -1,7 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'AlarmDataCommand'. + +Data structure: +* wcode: + * right 8 bit: Event ID + * bit 13: Start time = PM (12h has to be added to start time) + * bit 12: End time = PM (12h has to be added to start time) +* Start: 12h based start time of the event (PM indicator in wcode) +* End: 12h based start time of the event (PM indicator in wcode) + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 + |<-------------- First log entry -------------->| |<->| +----------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 01 00 01 80 01 00 01 91 EA 91 EA 00 00 00 00 00 8F 65 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ ^^ ^^ ^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx ? wcode ? Start End ? ? ? ? wcode CRC8 +*/ #include "AlarmLogParser.h" #include "../Hoymiles.h" #include @@ -55,11 +75,12 @@ const std::array AlarmLogParser::_alarmMe { AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" }, { AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" }, { AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" }, - { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Eletrizitätsnetzausfall", "Réseau: Panne du réseau électrique" }, + { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Elektrizitätsnetzausfall", "Réseau: Panne du réseau électrique" }, { AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" }, { AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection d’îlots" }, { AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" }, + { AlarmMessageType_t::ALL, 152, "Grid: Phase angle difference between two phases exceeded 5° >10 times", "", "" }, { AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" }, { AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" }, { AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" }, @@ -181,7 +202,7 @@ void AlarmLogParser::clearBuffer() _alarmLogLength = 0; } -void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void AlarmLogParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > ALARM_LOG_PAYLOAD_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\r\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE); @@ -191,7 +212,7 @@ void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t le _alarmLogLength += len; } -uint8_t AlarmLogParser::getEntryCount() +uint8_t AlarmLogParser::getEntryCount() const { if (_alarmLogLength < 2) { return 0; @@ -199,30 +220,30 @@ uint8_t AlarmLogParser::getEntryCount() return (_alarmLogLength - 2) / ALARM_LOG_ENTRY_SIZE; } -void AlarmLogParser::setLastAlarmRequestSuccess(LastCommandSuccess status) +void AlarmLogParser::setLastAlarmRequestSuccess(const LastCommandSuccess status) { _lastAlarmRequestSuccess = status; } -LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess() +LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess() const { return _lastAlarmRequestSuccess; } -void AlarmLogParser::setMessageType(AlarmMessageType_t type) +void AlarmLogParser::setMessageType(const AlarmMessageType_t type) { _messageType = type; } -void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmMessageLocale_t locale) +void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, const AlarmMessageLocale_t locale) { - uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE; + const uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE; - int timezoneOffset = getTimezoneOffset(); + const int timezoneOffset = getTimezoneOffset(); HOY_SEMAPHORE_TAKE(); - uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; + const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; uint32_t startTimeOffset = 0; if (((wcode >> 13) & 0x01) == 1) { startTimeOffset = 12 * 60 * 60; @@ -233,40 +254,40 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmM endTimeOffset = 12 * 60 * 60; } - entry->MessageId = _payloadAlarmLog[entryStartOffset + 1]; - entry->StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; - entry->EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); + entry.MessageId = _payloadAlarmLog[entryStartOffset + 1]; + entry.StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; + entry.EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); HOY_SEMAPHORE_GIVE(); - if (entry->EndTime > 0) { - entry->EndTime += (endTimeOffset + timezoneOffset); + if (entry.EndTime > 0) { + entry.EndTime += (endTimeOffset + timezoneOffset); } switch (locale) { case AlarmMessageLocale_t::DE: - entry->Message = "Unbekannt"; + entry.Message = "Unbekannt"; break; case AlarmMessageLocale_t::FR: - entry->Message = "Inconnu"; + entry.Message = "Inconnu"; break; default: - entry->Message = "Unknown"; + entry.Message = "Unknown"; } for (auto& msg : _alarmMessages) { - if (msg.MessageId == entry->MessageId) { + if (msg.MessageId == entry.MessageId) { if (msg.InverterType == _messageType) { - entry->Message = getLocaleMessage(&msg, locale); + entry.Message = getLocaleMessage(&msg, locale); break; } else if (msg.InverterType == AlarmMessageType_t::ALL) { - entry->Message = getLocaleMessage(&msg, locale); + entry.Message = getLocaleMessage(&msg, locale); } } } } -String AlarmLogParser::getLocaleMessage(const AlarmMessage_t* msg, AlarmMessageLocale_t locale) +String AlarmLogParser::getLocaleMessage(const AlarmMessage_t* msg, const AlarmMessageLocale_t locale) const { if (locale == AlarmMessageLocale_t::DE) { return msg->Message_de[0] != '\0' ? msg->Message_de : msg->Message_en; @@ -294,4 +315,4 @@ int AlarmLogParser::getTimezoneOffset() gmt = mktime(ptm); return static_cast(difftime(rawtime, gmt)); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 9189d175e..87413ce7a 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -8,7 +8,7 @@ #define ALARM_LOG_ENTRY_SIZE 12 #define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4) -#define ALARM_MSG_COUNT 130 +#define ALARM_MSG_COUNT 131 struct AlarmLogEntry_t { uint16_t MessageId; @@ -31,28 +31,28 @@ enum class AlarmMessageLocale_t { typedef struct { AlarmMessageType_t InverterType; uint16_t MessageId; - char Message_en[62]; - char Message_de[63]; - char Message_fr[64]; + const char* Message_en; + const char* Message_de; + const char* Message_fr; } AlarmMessage_t; class AlarmLogParser : public Parser { public: AlarmLogParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); - uint8_t getEntryCount(); - void getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry, AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN); + uint8_t getEntryCount() const; + void getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry, const AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN); - void setLastAlarmRequestSuccess(LastCommandSuccess status); - LastCommandSuccess getLastAlarmRequestSuccess(); + void setLastAlarmRequestSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastAlarmRequestSuccess() const; - void setMessageType(AlarmMessageType_t type); + void setMessageType(const AlarmMessageType_t type); private: static int getTimezoneOffset(); - String getLocaleMessage(const AlarmMessage_t *msg, AlarmMessageLocale_t locale); + String getLocaleMessage(const AlarmMessage_t* msg, const AlarmMessageLocale_t locale) const; uint8_t _payloadAlarmLog[ALARM_LOG_PAYLOAD_SIZE]; uint8_t _alarmLogLength = 0; @@ -62,4 +62,4 @@ class AlarmLogParser : public Parser { AlarmMessageType_t _messageType = AlarmMessageType_t::ALL; static const std::array _alarmMessages; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index bc28ce39d..c34ecb3f0 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -1,7 +1,32 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'DevInfoAllCommand' and 'DevInfoSimpleCommand'. +It contains version information of the hardware and firmware. It can also be used to determine +the exact inverter type. + +Data structure (DevInfoAllCommand): + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 27 1C 07 E5 04 01 07 2D 00 01 00 00 00 00 DF DD 1E -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx FW Version FW Year FW Month/Date FW Hour/Minute Bootloader ? ? CRC16 CRC8 + + +Data structure (DevInfoSimpleCommand): + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 27 1C 10 12 71 01 01 00 0A 00 20 01 00 00 E5 F8 95 +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx FW Version HW Part No. HW Version ? ? ? CRC16 CRC8 +*/ #include "DevInfoParser.h" #include "../Hoymiles.h" #include @@ -15,38 +40,48 @@ typedef struct { } devInfo_t; const devInfo_t devInfo[] = { - { { 0x10, 0x10, 0x10, ALL }, 300, "HM-300" }, - { { 0x10, 0x10, 0x20, ALL }, 350, "HM-350" }, - { { 0x10, 0x10, 0x30, ALL }, 400, "HM-400" }, - { { 0x10, 0x10, 0x40, ALL }, 400, "HM-400" }, - { { 0x10, 0x11, 0x10, ALL }, 600, "HM-600" }, - { { 0x10, 0x11, 0x20, ALL }, 700, "HM-700" }, - { { 0x10, 0x11, 0x30, ALL }, 800, "HM-800" }, - { { 0x10, 0x11, 0x40, ALL }, 800, "HM-800" }, - { { 0x10, 0x12, 0x10, ALL }, 1200, "HM-1200" }, - { { 0x10, 0x02, 0x30, ALL }, 1500, "MI-1500 Gen3" }, - { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500" }, - { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300" }, // HM-300 factory limitted to 70% - - { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350" }, // 00 - { { 0x10, 0x20, 0x41, ALL }, 400, "HMS-400" }, // 00 - { { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450" }, // 01 - { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500" }, // 02 - { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500 v2" }, // 02 - { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600" }, // 01 - { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800" }, // 00 - { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900" }, // 01 - { { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900" }, // 03 - { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000" }, // 05 - { { 0x10, 0x11, 0x71, ALL }, 1000, "HMS-1000" }, // 01 - { { 0x10, 0x22, 0x41, ALL }, 1600, "HMS-1600" }, // 4 - { { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800" }, // 01 - { { 0x10, 0x22, 0x51, ALL }, 1800, "HMS-1800" }, // 16 - { { 0x10, 0x12, 0x71, ALL }, 2000, "HMS-2000" }, // 01 - { { 0x10, 0x22, 0x71, ALL }, 2000, "HMS-2000" }, // 10 - - { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800" }, // 01 - { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250" } // 01 + { { 0x10, 0x10, 0x10, ALL }, 300, "HM-300-1T" }, + { { 0x10, 0x10, 0x20, ALL }, 350, "HM-350-1T" }, + { { 0x10, 0x10, 0x30, ALL }, 400, "HM-400-1T" }, + { { 0x10, 0x10, 0x40, ALL }, 400, "HM-400-1T" }, + { { 0x10, 0x11, 0x10, ALL }, 600, "HM-600-2T" }, + { { 0x10, 0x11, 0x20, ALL }, 700, "HM-700-2T" }, + { { 0x10, 0x11, 0x30, ALL }, 800, "HM-800-2T" }, + { { 0x10, 0x11, 0x40, ALL }, 800, "HM-800-2T" }, + { { 0x10, 0x12, 0x10, ALL }, 1200, "HM-1200-4T" }, + { { 0x10, 0x02, 0x30, ALL }, 1500, "MI-1500-4T Gen3" }, + { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500-4T" }, + { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300-1T" }, // HM-300 factory limitted to 70% + + { { 0x10, 0x20, 0x11, ALL }, 300, "HMS-300-1T" }, // 00 + { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350-1T" }, // 00 + { { 0x10, 0x20, 0x41, ALL }, 400, "HMS-400-1T" }, // 00 + { { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450-1T" }, // 01 + { { 0x10, 0x20, 0x51, ALL }, 450, "HMS-450-1T" }, // 03 + { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02 + { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 + { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 + { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 + { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 + { { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900-2T" }, // 03 + { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 05 + { { 0x10, 0x11, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 01 + { { 0x10, 0x22, 0x41, ALL }, 1600, "HMS-1600-4T" }, // 4 + { { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800-4T" }, // 01 + { { 0x10, 0x22, 0x51, ALL }, 1800, "HMS-1800-4T" }, // 16 + { { 0x10, 0x12, 0x71, ALL }, 2000, "HMS-2000-4T" }, // 01 + { { 0x10, 0x22, 0x71, ALL }, 2000, "HMS-2000-4T" }, // 10 + + { { 0x10, 0x32, 0x41, ALL }, 1600, "HMT-1600-4T" }, // 00 + { { 0x10, 0x32, 0x51, ALL }, 1800, "HMT-1800-4T" }, // 00 + { { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0 + + { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 + { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 + + { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 + { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 + { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 }; DevInfoParser::DevInfoParser() @@ -62,7 +97,7 @@ void DevInfoParser::clearBufferAll() _devInfoAllLength = 0; } -void DevInfoParser::appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len) +void DevInfoParser::appendFragmentAll(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > DEV_INFO_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info all packet too large for buffer\r\n", __FILE__, __LINE__); @@ -78,7 +113,7 @@ void DevInfoParser::clearBufferSimple() _devInfoSimpleLength = 0; } -void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len) +void DevInfoParser::appendFragmentSimple(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > DEV_INFO_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\r\n", __FILE__, __LINE__); @@ -88,37 +123,37 @@ void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8 _devInfoSimpleLength += len; } -uint32_t DevInfoParser::getLastUpdateAll() +uint32_t DevInfoParser::getLastUpdateAll() const { return _lastUpdateAll; } -void DevInfoParser::setLastUpdateAll(uint32_t lastUpdate) +void DevInfoParser::setLastUpdateAll(const uint32_t lastUpdate) { _lastUpdateAll = lastUpdate; setLastUpdate(lastUpdate); } -uint32_t DevInfoParser::getLastUpdateSimple() +uint32_t DevInfoParser::getLastUpdateSimple() const { return _lastUpdateSimple; } -void DevInfoParser::setLastUpdateSimple(uint32_t lastUpdate) +void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate) { _lastUpdateSimple = lastUpdate; setLastUpdate(lastUpdate); } -uint16_t DevInfoParser::getFwBuildVersion() +uint16_t DevInfoParser::getFwBuildVersion() const { HOY_SEMAPHORE_TAKE(); - uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; + const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; HOY_SEMAPHORE_GIVE(); return ret; } -time_t DevInfoParser::getFwBuildDateTime() +time_t DevInfoParser::getFwBuildDateTime() const { struct tm timeinfo = {}; HOY_SEMAPHORE_TAKE(); @@ -134,28 +169,33 @@ time_t DevInfoParser::getFwBuildDateTime() return timegm(&timeinfo); } -uint16_t DevInfoParser::getFwBootloaderVersion() +String DevInfoParser::getFwBuildDateTimeStr() const +{ + char timebuffer[32]; + const time_t t = getFwBuildDateTime(); + std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t)); + return timebuffer; +} + +uint16_t DevInfoParser::getFwBootloaderVersion() const { HOY_SEMAPHORE_TAKE(); - uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; + const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; HOY_SEMAPHORE_GIVE(); return ret; } -uint32_t DevInfoParser::getHwPartNumber() +uint32_t DevInfoParser::getHwPartNumber() const { - uint16_t hwpn_h; - uint16_t hwpn_l; - HOY_SEMAPHORE_TAKE(); - hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; - hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; + const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; + const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; HOY_SEMAPHORE_GIVE(); return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); } -String DevInfoParser::getHwVersion() +String DevInfoParser::getHwVersion() const { char buf[8]; HOY_SEMAPHORE_TAKE(); @@ -164,35 +204,35 @@ String DevInfoParser::getHwVersion() return buf; } -uint16_t DevInfoParser::getMaxPower() +uint16_t DevInfoParser::getMaxPower() const { - uint8_t idx = getDevIdx(); + const uint8_t idx = getDevIdx(); if (idx == 0xff) { return 0; } return devInfo[idx].maxPower; } -String DevInfoParser::getHwModelName() +String DevInfoParser::getHwModelName() const { - uint8_t idx = getDevIdx(); + const uint8_t idx = getDevIdx(); if (idx == 0xff) { return ""; } return devInfo[idx].modelName; } -bool DevInfoParser::containsValidData() +bool DevInfoParser::containsValidData() const { - time_t t = getFwBuildDateTime(); + const time_t t = getFwBuildDateTime(); struct tm info; localtime_r(&t, &info); - return info.tm_year > (2016 - 1900); + return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097; } -uint8_t DevInfoParser::getDevIdx() +uint8_t DevInfoParser::getDevIdx() const { uint8_t ret = 0xff; uint8_t pos; @@ -228,7 +268,7 @@ uint8_t DevInfoParser::getDevIdx() } /* struct tm to seconds since Unix epoch */ -time_t DevInfoParser::timegm(struct tm* t) +time_t DevInfoParser::timegm(const struct tm* t) { uint32_t year; time_t result; @@ -254,4 +294,4 @@ time_t DevInfoParser::timegm(struct tm* t) result -= 3600; /*@ -matchanyintegral @*/ return (result); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index 838ba1102..239055251 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -8,32 +8,33 @@ class DevInfoParser : public Parser { public: DevInfoParser(); void clearBufferAll(); - void appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragmentAll(const uint8_t offset, const uint8_t* payload, const uint8_t len); void clearBufferSimple(); - void appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragmentSimple(const uint8_t offset, const uint8_t* payload, const uint8_t len); - uint32_t getLastUpdateAll(); - void setLastUpdateAll(uint32_t lastUpdate); + uint32_t getLastUpdateAll() const; + void setLastUpdateAll(const uint32_t lastUpdate); - uint32_t getLastUpdateSimple(); - void setLastUpdateSimple(uint32_t lastUpdate); + uint32_t getLastUpdateSimple() const; + void setLastUpdateSimple(const uint32_t lastUpdate); - uint16_t getFwBuildVersion(); - time_t getFwBuildDateTime(); - uint16_t getFwBootloaderVersion(); + uint16_t getFwBuildVersion() const; + time_t getFwBuildDateTime() const; + String getFwBuildDateTimeStr() const; + uint16_t getFwBootloaderVersion() const; - uint32_t getHwPartNumber(); - String getHwVersion(); + uint32_t getHwPartNumber() const; + String getHwVersion() const; - uint16_t getMaxPower(); - String getHwModelName(); + uint16_t getMaxPower() const; + String getHwModelName() const; - bool containsValidData(); + bool containsValidData() const; private: - time_t timegm(struct tm* tm); - uint8_t getDevIdx(); + static time_t timegm(const struct tm* tm); + uint8_t getDevIdx() const; uint32_t _lastUpdateAll = 0; uint32_t _lastUpdateSimple = 0; @@ -43,4 +44,4 @@ class DevInfoParser : public Parser { uint8_t _payloadDevInfoSimple[DEV_INFO_SIZE] = {}; uint8_t _devInfoSimpleLength = 0; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 35f7689d5..489565e19 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -1,10 +1,366 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'GridOnProFilePara'. +It contains the whole grid profile of the inverter. + +Data structure: + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 + |<---------- Returns till the end of the payload ---------->| +--------------------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 01 0A 00 20 01 00 0C 08 FC 07 A3 00 0F 09 E2 00 1E E6 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx Profile ID Profile Version Section ID Section Version Value Value Value Value CRC16 CRC8 + +The number of values depends on the respective section and its version. After the last value of a section follows the next section id. +*/ #include "GridProfileParser.h" #include "../Hoymiles.h" #include +#include +#include + +const std::array GridProfileParser::_profileTypes = { { + { 0x02, 0x00, "US - NA_IEEE1547_240V" }, + { 0x03, 0x00, "DE - DE_VDE4105_2018" }, + { 0x03, 0x01, "DE - DE_VDE4105_2011" }, + { 0x0a, 0x00, "XX - EN 50549-1:2019" }, + { 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" }, + { 0x0d, 0x04, "XX - NF_EN_50549-1:2019" }, + { 0x10, 0x00, "ES - ES_RD1699" }, + { 0x12, 0x00, "PL - EU_EN50438" }, + { 0x29, 0x00, "NL - NL_NEN-EN50549-1_2019" }, + { 0x37, 0x00, "CH - CH_NA EEA-NE7-CH2020" }, +} }; + +constexpr frozen::map profileSection = { + { 0x00, "Voltage (H/LVRT)" }, + { 0x10, "Frequency (H/LFRT)" }, + { 0x20, "Island Detection (ID)" }, + { 0x30, "Reconnection (RT)" }, + { 0x40, "Ramp Rates (RR)" }, + { 0x50, "Frequency Watt (FW)" }, + { 0x60, "Volt Watt (VW)" }, + { 0x70, "Active Power Control (APC)" }, + { 0x80, "Volt Var (VV)" }, + { 0x90, "Specified Power Factor (SPF)" }, + { 0xA0, "Reactive Power Control (RPC)" }, + { 0xB0, "Watt Power Factor (WPF)" }, +}; + +struct GridProfileItemDefinition_t { + frozen::string Name; + frozen::string Unit; + uint8_t Divider; +}; + +constexpr GridProfileItemDefinition_t make_value(frozen::string Name, frozen::string Unit, uint8_t divisor) +{ + GridProfileItemDefinition_t v = { Name, Unit, divisor }; + return v; +} + +constexpr frozen::map itemDefinitions = { + { 0x01, make_value("Nominale Voltage (NV)", "V", 10) }, + { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, + { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x04, make_value("High Voltage 1 (HV1)", "V", 10) }, + { 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x06, make_value("Low Voltage 2 (LV2)", "V", 10) }, + { 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 100) }, + { 0x08, make_value("High Voltage 2 (HV2)", "V", 10) }, + { 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 100) }, + { 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) }, + { 0x0b, make_value("High Voltage 3 (HV3)", "V", 10) }, + { 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 100) }, + { 0x0d, make_value("Nominal Frequency", "Hz", 100) }, + { 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) }, + { 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x10, make_value("High Frequency 1 (HF1)", "Hz", 100) }, + { 0x11, make_value("HF1 Maximum Trip time (MTT)", "s", 10) }, + { 0x12, make_value("Low Frequency 2 (LF2)", "Hz", 100) }, + { 0x13, make_value("LF2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x14, make_value("High Frequency 2 (HF2)", "Hz", 100) }, + { 0x15, make_value("HF2 Maximum Trip time (MTT)", "s", 10) }, + { 0x16, make_value("ID Function Activated", "bool", 1) }, + { 0x17, make_value("Reconnect Time (RT)", "s", 10) }, + { 0x18, make_value("Reconnect High Voltage (RHV)", "V", 10) }, + { 0x19, make_value("Reconnect Low Voltage (RLV)", "V", 10) }, + { 0x1a, make_value("Reconnect High Frequency (RHF)", "Hz", 100) }, + { 0x1b, make_value("Reconnect Low Frequency (RLF)", "Hz", 100) }, + { 0x1c, make_value("Normal Ramp up Rate(RUR_NM)", "Rated%/s", 100) }, + { 0x1d, make_value("Soft Start Ramp up Rate (RUR_SS)", "Rated%/s", 100) }, + { 0x1e, make_value("FW Function Activated", "bool", 1) }, + { 0x1f, make_value("Start of Frequency Watt Droop (Fstart)", "Hz", 100) }, + { 0x20, make_value("FW Droop Slope (Kpower_Freq)", "Pn%/Hz", 10) }, + { 0x21, make_value("Recovery Ramp Rate (RRR)", "Pn%/s", 100) }, + { 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 10) }, + { 0x23, make_value("Recovery Low Frequency (RVLF)", "Hz", 100) }, + { 0x24, make_value("VW Function Activated", "bool", 1) }, + { 0x25, make_value("Start of Voltage Watt Droop (Vstart)", "V", 10) }, + { 0x26, make_value("End of Voltage Watt Droop (Vend)", "V", 10) }, + { 0x27, make_value("Droop Slope (Kpower_Volt)", "Pn%/V", 100) }, + { 0x28, make_value("APC Function Activated", "bool", 1) }, + { 0x29, make_value("Power Ramp Rate (PRR)", "Pn%/s", 100) }, + { 0x2a, make_value("VV Function Activated", "bool", 1) }, + { 0x2b, make_value("Voltage Set Point V1", "V", 10) }, + { 0x2c, make_value("Reactive Set Point Q1", "%Pn", 10) }, + { 0x2d, make_value("Voltage Set Point V2", "V", 10) }, + { 0x2e, make_value("Voltage Set Point V3", "V", 10) }, + { 0x2f, make_value("Voltage Set Point V4", "V", 10) }, + { 0x30, make_value("Reactive Set Point Q4", "%Pn", 10) }, + { 0x31, make_value("VV Setting Time (Tr)", "s", 10) }, + { 0x32, make_value("SPF Function Activated", "bool", 1) }, + { 0x33, make_value("Power Factor (PF)", "", 100) }, + { 0x34, make_value("RPC Function Activated", "bool", 1) }, + { 0x35, make_value("Reactive Power (VAR)", "%Sn", 1) }, + { 0x36, make_value("WPF Function Activated", "bool", 1) }, + { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, + { 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, + { 0x39, make_value("Low Voltage 3 (LV3)", "V", 10) }, + { 0x3a, make_value("LV3 Maximum Trip Time (MTT)", "s", 100) }, + { 0x3b, make_value("Momentary Cessition Low Voltage", "V", 10) }, + { 0x3c, make_value("Momentary Cessition High Voltage", "V", 10) }, + { 0x3d, make_value("FW Settling Time (Tr)", "s", 10) }, + { 0x3e, make_value("LF2 Maximum Trip Time (MTT)", "s", 100) }, + { 0x3f, make_value("HF2 Maximum Trip time (MTT)", "s", 100) }, + { 0x40, make_value("Short Interruption Reconnect Time (SRT)", "s", 10) }, + { 0x41, make_value("Short Interruption Time (SIT)", "s", 10) }, + { 0xff, make_value("Unkown Value", "", 1) }, +}; + +const std::array GridProfileParser::_profileValues = { { + // Voltage (H/LVRT) + // Version 0x00 + { 0x00, 0x00, 0x01 }, + { 0x00, 0x00, 0x02 }, + { 0x00, 0x00, 0x03 }, + { 0x00, 0x00, 0x04 }, + { 0x00, 0x00, 0x05 }, + + // Version 0x01 + { 0x00, 0x01, 0x01 }, + { 0x00, 0x01, 0x02 }, + { 0x00, 0x01, 0x03 }, + { 0x00, 0x01, 0x04 }, + { 0x00, 0x01, 0x05 }, + { 0x00, 0x01, 0x08 }, + { 0x00, 0x01, 0x09 }, + + // Version 0x02 + { 0x00, 0x02, 0x01 }, + { 0x00, 0x02, 0x02 }, + { 0x00, 0x02, 0x03 }, + { 0x00, 0x02, 0x04 }, + { 0x00, 0x02, 0x05 }, + { 0x00, 0x02, 0x06 }, + { 0x00, 0x02, 0x07 }, + + // Version 0x03 + { 0x00, 0x03, 0x01 }, + { 0x00, 0x03, 0x02 }, + { 0x00, 0x03, 0x03 }, + { 0x00, 0x03, 0x05 }, + { 0x00, 0x03, 0x06 }, + { 0x00, 0x03, 0x07 }, + { 0x00, 0x03, 0x08 }, + { 0x00, 0x03, 0x09 }, + + // Version 0x08 + { 0x00, 0x08, 0x01 }, + { 0x00, 0x08, 0x02 }, + { 0x00, 0x08, 0x03 }, + { 0x00, 0x08, 0x04 }, + { 0x00, 0x08, 0x05 }, + { 0x00, 0x08, 0xff }, + + // Version 0x0a + { 0x00, 0x0a, 0x01 }, + { 0x00, 0x0a, 0x02 }, + { 0x00, 0x0a, 0x03 }, + { 0x00, 0x0a, 0x04 }, + { 0x00, 0x0a, 0x05 }, + { 0x00, 0x0a, 0x06 }, + { 0x00, 0x0a, 0x07 }, + { 0x00, 0x0a, 0x0a }, + + // Version 0x0b + { 0x00, 0x0b, 0x01 }, + { 0x00, 0x0b, 0x02 }, + { 0x00, 0x0b, 0x03 }, + { 0x00, 0x0b, 0x04 }, + { 0x00, 0x0b, 0x05 }, + { 0x00, 0x0b, 0x06 }, + { 0x00, 0x0b, 0x07 }, + { 0x00, 0x0b, 0x08 }, + { 0x00, 0x0b, 0x09 }, + { 0x00, 0x0b, 0x0a }, + + // Version 0x0c + { 0x00, 0x0c, 0x01 }, + { 0x00, 0x0c, 0x02 }, + { 0x00, 0x0c, 0x03 }, + { 0x00, 0x0c, 0x04 }, + { 0x00, 0x0c, 0x05 }, + { 0x00, 0x0c, 0x06 }, + { 0x00, 0x0c, 0x07 }, + { 0x00, 0x0c, 0x08 }, + { 0x00, 0x0c, 0x09 }, + { 0x00, 0x0c, 0x0b }, + { 0x00, 0x0c, 0x0c }, + { 0x00, 0x0c, 0x0a }, + + // Version 0x35 + { 0x00, 0x35, 0x01 }, + { 0x00, 0x35, 0x02 }, + { 0x00, 0x35, 0x03 }, + { 0x00, 0x35, 0x04 }, + { 0x00, 0x35, 0x05 }, + { 0x00, 0x35, 0x06 }, + { 0x00, 0x35, 0x07 }, + { 0x00, 0x35, 0x08 }, + { 0x00, 0x35, 0x09 }, + { 0x00, 0x35, 0x39 }, + { 0x00, 0x35, 0x3a }, + { 0x00, 0x35, 0x3b }, + { 0x00, 0x35, 0x3c }, + + // Frequency (H/LFRT) + // Version 0x00 + { 0x10, 0x00, 0x0d }, + { 0x10, 0x00, 0x0e }, + { 0x10, 0x00, 0x0f }, + { 0x10, 0x00, 0x10 }, + { 0x10, 0x00, 0x11 }, + + // Version 0x03 + { 0x10, 0x03, 0x0d }, + { 0x10, 0x03, 0x0e }, + { 0x10, 0x03, 0x0f }, + { 0x10, 0x03, 0x10 }, + { 0x10, 0x03, 0x11 }, + { 0x10, 0x03, 0x12 }, + { 0x10, 0x03, 0x3e }, + { 0x10, 0x03, 0x14 }, + { 0x10, 0x03, 0x3f }, + + // Island Detection (ID) + // Version 0x00 + { 0x20, 0x00, 0x16 }, + + // Reconnection (RT) + // Version 0x03 + { 0x30, 0x03, 0x17 }, + { 0x30, 0x03, 0x18 }, + { 0x30, 0x03, 0x19 }, + { 0x30, 0x03, 0x1a }, + { 0x30, 0x03, 0x1b }, + + // Version 0x07 + { 0x30, 0x07, 0x17 }, + { 0x30, 0x07, 0x18 }, + { 0x30, 0x07, 0x19 }, + { 0x30, 0x07, 0x1a }, + { 0x30, 0x07, 0x1b }, + { 0x30, 0x07, 0x40 }, + { 0x30, 0x07, 0x41 }, + + // Ramp Rates (RR) + // Version 0x00 + { 0x40, 0x00, 0x1c }, + { 0x40, 0x00, 0x1d }, + + // Frequency Watt (FW) + // Version 0x00 + { 0x50, 0x00, 0x1e }, + { 0x50, 0x00, 0x1f }, + { 0x50, 0x00, 0x20 }, + { 0x50, 0x00, 0x21 }, + + // Version 0x01 + { 0x50, 0x01, 0x1e }, + { 0x50, 0x01, 0x1f }, + { 0x50, 0x01, 0x20 }, + { 0x50, 0x01, 0x21 }, + { 0x50, 0x01, 0x22 }, + + // Version 0x08 + { 0x50, 0x08, 0x1e }, + { 0x50, 0x08, 0x1f }, + { 0x50, 0x08, 0x20 }, + { 0x50, 0x08, 0x21 }, + { 0x50, 0x08, 0x22 }, + { 0x50, 0x08, 0x23 }, + + // Version 0x11 + { 0x50, 0x11, 0x1e }, + { 0x50, 0x11, 0x1f }, + { 0x50, 0x11, 0x20 }, + { 0x50, 0x11, 0x21 }, + { 0x50, 0x11, 0x3d }, + + // Volt Watt (VW) + // Version 0x00 + { 0x60, 0x00, 0x24 }, + { 0x60, 0x00, 0x25 }, + { 0x60, 0x00, 0x26 }, + { 0x60, 0x00, 0x27 }, + + // Version 0x04 + { 0x60, 0x04, 0x24 }, + { 0x60, 0x04, 0x25 }, + { 0x60, 0x04, 0x26 }, + { 0x60, 0x04, 0x27 }, + + // Active Power Control (APC) + // Version 0x00 + { 0x70, 0x00, 0x28 }, + + // Version 0x02 + { 0x70, 0x02, 0x28 }, + { 0x70, 0x02, 0x29 }, + + // Volt Var (VV) + // Version 0x00 + { 0x80, 0x00, 0x2a }, + { 0x80, 0x00, 0x2b }, + { 0x80, 0x00, 0x2c }, + { 0x80, 0x00, 0x2d }, + { 0x80, 0x00, 0x2e }, + { 0x80, 0x00, 0x2f }, + { 0x80, 0x00, 0x30 }, + + // Version 0x01 + { 0x80, 0x01, 0x2a }, + { 0x80, 0x01, 0x2b }, + { 0x80, 0x01, 0x2c }, + { 0x80, 0x01, 0x2d }, + { 0x80, 0x01, 0x2e }, + { 0x80, 0x01, 0x2f }, + { 0x80, 0x01, 0x30 }, + { 0x80, 0x01, 0x31 }, + + // Specified Power Factor (SPF) + // Version 0x00 + { 0x90, 0x00, 0x32 }, + { 0x90, 0x00, 0x33 }, + + // Reactive Power Control (RPC) + // Version 0x02 + { 0xa0, 0x02, 0x34 }, + { 0xa0, 0x02, 0x35 }, + + // Watt Power Factor (WPF) + // Version 0x00 + { 0xb0, 0x00, 0x36 }, + { 0xb0, 0x00, 0x37 }, + { 0xb0, 0x00, 0x38 }, +} }; GridProfileParser::GridProfileParser() : Parser() @@ -18,7 +374,7 @@ void GridProfileParser::clearBuffer() _gridProfileLength = 0; } -void GridProfileParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void GridProfileParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > GRID_PROFILE_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) grid profile packet too large for buffer\r\n", __FILE__, __LINE__); @@ -28,13 +384,109 @@ void GridProfileParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _gridProfileLength += len; } -std::vector GridProfileParser::getRawData() +String GridProfileParser::getProfileName() const +{ + for (auto& ptype : _profileTypes) { + if (ptype.lIdx == _payloadGridProfile[0] && ptype.hIdx == _payloadGridProfile[1]) { + return ptype.Name; + } + } + return "Unknown"; +} + +String GridProfileParser::getProfileVersion() const +{ + char buffer[10]; + HOY_SEMAPHORE_TAKE(); + snprintf(buffer, sizeof(buffer), "%d.%d.%d", (_payloadGridProfile[2] >> 4) & 0x0f, _payloadGridProfile[2] & 0x0f, _payloadGridProfile[3]); + HOY_SEMAPHORE_GIVE(); + return buffer; +} + +std::vector GridProfileParser::getRawData() const { std::vector ret; HOY_SEMAPHORE_TAKE(); - for (uint8_t i = 0; i < GRID_PROFILE_SIZE; i++) { + for (uint8_t i = 0; i < _gridProfileLength; i++) { ret.push_back(_payloadGridProfile[i]); } HOY_SEMAPHORE_GIVE(); return ret; } + +std::list GridProfileParser::getProfile() const +{ + std::list l; + + if (_gridProfileLength > 4) { + uint16_t pos = 4; + do { + const uint8_t section_id = _payloadGridProfile[pos]; + const uint8_t section_version = _payloadGridProfile[pos + 1]; + const int16_t section_start = getSectionStart(section_id, section_version); + const uint8_t section_size = getSectionSize(section_id, section_version); + pos += 2; + + GridProfileSection_t section; + try { + section.SectionName = profileSection.at(section_id).data(); + } catch (const std::out_of_range&) { + section.SectionName = "Unknown"; + break; + } + + if (section_start == -1) { + section.SectionName = "Unknown"; + break; + } + + for (uint8_t val_id = 0; val_id < section_size; val_id++) { + auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition); + + float value = (int16_t)((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]); + value /= itemDefinition.Divider; + + GridProfileItem_t v; + v.Name = itemDefinition.Name.data(); + v.Unit = itemDefinition.Unit.data(); + v.Value = value; + section.items.push_back(v); + + pos += 2; + } + + l.push_back(section); + + } while (pos < _gridProfileLength); + } + + return l; +} + +bool GridProfileParser::containsValidData() const +{ + return _gridProfileLength > 6; +} + +uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version) +{ + uint8_t count = 0; + for (auto& values : _profileValues) { + if (values.Section == section_id && values.Version == section_version) { + count++; + } + } + return count; +} + +int16_t GridProfileParser::getSectionStart(const uint8_t section_id, const uint8_t section_version) +{ + int16_t count = -1; + for (auto& values : _profileValues) { + count++; + if (values.Section == section_id && values.Version == section_version) { + break; + } + } + return count; +} diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index c2af52f87..7afdfb825 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -1,18 +1,57 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" +#include #define GRID_PROFILE_SIZE 141 +#define PROFILE_TYPE_COUNT 10 +#define SECTION_VALUE_COUNT 158 + +typedef struct { + uint8_t lIdx; + uint8_t hIdx; + const char* Name; +} ProfileType_t; + +struct GridProfileValue_t { + uint8_t Section; + uint8_t Version; + uint8_t ItemDefinition; +}; + +struct GridProfileItem_t { + String Name; + String Unit; + float Value; +}; + +struct GridProfileSection_t { + String SectionName; + std::list items; +}; class GridProfileParser : public Parser { public: GridProfileParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); + + String getProfileName() const; + String getProfileVersion() const; - std::vector getRawData(); + std::vector getRawData() const; + + std::list getProfile() const; + + bool containsValidData() const; private: + static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); + static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version); + uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; uint8_t _gridProfileLength = 0; -}; \ No newline at end of file + + static const std::array _profileTypes; + static const std::array _profileValues; +}; diff --git a/lib/Hoymiles/src/parser/Parser.cpp b/lib/Hoymiles/src/parser/Parser.cpp index b8e5e0e56..96681ce2a 100644 --- a/lib/Hoymiles/src/parser/Parser.cpp +++ b/lib/Hoymiles/src/parser/Parser.cpp @@ -10,12 +10,12 @@ Parser::Parser() HOY_SEMAPHORE_GIVE(); // release before first use } -uint32_t Parser::getLastUpdate() +uint32_t Parser::getLastUpdate() const { return _lastUpdate; } -void Parser::setLastUpdate(uint32_t lastUpdate) +void Parser::setLastUpdate(const uint32_t lastUpdate) { _lastUpdate = lastUpdate; } diff --git a/lib/Hoymiles/src/parser/Parser.h b/lib/Hoymiles/src/parser/Parser.h index 5d6df75df..dda0ef8a8 100644 --- a/lib/Hoymiles/src/parser/Parser.h +++ b/lib/Hoymiles/src/parser/Parser.h @@ -17,8 +17,8 @@ typedef enum { class Parser { public: Parser(); - uint32_t getLastUpdate(); - void setLastUpdate(uint32_t lastUpdate); + uint32_t getLastUpdate() const; + void setLastUpdate(const uint32_t lastUpdate); void beginAppendFragment(); void endAppendFragment(); diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.cpp b/lib/Hoymiles/src/parser/PowerCommandParser.cpp index d698dad8d..dc8dc7978 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.cpp +++ b/lib/Hoymiles/src/parser/PowerCommandParser.cpp @@ -4,22 +4,22 @@ */ #include "PowerCommandParser.h" -void PowerCommandParser::setLastPowerCommandSuccess(LastCommandSuccess status) +void PowerCommandParser::setLastPowerCommandSuccess(const LastCommandSuccess status) { _lastLimitCommandSuccess = status; } -LastCommandSuccess PowerCommandParser::getLastPowerCommandSuccess() +LastCommandSuccess PowerCommandParser::getLastPowerCommandSuccess() const { return _lastLimitCommandSuccess; } -uint32_t PowerCommandParser::getLastUpdateCommand() +uint32_t PowerCommandParser::getLastUpdateCommand() const { return _lastUpdateCommand; } -void PowerCommandParser::setLastUpdateCommand(uint32_t lastUpdate) +void PowerCommandParser::setLastUpdateCommand(const uint32_t lastUpdate) { _lastUpdateCommand = lastUpdate; setLastUpdate(lastUpdate); diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.h b/lib/Hoymiles/src/parser/PowerCommandParser.h index e005812e6..b448692e6 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.h +++ b/lib/Hoymiles/src/parser/PowerCommandParser.h @@ -4,10 +4,10 @@ class PowerCommandParser : public Parser { public: - void setLastPowerCommandSuccess(LastCommandSuccess status); - LastCommandSuccess getLastPowerCommandSuccess(); - uint32_t getLastUpdateCommand(); - void setLastUpdateCommand(uint32_t lastUpdate); + void setLastPowerCommandSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastPowerCommandSuccess() const; + uint32_t getLastUpdateCommand() const; + void setLastUpdateCommand(const uint32_t lastUpdate); private: LastCommandSuccess _lastLimitCommandSuccess = CMD_OK; // Set to OK because we have to assume nothing is done at startup diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 71c1ebbd2..bd4056113 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -5,12 +5,13 @@ #include "StatisticsParser.h" #include "../Hoymiles.h" -static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); -static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); -static float calcUdcCh(StatisticsParser* iv, uint8_t arg0); -static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0); -static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0); -static float calcIrradiation(StatisticsParser* iv, uint8_t arg0); +static float calcTotalYieldTotal(StatisticsParser* iv, uint8_t arg0); +static float calcTotalYieldDay(StatisticsParser* iv, uint8_t arg0); +static float calcChUdc(StatisticsParser* iv, uint8_t arg0); +static float calcTotalPowerDc(StatisticsParser* iv, uint8_t arg0); +static float calcTotalEffiency(StatisticsParser* iv, uint8_t arg0); +static float calcChIrradiation(StatisticsParser* iv, uint8_t arg0); +static float calcTotalCurrentAc(StatisticsParser* iv, uint8_t arg0); using func_t = float(StatisticsParser*, uint8_t); @@ -20,12 +21,13 @@ struct calcFunc_t { }; const calcFunc_t calcFunctions[] = { - { CALC_YT_CH0, &calcYieldTotalCh0 }, - { CALC_YD_CH0, &calcYieldDayCh0 }, - { CALC_UDC_CH, &calcUdcCh }, - { CALC_PDC_CH0, &calcPowerDcCh0 }, - { CALC_EFF_CH0, &calcEffiencyCh0 }, - { CALC_IRR_CH, &calcIrradiation } + { CALC_TOTAL_YT, &calcTotalYieldTotal }, + { CALC_TOTAL_YD, &calcTotalYieldDay }, + { CALC_CH_UDC, &calcChUdc }, + { CALC_TOTAL_PDC, &calcTotalPowerDc }, + { CALC_TOTAL_EFF, &calcTotalEffiency }, + { CALC_CH_IRR, &calcChIrradiation }, + { CALC_TOTAL_IAC, &calcTotalCurrentAc } }; const FieldId_t runtimeFields[] = { @@ -60,7 +62,7 @@ StatisticsParser::StatisticsParser() clearBuffer(); } -void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, uint8_t size) +void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t size) { _byteAssignment = byteAssignment; _byteAssignmentSize = size; @@ -84,7 +86,7 @@ void StatisticsParser::clearBuffer() _statisticLength = 0; } -void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void StatisticsParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > STATISTIC_PACKET_SIZE) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__); @@ -94,38 +96,60 @@ void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _statisticLength += len; } -const byteAssign_t* StatisticsParser::getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +void StatisticsParser::endAppendFragment() +{ + Parser::endAppendFragment(); + + if (!_enableYieldDayCorrection) { + resetYieldDayCorrection(); + return; + } + + for (auto& c : getChannelsByType(TYPE_DC)) { + // check if current yield day is smaller then last cached yield day + if (getChannelFieldValue(TYPE_DC, c, FLD_YD) < _lastYieldDay[static_cast(c)]) { + // currently all values are zero --> Add last known values to offset + Hoymiles.getMessageOutput()->printf("Yield Day reset detected!\r\n"); + + setChannelFieldOffset(TYPE_DC, c, FLD_YD, _lastYieldDay[static_cast(c)]); + + _lastYieldDay[static_cast(c)] = 0; + } else { + _lastYieldDay[static_cast(c)] = getChannelFieldValue(TYPE_DC, c, FLD_YD); + } + } +} + +const byteAssign_t* StatisticsParser::getAssignmentByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { for (uint8_t i = 0; i < _byteAssignmentSize; i++) { if (_byteAssignment[i].type == type && _byteAssignment[i].ch == channel && _byteAssignment[i].fieldId == fieldId) { return &_byteAssignment[i]; } } - return NULL; + return nullptr; } -fieldSettings_t* StatisticsParser::getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +fieldSettings_t* StatisticsParser::getSettingByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { for (auto& i : _fieldSettings) { if (i.type == type && i.ch == channel && i.fieldId == fieldId) { return &i; } } - return NULL; + return nullptr; } -float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +float StatisticsParser::getChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - - if (pos == NULL) { + if (pos == nullptr) { return 0; } uint8_t ptr = pos->start; - uint8_t end = ptr + pos->num; - uint16_t div = pos->div; + const uint8_t end = ptr + pos->num; + const uint16_t div = pos->div; if (CMD_CALC != div) { // Value is a static value @@ -147,7 +171,9 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch } result /= static_cast(div); - if (setting != NULL && _statisticLength > 0) { + + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr && _statisticLength > 0) { result += setting->offset; } return result; @@ -159,24 +185,23 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } -bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value) +bool StatisticsParser::setChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, float value) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - - if (pos == NULL) { + if (pos == nullptr) { return false; } uint8_t ptr = pos->start + pos->num - 1; - uint8_t end = pos->start; - uint16_t div = pos->div; + const uint8_t end = pos->start; + const uint16_t div = pos->div; if (CMD_CALC == div) { return false; } - if (setting != NULL) { + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr) { value -= setting->offset; } value *= static_cast(div); @@ -200,57 +225,57 @@ bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t cha return true; } -String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +String StatisticsParser::getChannelFieldValueString(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { return String( getChannelFieldValue(type, channel, fieldId), static_cast(getChannelFieldDigits(type, channel, fieldId))); } -bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +bool StatisticsParser::hasChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); - return pos != NULL; + return pos != nullptr; } -const char* StatisticsParser::getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +const char* StatisticsParser::getChannelFieldUnit(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return units[pos->unitId]; } -const char* StatisticsParser::getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +const char* StatisticsParser::getChannelFieldName(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return fields[pos->fieldId]; } -uint8_t StatisticsParser::getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +uint8_t StatisticsParser::getChannelFieldDigits(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); return pos->digits; } -float StatisticsParser::getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +float StatisticsParser::getChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { - fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - if (setting != NULL) { + const fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != nullptr) { return setting->offset; } return 0; } -void StatisticsParser::setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset) +void StatisticsParser::setChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const float offset) { fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); - if (setting != NULL) { + if (setting != nullptr) { setting->offset = offset; } else { _fieldSettings.push_back({ type, channel, fieldId, offset }); } } -std::list StatisticsParser::getChannelTypes() +std::list StatisticsParser::getChannelTypes() const { return { TYPE_AC, @@ -259,12 +284,12 @@ std::list StatisticsParser::getChannelTypes() }; } -const char* StatisticsParser::getChannelTypeName(ChannelType_t type) +const char* StatisticsParser::getChannelTypeName(const ChannelType_t type) const { return channelsTypes[type]; } -std::list StatisticsParser::getChannelsByType(ChannelType_t type) +std::list StatisticsParser::getChannelsByType(const ChannelType_t type) const { std::list l; for (uint8_t i = 0; i < _byteAssignmentSize; i++) { @@ -276,12 +301,12 @@ std::list StatisticsParser::getChannelsByType(ChannelType_t type) return l; } -uint16_t StatisticsParser::getStringMaxPower(uint8_t channel) +uint16_t StatisticsParser::getStringMaxPower(const uint8_t channel) const { return _stringMaxPower[channel]; } -void StatisticsParser::setStringMaxPower(uint8_t channel, uint16_t power) +void StatisticsParser::setStringMaxPower(const uint8_t channel, const uint16_t power) { if (channel < sizeof(_stringMaxPower) / sizeof(_stringMaxPower[0])) { _stringMaxPower[channel] = power; @@ -298,7 +323,7 @@ void StatisticsParser::incrementRxFailureCount() _rxFailureCount++; } -uint32_t StatisticsParser::getRxFailureCount() +uint32_t StatisticsParser::getRxFailureCount() const { return _rxFailureCount; } @@ -313,22 +338,32 @@ void StatisticsParser::zeroDailyData() zeroFields(dailyProductionFields); } -void StatisticsParser::setLastUpdate(uint32_t lastUpdate) +void StatisticsParser::setLastUpdate(const uint32_t lastUpdate) { Parser::setLastUpdate(lastUpdate); setLastUpdateFromInternal(lastUpdate); } -uint32_t StatisticsParser::getLastUpdateFromInternal() +uint32_t StatisticsParser::getLastUpdateFromInternal() const { return _lastUpdateFromInternal; } -void StatisticsParser::setLastUpdateFromInternal(uint32_t lastUpdate) +void StatisticsParser::setLastUpdateFromInternal(const uint32_t lastUpdate) { _lastUpdateFromInternal = lastUpdate; } +bool StatisticsParser::getYieldDayCorrection() const +{ + return _enableYieldDayCorrection; +} + +void StatisticsParser::setYieldDayCorrection(const bool enabled) +{ + _enableYieldDayCorrection = enabled; +} + void StatisticsParser::zeroFields(const FieldId_t* fields) { // Loop all channels @@ -344,7 +379,16 @@ void StatisticsParser::zeroFields(const FieldId_t* fields) setLastUpdateFromInternal(millis()); } -static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) +void StatisticsParser::resetYieldDayCorrection() +{ + // new day detected, reset counters + for (auto& c : getChannelsByType(TYPE_DC)) { + setChannelFieldOffset(TYPE_DC, c, FLD_YD, 0); + _lastYieldDay[static_cast(c)] = 0; + } +} + +static float calcTotalYieldTotal(StatisticsParser* iv, uint8_t arg0) { float yield = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -353,7 +397,7 @@ static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) return yield; } -static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalYieldDay(StatisticsParser* iv, uint8_t arg0) { float yield = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -363,12 +407,12 @@ static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) } // arg0 = channel of source -static float calcUdcCh(StatisticsParser* iv, uint8_t arg0) +static float calcChUdc(StatisticsParser* iv, uint8_t arg0) { return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_UDC); } -static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalPowerDc(StatisticsParser* iv, uint8_t arg0) { float dcPower = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -377,8 +421,7 @@ static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) return dcPower; } -// arg0 = channel -static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalEffiency(StatisticsParser* iv, uint8_t arg0) { float acPower = 0; for (auto& channel : iv->getChannelsByType(TYPE_AC)) { @@ -397,11 +440,20 @@ static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) } // arg0 = channel -static float calcIrradiation(StatisticsParser* iv, uint8_t arg0) +static float calcChIrradiation(StatisticsParser* iv, uint8_t arg0) { - if (NULL != iv) { + if (nullptr != iv) { if (iv->getStringMaxPower(arg0) > 0) return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_PDC) / iv->getStringMaxPower(arg0) * 100.0f; } return 0.0; } + +static float calcTotalCurrentAc(StatisticsParser* iv, uint8_t arg0) +{ + float acCurrent = 0; + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_1); + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_2); + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_3); + return acCurrent; +} diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index da291004f..90b9a5a9f 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -55,12 +55,13 @@ const char* const fields[] = { "Voltage", "Current", "Power", "YieldDay", "Yield // indices to calculation functions, defined in hmInverter.h enum { - CALC_YT_CH0 = 0, - CALC_YD_CH0, - CALC_UDC_CH, - CALC_PDC_CH0, - CALC_EFF_CH0, - CALC_IRR_CH + CALC_TOTAL_YT = 0, + CALC_TOTAL_YD, + CALC_CH_UDC, + CALC_TOTAL_PDC, + CALC_TOTAL_EFF, + CALC_CH_IRR, + CALC_TOTAL_IAC }; enum { CMD_CALC = 0xffff }; @@ -105,49 +106,53 @@ class StatisticsParser : public Parser { public: StatisticsParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); + void endAppendFragment(); - void setByteAssignment(const byteAssign_t* byteAssignment, uint8_t size); + void setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t size); // Returns 1 based amount of expected bytes of statistic data uint8_t getExpectedByteCount(); - const byteAssign_t* getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + const byteAssign_t* getAssignmentByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + fieldSettings_t* getSettingByChannelField(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); - float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - String getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + float getChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + String getChannelFieldValueString(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + bool hasChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + const char* getChannelFieldUnit(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + const char* getChannelFieldName(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; + uint8_t getChannelFieldDigits(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) const; - bool setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value); + bool setChannelFieldValue(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, float value); - float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); + float getChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + void setChannelFieldOffset(const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const float offset); - std::list getChannelTypes(); - const char* getChannelTypeName(ChannelType_t type); - std::list getChannelsByType(ChannelType_t type); + std::list getChannelTypes() const; + const char* getChannelTypeName(const ChannelType_t type) const; + std::list getChannelsByType(const ChannelType_t type) const; - uint16_t getStringMaxPower(uint8_t channel); - void setStringMaxPower(uint8_t channel, uint16_t power); + uint16_t getStringMaxPower(const uint8_t channel) const; + void setStringMaxPower(const uint8_t channel, const uint16_t power); void resetRxFailureCount(); void incrementRxFailureCount(); - uint32_t getRxFailureCount(); + uint32_t getRxFailureCount() const; void zeroRuntimeData(); void zeroDailyData(); + void resetYieldDayCorrection(); // Update time when new data from the inverter is received - void setLastUpdate(uint32_t lastUpdate); + void setLastUpdate(const uint32_t lastUpdate); // Update time when internal data structure changes (from inverter and by internal manipulation) - uint32_t getLastUpdateFromInternal(); - void setLastUpdateFromInternal(uint32_t lastUpdate); + uint32_t getLastUpdateFromInternal() const; + void setLastUpdateFromInternal(const uint32_t lastUpdate); + bool getYieldDayCorrection() const; + void setYieldDayCorrection(const bool enabled); private: void zeroFields(const FieldId_t* fields); @@ -162,4 +167,7 @@ class StatisticsParser : public Parser { uint32_t _rxFailureCount = 0; uint32_t _lastUpdateFromInternal = 0; -}; \ No newline at end of file + + bool _enableYieldDayCorrection = false; + float _lastYieldDay[CH_CNT] = {}; +}; diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index d1ed30b63..346b5d468 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -1,7 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'SystemConfigParaCommand'. +It contains the set inverter limit. + +Data structure: + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +--------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 00 01 03 E8 00 00 03 E8 00 00 00 00 00 00 3C F8 2E -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx ? Limit percent ? ? ? ? ? CRC16 CRC8 +*/ #include "SystemConfigParaParser.h" #include "../Hoymiles.h" #include @@ -18,7 +32,7 @@ void SystemConfigParaParser::clearBuffer() _payloadLength = 0; } -void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) { if (offset + len > (SYSTEM_CONFIG_PARA_SIZE)) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__); @@ -28,15 +42,15 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui _payloadLength += len; } -float SystemConfigParaParser::getLimitPercent() +float SystemConfigParaParser::getLimitPercent() const { HOY_SEMAPHORE_TAKE(); - float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; + const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; HOY_SEMAPHORE_GIVE(); return ret; } -void SystemConfigParaParser::setLimitPercent(float value) +void SystemConfigParaParser::setLimitPercent(const float value) { HOY_SEMAPHORE_TAKE(); _payload[2] = ((uint16_t)(value * 10)) >> 8; @@ -44,49 +58,49 @@ void SystemConfigParaParser::setLimitPercent(float value) HOY_SEMAPHORE_GIVE(); } -void SystemConfigParaParser::setLastLimitCommandSuccess(LastCommandSuccess status) +void SystemConfigParaParser::setLastLimitCommandSuccess(const LastCommandSuccess status) { _lastLimitCommandSuccess = status; } -LastCommandSuccess SystemConfigParaParser::getLastLimitCommandSuccess() +LastCommandSuccess SystemConfigParaParser::getLastLimitCommandSuccess() const { return _lastLimitCommandSuccess; } -uint32_t SystemConfigParaParser::getLastUpdateCommand() +uint32_t SystemConfigParaParser::getLastUpdateCommand() const { return _lastUpdateCommand; } -void SystemConfigParaParser::setLastUpdateCommand(uint32_t lastUpdate) +void SystemConfigParaParser::setLastUpdateCommand(const uint32_t lastUpdate) { _lastUpdateCommand = lastUpdate; setLastUpdate(lastUpdate); } -void SystemConfigParaParser::setLastLimitRequestSuccess(LastCommandSuccess status) +void SystemConfigParaParser::setLastLimitRequestSuccess(const LastCommandSuccess status) { _lastLimitRequestSuccess = status; } -LastCommandSuccess SystemConfigParaParser::getLastLimitRequestSuccess() +LastCommandSuccess SystemConfigParaParser::getLastLimitRequestSuccess() const { return _lastLimitRequestSuccess; } -uint32_t SystemConfigParaParser::getLastUpdateRequest() +uint32_t SystemConfigParaParser::getLastUpdateRequest() const { return _lastUpdateRequest; } -void SystemConfigParaParser::setLastUpdateRequest(uint32_t lastUpdate) +void SystemConfigParaParser::setLastUpdateRequest(const uint32_t lastUpdate) { _lastUpdateRequest = lastUpdate; setLastUpdate(lastUpdate); } -uint8_t SystemConfigParaParser::getExpectedByteCount() +uint8_t SystemConfigParaParser::getExpectedByteCount() const { return SYSTEM_CONFIG_PARA_SIZE; } diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.h b/lib/Hoymiles/src/parser/SystemConfigParaParser.h index 300a81822..847a5d1d0 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.h +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.h @@ -8,23 +8,23 @@ class SystemConfigParaParser : public Parser { public: SystemConfigParaParser(); void clearBuffer(); - void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + void appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len); - float getLimitPercent(); - void setLimitPercent(float value); + float getLimitPercent() const; + void setLimitPercent(const float value); - void setLastLimitCommandSuccess(LastCommandSuccess status); - LastCommandSuccess getLastLimitCommandSuccess(); - uint32_t getLastUpdateCommand(); - void setLastUpdateCommand(uint32_t lastUpdate); + void setLastLimitCommandSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastLimitCommandSuccess() const; + uint32_t getLastUpdateCommand() const; + void setLastUpdateCommand(const uint32_t lastUpdate); - void setLastLimitRequestSuccess(LastCommandSuccess status); - LastCommandSuccess getLastLimitRequestSuccess(); - uint32_t getLastUpdateRequest(); - void setLastUpdateRequest(uint32_t lastUpdate); + void setLastLimitRequestSuccess(const LastCommandSuccess status); + LastCommandSuccess getLastLimitRequestSuccess() const; + uint32_t getLastUpdateRequest() const; + void setLastUpdateRequest(const uint32_t lastUpdate); // Returns 1 based amount of expected bytes of data - uint8_t getExpectedByteCount(); + uint8_t getExpectedByteCount() const; private: uint8_t _payload[SYSTEM_CONFIG_PARA_SIZE]; diff --git a/lib/ResetReason/src/ResetReason.cpp b/lib/ResetReason/src/ResetReason.cpp index b00dab79c..52367d4a5 100644 --- a/lib/ResetReason/src/ResetReason.cpp +++ b/lib/ResetReason/src/ResetReason.cpp @@ -20,7 +20,7 @@ #include "rom/rtc.h" #endif -String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) +String ResetReason::get_reset_reason_verbose(const uint8_t cpu_id) { RESET_REASON reason; reason = rtc_get_reset_reason(cpu_id); @@ -86,7 +86,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) return reason_str; } -String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) +String ResetReason::get_reset_reason_short(const uint8_t cpu_id) { RESET_REASON reason; reason = rtc_get_reset_reason(cpu_id); @@ -150,6 +150,4 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) } return reason_str; -} - -ResetReasonClass ResetReason; +} \ No newline at end of file diff --git a/lib/ResetReason/src/ResetReason.h b/lib/ResetReason/src/ResetReason.h index 34427bfaa..0238cab2b 100644 --- a/lib/ResetReason/src/ResetReason.h +++ b/lib/ResetReason/src/ResetReason.h @@ -3,10 +3,8 @@ #include -class ResetReasonClass { +class ResetReason { public: - String get_reset_reason_verbose(uint8_t cpu_id); - String get_reset_reason_short(uint8_t cpu_id); -}; - -extern ResetReasonClass ResetReason; \ No newline at end of file + static String get_reset_reason_verbose(const uint8_t cpu_id); + static String get_reset_reason_short(const uint8_t cpu_id); +}; \ No newline at end of file diff --git a/lib/ThreadSafeQueue/README.md b/lib/ThreadSafeQueue/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/ThreadSafeQueue/library.json b/lib/ThreadSafeQueue/library.json new file mode 100644 index 000000000..768cb8b23 --- /dev/null +++ b/lib/ThreadSafeQueue/library.json @@ -0,0 +1,13 @@ +{ + "name": "ThreadSafeQueue", + "keywords": "queue, threadsafe", + "description": "An Arduino for ESP32 thread safe queue implementation", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/ThreadSafeQueue/ThreadSafeQueue.h b/lib/ThreadSafeQueue/src/ThreadSafeQueue.h similarity index 100% rename from lib/ThreadSafeQueue/ThreadSafeQueue.h rename to lib/ThreadSafeQueue/src/ThreadSafeQueue.h diff --git a/lib/TimeoutHelper/README.md b/lib/TimeoutHelper/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/TimeoutHelper/library.json b/lib/TimeoutHelper/library.json new file mode 100644 index 000000000..0e0472ba6 --- /dev/null +++ b/lib/TimeoutHelper/library.json @@ -0,0 +1,13 @@ +{ + "name": "TimeoutHelper", + "keywords": "timeout", + "description": "An Arduino for ESP32 timeout helper", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/TimeoutHelper/TimeoutHelper.cpp b/lib/TimeoutHelper/src/TimeoutHelper.cpp similarity index 76% rename from lib/TimeoutHelper/TimeoutHelper.cpp rename to lib/TimeoutHelper/src/TimeoutHelper.cpp index 975a9bbab..3f00c2bc4 100644 --- a/lib/TimeoutHelper/TimeoutHelper.cpp +++ b/lib/TimeoutHelper/src/TimeoutHelper.cpp @@ -11,13 +11,13 @@ TimeoutHelper::TimeoutHelper() startMillis = 0; } -void TimeoutHelper::set(uint32_t ms) +void TimeoutHelper::set(const uint32_t ms) { timeout = ms; startMillis = millis(); } -void TimeoutHelper::extend(uint32_t ms) +void TimeoutHelper::extend(const uint32_t ms) { timeout += ms; } @@ -27,7 +27,7 @@ void TimeoutHelper::reset() startMillis = millis(); } -bool TimeoutHelper::occured() +bool TimeoutHelper::occured() const { return millis() > (startMillis + timeout); } \ No newline at end of file diff --git a/lib/TimeoutHelper/TimeoutHelper.h b/lib/TimeoutHelper/src/TimeoutHelper.h similarity index 68% rename from lib/TimeoutHelper/TimeoutHelper.h rename to lib/TimeoutHelper/src/TimeoutHelper.h index 369749e6e..058de09dc 100644 --- a/lib/TimeoutHelper/TimeoutHelper.h +++ b/lib/TimeoutHelper/src/TimeoutHelper.h @@ -6,10 +6,10 @@ class TimeoutHelper { public: TimeoutHelper(); - void set(uint32_t ms); - void extend(uint32_t ms); + void set(const uint32_t ms); + void extend(const uint32_t ms); void reset(); - bool occured(); + bool occured() const; private: uint32_t startMillis; diff --git a/partitions_custom_16mb.csv b/partitions_custom_16mb.csv new file mode 100644 index 000000000..1c48e6bbe --- /dev/null +++ b/partitions_custom_16mb.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000 +otadata, data, ota, 0xE000, 0x2000 +app0, app, ota_0, 0x10000, 0x7E0000 +app1, app, ota_1, 0x7F0000, 0x7E0000 +spiffs, data, spiffs, 0xFD0000, 0x30000 diff --git a/partitions_custom.csv b/partitions_custom_4mb.csv similarity index 100% rename from partitions_custom.csv rename to partitions_custom_4mb.csv diff --git a/patches/esp32c3/EspAsyncWebserver.patch b/patches/esp32c3/EspAsyncWebserver.patch deleted file mode 100644 index 079c164d4..000000000 --- a/patches/esp32c3/EspAsyncWebserver.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -index 12be5f8..8505f73 100644 ---- a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -+++ b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) - IPAddress AsyncWebSocketClient::remoteIP() const - { - if (!_client) -- return IPAddress(0U); -+ return IPAddress((uint32_t)0); - - return _client->remoteIP(); - } diff --git a/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index 2f15cc959..26e1bd65a 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -2,28 +2,72 @@ # # Copyright (C) 2022 Thomas Basler and others # -import pkg_resources +import os Import("env") -required_pkgs = {'dulwich'} -installed_pkgs = {pkg.key for pkg in pkg_resources.working_set} -missing_pkgs = required_pkgs - installed_pkgs - -if missing_pkgs: +try: + from dulwich import porcelain +except ModuleNotFoundError: env.Execute('"$PYTHONEXE" -m pip install dulwich') + from dulwich import porcelain -from dulwich import porcelain -def get_firmware_specifier_build_flag(): +def updateFileIfChanged(filename, content): + mustUpdate = True + try: + with open(filename, "rb") as fp: + if fp.read() == content: + mustUpdate = False + except: + pass + if mustUpdate: + with open(filename, "wb") as fp: + fp.write(content) + return mustUpdate + + +def get_build_version(): try: build_version = porcelain.describe('.') # '.' refers to the repository root dir except: build_version = "g0000000" - build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" print ("Firmware Revision: " + build_version) + return build_version + + +def get_firmware_specifier_build_flag(): + build_version = get_build_version() + build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" return (build_flag) -env.Append( - BUILD_FLAGS=[get_firmware_specifier_build_flag()] -) \ No newline at end of file + +def do_main(): + if 0: + # this results in a full recompilation of the whole project after each commit + env.Append( + BUILD_FLAGS=[get_firmware_specifier_build_flag()] + ) + else: + # we just create a .c file containing the needed datas + targetfile = os.path.join(env.subst("$BUILD_DIR"), "__compiled_constants.c") + lines = "" + lines += "/* Generated file within build process - Do NOT edit */\n" + + if 0: + # Add the current date and time as string in UTC timezone + from datetime import datetime, timezone + now = datetime.now(tz=timezone.utc) + COMPILED_DATE_TIME_UTC_STR = now.strftime("%Y/%m/%d %H:%M:%S") + lines += 'const char *__COMPILED_DATE_TIME_UTC_STR__ = "%s";\n' % (COMPILED_DATE_TIME_UTC_STR) + + if 1: + # Add the description of the current git revision + lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version()) + + updateFileIfChanged(targetfile, bytes(lines, "utf-8")) + + # Add the created file to the buildfiles - platformio knows how to handle *.c files + env.AppendUnique(PIOBUILDFILES=[targetfile]) + +do_main() diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py index 56f71c4b1..d394998b2 100644 --- a/pio-scripts/create_factory_bin.py +++ b/pio-scripts/create_factory_bin.py @@ -21,7 +21,7 @@ platform = env.PioPlatform() import sys -from os.path import join +from os.path import join, getsize sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) import esptool @@ -60,6 +60,14 @@ def esp32_create_combined_bin(source, target, env): flash_size, ] + # platformio estimates the amount of flash used to store the firmware. this + # estimate is not accurate. we perform a final check on the firmware bin + # size by comparing it against the respective partition size. + max_size = env.BoardConfig().get("upload.maximum_size", 1) + fw_size = getsize(firmware_name) + if (fw_size > max_size): + raise Exception("firmware binary too large: %d > %d" % (fw_size, max_size)) + print(" Offset | File") for section in sections: sect_adr, sect_file = section.split(" ", 1) diff --git a/pio-scripts/patch_apply.py b/pio-scripts/patch_apply.py index 17e25584c..5734c4aa0 100644 --- a/pio-scripts/patch_apply.py +++ b/pio-scripts/patch_apply.py @@ -9,7 +9,10 @@ Import("env") def getPatchPath(env): - return os.path.join(env["PROJECT_DIR"], "patches", env.GetProjectOption('custom_patches')) + patchList = [] + for patch in env.GetProjectOption('custom_patches').split(","): + patchList.append(os.path.join(env["PROJECT_DIR"], "patches", patch)) + return patchList def is_tool(name): """Check whether `name` is on PATH and marked as executable.""" @@ -44,35 +47,36 @@ def main(): print('Git not found. Will not apply custom patches!') return - directory = getPatchPath(env) - if (not os.path.isdir(directory)): - print('Patch directory not found: ' + directory) - return - - for file in os.listdir(directory): - if (not file.endswith('.patch')): - continue - - fullPath = os.path.join(directory, file) - preparePath = fullPath + '.prepare' - replaceInFile(fullPath, preparePath, '$$$env$$$', env['PIOENV']) - print('Working on patch: ' + fullPath + '... ', end='') + directories = getPatchPath(env) + for directory in directories: + if (not os.path.isdir(directory)): + print('Patch directory not found: ' + directory) + return + + for file in os.listdir(directory): + if (not file.endswith('.patch')): + continue + + fullPath = os.path.join(directory, file) + preparePath = fullPath + '.prepare' + replaceInFile(fullPath, preparePath, '$$$env$$$', env['PIOENV']) + print('Working on patch: ' + fullPath + '... ', end='') + + # Check if patch was already applied + process = subprocess.run(['git', 'apply', '--reverse', '--check', preparePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if (process.returncode == 0): + print('already applied') + os.remove(preparePath) + continue + + # Apply patch + process = subprocess.run(['git', 'apply', preparePath]) + if (process.returncode == 0): + print('applied') + else: + print('failed') - # Check if patch was already applied - process = subprocess.run(['git', 'apply', '--reverse', '--check', preparePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if (process.returncode == 0): - print('already applied') os.remove(preparePath) - continue - - # Apply patch - process = subprocess.run(['git', 'apply', preparePath]) - if (process.returncode == 0): - print('applied') - else: - print('failed') - - os.remove(preparePath) -main() \ No newline at end of file +main() diff --git a/platformio.ini b/platformio.ini index 7a75f289c..83fe13239 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,3 +1,4 @@ +<<<<<<< HEAD ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter @@ -240,4 +241,257 @@ build_flags = ${env.build_flags} -DCMT_GPIO3=8 -DCMT_SDIO=5 -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 \ No newline at end of file + -DARDUINO_USB_CDC_ON_BOOT=1 +======= +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = generic_esp32 +extra_configs = + platformio_override.ini + +[env] +; Make sure to NOT add any spaces in the custom_ci_action property +; (also the position in the file is important) +custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb + +framework = arduino +platform = espressif32@6.7.0 + +build_flags = + -DPIOENV=\"$PIOENV\" + -D_TASK_STD_FUNCTION=1 + -D_TASK_THREAD_SAFE=1 + -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 + -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference +; Have to remove -Werror because of +; https://github.com/espressif/arduino-esp32/issues/9044 and +; https://github.com/espressif/arduino-esp32/issues/9045 +; -Werror + -std=c++17 + -std=gnu++17 +build_unflags = + -std=gnu++11 + +lib_deps = + mathieucarbou/ESP Async WebServer @ 2.10.8 + bblanchon/ArduinoJson @ 7.0.4 + https://github.com/bertmelis/espMqttClient.git#v1.7.0 + nrf24/RF24 @ 1.4.8 + olikraus/U8g2 @ 2.35.19 + buelowp/sunset @ 1.1.7 + https://github.com/arkhipenko/TaskScheduler#testing + +extra_scripts = + pre:pio-scripts/auto_firmware_version.py + pre:pio-scripts/patch_apply.py + post:pio-scripts/create_factory_bin.py + +board_build.partitions = partitions_custom_4mb.csv +board_build.filesystem = littlefs +board_build.embed_files = + webapp_dist/index.html.gz + webapp_dist/zones.json.gz + webapp_dist/favicon.ico + webapp_dist/favicon.png + webapp_dist/js/app.js.gz + webapp_dist/site.webmanifest + +custom_patches = + +monitor_filters = esp32_exception_decoder, time, log2file, colorize +monitor_speed = 115200 +upload_protocol = esptool + +; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection. +; monitor_port = COM4 +; upload_port = COM4 + + +[env:generic_esp32] +board = esp32dev +build_flags = ${env.build_flags} + + +[env:generic_esp32_16mb_psram] +board = esp32dev +board_build.flash_mode = qio +board_build.partitions = partitions_custom_16mb.csv +board_upload.flash_size = 16MB +build_flags = ${env.build_flags} + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + + +[env:generic_esp32c3] +board = esp32-c3-devkitc-02 +custom_patches = ${env.custom_patches} +build_flags = ${env.build_flags} + + +[env:generic_esp32c3_usb] +board = esp32-c3-devkitc-02 +custom_patches = ${env.custom_patches} +build_flags = ${env.build_flags} + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + + +[env:generic_esp32s3] +board = esp32-s3-devkitc-1 +build_flags = ${env.build_flags} + + +[env:generic_esp32s3_usb] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +build_flags = ${env.build_flags} + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + + +[env:generic] +board = esp32dev +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=4 + -DHOYMILES_PIN_CS=5 + + +[env:olimex_esp32_poe] +; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware +board = esp32-poe +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=15 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=14 + -DHOYMILES_PIN_IRQ=13 + -DHOYMILES_PIN_CE=16 + -DHOYMILES_PIN_CS=5 + -DOPENDTU_ETHERNET + + +[env:olimex_esp32_evb] +; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware +board = esp32-evb +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=15 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=14 + -DHOYMILES_PIN_IRQ=13 + -DHOYMILES_PIN_CE=16 + -DHOYMILES_PIN_CS=17 + -DOPENDTU_ETHERNET + + +[env:d1_mini_esp32] +board = wemos_d1_mini32 +build_flags = + ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=17 + -DHOYMILES_PIN_CS=5 + + +[env:wt32_eth01] +; http://www.wireless-tag.com/portfolio/wt32-eth01/ +board = wt32-eth01 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=4 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=32 + -DHOYMILES_PIN_IRQ=33 + -DHOYMILES_PIN_CE=14 + -DHOYMILES_PIN_CS=15 + -DOPENDTU_ETHERNET + + +[env:esp_s3_12k_kit] +; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit +board = esp32-s3-devkitc-1 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=16 + -DHOYMILES_PIN_MOSI=17 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=3 + -DHOYMILES_PIN_CE=4 + -DHOYMILES_PIN_CS=5 + + +[env:lolin32_lite] +; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/ +; https://www.az-delivery.de/products/esp32-lolin-lolin32 +board = lolin32_lite +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=17 + -DHOYMILES_PIN_CS=5 + +[env:lolin_s2_mini] +board = lolin_s2_mini +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=13 + -DHOYMILES_PIN_MOSI=11 + -DHOYMILES_PIN_SCLK=12 + -DHOYMILES_PIN_CS=10 + -DHOYMILES_PIN_IRQ=4 + -DHOYMILES_PIN_CE=5 + + +[env:opendtufusionv1] +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 + -DARDUINO_USB_MODE=1 + +[env:opendtufusionv2] +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 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e90f71915..db47d9c8a 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -1,12 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "Configuration.h" #include "MessageOutput.h" +#include "NetworkSettings.h" +#include "Utils.h" #include "defaults.h" #include #include +#include CONFIG_T config; @@ -21,91 +24,102 @@ bool ConfigurationClass::write() if (!f) { return false; } - config.Cfg_SaveCount++; - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - JsonObject cfg = doc.createNestedObject("cfg"); - cfg["version"] = config.Cfg_Version; - cfg["save_count"] = config.Cfg_SaveCount; - - JsonObject wifi = doc.createNestedObject("wifi"); - wifi["ssid"] = config.WiFi_Ssid; - wifi["password"] = config.WiFi_Password; - wifi["ip"] = IPAddress(config.WiFi_Ip).toString(); - wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString(); - wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString(); - wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString(); - wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString(); - wifi["dhcp"] = config.WiFi_Dhcp; - wifi["hostname"] = config.WiFi_Hostname; - wifi["aptimeout"] = config.WiFi_ApTimeout; - - JsonObject mdns = doc.createNestedObject("mdns"); - mdns["enabled"] = config.Mdns_Enabled; - - JsonObject ntp = doc.createNestedObject("ntp"); - ntp["server"] = config.Ntp_Server; - ntp["timezone"] = config.Ntp_Timezone; - ntp["timezone_descr"] = config.Ntp_TimezoneDescr; - ntp["latitude"] = config.Ntp_Latitude; - ntp["longitude"] = config.Ntp_Longitude; - ntp["sunsettype"] = config.Ntp_SunsetType; - - JsonObject mqtt = doc.createNestedObject("mqtt"); - mqtt["enabled"] = config.Mqtt_Enabled; - mqtt["hostname"] = config.Mqtt_Hostname; - mqtt["port"] = config.Mqtt_Port; - mqtt["username"] = config.Mqtt_Username; - mqtt["password"] = config.Mqtt_Password; - mqtt["topic"] = config.Mqtt_Topic; - mqtt["retain"] = config.Mqtt_Retain; - mqtt["publish_interval"] = config.Mqtt_PublishInterval; - mqtt["clean_session"] = config.Mqtt_CleanSession; - - JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); - mqtt_lwt["topic"] = config.Mqtt_LwtTopic; - mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; - mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; - - JsonObject mqtt_tls = mqtt.createNestedObject("tls"); - mqtt_tls["enabled"] = config.Mqtt_Tls; - mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; - mqtt_tls["certlogin"] = config.Mqtt_TlsCertLogin; - mqtt_tls["client_cert"] = config.Mqtt_ClientCert; - mqtt_tls["client_key"] = config.Mqtt_ClientKey; - - JsonObject mqtt_hass = mqtt.createNestedObject("hass"); - mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; - mqtt_hass["retain"] = config.Mqtt_Hass_Retain; - mqtt_hass["topic"] = config.Mqtt_Hass_Topic; - mqtt_hass["individual_panels"] = config.Mqtt_Hass_IndividualPanels; - mqtt_hass["expire"] = config.Mqtt_Hass_Expire; - - JsonObject dtu = doc.createNestedObject("dtu"); - dtu["serial"] = config.Dtu_Serial; - dtu["poll_interval"] = config.Dtu_PollInterval; - dtu["nrf_pa_level"] = config.Dtu_NrfPaLevel; - dtu["cmt_pa_level"] = config.Dtu_CmtPaLevel; - dtu["cmt_frequency"] = config.Dtu_CmtFrequency; - - JsonObject security = doc.createNestedObject("security"); - security["password"] = config.Security_Password; - security["allow_readonly"] = config.Security_AllowReadonly; - - JsonObject device = doc.createNestedObject("device"); + config.Cfg.SaveCount++; + + JsonDocument doc; + + JsonObject cfg = doc["cfg"].to(); + cfg["version"] = config.Cfg.Version; + cfg["save_count"] = config.Cfg.SaveCount; + + JsonObject wifi = doc["wifi"].to(); + wifi["ssid"] = config.WiFi.Ssid; + wifi["password"] = config.WiFi.Password; + wifi["ip"] = IPAddress(config.WiFi.Ip).toString(); + wifi["netmask"] = IPAddress(config.WiFi.Netmask).toString(); + wifi["gateway"] = IPAddress(config.WiFi.Gateway).toString(); + wifi["dns1"] = IPAddress(config.WiFi.Dns1).toString(); + wifi["dns2"] = IPAddress(config.WiFi.Dns2).toString(); + wifi["dhcp"] = config.WiFi.Dhcp; + wifi["hostname"] = config.WiFi.Hostname; + wifi["aptimeout"] = config.WiFi.ApTimeout; + + JsonObject mdns = doc["mdns"].to(); + mdns["enabled"] = config.Mdns.Enabled; + + JsonObject ntp = doc["ntp"].to(); + ntp["server"] = config.Ntp.Server; + ntp["timezone"] = config.Ntp.Timezone; + ntp["timezone_descr"] = config.Ntp.TimezoneDescr; + ntp["latitude"] = config.Ntp.Latitude; + ntp["longitude"] = config.Ntp.Longitude; + ntp["sunsettype"] = config.Ntp.SunsetType; + + JsonObject mqtt = doc["mqtt"].to(); + mqtt["enabled"] = config.Mqtt.Enabled; + mqtt["hostname"] = config.Mqtt.Hostname; + mqtt["port"] = config.Mqtt.Port; + mqtt["clientid"] = config.Mqtt.ClientId; + mqtt["username"] = config.Mqtt.Username; + mqtt["password"] = config.Mqtt.Password; + mqtt["topic"] = config.Mqtt.Topic; + mqtt["retain"] = config.Mqtt.Retain; + mqtt["publish_interval"] = config.Mqtt.PublishInterval; + mqtt["clean_session"] = config.Mqtt.CleanSession; + + JsonObject mqtt_lwt = mqtt["lwt"].to(); + mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic; + mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online; + mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline; + mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos; + + JsonObject mqtt_tls = mqtt["tls"].to(); + mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled; + mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert; + mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin; + mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert; + mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey; + + JsonObject mqtt_hass = mqtt["hass"].to(); + mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled; + mqtt_hass["retain"] = config.Mqtt.Hass.Retain; + mqtt_hass["topic"] = config.Mqtt.Hass.Topic; + mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels; + mqtt_hass["expire"] = config.Mqtt.Hass.Expire; + + JsonObject dtu = doc["dtu"].to(); + dtu["serial"] = config.Dtu.Serial; + dtu["poll_interval"] = config.Dtu.PollInterval; + dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel; + dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel; + dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency; + dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode; + + JsonObject security = doc["security"].to(); + security["password"] = config.Security.Password; + security["allow_readonly"] = config.Security.AllowReadonly; + + JsonObject device = doc["device"].to(); device["pinmapping"] = config.Dev_PinMapping; - JsonObject display = device.createNestedObject("display"); - display["powersafe"] = config.Display_PowerSafe; - display["screensaver"] = config.Display_ScreenSaver; - display["rotation"] = config.Display_Rotation; - display["contrast"] = config.Display_Contrast; - display["language"] = config.Display_Language; + JsonObject display = device["display"].to(); + display["powersafe"] = config.Display.PowerSafe; + display["screensaver"] = config.Display.ScreenSaver; + display["rotation"] = config.Display.Rotation; + display["contrast"] = config.Display.Contrast; + display["language"] = config.Display.Language; + display["diagram_duration"] = config.Display.Diagram.Duration; + display["diagram_mode"] = config.Display.Diagram.Mode; + + JsonArray leds = device["led"].to(); + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + JsonObject led = leds.add(); + led["brightness"] = config.Led_Single[i].Brightness; + } - JsonArray inverters = doc.createNestedArray("inverters"); + JsonArray inverters = doc["inverters"].to(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters.createNestedObject(); + JsonObject inv = inverters.add(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; inv["order"] = config.Inverter[i].Order; @@ -116,16 +130,22 @@ bool ConfigurationClass::write() inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + inv["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight; + inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; - JsonArray channel = inv.createNestedArray("channel"); + JsonArray channel = inv["channel"].to(); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return false; + } + // Serialize JSON to file if (serializeJson(doc, f) == 0) { MessageOutput.println("Failed to write file"); @@ -140,121 +160,137 @@ bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; + // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); + const DeserializationError error = deserializeJson(doc, f); if (error) { MessageOutput.println("Failed to read file, using default configuration"); } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return false; + } + JsonObject cfg = doc["cfg"]; - config.Cfg_Version = cfg["version"] | CONFIG_VERSION; - config.Cfg_SaveCount = cfg["save_count"] | 0; + config.Cfg.Version = cfg["version"] | CONFIG_VERSION; + config.Cfg.SaveCount = cfg["save_count"] | 0; JsonObject wifi = doc["wifi"]; - strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid)); - strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password)); - strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname)); + strlcpy(config.WiFi.Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi.Ssid)); + strlcpy(config.WiFi.Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi.Password)); + strlcpy(config.WiFi.Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi.Hostname)); IPAddress wifi_ip; wifi_ip.fromString(wifi["ip"] | ""); - config.WiFi_Ip[0] = wifi_ip[0]; - config.WiFi_Ip[1] = wifi_ip[1]; - config.WiFi_Ip[2] = wifi_ip[2]; - config.WiFi_Ip[3] = wifi_ip[3]; + config.WiFi.Ip[0] = wifi_ip[0]; + config.WiFi.Ip[1] = wifi_ip[1]; + config.WiFi.Ip[2] = wifi_ip[2]; + config.WiFi.Ip[3] = wifi_ip[3]; IPAddress wifi_netmask; wifi_netmask.fromString(wifi["netmask"] | ""); - config.WiFi_Netmask[0] = wifi_netmask[0]; - config.WiFi_Netmask[1] = wifi_netmask[1]; - config.WiFi_Netmask[2] = wifi_netmask[2]; - config.WiFi_Netmask[3] = wifi_netmask[3]; + config.WiFi.Netmask[0] = wifi_netmask[0]; + config.WiFi.Netmask[1] = wifi_netmask[1]; + config.WiFi.Netmask[2] = wifi_netmask[2]; + config.WiFi.Netmask[3] = wifi_netmask[3]; IPAddress wifi_gateway; wifi_gateway.fromString(wifi["gateway"] | ""); - config.WiFi_Gateway[0] = wifi_gateway[0]; - config.WiFi_Gateway[1] = wifi_gateway[1]; - config.WiFi_Gateway[2] = wifi_gateway[2]; - config.WiFi_Gateway[3] = wifi_gateway[3]; + config.WiFi.Gateway[0] = wifi_gateway[0]; + config.WiFi.Gateway[1] = wifi_gateway[1]; + config.WiFi.Gateway[2] = wifi_gateway[2]; + config.WiFi.Gateway[3] = wifi_gateway[3]; IPAddress wifi_dns1; wifi_dns1.fromString(wifi["dns1"] | ""); - config.WiFi_Dns1[0] = wifi_dns1[0]; - config.WiFi_Dns1[1] = wifi_dns1[1]; - config.WiFi_Dns1[2] = wifi_dns1[2]; - config.WiFi_Dns1[3] = wifi_dns1[3]; + config.WiFi.Dns1[0] = wifi_dns1[0]; + config.WiFi.Dns1[1] = wifi_dns1[1]; + config.WiFi.Dns1[2] = wifi_dns1[2]; + config.WiFi.Dns1[3] = wifi_dns1[3]; IPAddress wifi_dns2; wifi_dns2.fromString(wifi["dns2"] | ""); - config.WiFi_Dns2[0] = wifi_dns2[0]; - config.WiFi_Dns2[1] = wifi_dns2[1]; - config.WiFi_Dns2[2] = wifi_dns2[2]; - config.WiFi_Dns2[3] = wifi_dns2[3]; + config.WiFi.Dns2[0] = wifi_dns2[0]; + config.WiFi.Dns2[1] = wifi_dns2[1]; + config.WiFi.Dns2[2] = wifi_dns2[2]; + config.WiFi.Dns2[3] = wifi_dns2[3]; - config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP; - config.WiFi_ApTimeout = wifi["aptimeout"] | ACCESS_POINT_TIMEOUT; + config.WiFi.Dhcp = wifi["dhcp"] | WIFI_DHCP; + config.WiFi.ApTimeout = wifi["aptimeout"] | ACCESS_POINT_TIMEOUT; JsonObject mdns = doc["mdns"]; - config.Mdns_Enabled = mdns["enabled"] | MDNS_ENABLED; + config.Mdns.Enabled = mdns["enabled"] | MDNS_ENABLED; JsonObject ntp = doc["ntp"]; - strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); - config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; - config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; - config.Ntp_SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE; + strlcpy(config.Ntp.Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp.Server)); + strlcpy(config.Ntp.Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp.Timezone)); + strlcpy(config.Ntp.TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp.TimezoneDescr)); + config.Ntp.Latitude = ntp["latitude"] | NTP_LATITUDE; + config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE; + config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE; JsonObject mqtt = doc["mqtt"]; - config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; - strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname)); - config.Mqtt_Port = mqtt["port"] | MQTT_PORT; - strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); - config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; - config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; - config.Mqtt_CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION; + config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED; + strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname)); + config.Mqtt.Port = mqtt["port"] | MQTT_PORT; + strlcpy(config.Mqtt.ClientId, mqtt["clientid"] | NetworkSettings.getApName().c_str(), sizeof(config.Mqtt.ClientId)); + strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username)); + strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password)); + strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic)); + config.Mqtt.Retain = mqtt["retain"] | MQTT_RETAIN; + config.Mqtt.PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; + config.Mqtt.CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION; JsonObject mqtt_lwt = mqtt["lwt"]; - strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + strlcpy(config.Mqtt.Lwt.Topic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt.Lwt.Topic)); + strlcpy(config.Mqtt.Lwt.Value_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt.Lwt.Value_Online)); + strlcpy(config.Mqtt.Lwt.Value_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt.Lwt.Value_Offline)); + config.Mqtt.Lwt.Qos = mqtt_lwt["qos"] | MQTT_LWT_QOS; JsonObject mqtt_tls = mqtt["tls"]; - config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; - strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); - config.Mqtt_TlsCertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN; - strlcpy(config.Mqtt_ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt_ClientCert)); - strlcpy(config.Mqtt_ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt_ClientKey)); + config.Mqtt.Tls.Enabled = mqtt_tls["enabled"] | MQTT_TLS; + strlcpy(config.Mqtt.Tls.RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt.Tls.RootCaCert)); + config.Mqtt.Tls.CertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN; + strlcpy(config.Mqtt.Tls.ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt.Tls.ClientCert)); + strlcpy(config.Mqtt.Tls.ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt.Tls.ClientKey)); JsonObject mqtt_hass = mqtt["hass"]; - config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; - config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; - config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; - config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; - strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt.Hass.Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; + config.Mqtt.Hass.Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; + config.Mqtt.Hass.Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; + config.Mqtt.Hass.IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; + strlcpy(config.Mqtt.Hass.Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt.Hass.Topic)); JsonObject dtu = doc["dtu"]; - config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; - config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; - config.Dtu_NrfPaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; - config.Dtu_CmtPaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; - config.Dtu_CmtFrequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY; + config.Dtu.Serial = dtu["serial"] | DTU_SERIAL; + config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; + config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; + config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; + config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY; + config.Dtu.Cmt.CountryMode = dtu["cmt_country_mode"] | DTU_CMT_COUNTRY_MODE; JsonObject security = doc["security"]; - strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); - config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; + strlcpy(config.Security.Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security.Password)); + config.Security.AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; JsonObject device = doc["device"]; strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping)); JsonObject display = device["display"]; - config.Display_PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; - config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; - config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; - config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; - config.Display_Language = display["language"] | DISPLAY_LANGUAGE; + config.Display.PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; + config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; + config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION; + config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; + config.Display.Language = display["language"] | DISPLAY_LANGUAGE; + config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; + config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE; + + JsonArray leds = device["led"]; + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + JsonObject led = leds[i].as(); + config.Led_Single[i].Brightness = led["brightness"] | LED_BRIGHTNESS; + } JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -270,6 +306,8 @@ bool ConfigurationClass::read() config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD; config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false; config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false; + config.Inverter[i].ClearEventlogOnMidnight = inv["clear_eventlog"] | false; + config.Inverter[i].YieldDayCorrection = inv["yieldday_correction"] | false; JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -291,15 +329,20 @@ void ConfigurationClass::migrate() return; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; + // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); + const DeserializationError error = deserializeJson(doc, f); if (error) { MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); return; } - if (config.Cfg_Version < 0x00011700) { + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + + if (config.Cfg.Version < 0x00011700) { JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); @@ -311,19 +354,38 @@ void ConfigurationClass::migrate() } } - if (config.Cfg_Version < 0x00011800) { + if (config.Cfg.Version < 0x00011800) { JsonObject mqtt = doc["mqtt"]; - config.Mqtt_PublishInterval = mqtt["publish_invterval"]; + config.Mqtt.PublishInterval = mqtt["publish_invterval"]; } - if (config.Cfg_Version < 0x00011900) { + if (config.Cfg.Version < 0x00011900) { JsonObject dtu = doc["dtu"]; - config.Dtu_NrfPaLevel = dtu["pa_level"]; + config.Dtu.Nrf.PaLevel = dtu["pa_level"]; + } + + if (config.Cfg.Version < 0x00011a00) { + // This migration fixes this issue: https://github.com/espressif/arduino-esp32/issues/8828 + // It occours when migrating from Core 2.0.9 to 2.0.14 + // which was done by updating ESP32 PlatformIO from 6.3.2 to 6.5.0 + nvs_flash_erase(); + nvs_flash_init(); + } + + if (config.Cfg.Version < 0x00011b00) { + // Convert from kHz to Hz + config.Dtu.Cmt.Frequency *= 1000; + } + + if (config.Cfg.Version < 0x00011c00) { + if (!strcmp(config.Ntp.Server, NTP_SERVER_OLD)) { + strlcpy(config.Ntp.Server, NTP_SERVER, sizeof(config.Ntp.Server)); + } } f.close(); - config.Cfg_Version = CONFIG_VERSION; + config.Cfg.Version = CONFIG_VERSION; write(); read(); } @@ -341,10 +403,10 @@ INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() } } - return NULL; + return nullptr; } -INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) +INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial == serial) { @@ -352,7 +414,29 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) } } - return NULL; + return nullptr; +} + +void ConfigurationClass::deleteInverterById(const uint8_t id) +{ + config.Inverter[id].Serial = 0ULL; + strlcpy(config.Inverter[id].Name, "", sizeof(config.Inverter[id].Name)); + config.Inverter[id].Order = 0; + + config.Inverter[id].Poll_Enable = true; + config.Inverter[id].Poll_Enable_Night = true; + config.Inverter[id].Command_Enable = true; + config.Inverter[id].Command_Enable_Night = true; + config.Inverter[id].ReachableThreshold = REACHABLE_THRESHOLD; + config.Inverter[id].ZeroRuntimeDataIfUnrechable = false; + config.Inverter[id].ZeroYieldDayOnMidnight = false; + config.Inverter[id].YieldDayCorrection = false; + + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + config.Inverter[id].channel[c].MaxChannelPower = 0; + config.Inverter[id].channel[c].YieldTotalOffset = 0.0f; + strlcpy(config.Inverter[id].channel[c].Name, "", sizeof(config.Inverter[id].channel[c].Name)); + } } -ConfigurationClass Configuration; \ No newline at end of file +ConfigurationClass Configuration; diff --git a/src/Datastore.cpp b/src/Datastore.cpp index 4ff67b803..15a6dad0f 100644 --- a/src/Datastore.cpp +++ b/src/Datastore.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "Datastore.h" #include "Configuration.h" @@ -8,105 +8,114 @@ DatastoreClass Datastore; -void DatastoreClass::init() +DatastoreClass::DatastoreClass() + : _loopTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&DatastoreClass::loop, this)) { - _updateTimeout.set(1000); +} + +void DatastoreClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.enable(); } void DatastoreClass::loop() { - if (Hoymiles.isAllRadioIdle() && _updateTimeout.occured()) { + if (!Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); + return; + } - uint8_t isProducing = 0; - uint8_t isReachable = 0; - uint8_t pollEnabledCount = 0; + uint8_t isProducing = 0; + uint8_t isReachable = 0; + uint8_t pollEnabledCount = 0; - std::lock_guard lock(_mutex); + std::lock_guard lock(_mutex); - _totalAcYieldTotalEnabled = 0; - _totalAcYieldTotalDigits = 0; + _totalAcYieldTotalEnabled = 0; + _totalAcYieldTotalDigits = 0; - _totalAcYieldDayEnabled = 0; - _totalAcYieldDayDigits = 0; + _totalAcYieldDayEnabled = 0; + _totalAcYieldDayDigits = 0; - _totalAcPowerEnabled = 0; - _totalAcPowerDigits = 0; + _totalAcPowerEnabled = 0; + _totalAcPowerDigits = 0; - _totalDcPowerEnabled = 0; - _totalDcPowerDigits = 0; + _totalDcPowerEnabled = 0; + _totalDcPowerDigits = 0; - _totalDcPowerIrradiation = 0; - _totalDcIrradiationInstalled = 0; + _totalDcPowerIrradiation = 0; + _totalDcIrradiationInstalled = 0; - _isAllEnabledProducing = true; - _isAllEnabledReachable = true; + _isAllEnabledProducing = true; + _isAllEnabledReachable = true; - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - if (inv == nullptr) { - continue; - } + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } - auto cfg = Configuration.getInverterConfig(inv->serial()); - if (cfg == nullptr) { - continue; - } + auto cfg = Configuration.getInverterConfig(inv->serial()); + if (cfg == nullptr) { + continue; + } + + if (inv->getEnablePolling()) { + pollEnabledCount++; + } + if (inv->isProducing()) { + isProducing++; + } else { if (inv->getEnablePolling()) { - pollEnabledCount++; + _isAllEnabledProducing = false; } + } - if (inv->isProducing()) { - isProducing++; - } else { - if (inv->getEnablePolling()) { - _isAllEnabledProducing = false; - } + if (inv->isReachable()) { + isReachable++; + } else { + if (inv->getEnablePolling()) { + _isAllEnabledReachable = false; } + } - if (inv->isReachable()) { - isReachable++; - } else { - if (inv->getEnablePolling()) { - _isAllEnabledReachable = false; - } - } + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_INV)) { + if (cfg->Poll_Enable) { + _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YT); + _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YD); - for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { - if (cfg->Poll_Enable) { - _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); - _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); + _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YT)); + _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YD)); + } + } - _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); - _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); - } - if (inv->getEnablePolling()) { - _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); - _totalAcPowerDigits = max(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); - } + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { + if (inv->getEnablePolling()) { + _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + _totalAcPowerDigits = max(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); } + } - for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) { - if (inv->getEnablePolling()) { - _totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); - _totalDcPowerDigits = max(_totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC)); + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) { + if (inv->getEnablePolling()) { + _totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); + _totalDcPowerDigits = max(_totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC)); - if (inv->Statistics()->getStringMaxPower(c) > 0) { - _totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); - _totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c); - } + if (inv->Statistics()->getStringMaxPower(c) > 0) { + _totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); + _totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c); } } } + } - _isAtLeastOneProducing = isProducing > 0; - _isAtLeastOneReachable = isReachable > 0; - _isAtLeastOnePollEnabled = pollEnabledCount > 0; - - _totalDcIrradiation = _totalDcIrradiationInstalled > 0 ? _totalDcPowerIrradiation / _totalDcIrradiationInstalled * 100.0f : 0; + _isAtLeastOneProducing = isProducing > 0; + _isAtLeastOneReachable = isReachable > 0; + _isAtLeastOnePollEnabled = pollEnabledCount > 0; - _updateTimeout.reset(); - } + _totalDcIrradiation = _totalDcIrradiationInstalled > 0 ? _totalDcPowerIrradiation / _totalDcIrradiationInstalled * 100.0f : 0; } float DatastoreClass::getTotalAcYieldTotalEnabled() diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 26991cb5d..4433c4342 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -1,4 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023-2024 Thomas Basler and others + */ #include "Display_Graphic.h" #include "Datastore.h" #include @@ -9,6 +12,8 @@ std::map { DisplayType_t::PCD8544, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_PCD8544_84X48_F_4W_HW_SPI(U8G2_R0, cs, data, reset); } }, { DisplayType_t::SSD1306, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, { DisplayType_t::SH1106, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, + { DisplayType_t::SSD1309, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(U8G2_R0, reset, clock, data); } }, + { 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 @@ -24,13 +29,20 @@ const uint8_t languages[] = { }; static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; -static const char* const i18n_current_power_w[] = { "%3.0f W", "%3.0f W", "%3.0f W" }; -static const char* const i18n_current_power_kw[] = { "%2.1f kW", "%2.1f kW", "%2.1f kW" }; + +static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; +static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" }; + static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" }; +static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" }; + static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" }; +static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" }; + static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" }; DisplayGraphicClass::DisplayGraphicClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&DisplayGraphicClass::loop, this)) { } @@ -39,29 +51,45 @@ DisplayGraphicClass::~DisplayGraphicClass() delete _display; } -void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset) +void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset) { _display_type = type; - if (_display_type > DisplayType_t::None) { + if (isValidDisplay()) { auto constructor = display_types[_display_type]; _display = constructor(reset, clk, data, cs); + if (_display_type == DisplayType_t::ST7567_GM12864I_59N) { + _display->setI2CAddress(0x3F << 1); + } _display->begin(); setContrast(DISPLAY_CONTRAST); setStatus(true); + _diagram.init(scheduler, _display); + + scheduler.addTask(_loopTask); + _loopTask.setInterval(_period); + _loopTask.enable(); } } void DisplayGraphicClass::calcLineHeights() { - uint8_t yOff = 0; + bool diagram = (_isLarge && _diagram_mode == DiagramMode_t::Small); + // the diagram needs space. we need to keep + // away from the y-axis label in particular. + uint8_t yOff = (diagram ? 7 : 0); for (uint8_t i = 0; i < 4; i++) { setFont(i); - yOff += (_display->getMaxCharHeight()); + yOff += _display->getAscent(); _lineOffsets[i] = yOff; + yOff += ((!_isLarge || diagram) ? 2 : 3); + // the descent is a negative value and moves the *next* line's + // baseline. the first line never uses a letter with descent and + // we need that space when showing the small diagram. + yOff -= ((i == 0 && diagram) ? 0 : _display->getDescent()); } } -void DisplayGraphicClass::setFont(uint8_t line) +void DisplayGraphicClass::setFont(const uint8_t line) { switch (line) { case 0: @@ -76,23 +104,46 @@ void DisplayGraphicClass::setFont(uint8_t line) } } -void DisplayGraphicClass::printText(const char* text, uint8_t line) +bool DisplayGraphicClass::isValidDisplay() { + return _display_type > DisplayType_t::None && _display_type < DisplayType_Max; +} + +void DisplayGraphicClass::printText(const char* text, const uint8_t line) +{ + setFont(line); + uint8_t dispX; if (!_isLarge) { dispX = (line == 0) ? 5 : 0; } else { - dispX = (line == 0) ? 20 : 5; + if (line == 0 && _diagram_mode == DiagramMode_t::Small) { + // Center between left border and diagram + dispX = (CHART_POSX - _display->getStrWidth(text)) / 2; + } else { + // Center on screen + dispX = (_display->getDisplayWidth() - _display->getStrWidth(text)) / 2; + } + } + + if (enableScreensaver) { + unsigned maxOffset = (_isLarge ? 8 : 6); + unsigned period = 2 * maxOffset; + unsigned step = _mExtra % period; + int offset = (step <= maxOffset) ? step : (period - step); + offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens + dispX += offset; } - setFont(line); - dispX += enableScreensaver ? (_mExtra % 7) : 0; + if (dispX > _display->getDisplayWidth()) { + dispX = 0; + } _display->drawStr(dispX, _lineOffsets[line], text); } -void DisplayGraphicClass::setOrientation(uint8_t rotation) +void DisplayGraphicClass::setOrientation(const uint8_t rotation) { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } @@ -115,14 +166,21 @@ void DisplayGraphicClass::setOrientation(uint8_t rotation) calcLineHeights(); } -void DisplayGraphicClass::setLanguage(uint8_t language) +void DisplayGraphicClass::setLanguage(const uint8_t language) { _display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; } +void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode) +{ + if (mode < DiagramMode_t::DisplayMode_Max) { + _diagram_mode = mode; + } +} + void DisplayGraphicClass::setStartupDisplay() { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } @@ -131,50 +189,81 @@ void DisplayGraphicClass::setStartupDisplay() _display->sendBuffer(); } -void DisplayGraphicClass::loop() +DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram() { - if (_display_type == DisplayType_t::None) { - return; - } + return _diagram; +} - if ((millis() - _lastDisplayUpdate) > _period) { +void DisplayGraphicClass::loop() +{ + _loopTask.setInterval(_period); - _display->clearBuffer(); - bool displayPowerSave = false; + _display->clearBuffer(); + bool displayPowerSave = false; + bool showText = true; - //=====> Actual Production ========== - if (Datastore.getIsAtLeastOneReachable()) { - displayPowerSave = false; - if (Datastore.getTotalAcPowerEnabled() > 999) { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); + //=====> Actual Production ========== + if (Datastore.getIsAtLeastOneReachable()) { + displayPowerSave = false; + if (_isLarge) { + uint8_t screenSaverOffsetX = enableScreensaver ? (_mExtra % 7) : 0; + switch (_diagram_mode) { + case DiagramMode_t::Small: + _diagram.redraw(screenSaverOffsetX, CHART_POSX, CHART_POSY, CHART_WIDTH, CHART_HEIGHT, false); + break; + case DiagramMode_t::Fullscreen: + // Every 10 seconds + if (_mExtra % (10 * 2) < 10) { + _diagram.redraw(screenSaverOffsetX, 10, 0, _display->getDisplayWidth() - 12, _display->getDisplayHeight() - 3, true); + showText = false; + } + break; + default: + break; + } + } + if (showText) { + const float watts = Datastore.getTotalAcPowerEnabled(); + if (watts > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000); } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.getTotalAcPowerEnabled()); + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts); } printText(_fmtText, 0); - _previousMillis = millis(); } - //<======================= - - //=====> Offline =========== - else { - printText(i18n_offline[_display_language], 0); - // check if it's time to enter power saving mode - if (millis() - _previousMillis >= (_interval * 2)) { - displayPowerSave = enablePowerSafe; - } + _previousMillis = millis(); + } + //<======================= + + //=====> Offline =========== + else { + printText(i18n_offline[_display_language], 0); + // check if it's time to enter power saving mode + if (millis() - _previousMillis >= (_interval * 2)) { + displayPowerSave = enablePowerSafe; } - //<======================= + } + //<======================= - //=====> Today & Total Production ======= - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); + if (showText) { + // Daily production + float wattsToday = Datastore.getTotalAcYieldDayEnabled(); + if (wattsToday >= 10000) { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); + } printText(_fmtText, 1); - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled()); + // 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); printText(_fmtText, 2); - //<======================= //=====> IP or Date-Time ======== - if (!(_mExtra % 10) && NetworkSettings.localIP()) { + // Change every 3 seconds + if (!(_mExtra % (3 * 2) < 3) && NetworkSettings.localIP()) { printText(NetworkSettings.localIP().toString().c_str(), 3); } else { // Get current time @@ -182,30 +271,30 @@ void DisplayGraphicClass::loop() strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); printText(_fmtText, 3); } - _display->sendBuffer(); + } - _mExtra++; - _lastDisplayUpdate = millis(); + _display->sendBuffer(); - if (!_displayTurnedOn) { - displayPowerSave = true; - } + _mExtra++; - _display->setPowerSave(displayPowerSave); + if (!_displayTurnedOn) { + displayPowerSave = true; } + + _display->setPowerSave(displayPowerSave); } -void DisplayGraphicClass::setContrast(uint8_t contrast) +void DisplayGraphicClass::setContrast(const uint8_t contrast) { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } _display->setContrast(contrast * 2.55f); } -void DisplayGraphicClass::setStatus(bool turnOn) +void DisplayGraphicClass::setStatus(const bool turnOn) { _displayTurnedOn = turnOn; } -DisplayGraphicClass Display; \ No newline at end of file +DisplayGraphicClass Display; diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp new file mode 100644 index 000000000..b52968829 --- /dev/null +++ b/src/Display_Graphic_Diagram.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023-2024 Thomas Basler and others + */ +#include "Display_Graphic_Diagram.h" +#include "Configuration.h" +#include "Datastore.h" +#include + +DisplayGraphicDiagramClass::DisplayGraphicDiagramClass() + : _averageTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&DisplayGraphicDiagramClass::averageLoop, this)) + , _dataPointTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&DisplayGraphicDiagramClass::dataPointLoop, this)) +{ +} + +void DisplayGraphicDiagramClass::init(Scheduler& scheduler, U8G2* display) +{ + _display = display; + + scheduler.addTask(_averageTask); + _averageTask.enable(); + + scheduler.addTask(_dataPointTask); + updatePeriod(); + _dataPointTask.enable(); +} + +void DisplayGraphicDiagramClass::averageLoop() +{ + const float currentWatts = Datastore.getTotalAcPowerEnabled(); // get the current AC production + _iRunningAverage += currentWatts; + _iRunningAverageCnt++; +} + +void DisplayGraphicDiagramClass::dataPointLoop() +{ + if (_graphValuesCount >= std::size(_graphValues)) { + for (uint8_t i = 0; i < std::size(_graphValues) - 1; i++) { + _graphValues[i] = _graphValues[i + 1]; + } + _graphValuesCount = std::size(_graphValues) - 1; + } + if (_iRunningAverageCnt != 0) { + _graphValues[_graphValuesCount++] = _iRunningAverage / _iRunningAverageCnt; + _iRunningAverage = 0; + _iRunningAverageCnt = 0; + } +} + +uint32_t DisplayGraphicDiagramClass::getSecondsPerDot() +{ + return Configuration.get().Display.Diagram.Duration / _chartWidth; +} + +void DisplayGraphicDiagramClass::updatePeriod() +{ + // Calculate seconds per datapoint + _dataPointTask.setInterval(Configuration.get().Display.Diagram.Duration * TASK_SECOND / MAX_DATAPOINTS); +} + +void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos, uint8_t yPos, uint8_t width, uint8_t height, bool isFullscreen) +{ + _chartWidth = width; + + // screenSaverOffsetX expected to be in range 0..6 + const uint8_t graphPosX = xPos + ((screenSaverOffsetX > 3) ? 1 : 0); + const uint8_t graphPosY = yPos + ((screenSaverOffsetX > 3) ? 1 : 0); + + const uint8_t horizontal_line_y = graphPosY + height - 1; + const uint8_t arrow_size = 2; + + // draw diagram axis + _display->drawVLine(graphPosX, graphPosY, height); + _display->drawHLine(graphPosX, horizontal_line_y, width); + + // UP-arrow + _display->drawLine(graphPosX, graphPosY, graphPosX + arrow_size, graphPosY + arrow_size); + _display->drawLine(graphPosX, graphPosY, graphPosX - arrow_size, graphPosY + arrow_size); + + // LEFT-arrow + _display->drawLine(graphPosX + width - 1, horizontal_line_y, graphPosX + width - 1 - arrow_size, horizontal_line_y - arrow_size); + _display->drawLine(graphPosX + width - 1, horizontal_line_y, graphPosX + width - 1 - arrow_size, horizontal_line_y + arrow_size); + + // draw AC value + char fmtText[7]; + const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end()); + if (maxWatts > 999) { + snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000); + } else { + snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); + } + + if (isFullscreen) { + _display->setFont(u8g2_font_5x8_tr); + _display->setFontDirection(3); + _display->drawStr(graphPosX - arrow_size, graphPosY + _display->getStrWidth(fmtText), fmtText); + _display->setFontDirection(0); + } else { + // 4 pixels per char + _display->setFont(u8g2_font_tom_thumb_4x6_mr); + _display->drawStr(graphPosX - arrow_size - _display->getStrWidth(fmtText), graphPosY + 5, fmtText); + } + + // draw chart + const float scaleFactorY = maxWatts / static_cast(height); + const float scaleFactorX = static_cast(MAX_DATAPOINTS) / static_cast(_chartWidth); + + if (maxWatts > 0 && isFullscreen) { + // draw y axis ticks + const uint16_t yAxisWattPerTick = maxWatts <= 100 ? 10 : maxWatts <= 1000 ? 100 + : maxWatts < 5000 ? 500 + : 1000; + const uint8_t yAxisTickSizePixel = height / (maxWatts / yAxisWattPerTick); + + for (int16_t tickYPos = graphPosY + height; tickYPos > graphPosY - arrow_size; tickYPos -= yAxisTickSizePixel) { + _display->drawPixel(graphPosX - 1, tickYPos); + } + } + + uint8_t xAxisTicks = 1; + for (uint8_t i = 1; i < _graphValuesCount; i++) { + // draw one tick per hour to the x-axis + if (i * getSecondsPerDot() > (3600u * xAxisTicks)) { + _display->drawPixel((graphPosX + 1 + i) * scaleFactorX, graphPosY + height); + xAxisTicks++; + } + + if (scaleFactorY == 0 || scaleFactorX == 0) { + continue; + } + + _display->drawLine( + graphPosX + (i - 1) / scaleFactorX, horizontal_line_y - std::max(0, _graphValues[i - 1] / scaleFactorY - 0.5), + graphPosX + i / scaleFactorX, horizontal_line_y - std::max(0, _graphValues[i] / scaleFactorY - 0.5)); + } +} diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index c5050bffd..0e903187d 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "InverterSettings.h" #include "Configuration.h" @@ -25,7 +25,13 @@ InverterSettingsClass InverterSettings; -void InverterSettingsClass::init() +InverterSettingsClass::InverterSettingsClass() + : _settingsTask(INVERTER_UPDATE_SETTINGS_INTERVAL, TASK_FOREVER, std::bind(&InverterSettingsClass::settingsLoop, this)) + , _hoyTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&InverterSettingsClass::hoyLoop, this)) +{ +} + +void InverterSettingsClass::init(Scheduler& scheduler) { const CONFIG_T& config = Configuration.get(); const PinMapping_t& pin = PinMapping.get(); @@ -45,20 +51,22 @@ void InverterSettingsClass::init() if (PinMapping.isValidCmt2300Config()) { Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); - MessageOutput.println(F(" Setting CMT target frequency... ")); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); + MessageOutput.println(" Setting country mode... "); + Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); + MessageOutput.println(" Setting CMT target frequency... "); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); } MessageOutput.println(" Setting radio PA level... "); - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); - Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel); + Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel); MessageOutput.println(" Setting DTU serial... "); - Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial); MessageOutput.println(" Setting poll interval... "); - Hoymiles.setPollInterval(config.Dtu_PollInterval); + Hoymiles.setPollInterval(config.Dtu.PollInterval); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { @@ -74,6 +82,8 @@ void InverterSettingsClass::init() inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight); + inv->setClearEventlogOnMidnight(config.Inverter[i].ClearEventlogOnMidnight); + inv->Statistics()->setYieldDayCorrection(config.Inverter[i].YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); @@ -86,27 +96,35 @@ void InverterSettingsClass::init() } else { MessageOutput.println("Invalid pin config"); } + + scheduler.addTask(_hoyTask); + _hoyTask.enable(); + + scheduler.addTask(_settingsTask); + _settingsTask.enable(); } -void InverterSettingsClass::loop() +void InverterSettingsClass::settingsLoop() { - if (millis() - _lastUpdate > INVERTER_UPDATE_SETTINGS_INTERVAL) { - const CONFIG_T& config = Configuration.get(); - - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - auto const& inv_cfg = config.Inverter[i]; - if (inv_cfg.Serial == 0) { - continue; - } - auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); - if (inv == nullptr) { - continue; - } + const CONFIG_T& config = Configuration.get(); + const bool isDayPeriod = SunPosition.isDayPeriod(); - inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night)); - inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night)); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + auto const& inv_cfg = config.Inverter[i]; + if (inv_cfg.Serial == 0) { + continue; + } + auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); + if (inv == nullptr) { + continue; } + + inv->setEnablePolling(inv_cfg.Poll_Enable && (isDayPeriod || inv_cfg.Poll_Enable_Night)); + inv->setEnableCommands(inv_cfg.Command_Enable && (isDayPeriod || inv_cfg.Command_Enable_Night)); } +} +void InverterSettingsClass::hoyLoop() +{ Hoymiles.loop(); } diff --git a/src/Led_Single.cpp b/src/Led_Single.cpp index 09658c85c..40320b1f1 100644 --- a/src/Led_Single.cpp +++ b/src/Led_Single.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "Led_Single.h" #include "Configuration.h" @@ -12,85 +12,113 @@ LedSingleClass LedSingle; +/* + The table is calculated using the following formula + (See https://www.mikrocontroller.net/articles/LED-Fading) + a = Step count: 101 --> 0 - 100 + b = PWM resolution: 256: 0 - 255 + y = Calculated value of index x: + y = 0 if x = 0 + y = pow(2, log2(b-1) * (x+1) / a) if x > 0 +*/ +const uint8_t pwmTable[] = { + 0, + 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, + 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 7, 7, 8, 8, 8, 9, 9, + 10, 11, 11, 12, 12, 13, 14, 15, 16, 16, + 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, + 30, 32, 33, 35, 37, 39, 42, 44, 47, 49, + 52, 55, 58, 61, 65, 68, 72, 76, 81, 85, + 90, 95, 100, 106, 112, 118, 125, 132, 139, 147, + 156, 164, 174, 183, 194, 205, 216, 228, 241, 255 +}; + +#define LED_OFF 0 + LedSingleClass::LedSingleClass() + : _setTask(LEDSINGLE_UPDATE_INTERVAL * TASK_MILLISECOND, TASK_FOREVER, std::bind(&LedSingleClass::setLoop, this)) + , _outputTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&LedSingleClass::outputLoop, this)) { } -void LedSingleClass::init() +void LedSingleClass::init(Scheduler& scheduler) { + bool ledActive = false; + _blinkTimeout.set(500); - _updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL); turnAllOn(); - auto& pin = PinMapping.get(); + const auto& pin = PinMapping.get(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { if (pin.led[i] >= 0) { pinMode(pin.led[i], OUTPUT); - digitalWrite(pin.led[i], LOW); - _ledActive++; + setLed(i, false); + ledActive = true; } - _ledState[i] = LedState_t::Off; + _ledMode[i] = LedState_t::Off; } -} -void LedSingleClass::loop() -{ - if (_ledActive == 0) { - return; + if (ledActive) { + scheduler.addTask(_outputTask); + _outputTask.enable(); + + scheduler.addTask(_setTask); + _setTask.enable(); } +} - if (_updateTimeout.occured() && _allState == LedState_t::On) { +void LedSingleClass::setLoop() +{ + if (_allMode == LedState_t::On) { const CONFIG_T& config = Configuration.get(); // Update network status - _ledState[0] = LedState_t::Off; + _ledMode[0] = LedState_t::Off; if (NetworkSettings.isConnected()) { - _ledState[0] = LedState_t::Blink; + _ledMode[0] = LedState_t::Blink; } struct tm timeinfo; - if (getLocalTime(&timeinfo, 5) && (!config.Mqtt_Enabled || (config.Mqtt_Enabled && MqttSettings.getConnected()))) { - _ledState[0] = LedState_t::On; + if (getLocalTime(&timeinfo, 5) && (!config.Mqtt.Enabled || (config.Mqtt.Enabled && MqttSettings.getConnected()))) { + _ledMode[0] = LedState_t::On; } // Update inverter status - _ledState[1] = LedState_t::Off; + _ledMode[1] = LedState_t::Off; if (Hoymiles.getNumInverters() && Datastore.getIsAtLeastOnePollEnabled()) { // set LED status if (Datastore.getIsAllEnabledReachable() && Datastore.getIsAllEnabledProducing()) { - _ledState[1] = LedState_t::On; + _ledMode[1] = LedState_t::On; } if (Datastore.getIsAllEnabledReachable() && !Datastore.getIsAllEnabledProducing()) { - _ledState[1] = LedState_t::Blink; + _ledMode[1] = LedState_t::Blink; } } - _updateTimeout.reset(); - } else if (_updateTimeout.occured() && _allState == LedState_t::Off) { - _ledState[0] = LedState_t::Off; - _ledState[1] = LedState_t::Off; + } else if (_allMode == LedState_t::Off) { + _ledMode[0] = LedState_t::Off; + _ledMode[1] = LedState_t::Off; } +} - auto& pin = PinMapping.get(); +void LedSingleClass::outputLoop() +{ for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - - if (pin.led[i] < 0) { - continue; - } - - switch (_ledState[i]) { + switch (_ledMode[i]) { case LedState_t::Off: - digitalWrite(pin.led[i], LOW); + setLed(i, false); break; case LedState_t::On: - digitalWrite(pin.led[i], HIGH); + setLed(i, true); break; case LedState_t::Blink: if (_blinkTimeout.occured()) { - digitalWrite(pin.led[i], !digitalRead(pin.led[i])); + setLed(i, !_ledStateCurrent[i]); _blinkTimeout.reset(); } break; @@ -98,12 +126,32 @@ void LedSingleClass::loop() } } +void LedSingleClass::setLed(const uint8_t ledNo, const bool ledState) +{ + const auto& pin = PinMapping.get(); + const auto& config = Configuration.get(); + + if (pin.led[ledNo] < 0) { + return; + } + + const uint32_t currentPWM = ledcRead(analogGetChannel(pin.led[ledNo])); + const uint32_t targetPWM = ledState ? pwmTable[config.Led_Single[ledNo].Brightness] : LED_OFF; + + if (currentPWM == targetPWM) { + return; + } + + analogWrite(pin.led[ledNo], targetPWM); + _ledStateCurrent[ledNo] = ledState; +} + void LedSingleClass::turnAllOff() { - _allState = LedState_t::Off; + _allMode = LedState_t::Off; } void LedSingleClass::turnAllOn() { - _allState = LedState_t::On; + _allMode = LedState_t::On; } diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index 23c644bc9..4ed137711 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -1,55 +1,66 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "MessageOutput.h" - -#include - -MessageOutputClass MessageOutput; - -void MessageOutputClass::register_ws_output(AsyncWebSocket* output) -{ - _ws = output; -} - -size_t MessageOutputClass::write(uint8_t c) -{ - if (_buff_pos < BUFFER_SIZE) { - std::lock_guard lock(_msgLock); - _buffer[_buff_pos] = c; - _buff_pos++; - } else { - _forceSend = true; - } - - return Serial.write(c); -} - -size_t MessageOutputClass::write(const uint8_t* buffer, size_t size) -{ - std::lock_guard lock(_msgLock); - if (_buff_pos + size < BUFFER_SIZE) { - memcpy(&_buffer[_buff_pos], buffer, size); - _buff_pos += size; - } - _forceSend = true; - - return Serial.write(buffer, size); -} - -void MessageOutputClass::loop() -{ - // Send data via websocket if either time is over or buffer is full - if (_forceSend || (millis() - _lastSend > 1000)) { - std::lock_guard lock(_msgLock); - if (_ws && _buff_pos > 0) { - _ws->textAll(_buffer, _buff_pos); - _buff_pos = 0; - } - if (_forceSend) { - _buff_pos = 0; - } - _forceSend = false; - } -} \ No newline at end of file +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "MessageOutput.h" + +#include + +MessageOutputClass MessageOutput; + +MessageOutputClass::MessageOutputClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MessageOutputClass::loop, this)) +{ +} + +void MessageOutputClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.enable(); +} + +void MessageOutputClass::register_ws_output(AsyncWebSocket* output) +{ + _ws = output; +} + +size_t MessageOutputClass::write(uint8_t c) +{ + if (_buff_pos < BUFFER_SIZE) { + std::lock_guard lock(_msgLock); + _buffer[_buff_pos] = c; + _buff_pos++; + } else { + _forceSend = true; + } + + return Serial.write(c); +} + +size_t MessageOutputClass::write(const uint8_t* buffer, size_t size) +{ + std::lock_guard lock(_msgLock); + if (_buff_pos + size < BUFFER_SIZE) { + memcpy(&_buffer[_buff_pos], buffer, size); + _buff_pos += size; + } + _forceSend = true; + + return Serial.write(buffer, size); +} + +void MessageOutputClass::loop() +{ + // Send data via websocket if either time is over or buffer is full + if (_forceSend || (millis() - _lastSend > 1000)) { + std::lock_guard lock(_msgLock); + if (_ws && _buff_pos > 0) { + _ws->textAll(_buffer, _buff_pos); + _buff_pos = 0; + } + if (_forceSend) { + _buff_pos = 0; + } + _forceSend = false; + } +} diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index ee5ad417f..e8192b2e2 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "MqttHandleDtu.h" #include "Configuration.h" @@ -10,27 +10,32 @@ MqttHandleDtuClass MqttHandleDtu; -void MqttHandleDtuClass::init() +MqttHandleDtuClass::MqttHandleDtuClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandleDtuClass::loop, this)) { } +void MqttHandleDtuClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); +} + void MqttHandleDtuClass::loop() { + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); return; } - const CONFIG_T& config = Configuration.get(); - - if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { - MqttSettings.publish("dtu/uptime", String(millis() / 1000)); - MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); - MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); - if (NetworkSettings.NetworkMode() == network_mode::WiFi) { - MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); - MqttSettings.publish("dtu/bssid", String(WiFi.BSSIDstr())); - } - - _lastPublish = millis(); + MqttSettings.publish("dtu/uptime", String(millis() / 1000)); + MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); + MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); + if (NetworkSettings.NetworkMode() == network_mode::WiFi) { + MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); + MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); } -} \ No newline at end of file +} diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index dd2f56088..b286eaa23 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -1,18 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "MqttHandleHass.h" #include "MqttHandleInverter.h" #include "MqttSettings.h" #include "NetworkSettings.h" +#include "Utils.h" +#include "defaults.h" +#include "__compiled_constants.h" MqttHandleHassClass MqttHandleHass; -void MqttHandleHassClass::init() +MqttHandleHassClass::MqttHandleHassClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandleHassClass::loop, this)) { } +void MqttHandleHassClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.enable(); +} + void MqttHandleHassClass::loop() { if (_updateForced) { @@ -37,7 +47,7 @@ void MqttHandleHassClass::forceUpdate() void MqttHandleHassClass::publishConfig() { - if (!Configuration.get().Mqtt_Hass_Enabled) { + if (!Configuration.get().Mqtt.Hass.Enabled) { return; } @@ -47,6 +57,14 @@ void MqttHandleHassClass::publishConfig() const CONFIG_T& config = Configuration.get(); + // publish DTU sensors + publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", ""); + publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi"); + publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", ""); + publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); + + yield(); + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -55,11 +73,11 @@ void MqttHandleHassClass::publishConfig() publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%"); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%"); + publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100); + publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100); - publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 10, 2250); - publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 10, 2250); + publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); + publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); @@ -69,10 +87,10 @@ void MqttHandleHassClass::publishConfig() for (auto& c : inv->Statistics()->getChannelsByType(t)) { for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) { bool clear = false; - if (t == TYPE_DC && !config.Mqtt_Hass_IndividualPanels) { + if (t == TYPE_DC && !config.Mqtt.Hass.IndividualPanels) { clear = true; } - publishField(inv, t, c, deviceFieldAssignment[f], clear); + publishInverterField(inv, t, c, deviceFieldAssignment[f], clear); } } } @@ -81,16 +99,16 @@ void MqttHandleHassClass::publishConfig() } } -void MqttHandleHassClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) +void MqttHandleHassClass::publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear) { if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldType.fieldId)) { return; } - String serial = inv->serialString(); + const String serial = inv->serialString(); String fieldName; - if (type == TYPE_AC && fieldType.fieldId == FLD_PDC) { + if (type == TYPE_INV && fieldType.fieldId == FLD_PDC) { fieldName = "PowerDC"; } else { fieldName = inv->Statistics()->getChannelFieldName(type, channel, fieldType.fieldId); @@ -104,12 +122,12 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch chanNum = channel; } - String configTopic = "sensor/dtu_" + serial + const String configTopic = "sensor/dtu_" + serial + "/" + "ch" + chanNum + "_" + fieldName + "/config"; if (!clear) { - String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); + const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); const char* devCls = deviceClasses[fieldType.deviceClsId]; const char* stateCls = stateClasses[fieldType.stateClsId]; @@ -120,7 +138,8 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch name = "CH" + chanNum + " " + fieldName; } - DynamicJsonDocument root(1024); + JsonDocument root; + root["name"] = name; root["stat_t"] = stateTopic; root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName; @@ -130,11 +149,10 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch root["unit_of_meas"] = unit_of_measure; } - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); - if (Configuration.get().Mqtt_Hass_Expire) { - root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt_PublishInterval) * inv->getReachableThreshold(); + if (Configuration.get().Mqtt.Hass.Expire) { + root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); } if (devCls != 0) { root["dev_cla"] = devCls; @@ -143,6 +161,10 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch root["stat_cla"] = stateCls; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + String buffer; serializeJson(root, buffer); publish(configTopic, buffer); @@ -153,19 +175,20 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload) { - String serial = inv->serialString(); + const String serial = inv->serialString(); String buttonId = caption; buttonId.replace(" ", "_"); buttonId.toLowerCase(); - String configTopic = "button/dtu_" + serial + const String configTopic = "button/dtu_" + serial + "/" + buttonId + "/config"; - String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + + JsonDocument root; - DynamicJsonDocument root(1024); root["name"] = caption; root["uniq_id"] = serial + "_" + buttonId; if (strcmp(icon, "")) { @@ -178,8 +201,11 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, - int16_t min, int16_t max) + const int16_t min, const int16_t max) { - String serial = inv->serialString(); + const String serial = inv->serialString(); String buttonId = caption; buttonId.replace(" ", "_"); buttonId.toLowerCase(); - String configTopic = "number/dtu_" + serial + const String configTopic = "number/dtu_" + serial + "/" + buttonId + "/config"; - String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; - String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; + + JsonDocument root; - DynamicJsonDocument root(1024); root["name"] = caption; root["uniq_id"] = serial + "_" + buttonId; if (strcmp(icon, "")) { @@ -217,8 +244,11 @@ void MqttHandleHassClass::publishInverterNumber( root["min"] = min; root["max"] = max; - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } String buffer; serializeJson(root, buffer); @@ -227,46 +257,179 @@ void MqttHandleHassClass::publishInverterNumber( void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) { - String serial = inv->serialString(); + const String serial = inv->serialString(); String sensorId = caption; sensorId.replace(" ", "_"); sensorId.toLowerCase(); - String configTopic = "binary_sensor/dtu_" + serial + const String configTopic = "binary_sensor/dtu_" + serial + "/" + sensorId + "/config"; - String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; + + JsonDocument root; - DynamicJsonDocument root(1024); root["name"] = caption; root["uniq_id"] = serial + "_" + sensorId; root["stat_t"] = statTopic; root["pl_on"] = payload_on; root["pl_off"] = payload_off; - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } String buffer; serializeJson(root, buffer); publish(configTopic, buffer); } -void MqttHandleHassClass::createDeviceInfo(JsonObject& object, std::shared_ptr inv) +void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic) { - object["name"] = inv->name(); - object["ids"] = inv->serialString(); - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); - object["mf"] = "OpenDTU"; - object["mdl"] = inv->typeName(); - object["sw"] = AUTO_GIT_HASH; + 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); +} + +void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr inv) +{ + createDeviceInfo( + root, + inv->name(), + inv->serialString(), + getDtuUrl(), + "OpenDTU", + inv->typeName(), + __COMPILED_GIT_HASH__, + getDtuUniqueId()); +} + +void MqttHandleHassClass::createDtuInfo(JsonDocument& root) +{ + createDeviceInfo( + root, + NetworkSettings.getHostname(), + getDtuUniqueId(), + getDtuUrl(), + "OpenDTU", + "OpenDTU", + __COMPILED_GIT_HASH__); +} + +void MqttHandleHassClass::createDeviceInfo( + JsonDocument& root, + const String& name, const String& identifiers, const String& configuration_url, + const String& manufacturer, const String& model, const String& sw_version, + const String& via_device) +{ + auto object = root["dev"].to(); + + object["name"] = name; + object["ids"] = identifiers; + object["cu"] = configuration_url; + object["mf"] = manufacturer; + object["mdl"] = model; + object["sw"] = sw_version; + + if (via_device != "") { + object["via_device"] = via_device; + } +} + +String MqttHandleHassClass::getDtuUniqueId() +{ + return NetworkSettings.getHostname() + "_" + Utils::getChipId(); +} + +String MqttHandleHassClass::getDtuUrl() +{ + return String("http://") + NetworkSettings.localIP().toString(); } void MqttHandleHassClass::publish(const String& subtopic, const String& payload) { - String topic = Configuration.get().Mqtt_Hass_Topic; + String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; - MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); -} \ No newline at end of file + MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain); +} diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 9048e06fb..624033e1c 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "MqttHandleInverter.h" #include "MessageOutput.h" @@ -18,112 +18,102 @@ MqttHandleInverterClass MqttHandleInverter; -void MqttHandleInverterClass::init() +MqttHandleInverterClass::MqttHandleInverterClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandleInverterClass::loop, this)) { - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; +} + +void MqttHandleInverterClass::init(Scheduler& scheduler) +{ + subscribeTopics(); - String topic = MqttSettings.getPrefix(); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + scheduler.addTask(_loopTask); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); } void MqttHandleInverterClass::loop() { + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); return; } - const CONFIG_T& config = Configuration.get(); - - if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { - // Loop all inverters - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); - String subtopic = inv->serialString(); + const String subtopic = inv->serialString(); - // Name - MqttSettings.publish(subtopic + "/name", inv->name()); + // Name + MqttSettings.publish(subtopic + "/name", inv->name()); - if (inv->DevInfo()->getLastUpdate() > 0) { - // Bootloader Version - MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); + if (inv->DevInfo()->getLastUpdate() > 0) { + // Bootloader Version + MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); - // Firmware Version - MqttSettings.publish(subtopic + "/device/fwbuildversion", String(inv->DevInfo()->getFwBuildVersion())); + // Firmware Version + MqttSettings.publish(subtopic + "/device/fwbuildversion", String(inv->DevInfo()->getFwBuildVersion())); - // Firmware Build DateTime - char timebuffer[32]; - const time_t t = inv->DevInfo()->getFwBuildDateTime(); - std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t)); - MqttSettings.publish(subtopic + "/device/fwbuilddatetime", String(timebuffer)); + // Firmware Build DateTime + MqttSettings.publish(subtopic + "/device/fwbuilddatetime", inv->DevInfo()->getFwBuildDateTimeStr()); - // Hardware part number - MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); + // Hardware part number + MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); - // Hardware version - MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); - } + // Hardware version + MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); + } - if (inv->SystemConfigPara()->getLastUpdate() > 0) { - // Limit - MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent())); + if (inv->SystemConfigPara()->getLastUpdate() > 0) { + // Limit + MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent())); - uint16_t maxpower = inv->DevInfo()->getMaxPower(); - if (maxpower > 0) { - MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100)); - } + uint16_t maxpower = inv->DevInfo()->getMaxPower(); + if (maxpower > 0) { + MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100)); } + } - MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); - MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing())); + MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); + MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing())); - if (inv->Statistics()->getLastUpdate() > 0) { - MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000)); - } else { - MqttSettings.publish(subtopic + "/status/last_update", String(0)); - } + if (inv->Statistics()->getLastUpdate() > 0) { + MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000)); + } else { + MqttSettings.publish(subtopic + "/status/last_update", String(0)); + } - uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); - if (inv->Statistics()->getLastUpdate() > 0 && (lastUpdateInternal != _lastPublishStats[i])) { - _lastPublishStats[i] = lastUpdateInternal; - - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - if (t == TYPE_DC) { - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg != nullptr) { - // TODO(tbnobody) - MqttSettings.publish(inv->serialString() + "/" + String(static_cast(c) + 1) + "/name", inv_cfg->channel[c].Name); - } - } - for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(FieldId_t); f++) { - publishField(inv, t, c, _publishFields[f]); + const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (inv->Statistics()->getLastUpdate() > 0 && (lastUpdateInternal != _lastPublishStats[i])) { + _lastPublishStats[i] = lastUpdateInternal; + + // Loop all channels + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg != nullptr) { + // TODO(tbnobody) + MqttSettings.publish(inv->serialString() + "/" + String(static_cast(c) + 1) + "/name", inv_cfg->channel[c].Name); } } + for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(FieldId_t); f++) { + publishField(inv, t, c, _publishFields[f]); + } } } - - yield(); } - _lastPublish = millis(); + yield(); } } -void MqttHandleInverterClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +void MqttHandleInverterClass::publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { - String topic = getTopic(inv, type, channel, fieldId); + const String topic = getTopic(inv, type, channel, fieldId); if (topic == "") { return; } @@ -131,14 +121,14 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv MqttSettings.publish(topic, inv->Statistics()->getChannelFieldValueString(type, channel, fieldId)); } -String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +String MqttHandleInverterClass::getTopic(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId) { if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { - return String(""); + return ""; } String chanName; - if (type == TYPE_AC && fieldId == FLD_PDC) { + if (type == TYPE_INV && fieldId == FLD_PDC) { chanName = "powerdc"; } else { chanName = inv->Statistics()->getChannelFieldName(type, channel, fieldId); @@ -156,7 +146,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr inv, return inv->serialString() + "/" + chanNum + "/" + chanName; } -void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) { const CONFIG_T& config = Configuration.get(); @@ -166,7 +156,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* serial_str; char* subtopic; char* setting; - char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + char* rest = &token_topic[strlen(config.Mqtt.Topic)]; serial_str = strtok_r(rest, "/", &rest); subtopic = strtok_r(rest, "/", &rest); @@ -176,8 +166,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro return; } - uint64_t serial; - serial = strtoull(serial_str, 0, 16); + const uint64_t serial = strtoull(serial_str, 0, 16); auto inv = Hoymiles.getInverterBySerial(serial); @@ -194,7 +183,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* strlimit = new char[len + 1]; memcpy(strlimit, payload, len); strlimit[len] = '\0'; - int32_t payload_val = strtol(strlimit, NULL, 10); + const int32_t payload_val = strtol(strlimit, NULL, 10); delete[] strlimit; if (payload_val < 0) { @@ -244,4 +233,33 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro MessageOutput.println("Ignored because retained"); } } -} \ No newline at end of file +} + +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)); +} + +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)); +} diff --git a/src/MqttHandleInverterTotal.cpp b/src/MqttHandleInverterTotal.cpp index ac8e6a4ed..5f5be6a92 100644 --- a/src/MqttHandleInverterTotal.cpp +++ b/src/MqttHandleInverterTotal.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "MqttHandleInverterTotal.h" #include "Configuration.h" @@ -10,26 +10,33 @@ MqttHandleInverterTotalClass MqttHandleInverterTotal; -void MqttHandleInverterTotalClass::init() +MqttHandleInverterTotalClass::MqttHandleInverterTotalClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandleInverterTotalClass::loop, this)) { - _lastPublish.set(Configuration.get().Mqtt_PublishInterval * 1000); +} + +void MqttHandleInverterTotalClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); } void MqttHandleInverterTotalClass::loop() { + // Update interval from config + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { + _loopTask.forceNextIteration(); return; } - if (_lastPublish.occured()) { - MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits())); - MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits())); - MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits())); - MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable())); - MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits())); - MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3)); - MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable())); - - _lastPublish.set(Configuration.get().Mqtt_PublishInterval * 1000); - } + MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits())); + MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits())); + MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits())); + MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable())); + MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits())); + MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3)); + MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable())); } diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index eeb92bb4f..b86c51f54 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -19,33 +19,33 @@ void MqttSettingsClass::NetworkEvent(network_event event) break; case network_event::NETWORK_DISCONNECTED: MessageOutput.println("Network lost connection"); - mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi + _mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi break; default: break; } } -void MqttSettingsClass::onMqttConnect(bool sessionPresent) +void MqttSettingsClass::onMqttConnect(const bool sessionPresent) { MessageOutput.println("Connected to MQTT."); const CONFIG_T& config = Configuration.get(); - publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online); + publish(config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { + if (_mqttClient != nullptr) { for (const auto& cb : _mqttSubscribeParser.get_callbacks()) { - mqttClient->subscribe(cb.topic.c_str(), cb.qos); + _mqttClient->subscribe(cb.topic.c_str(), cb.qos); } } } -void MqttSettingsClass::subscribe(const String& topic, uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) +void MqttSettingsClass::subscribe(const String& topic, const uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb) { _mqttSubscribeParser.register_callback(topic.c_str(), qos, cb); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - mqttClient->subscribe(topic.c_str(), qos); + if (_mqttClient != nullptr) { + _mqttClient->subscribe(topic.c_str(), qos); } } @@ -53,8 +53,8 @@ void MqttSettingsClass::unsubscribe(const String& topic) { _mqttSubscribeParser.unregister_callback(topic.c_str()); std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - mqttClient->unsubscribe(topic.c_str()); + if (_mqttClient != nullptr) { + _mqttClient->unsubscribe(topic.c_str()); } } @@ -85,11 +85,11 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re default: MessageOutput.println("Unknown"); } - mqttReconnectTimer.once( + _mqttReconnectTimer.once( 2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this); } -void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) { MessageOutput.print("Received MQTT message on topic: "); MessageOutput.println(topic); @@ -99,7 +99,7 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie void MqttSettingsClass::performConnect() { - if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) { + if (NetworkSettings.isConnected() && Configuration.get().Mqtt.Enabled) { using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; @@ -108,52 +108,52 @@ void MqttSettingsClass::performConnect() using std::placeholders::_6; std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } MessageOutput.println("Connecting to MQTT..."); const CONFIG_T& config = Configuration.get(); - willTopic = getPrefix() + config.Mqtt_LwtTopic; - clientId = NetworkSettings.getApName(); - if (config.Mqtt_Tls) { - static_cast(mqttClient)->setCACert(config.Mqtt_RootCaCert); - static_cast(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port); - if (config.Mqtt_TlsCertLogin) { - static_cast(mqttClient)->setCertificate(config.Mqtt_ClientCert); - static_cast(mqttClient)->setPrivateKey(config.Mqtt_ClientKey); + const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic; + String clientId = getClientId(); + if (config.Mqtt.Tls.Enabled) { + static_cast(_mqttClient)->setCACert(config.Mqtt.Tls.RootCaCert); + static_cast(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port); + if (config.Mqtt.Tls.CertLogin) { + static_cast(_mqttClient)->setCertificate(config.Mqtt.Tls.ClientCert); + static_cast(_mqttClient)->setPrivateKey(config.Mqtt.Tls.ClientKey); } else { - static_cast(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); + static_cast(_mqttClient)->setCredentials(config.Mqtt.Username, config.Mqtt.Password); } - static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); - static_cast(mqttClient)->setClientId(clientId.c_str()); - static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); - static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); - static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); - static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + static_cast(_mqttClient)->setWill(willTopic.c_str(), config.Mqtt.Lwt.Qos, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); + static_cast(_mqttClient)->setClientId(clientId.c_str()); + static_cast(_mqttClient)->setCleanSession(config.Mqtt.CleanSession); + static_cast(_mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); + static_cast(_mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); + static_cast(_mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } else { - static_cast(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port); - static_cast(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); - static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); - static_cast(mqttClient)->setClientId(clientId.c_str()); - static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); - static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); - static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); - static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + static_cast(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port); + static_cast(_mqttClient)->setCredentials(config.Mqtt.Username, config.Mqtt.Password); + static_cast(_mqttClient)->setWill(willTopic.c_str(), config.Mqtt.Lwt.Qos, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); + static_cast(_mqttClient)->setClientId(clientId.c_str()); + static_cast(_mqttClient)->setCleanSession(config.Mqtt.CleanSession); + static_cast(_mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); + static_cast(_mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); + static_cast(_mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } - mqttClient->connect(); + _mqttClient->connect(); } } void MqttSettingsClass::performDisconnect() { const CONFIG_T& config = Configuration.get(); - publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Offline); + publish(config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Offline); std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } - mqttClient->disconnect(); + _mqttClient->disconnect(); } void MqttSettingsClass::performReconnect() @@ -162,22 +162,31 @@ void MqttSettingsClass::performReconnect() createMqttClientObject(); - mqttReconnectTimer.once( + _mqttReconnectTimer.once( 2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this); } bool MqttSettingsClass::getConnected() { std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return false; } - return mqttClient->connected(); + return _mqttClient->connected(); +} + +String MqttSettingsClass::getPrefix() const +{ + return Configuration.get().Mqtt.Topic; } -String MqttSettingsClass::getPrefix() +String MqttSettingsClass::getClientId() { - return Configuration.get().Mqtt_Topic; + String clientId = Configuration.get().Mqtt.ClientId; + if (clientId == "") { + clientId = NetworkSettings.getApName(); + } + return clientId; } void MqttSettingsClass::publish(const String& subtopic, const String& payload) @@ -188,16 +197,16 @@ void MqttSettingsClass::publish(const String& subtopic, const String& payload) String value = payload; value.trim(); - publishGeneric(topic, value, Configuration.get().Mqtt_Retain, 0); + publishGeneric(topic, value, Configuration.get().Mqtt.Retain, 0); } -void MqttSettingsClass::publishGeneric(const String& topic, const String& payload, bool retain, uint8_t qos) +void MqttSettingsClass::publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos) { std::lock_guard lock(_clientLock); - if (mqttClient == nullptr) { + if (_mqttClient == nullptr) { return; } - mqttClient->publish(topic.c_str(), qos, retain, payload.c_str()); + _mqttClient->publish(topic.c_str(), qos, retain, payload.c_str()); } void MqttSettingsClass::init() @@ -211,16 +220,16 @@ void MqttSettingsClass::init() void MqttSettingsClass::createMqttClientObject() { std::lock_guard lock(_clientLock); - if (mqttClient != nullptr) { - delete mqttClient; - mqttClient = nullptr; + if (_mqttClient != nullptr) { + delete _mqttClient; + _mqttClient = nullptr; } const CONFIG_T& config = Configuration.get(); - if (config.Mqtt_Tls) { - mqttClient = static_cast(new espMqttClientSecure); + if (config.Mqtt.Tls.Enabled) { + _mqttClient = static_cast(new espMqttClientSecure); } else { - mqttClient = static_cast(new espMqttClient); + _mqttClient = static_cast(new espMqttClient); } } -MqttSettingsClass MqttSettings; \ No newline at end of file +MqttSettingsClass MqttSettings; diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 2e3667255..b7194862e 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "NetworkSettings.h" #include "Configuration.h" @@ -10,16 +10,21 @@ #include "defaults.h" #include #include +<<<<<<< HEAD #include +======= +#include "__compiled_constants.h" +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 NetworkSettingsClass::NetworkSettingsClass() - : apIp(192, 168, 4, 1) - , apNetmask(255, 255, 255, 0) + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this)) + , _apIp(192, 168, 4, 1) + , _apNetmask(255, 255, 255, 0) { - dnsServer.reset(new DNSServer()); + _dnsServer.reset(new DNSServer()); } -void NetworkSettingsClass::init() +void NetworkSettingsClass::init(Scheduler& scheduler) { using std::placeholders::_1; @@ -28,9 +33,12 @@ void NetworkSettingsClass::init() WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); setupMode(); + + scheduler.addTask(_loopTask); + _loopTask.enable(); } -void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) +void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) { switch (event) { case ARDUINO_EVENT_ETH_START: @@ -88,7 +96,7 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event) } } -bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, network_event event) +bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event) { if (!cbEvent) { return pdFALSE; @@ -100,10 +108,10 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, network_event event) return true; } -void NetworkSettingsClass::raiseEvent(network_event event) +void NetworkSettingsClass::raiseEvent(const network_event event) { for (uint32_t i = 0; i < _cbEventList.size(); i++) { - NetworkEventCbList_t entry = _cbEventList[i]; + const NetworkEventCbList_t entry = _cbEventList[i]; if (entry.cb) { if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) { entry.cb(event); @@ -114,13 +122,13 @@ void NetworkSettingsClass::raiseEvent(network_event event) void NetworkSettingsClass::handleMDNS() { - bool mdnsEnabled = Configuration.get().Mdns_Enabled; + const bool mdnsEnabled = Configuration.get().Mdns.Enabled; - if (lastMdnsEnabled == mdnsEnabled) { + if (_lastMdnsEnabled == mdnsEnabled) { return; } - lastMdnsEnabled = mdnsEnabled; + _lastMdnsEnabled = mdnsEnabled; MDNS.end(); @@ -133,7 +141,7 @@ void NetworkSettingsClass::handleMDNS() MDNS.addService("http", "tcp", 80); MDNS.addService("opendtu", "tcp", 80); - MDNS.addServiceTxt("opendtu", "tcp", "git_hash", AUTO_GIT_HASH); + MDNS.addServiceTxt("opendtu", "tcp", "git_hash", __COMPILED_GIT_HASH__); MessageOutput.println("done"); } else { @@ -143,17 +151,17 @@ void NetworkSettingsClass::handleMDNS() void NetworkSettingsClass::setupMode() { - if (adminEnabled) { + if (_adminEnabled) { WiFi.mode(WIFI_AP_STA); String ssidString = getApName(); - WiFi.softAPConfig(apIp, apIp, apNetmask); - WiFi.softAP((const char*)ssidString.c_str(), Configuration.get().Security_Password); - dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); - dnsServerStatus = true; + WiFi.softAPConfig(_apIp, _apIp, _apNetmask); + WiFi.softAP(ssidString.c_str(), Configuration.get().Security.Password); + _dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + _dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); + _dnsServerStatus = true; } else { - dnsServerStatus = false; - dnsServer->stop(); + _dnsServerStatus = false; + _dnsServer->stop(); if (_networkMode == network_mode::WiFi) { WiFi.mode(WIFI_STA); } else { @@ -169,13 +177,13 @@ void NetworkSettingsClass::setupMode() void NetworkSettingsClass::enableAdminMode() { - adminEnabled = true; - adminTimeoutCounter = 0; - adminTimeoutCounterMax = Configuration.get().WiFi_ApTimeout * 60; + _adminEnabled = true; + _adminTimeoutCounter = 0; + _adminTimeoutCounterMax = Configuration.get().WiFi.ApTimeout * 60; setupMode(); } -String NetworkSettingsClass::getApName() +String NetworkSettingsClass::getApName() const { return String(ACCESS_POINT_NAME + String(Utils::getChipId())); } @@ -199,26 +207,26 @@ void NetworkSettingsClass::loop() applyConfig(); } - if (millis() - lastTimerCall > 1000) { - if (adminEnabled && adminTimeoutCounterMax > 0) { - adminTimeoutCounter++; - if (adminTimeoutCounter % 10 == 0) { - MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", adminTimeoutCounter, adminTimeoutCounterMax); + if (millis() - _lastTimerCall > 1000) { + if (_adminEnabled && _adminTimeoutCounterMax > 0) { + _adminTimeoutCounter++; + if (_adminTimeoutCounter % 10 == 0) { + MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); } } - connectTimeoutTimer++; - connectRedoTimer++; - lastTimerCall = millis(); + _connectTimeoutTimer++; + _connectRedoTimer++; + _lastTimerCall = millis(); } - if (adminEnabled) { + if (_adminEnabled) { // Don't disable the admin mode when network is not available if (!isConnected()) { - adminTimeoutCounter = 0; + _adminTimeoutCounter = 0; } // If WiFi is connected to AP for more than adminTimeoutCounterMax // seconds, disable the internal Access Point - if (adminTimeoutCounter > adminTimeoutCounterMax) { - adminEnabled = false; + if (_adminTimeoutCounter > _adminTimeoutCounterMax) { + _adminEnabled = false; MessageOutput.println("Admin mode disabled"); setupMode(); } @@ -226,28 +234,28 @@ void NetworkSettingsClass::loop() // WiFi is searching for an AP. So disable searching afer // WIFI_RECONNECT_TIMEOUT and repeat after WIFI_RECONNECT_REDO_TIMEOUT if (isConnected()) { - connectTimeoutTimer = 0; - connectRedoTimer = 0; + _connectTimeoutTimer = 0; + _connectRedoTimer = 0; } else { - if (connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !forceDisconnection) { + if (_connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !_forceDisconnection) { MessageOutput.print("Disable search for AP... "); WiFi.mode(WIFI_AP); MessageOutput.println("done"); - connectRedoTimer = 0; - forceDisconnection = true; + _connectRedoTimer = 0; + _forceDisconnection = true; } - if (connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && forceDisconnection) { + if (_connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && _forceDisconnection) { MessageOutput.print("Enable search for AP... "); WiFi.mode(WIFI_AP_STA); MessageOutput.println("done"); applyConfig(); - connectTimeoutTimer = 0; - forceDisconnection = false; + _connectTimeoutTimer = 0; + _forceDisconnection = false; } } } - if (dnsServerStatus) { - dnsServer->processNextRequest(); + if (_dnsServerStatus) { + _dnsServer->processNextRequest(); } handleMDNS(); @@ -256,15 +264,16 @@ void NetworkSettingsClass::loop() void NetworkSettingsClass::applyConfig() { setHostname(); - if (!strcmp(Configuration.get().WiFi_Ssid, "")) { + if (!strcmp(Configuration.get().WiFi.Ssid, "")) { return; } MessageOutput.print("Configuring WiFi STA using "); - if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi_Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi_Password)) { + if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi.Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi.Password)) { MessageOutput.print("new credentials... "); WiFi.begin( - Configuration.get().WiFi_Ssid, - Configuration.get().WiFi_Password); + Configuration.get().WiFi.Ssid, + Configuration.get().WiFi.Password, + WIFI_ALL_CHANNEL_SCAN); } else { MessageOutput.print("existing credentials... "); WiFi.begin(); @@ -300,39 +309,39 @@ void NetworkSettingsClass::setHostname() void NetworkSettingsClass::setStaticIp() { if (_networkMode == network_mode::WiFi) { - if (Configuration.get().WiFi_Dhcp) { + if (Configuration.get().WiFi.Dhcp) { MessageOutput.print("Configuring WiFi STA DHCP IP... "); WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); MessageOutput.println("done"); } else { MessageOutput.print("Configuring WiFi STA static IP... "); WiFi.config( - IPAddress(Configuration.get().WiFi_Ip), - IPAddress(Configuration.get().WiFi_Gateway), - IPAddress(Configuration.get().WiFi_Netmask), - IPAddress(Configuration.get().WiFi_Dns1), - IPAddress(Configuration.get().WiFi_Dns2)); + IPAddress(Configuration.get().WiFi.Ip), + IPAddress(Configuration.get().WiFi.Gateway), + IPAddress(Configuration.get().WiFi.Netmask), + IPAddress(Configuration.get().WiFi.Dns1), + IPAddress(Configuration.get().WiFi.Dns2)); MessageOutput.println("done"); } } else if (_networkMode == network_mode::Ethernet) { - if (Configuration.get().WiFi_Dhcp) { + if (Configuration.get().WiFi.Dhcp) { MessageOutput.print("Configuring Ethernet DHCP IP... "); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); MessageOutput.println("done"); } else { MessageOutput.print("Configuring Ethernet static IP... "); ETH.config( - IPAddress(Configuration.get().WiFi_Ip), - IPAddress(Configuration.get().WiFi_Gateway), - IPAddress(Configuration.get().WiFi_Netmask), - IPAddress(Configuration.get().WiFi_Dns1), - IPAddress(Configuration.get().WiFi_Dns2)); + IPAddress(Configuration.get().WiFi.Ip), + IPAddress(Configuration.get().WiFi.Gateway), + IPAddress(Configuration.get().WiFi.Netmask), + IPAddress(Configuration.get().WiFi.Dns1), + IPAddress(Configuration.get().WiFi.Dns2)); MessageOutput.println("done"); } } } -IPAddress NetworkSettingsClass::localIP() +IPAddress NetworkSettingsClass::localIP() const { switch (_networkMode) { case network_mode::Ethernet: @@ -346,7 +355,7 @@ IPAddress NetworkSettingsClass::localIP() } } -IPAddress NetworkSettingsClass::subnetMask() +IPAddress NetworkSettingsClass::subnetMask() const { switch (_networkMode) { case network_mode::Ethernet: @@ -360,7 +369,7 @@ IPAddress NetworkSettingsClass::subnetMask() } } -IPAddress NetworkSettingsClass::gatewayIP() +IPAddress NetworkSettingsClass::gatewayIP() const { switch (_networkMode) { case network_mode::Ethernet: @@ -374,7 +383,7 @@ IPAddress NetworkSettingsClass::gatewayIP() } } -IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no) +IPAddress NetworkSettingsClass::dnsIP(const uint8_t dns_no) const { switch (_networkMode) { case network_mode::Ethernet: @@ -388,7 +397,7 @@ IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no) } } -String NetworkSettingsClass::macAddress() +String NetworkSettingsClass::macAddress() const { switch (_networkMode) { case network_mode::Ethernet: @@ -409,8 +418,8 @@ String NetworkSettingsClass::getHostname() char resultHostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; uint8_t pos = 0; - uint32_t chipId = Utils::getChipId(); - snprintf(preparedHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, config.WiFi_Hostname, chipId); + const uint32_t chipId = Utils::getChipId(); + snprintf(preparedHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, config.WiFi.Hostname, chipId); const char* pC = preparedHostname; while (*pC && pos < WIFI_MAX_HOSTNAME_STRLEN) { // while !null and not over length @@ -441,12 +450,12 @@ String NetworkSettingsClass::getHostname() return resultHostname; } -bool NetworkSettingsClass::isConnected() +bool NetworkSettingsClass::isConnected() const { return WiFi.localIP()[0] != 0 || ETH.localIP()[0] != 0; } -network_mode NetworkSettingsClass::NetworkMode() +network_mode NetworkSettingsClass::NetworkMode() const { return _networkMode; } diff --git a/src/NtpSettings.cpp b/src/NtpSettings.cpp index ce043384d..b89904f35 100644 --- a/src/NtpSettings.cpp +++ b/src/NtpSettings.cpp @@ -19,12 +19,12 @@ void NtpSettingsClass::init() void NtpSettingsClass::setServer() { - configTime(0, 0, Configuration.get().Ntp_Server); + configTime(0, 0, Configuration.get().Ntp.Server); } void NtpSettingsClass::setTimezone() { - setenv("TZ", Configuration.get().Ntp_Timezone, 1); + setenv("TZ", Configuration.get().Ntp.Timezone, 1); tzset(); } diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 68395b6f2..74f282855 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -8,8 +8,6 @@ #include #include -#define JSON_BUFFER_SIZE 6144 - #ifndef DISPLAY_TYPE #define DISPLAY_TYPE 0U #endif @@ -141,7 +139,7 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); if (error) { @@ -195,7 +193,7 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } -bool PinMappingClass::isValidNrf24Config() +bool PinMappingClass::isValidNrf24Config() const { return _pinMapping.nrf24_clk >= 0 && _pinMapping.nrf24_cs >= 0 @@ -205,7 +203,7 @@ bool PinMappingClass::isValidNrf24Config() && _pinMapping.nrf24_mosi >= 0; } -bool PinMappingClass::isValidCmt2300Config() +bool PinMappingClass::isValidCmt2300Config() const { return _pinMapping.cmt_clk >= 0 && _pinMapping.cmt_cs >= 0 @@ -213,7 +211,7 @@ bool PinMappingClass::isValidCmt2300Config() && _pinMapping.cmt_sdio >= 0; } -bool PinMappingClass::isValidEthConfig() +bool PinMappingClass::isValidEthConfig() const { return _pinMapping.eth_enabled; -} \ No newline at end of file +} diff --git a/src/Scheduler.cpp b/src/Scheduler.cpp new file mode 100644 index 000000000..79dfd9c8b --- /dev/null +++ b/src/Scheduler.cpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Scheduler.h" + +Scheduler scheduler; \ No newline at end of file diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp index 0d4d419ba..f1e1bee4a 100644 --- a/src/SunPosition.cpp +++ b/src/SunPosition.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "SunPosition.h" #include "Configuration.h" @@ -10,21 +10,24 @@ SunPositionClass SunPosition; SunPositionClass::SunPositionClass() + : _loopTask(5 * TASK_SECOND, TASK_FOREVER, std::bind(&SunPositionClass::loop, this)) { } -void SunPositionClass::init() +void SunPositionClass::init(Scheduler& scheduler) { + scheduler.addTask(_loopTask); + _loopTask.enable(); } void SunPositionClass::loop() { - if (getDoRecalc() || checkRecalcDayChanged()) { + if (_doRecalc || checkRecalcDayChanged()) { updateSunData(); } } -bool SunPositionClass::isDayPeriod() +bool SunPositionClass::isDayPeriod() const { if (!_isValidInfo) { return true; @@ -32,28 +35,21 @@ bool SunPositionClass::isDayPeriod() struct tm timeinfo; getLocalTime(&timeinfo, 5); - uint32_t minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; + const uint32_t minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; return (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); } -bool SunPositionClass::isSunsetAvailable() +bool SunPositionClass::isSunsetAvailable() const { return _isSunsetAvailable; } -void SunPositionClass::setDoRecalc(bool doRecalc) +void SunPositionClass::setDoRecalc(const bool doRecalc) { - std::lock_guard lock(_recalcLock); _doRecalc = doRecalc; } -bool SunPositionClass::getDoRecalc() -{ - std::lock_guard lock(_recalcLock); - return _doRecalc; -} - -bool SunPositionClass::checkRecalcDayChanged() +bool SunPositionClass::checkRecalcDayChanged() const { time_t now; struct tm timeinfo; @@ -61,39 +57,31 @@ bool SunPositionClass::checkRecalcDayChanged() time(&now); localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms - uint32_t ymd; - ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; + const uint32_t ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; - if (_lastSunPositionCalculatedYMD != ymd) { - return true; - } - return false; + return _lastSunPositionCalculatedYMD != ymd; } void SunPositionClass::updateSunData() { struct tm timeinfo; - bool gotLocalTime; + const bool gotLocalTime = getLocalTime(&timeinfo, 5); - gotLocalTime = getLocalTime(&timeinfo, 5); _lastSunPositionCalculatedYMD = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; setDoRecalc(false); if (!gotLocalTime) { _sunriseMinutes = 0; _sunsetMinutes = 0; + _isSunsetAvailable = true; _isValidInfo = false; return; } CONFIG_T const& config = Configuration.get(); - int offset = Utils::getTimezoneOffset() / 3600; - - _sun.setPosition(config.Ntp_Latitude, config.Ntp_Longitude, offset); - _sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); double sunset_type; - switch (config.Ntp_SunsetType) { + switch (config.Ntp.SunsetType) { case 0: sunset_type = SunSet::SUNSET_OFFICIAL; break; @@ -108,15 +96,21 @@ void SunPositionClass::updateSunData() break; } - double sunriseRaw = _sun.calcCustomSunrise(sunset_type); - double sunsetRaw = _sun.calcCustomSunset(sunset_type); + const int offset = Utils::getTimezoneOffset() / 3600; + + SunSet sun; + sun.setPosition(config.Ntp.Latitude, config.Ntp.Longitude, offset); + sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); + + const double sunriseRaw = sun.calcCustomSunrise(sunset_type); + const double sunsetRaw = sun.calcCustomSunset(sunset_type); // If no sunset/sunrise exists (e.g. astronomical calculation in summer) // assume it's day period if (std::isnan(sunriseRaw) || std::isnan(sunsetRaw)) { - _isSunsetAvailable = false; _sunriseMinutes = 0; _sunsetMinutes = 0; + _isSunsetAvailable = false; _isValidInfo = false; return; } @@ -128,7 +122,7 @@ void SunPositionClass::updateSunData() _isValidInfo = true; } -bool SunPositionClass::sunsetTime(struct tm* info) +bool SunPositionClass::getSunTime(struct tm* info, const uint32_t offset) const { // Get today's date time_t aTime = time(NULL); @@ -137,29 +131,21 @@ bool SunPositionClass::sunsetTime(struct tm* info) struct tm tm; localtime_r(&aTime, &tm); tm.tm_sec = 0; - tm.tm_min = _sunsetMinutes; + tm.tm_min = offset; tm.tm_hour = 0; tm.tm_isdst = -1; - time_t midnight = mktime(&tm); + const time_t midnight = mktime(&tm); localtime_r(&midnight, info); return _isValidInfo; } -bool SunPositionClass::sunriseTime(struct tm* info) +bool SunPositionClass::sunsetTime(struct tm* info) const { - // Get today's date - time_t aTime = time(NULL); - - // Set the time to midnight - struct tm tm; - localtime_r(&aTime, &tm); - tm.tm_sec = 0; - tm.tm_min = _sunriseMinutes; - tm.tm_hour = 0; - tm.tm_isdst = -1; - time_t midnight = mktime(&tm); + return getSunTime(info, _sunsetMinutes); +} - localtime_r(&midnight, info); - return _isValidInfo; +bool SunPositionClass::sunriseTime(struct tm* info) const +{ + return getSunTime(info, _sunriseMinutes); } diff --git a/src/Utils.cpp b/src/Utils.cpp index 893509612..6abe4dd19 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1,11 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + #include "Utils.h" #include "Display_Graphic.h" #include "Led_Single.h" +#include "MessageOutput.h" #include +#include uint32_t Utils::getChipId() { @@ -65,3 +68,27 @@ void Utils::restartDtu() 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); + return false; + } + + return true; +} + +/// @brief Remove all files but the PINMAPPING_FILENAME +void Utils::removeAllFiles() +{ + auto root = LittleFS.open("/"); + auto file = root.getNextFileName(); + + while (file != "") { + if (file != PINMAPPING_FILENAME) { + LittleFS.remove(file); + } + file = root.getNextFileName(); + } +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 511a3845d..1a5b28709 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -1,73 +1,48 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi.h" #include "Configuration.h" +#include "MessageOutput.h" #include "defaults.h" #include WebApiClass::WebApiClass() : _server(HTTP_PORT) - , _events("/events") { } -void WebApiClass::init() +void WebApiClass::init(Scheduler& scheduler) { - _server.addHandler(&_events); - - _webApiConfig.init(&_server); - _webApiDevice.init(&_server); - _webApiDevInfo.init(&_server); - _webApiDtu.init(&_server); - _webApiEventlog.init(&_server); - _webApiFirmware.init(&_server); - _webApiGridprofile.init(&_server); - _webApiInverter.init(&_server); - _webApiLimit.init(&_server); - _webApiMaintenance.init(&_server); - _webApiMqtt.init(&_server); - _webApiNetwork.init(&_server); - _webApiNtp.init(&_server); - _webApiPower.init(&_server); - _webApiPrometheus.init(&_server); - _webApiSecurity.init(&_server); - _webApiSysstatus.init(&_server); - _webApiWebapp.init(&_server); - _webApiWsConsole.init(&_server); - _webApiWsLive.init(&_server); + _webApiConfig.init(_server, scheduler); + _webApiDevice.init(_server, scheduler); + _webApiDevInfo.init(_server, scheduler); + _webApiDtu.init(_server, scheduler); + _webApiEventlog.init(_server, scheduler); + _webApiFirmware.init(_server, scheduler); + _webApiGridprofile.init(_server, scheduler); + _webApiInverter.init(_server, scheduler); + _webApiLimit.init(_server, scheduler); + _webApiMaintenance.init(_server, scheduler); + _webApiMqtt.init(_server, scheduler); + _webApiNetwork.init(_server, scheduler); + _webApiNtp.init(_server, scheduler); + _webApiPower.init(_server, scheduler); + _webApiPrometheus.init(_server, scheduler); + _webApiSecurity.init(_server, scheduler); + _webApiSysstatus.init(_server, scheduler); + _webApiWebapp.init(_server, scheduler); + _webApiWsConsole.init(_server, scheduler); + _webApiWsLive.init(_server, scheduler); _server.begin(); } -void WebApiClass::loop() -{ - _webApiConfig.loop(); - _webApiDevice.loop(); - _webApiDevInfo.loop(); - _webApiDtu.loop(); - _webApiEventlog.loop(); - _webApiFirmware.loop(); - _webApiGridprofile.loop(); - _webApiInverter.loop(); - _webApiLimit.loop(); - _webApiMaintenance.loop(); - _webApiMqtt.loop(); - _webApiNetwork.loop(); - _webApiNtp.loop(); - _webApiPower.loop(); - _webApiSecurity.loop(); - _webApiSysstatus.loop(); - _webApiWebapp.loop(); - _webApiWsConsole.loop(); - _webApiWsLive.loop(); -} - bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) { CONFIG_T& config = Configuration.get(); - if (request->authenticate(AUTH_USERNAME, config.Security_Password)) { + if (request->authenticate(AUTH_USERNAME, config.Security.Password)) { return true; } @@ -85,7 +60,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) { CONFIG_T& config = Configuration.get(); - if (config.Security_AllowReadonly) { + if (config.Security.AllowReadonly) { return true; } else { return checkCredentials(request); @@ -99,4 +74,70 @@ void WebApiClass::sendTooManyRequests(AsyncWebServerRequest* request) request->send(response); } -WebApiClass WebApi; \ No newline at end of file +void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const String& message) +{ + if (!Configuration.write()) { + retMsg["message"] = "Write failed!"; + retMsg["code"] = WebApiError::GenericWriteFailed; + } else { + retMsg["type"] = "success"; + retMsg["message"] = message; + retMsg["code"] = code; + } +} + +bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document) +{ + auto& retMsg = response->getRoot(); + retMsg["type"] = "warning"; + + if (!request->hasParam("data", true)) { + retMsg["message"] = "No values found!"; + retMsg["code"] = WebApiError::GenericNoValueFound; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return false; + } + + const String json = request->getParam("data", true)->value(); + const DeserializationError error = deserializeJson(json_document, json); + if (error) { + retMsg["message"] = "Failed to parse data!"; + retMsg["code"] = WebApiError::GenericParseError; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return false; + } + + return true; +} + +uint64_t WebApiClass::parseSerialFromRequest(AsyncWebServerRequest* request, String param_name) +{ + if (request->hasParam(param_name)) { + String s = request->getParam(param_name)->value(); + return strtoll(s.c_str(), NULL, 16); + } + + return 0; +} + +bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line) +{ + bool ret_val = true; + if (response->overflowed()) { + auto& root = response->getRoot(); + + root.clear(); + root["message"] = String("500 Internal Server Error: ") + function + ", " + line; + root["code"] = WebApiError::GenericInternalServerError; + root["type"] = "danger"; + response->setCode(500); + MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); + ret_val = false; + } + + response->setLength(); + request->send(response); + return ret_val; +} + +WebApiClass WebApi; diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 08b86d654..759b6b243 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_config.h" #include "Configuration.h" @@ -10,7 +10,7 @@ #include #include -void WebApiConfigClass::init(AsyncWebServer* server) +void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; using std::placeholders::_2; @@ -19,20 +19,14 @@ void WebApiConfigClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; - - _server->on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1)); - _server->on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1)); - _server->on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1)); - _server->on("/api/config/upload", HTTP_POST, + 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)); } -void WebApiConfigClass::loop() -{ -} - void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { @@ -46,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) requestFile = name; } else { request->send(404); + return; } } @@ -59,51 +54,24 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("delete"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["delete"].as() == false) { retMsg["message"] = "Not deleted anything!"; retMsg["code"] = WebApiError::ConfigNotDeleted; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -111,10 +79,9 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) retMsg["message"] = "Configuration resettet. Rebooting now..."; retMsg["code"] = WebApiError::ConfigSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - LittleFS.remove(CONFIG_FILENAME); + Utils::removeAllFiles(); Utils::restartDtu(); } @@ -125,8 +92,8 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - JsonArray data = root.createNestedArray("configs"); + auto& root = response->getRoot(); + auto data = root["configs"].to(); File rootfs = LittleFS.open("/"); File file = rootfs.openNextFile(); @@ -134,15 +101,14 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) if (file.isDirectory()) { continue; } - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["name"] = String(file.name()); file = rootfs.openNextFile(); } file.close(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) @@ -173,7 +139,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi request->send(500); return; } - String name = "/" + request->getParam("file")->value(); + const String name = "/" + request->getParam("file")->value(); request->_tempFile = LittleFS.open(name, "w"); } @@ -186,4 +152,4 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi // close the file handle as the upload is now done request->_tempFile.close(); } -} \ No newline at end of file +} diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 507a7e16b..078d5b4a1 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_device.h" #include "Configuration.h" @@ -12,18 +12,12 @@ #include "helper.h" #include -void WebApiDeviceClass::init(AsyncWebServer* server) +void WebApiDeviceClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); - _server->on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); -} - -void WebApiDeviceClass::loop() -{ + server.on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); + server.on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); } void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) @@ -32,15 +26,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject root = response->getRoot(); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); const PinMapping_t& pin = PinMapping.get(); - JsonObject curPin = root.createNestedObject("curPin"); + auto curPin = root["curPin"].to(); curPin["name"] = config.Dev_PinMapping; - JsonObject nrfPinObj = curPin.createNestedObject("nrf24"); + auto nrfPinObj = curPin["nrf24"].to(); nrfPinObj["clk"] = pin.nrf24_clk; nrfPinObj["cs"] = pin.nrf24_cs; nrfPinObj["en"] = pin.nrf24_en; @@ -48,7 +42,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) nrfPinObj["miso"] = pin.nrf24_miso; nrfPinObj["mosi"] = pin.nrf24_mosi; - JsonObject cmtPinObj = curPin.createNestedObject("cmt"); + auto cmtPinObj = curPin["cmt"].to(); cmtPinObj["clk"] = pin.cmt_clk; cmtPinObj["cs"] = pin.cmt_cs; cmtPinObj["fcs"] = pin.cmt_fcs; @@ -56,7 +50,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio3"] = pin.cmt_gpio3; - JsonObject ethPinObj = curPin.createNestedObject("eth"); + auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; ethPinObj["power"] = pin.eth_power; @@ -65,26 +59,34 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) ethPinObj["type"] = pin.eth_type; ethPinObj["clk_mode"] = pin.eth_clk_mode; - JsonObject displayPinObj = curPin.createNestedObject("display"); + auto displayPinObj = curPin["display"].to(); displayPinObj["type"] = pin.display_type; displayPinObj["data"] = pin.display_data; displayPinObj["clk"] = pin.display_clk; displayPinObj["cs"] = pin.display_cs; displayPinObj["reset"] = pin.display_reset; - JsonObject ledPinObj = curPin.createNestedObject("led"); - ledPinObj["led0"] = pin.led[0]; - ledPinObj["led1"] = pin.led[1]; + auto ledPinObj = curPin["led"].to(); + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + ledPinObj["led" + String(i)] = pin.led[i]; + } - JsonObject display = root.createNestedObject("display"); - display["rotation"] = config.Display_Rotation; - display["power_safe"] = config.Display_PowerSafe; - display["screensaver"] = config.Display_ScreenSaver; - display["contrast"] = config.Display_Contrast; - display["language"] = config.Display_Language; + auto display = root["display"].to(); + display["rotation"] = config.Display.Rotation; + display["power_safe"] = config.Display.PowerSafe; + display["screensaver"] = config.Display.ScreenSaver; + display["contrast"] = config.Display.Contrast; + display["language"] = config.Display.Language; + display["diagramduration"] = config.Display.Diagram.Duration; + display["diagrammode"] = config.Display.Diagram.Mode; + + auto leds = root["led"].to(); + for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { + auto led = leds.add(); + led["brightness"] = config.Led_Single[i].Brightness; + } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) @@ -93,45 +95,19 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("curPin") || root.containsKey("display"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -139,8 +115,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::HardwarePinMappingLength; retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -148,28 +123,32 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping; strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as().c_str(), sizeof(config.Dev_PinMapping)); - config.Display_Rotation = root["display"]["rotation"].as(); - config.Display_PowerSafe = root["display"]["power_safe"].as(); - config.Display_ScreenSaver = root["display"]["screensaver"].as(); - config.Display_Contrast = root["display"]["contrast"].as(); - config.Display_Language = root["display"]["language"].as(); - - Display.setOrientation(config.Display_Rotation); - Display.enablePowerSafe = config.Display_PowerSafe; - Display.enableScreensaver = config.Display_ScreenSaver; - Display.setContrast(config.Display_Contrast); - Display.setLanguage(config.Display_Language); + config.Display.Rotation = root["display"]["rotation"].as(); + config.Display.PowerSafe = root["display"]["power_safe"].as(); + config.Display.ScreenSaver = root["display"]["screensaver"].as(); + config.Display.Contrast = root["display"]["contrast"].as(); + config.Display.Language = root["display"]["language"].as(); + config.Display.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); + } - Configuration.write(); + 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.Diagram().updatePeriod(); - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { Utils::restartDtu(); } -} \ No newline at end of file +} diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 31d1d0ca7..449cd1772 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_devinfo.h" #include "WebApi.h" @@ -8,17 +8,11 @@ #include #include -void WebApiDevInfoClass::init(AsyncWebServer* server) +void WebApiDevInfoClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/devinfo/status", HTTP_GET, std::bind(&WebApiDevInfoClass::onDevInfoStatus, this, _1)); -} - -void WebApiDevInfoClass::loop() -{ + server.on("/api/devinfo/status", HTTP_GET, std::bind(&WebApiDevInfoClass::onDevInfoStatus, this, _1)); } void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) @@ -28,14 +22,8 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto& root = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { @@ -46,13 +34,8 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) root["hw_version"] = inv->DevInfo()->getHwVersion(); root["hw_model_name"] = inv->DevInfo()->getHwModelName(); root["max_power"] = inv->DevInfo()->getMaxPower(); - - char timebuffer[32]; - const time_t t = inv->DevInfo()->getFwBuildDateTime(); - std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t)); - root["fw_build_datetime"] = String(timebuffer); + root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr(); } - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 1ae7c408f..9b67ec39f 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_dtu.h" #include "Configuration.h" @@ -9,18 +9,32 @@ #include #include -void WebApiDtuClass::init(AsyncWebServer* server) +WebApiDtuClass::WebApiDtuClass() + : _applyDataTask(TASK_IMMEDIATE, TASK_ONCE, std::bind(&WebApiDtuClass::applyDataTaskCb, this)) +{ +} + +void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; + server.on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); + server.on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); - _server->on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); - _server->on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); + scheduler.addTask(_applyDataTask); } -void WebApiDtuClass::loop() +void WebApiDtuClass::applyDataTaskCb() { + // Execute stuff in main thread to avoid busy SPI bus + CONFIG_T& 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); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial); + Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); + Hoymiles.setPollInterval(config.Dtu.PollInterval); } void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) @@ -30,24 +44,36 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); // DTU Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; snprintf(buffer, sizeof(buffer), "%0x%08x", - ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); + ((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)), + ((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF))); root["serial"] = buffer; - root["pollinterval"] = config.Dtu_PollInterval; + root["pollinterval"] = config.Dtu.PollInterval; root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized(); - root["nrf_palevel"] = config.Dtu_NrfPaLevel; + root["nrf_palevel"] = config.Dtu.Nrf.PaLevel; root["cmt_enabled"] = Hoymiles.getRadioCmt()->isInitialized(); - root["cmt_palevel"] = config.Dtu_CmtPaLevel; - root["cmt_frequency"] = config.Dtu_CmtFrequency; + root["cmt_palevel"] = config.Dtu.Cmt.PaLevel; + root["cmt_frequency"] = config.Dtu.Cmt.Frequency; + root["cmt_country"] = config.Dtu.Cmt.CountryMode; + root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth(); + + auto data = root["country_def"].to(); + auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList(); + for (const auto& definition : countryDefs) { + auto obj = data.add(); + obj["freq_default"] = definition.definition.Freq_Default; + obj["freq_min"] = definition.definition.Freq_Min; + obj["freq_max"] = definition.definition.Freq_Max; + obj["freq_legal_min"] = definition.definition.Freq_Legal_Min; + obj["freq_legal_max"] = definition.definition.Freq_Legal_Max; + } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) @@ -57,116 +83,89 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + 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_frequency") + && root.containsKey("cmt_country"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["serial"].as() == 0) { + // 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); + + if (serial == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["pollinterval"].as() == 0) { retMsg["message"] = "Poll interval must be greater zero!"; retMsg["code"] = WebApiError::DtuPollZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["nrf_palevel"].as() > 3) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["cmt_palevel"].as() < -10 || root["cmt_palevel"].as() > 20) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } + + if (root["cmt_country"].as() >= CountryModeId_t::CountryModeId_Max) { + retMsg["message"] = "Invalid country setting!"; + retMsg["code"] = WebApiError::DtuInvalidCmtCountry; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["cmt_frequency"].as() < Hoymiles.getRadioCmt()->getMinFrequency() - || root["cmt_frequency"].as() > Hoymiles.getRadioCmt()->getMaxFrequency() - || root["cmt_frequency"].as() % 250 > 0) { + auto FrequencyDefinition = Hoymiles.getRadioCmt()->getCountryFrequencyList()[root["cmt_country"].as()].definition; + if (root["cmt_frequency"].as() < FrequencyDefinition.Freq_Min + || root["cmt_frequency"].as() > FrequencyDefinition.Freq_Max + || root["cmt_frequency"].as() % Hoymiles.getRadioCmt()->getChannelWidth() > 0) { retMsg["message"] = "Invalid CMT frequency setting!"; retMsg["code"] = WebApiError::DtuInvalidCmtFrequency; - retMsg["param"]["min"] = Hoymiles.getRadioCmt()->getMinFrequency(); - retMsg["param"]["max"] = Hoymiles.getRadioCmt()->getMaxFrequency(); - response->setLength(); - request->send(response); + retMsg["param"]["min"] = FrequencyDefinition.Freq_Min; + retMsg["param"]["max"] = FrequencyDefinition.Freq_Max; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } CONFIG_T& config = Configuration.get(); - // Interpret the string as a hex value and convert it to uint64_t - config.Dtu_Serial = strtoll(root["serial"].as().c_str(), NULL, 16); - config.Dtu_PollInterval = root["pollinterval"].as(); - config.Dtu_NrfPaLevel = root["nrf_palevel"].as(); - config.Dtu_CmtPaLevel = root["cmt_palevel"].as(); - config.Dtu_CmtFrequency = root["cmt_frequency"].as(); - Configuration.write(); - - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; - - response->setLength(); - request->send(response); - - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); - Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); - Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); - Hoymiles.setPollInterval(config.Dtu_PollInterval); -} \ No newline at end of file + 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); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + _applyDataTask.enable(); + _applyDataTask.restart(); +} diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 2b2672722..ec8b78c30 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -1,23 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_eventlog.h" #include "WebApi.h" #include #include -void WebApiEventlogClass::init(AsyncWebServer* server) +void WebApiEventlogClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/eventlog/status", HTTP_GET, std::bind(&WebApiEventlogClass::onEventlogStatus, this, _1)); -} - -void WebApiEventlogClass::loop() -{ + server.on("/api/eventlog/status", HTTP_GET, std::bind(&WebApiEventlogClass::onEventlogStatus, this, _1)); } void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) @@ -26,14 +20,9 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); - JsonObject root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN; if (request->hasParam("locale")) { @@ -53,13 +42,13 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) uint8_t logEntryCount = inv->EventLog()->getEntryCount(); root["count"] = logEntryCount; - JsonArray eventsArray = root.createNestedArray("events"); + JsonArray eventsArray = root["events"].to(); for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) { - JsonObject eventsObject = eventsArray.createNestedObject(); + JsonObject eventsObject = eventsArray.add(); AlarmLogEntry_t entry; - inv->EventLog()->getLogEntry(logEntry, &entry, locale); + inv->EventLog()->getLogEntry(logEntry, entry, locale); eventsObject["message_id"] = entry.MessageId; eventsObject["message"] = entry.Message; @@ -68,6 +57,5 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 62cf56155..9491f935d 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_firmware.h" #include "Configuration.h" @@ -10,7 +10,7 @@ #include "helper.h" #include -void WebApiFirmwareClass::init(AsyncWebServer* server) +void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; using std::placeholders::_2; @@ -19,17 +19,11 @@ void WebApiFirmwareClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; - _server = server; - - _server->on("/api/firmware/update", HTTP_POST, + server.on("/api/firmware/update", HTTP_POST, std::bind(&WebApiFirmwareClass::onFirmwareUpdateFinish, this, _1), std::bind(&WebApiFirmwareClass::onFirmwareUpdateUpload, this, _1, _2, _3, _4, _5, _6)); } -void WebApiFirmwareClass::loop() -{ -} - void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { @@ -83,4 +77,4 @@ void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, } else { return; } -} \ No newline at end of file +} diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index c9d2adb8d..9fc05b032 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -1,23 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_gridprofile.h" #include "WebApi.h" #include #include -void WebApiGridProfileClass::init(AsyncWebServer* server) +void WebApiGridProfileClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); -} - -void WebApiGridProfileClass::loop() -{ + server.on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); + server.on("/api/gridprofile/rawdata", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileRawdata, this, _1)); } void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) @@ -26,24 +21,54 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); - JsonObject root = response->getRoot(); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv != nullptr) { + root["name"] = inv->GridProfile()->getProfileName(); + root["version"] = inv->GridProfile()->getProfileVersion(); + + auto jsonSections = root["sections"].to(); + auto profSections = inv->GridProfile()->getProfile(); + + for (auto &profSection : profSections) { + auto jsonSection = jsonSections.add(); + jsonSection["name"] = profSection.SectionName; + + auto jsonItems = jsonSection["items"].to(); - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); + for (auto &profItem : profSection.items) { + auto jsonItem = jsonItems.add(); + + jsonItem["n"] = profItem.Name; + jsonItem["u"] = profItem.Unit; + jsonItem["v"] = profItem.Value; + } + } } + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { - auto raw = root.createNestedArray("raw"); + auto raw = root["raw"].to(); auto data = inv->GridProfile()->getRawData(); copyArray(&data[0], data.size(), raw); } - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index d5ea9b45a..5a8585f70 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_inverter.h" #include "Configuration.h" @@ -12,21 +12,15 @@ #include #include -void WebApiInverterClass::init(AsyncWebServer* server) +void WebApiInverterClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); - _server->on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); - _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)); -} - -void WebApiInverterClass::loop() -{ + server.on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); + server.on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); + 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)); } void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) @@ -35,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); - JsonObject root = response->getRoot(); - JsonArray data = root.createNestedArray("inverter"); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + JsonArray data = root["inverter"].to(); const CONFIG_T& config = Configuration.get(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["id"] = i; obj["name"] = String(config.Inverter[i].Name); obj["order"] = config.Inverter[i].Order; @@ -61,6 +55,8 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; obj["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + obj["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight; + obj["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -72,9 +68,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size(); } - JsonArray channel = obj.createNestedArray("channel"); + JsonArray channel = obj["channel"].to(); for (uint8_t c = 0; c < max_channels; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; @@ -82,8 +78,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) @@ -93,52 +88,28 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && root.containsKey("name"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["serial"].as() == 0) { + // 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); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -146,8 +117,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -157,23 +127,18 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!"; retMsg["code"] = WebApiError::InverterCount; retMsg["param"]["max"] = INV_MAX_COUNT; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } // Interpret the string as a hex value and convert it to uint64_t - inverter->Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + inverter->Serial = serial; strncpy(inverter->Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); - Configuration.write(); - retMsg["type"] = "success"; - retMsg["message"] = "Inverter created!"; - retMsg["code"] = WebApiError::InverterAdded; + WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial); @@ -193,59 +158,34 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["serial"].as() == 0) { + // 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); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -253,8 +193,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -262,44 +201,40 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { retMsg["message"] = "Invalid amount of max channel setting given!"; retMsg["code"] = WebApiError::InverterInvalidMaxChannel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as()]; - uint64_t new_serial = strtoll(root["serial"].as().c_str(), NULL, 16); + 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)); - 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; - arrayCount++; } - Configuration.write(); - - retMsg["type"] = "success"; - retMsg["code"] = WebApiError::InverterChanged; - retMsg["message"] = "Inverter changed!"; + WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); std::shared_ptr inv = Hoymiles.getInverterBySerial(old_serial); @@ -321,6 +256,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inv->setReachableThreshold(inverter.ReachableThreshold); inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(inverter.ZeroYieldDayOnMidnight); + inv->setClearEventlogOnMidnight(inverter.ClearEventlogOnMidnight); + inv->Statistics()->setYieldDayCorrection(inverter.YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); @@ -337,51 +274,24 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("id"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -390,16 +300,11 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) Hoymiles.removeInverterBySerial(inverter.Serial); - inverter.Serial = 0; - strncpy(inverter.Name, "", sizeof(inverter.Name)); - Configuration.write(); + Configuration.deleteInverterById(inverter_id); - retMsg["type"] = "success"; - retMsg["message"] = "Inverter deleted!"; - retMsg["code"] = WebApiError::InverterDeleted; + WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttHandleHass.forceUpdate(); } @@ -411,43 +316,17 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("order"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -463,12 +342,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) order++; } - Configuration.write(); + WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); - retMsg["type"] = "success"; - retMsg["message"] = "Inverter order saved!"; - retMsg["code"] = WebApiError::InverterOrdered; - - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 9470e4cac..9a622deae 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -1,25 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_limit.h" #include "WebApi.h" #include "WebApi_errors.h" +#include "defaults.h" +#include "helper.h" #include #include -void WebApiLimitClass::init(AsyncWebServer* server) +void WebApiLimitClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1)); - _server->on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1)); -} - -void WebApiLimitClass::loop() -{ + server.on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1)); + server.on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1)); } void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) @@ -29,7 +25,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -51,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) root[serial]["limit_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) @@ -62,62 +57,37 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && root.containsKey("limit_value") && root.containsKey("limit_type"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["serial"].as() == 0) { + // 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); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::LimitSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["limit_value"].as() == 0 || root["limit_value"].as() > 2250) { - retMsg["message"] = "Limit must between 1 and 2250!"; + if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { + retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; - retMsg["param"]["max"] = 2250; - response->setLength(); - request->send(response); + retMsg["param"]["max"] = MAX_INVERTER_LIMIT; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -128,12 +98,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Invalid type specified!"; retMsg["code"] = WebApiError::LimitInvalidType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); uint16_t limit = root["limit_value"].as(); PowerLimitControlType type = root["limit_type"].as(); @@ -141,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::LimitInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -152,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index ed2d68673..1504f9d75 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_maintenance.h" @@ -9,17 +9,11 @@ #include "WebApi_errors.h" #include -void WebApiMaintenanceClass::init(AsyncWebServer* server) +void WebApiMaintenanceClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); -} - -void WebApiMaintenanceClass::loop() -{ + server.on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); } void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) @@ -28,44 +22,18 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("reboot"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -74,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["message"] = "Reboot triggered!"; retMsg["code"] = WebApiError::MaintenanceRebootTriggered; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::restartDtu(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } -} \ No newline at end of file +} diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index aedcd51a9..af40643ee 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -1,29 +1,24 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_mqtt.h" #include "Configuration.h" #include "MqttHandleHass.h" +#include "MqttHandleInverter.h" #include "MqttSettings.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" #include -void WebApiMqttClass::init(AsyncWebServer* server) +void WebApiMqttClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); - _server->on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); - _server->on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiMqttClass::onMqttAdminPost, this, _1)); -} - -void WebApiMqttClass::loop() -{ + server.on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); + server.on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); + server.on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiMqttClass::onMqttAdminPost, this, _1)); } void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) @@ -32,32 +27,32 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject root = response->getRoot(); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["mqtt_enabled"] = config.Mqtt_Enabled; - root["mqtt_hostname"] = config.Mqtt_Hostname; - root["mqtt_port"] = config.Mqtt_Port; - root["mqtt_username"] = config.Mqtt_Username; - root["mqtt_topic"] = config.Mqtt_Topic; + root["mqtt_enabled"] = config.Mqtt.Enabled; + root["mqtt_hostname"] = config.Mqtt.Hostname; + root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_clientid"] = MqttSettings.getClientId(); + root["mqtt_username"] = config.Mqtt.Username; + root["mqtt_topic"] = config.Mqtt.Topic; root["mqtt_connected"] = MqttSettings.getConnected(); - root["mqtt_retain"] = config.Mqtt_Retain; - root["mqtt_tls"] = config.Mqtt_Tls; - root["mqtt_root_ca_cert_info"] = getTlsCertInfo(config.Mqtt_RootCaCert); - root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin; - root["mqtt_client_cert_info"] = getTlsCertInfo(config.Mqtt_ClientCert); - root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; - root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; - root["mqtt_clean_session"] = config.Mqtt_CleanSession; - root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; - root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; - root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; - root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic; - root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels; - - response->setLength(); - request->send(response); + root["mqtt_retain"] = config.Mqtt.Retain; + root["mqtt_tls"] = config.Mqtt.Tls.Enabled; + root["mqtt_root_ca_cert_info"] = getTlsCertInfo(config.Mqtt.Tls.RootCaCert); + root["mqtt_tls_cert_login"] = config.Mqtt.Tls.CertLogin; + root["mqtt_client_cert_info"] = getTlsCertInfo(config.Mqtt.Tls.ClientCert); + root["mqtt_lwt_topic"] = String(config.Mqtt.Topic) + config.Mqtt.Lwt.Topic; + root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; + root["mqtt_clean_session"] = config.Mqtt.CleanSession; + root["mqtt_hass_enabled"] = config.Mqtt.Hass.Enabled; + root["mqtt_hass_expire"] = config.Mqtt.Hass.Expire; + root["mqtt_hass_retain"] = config.Mqtt.Hass.Retain; + root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; + root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) @@ -66,35 +61,36 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject root = response->getRoot(); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["mqtt_enabled"] = config.Mqtt_Enabled; - root["mqtt_hostname"] = config.Mqtt_Hostname; - root["mqtt_port"] = config.Mqtt_Port; - root["mqtt_username"] = config.Mqtt_Username; - root["mqtt_password"] = config.Mqtt_Password; - root["mqtt_topic"] = config.Mqtt_Topic; - root["mqtt_retain"] = config.Mqtt_Retain; - root["mqtt_tls"] = config.Mqtt_Tls; - root["mqtt_root_ca_cert"] = config.Mqtt_RootCaCert; - root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin; - root["mqtt_client_cert"] = config.Mqtt_ClientCert; - root["mqtt_client_key"] = config.Mqtt_ClientKey; - root["mqtt_lwt_topic"] = config.Mqtt_LwtTopic; - root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online; - root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline; - root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; - root["mqtt_clean_session"] = config.Mqtt_CleanSession; - root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; - root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; - root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; - root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic; - root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels; - - response->setLength(); - request->send(response); + root["mqtt_enabled"] = config.Mqtt.Enabled; + root["mqtt_hostname"] = config.Mqtt.Hostname; + root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_clientid"] = config.Mqtt.ClientId; + root["mqtt_username"] = config.Mqtt.Username; + root["mqtt_password"] = config.Mqtt.Password; + root["mqtt_topic"] = config.Mqtt.Topic; + root["mqtt_retain"] = config.Mqtt.Retain; + root["mqtt_tls"] = config.Mqtt.Tls.Enabled; + root["mqtt_root_ca_cert"] = config.Mqtt.Tls.RootCaCert; + root["mqtt_tls_cert_login"] = config.Mqtt.Tls.CertLogin; + root["mqtt_client_cert"] = config.Mqtt.Tls.ClientCert; + root["mqtt_client_key"] = config.Mqtt.Tls.ClientKey; + root["mqtt_lwt_topic"] = config.Mqtt.Lwt.Topic; + root["mqtt_lwt_online"] = config.Mqtt.Lwt.Value_Online; + root["mqtt_lwt_offline"] = config.Mqtt.Lwt.Value_Offline; + root["mqtt_lwt_qos"] = config.Mqtt.Lwt.Qos; + root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; + root["mqtt_clean_session"] = config.Mqtt.CleanSession; + root["mqtt_hass_enabled"] = config.Mqtt.Hass.Enabled; + root["mqtt_hass_expire"] = config.Mqtt.Hass.Expire; + root["mqtt_hass_retain"] = config.Mqtt.Hass.Retain; + root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; + root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) @@ -103,42 +99,18 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + 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") @@ -150,6 +122,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") + && root.containsKey("mqtt_lwt_qos") && root.containsKey("mqtt_publish_interval") && root.containsKey("mqtt_clean_session") && root.containsKey("mqtt_hass_enabled") @@ -159,8 +132,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_hass_individualpanels"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -169,57 +141,57 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::MqttHostnameLength; retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } + if (root["mqtt_clientid"].as().length() > MQTT_MAX_CLIENTID_STRLEN) { + retMsg["message"] = "Client ID must not be longer than " STR(MQTT_MAX_CLIENTID_STRLEN) " characters!"; + retMsg["code"] = WebApiError::MqttClientIdLength; + retMsg["param"]["max"] = MQTT_MAX_CLIENTID_STRLEN; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } if (root["mqtt_username"].as().length() > MQTT_MAX_USERNAME_STRLEN) { retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttUsernameLength; retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_password"].as().length() > MQTT_MAX_PASSWORD_STRLEN) { retMsg["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttPasswordLength; retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (!root["mqtt_topic"].as().endsWith("/")) { retMsg["message"] = "Topic must end with a slash (/)!"; retMsg["code"] = WebApiError::MqttTopicTrailingSlash; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_port"].as() == 0 || root["mqtt_port"].as() > 65535) { retMsg["message"] = "Port must be a number between 1 and 65535!"; retMsg["code"] = WebApiError::MqttPort; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -229,8 +201,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttCertificateLength; retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -238,16 +209,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_lwt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "LWT topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttLwtTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -255,8 +224,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOnlineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -264,8 +232,15 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOfflineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } + + if (root["mqtt_lwt_qos"].as() > 2) { + retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!"; + retMsg["code"] = WebApiError::MqttLwtQos; + retMsg["param"]["max"] = 2; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -274,8 +249,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MqttPublishInterval; retMsg["param"]["min"] = 5; retMsg["param"]["max"] = 65535; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -284,52 +258,54 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttHassTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_hass_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Hass topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttHassTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } } CONFIG_T& config = Configuration.get(); - config.Mqtt_Enabled = root["mqtt_enabled"].as(); - config.Mqtt_Retain = root["mqtt_retain"].as(); - config.Mqtt_Tls = root["mqtt_tls"].as(); - strlcpy(config.Mqtt_RootCaCert, root["mqtt_root_ca_cert"].as().c_str(), sizeof(config.Mqtt_RootCaCert)); - config.Mqtt_TlsCertLogin = root["mqtt_tls_cert_login"].as(); - strlcpy(config.Mqtt_ClientCert, root["mqtt_client_cert"].as().c_str(), sizeof(config.Mqtt_ClientCert)); - strlcpy(config.Mqtt_ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt_ClientKey)); - config.Mqtt_Port = root["mqtt_port"].as(); - strlcpy(config.Mqtt_Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt_Hostname)); - strlcpy(config.Mqtt_Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt_Topic)); - strlcpy(config.Mqtt_LwtTopic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt_LwtValue_Offline)); - config.Mqtt_PublishInterval = root["mqtt_publish_interval"].as(); - config.Mqtt_CleanSession = root["mqtt_clean_session"].as(); - config.Mqtt_Hass_Enabled = root["mqtt_hass_enabled"].as(); - config.Mqtt_Hass_Expire = root["mqtt_hass_expire"].as(); - config.Mqtt_Hass_Retain = root["mqtt_hass_retain"].as(); - config.Mqtt_Hass_IndividualPanels = root["mqtt_hass_individualpanels"].as(); - strlcpy(config.Mqtt_Hass_Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt_Hass_Topic)); - Configuration.write(); - - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; - - response->setLength(); - request->send(response); + 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); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttSettings.performReconnect(); MqttHandleHass.forceUpdate(); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 849c5f8a8..7fec44b2a 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_network.h" #include "Configuration.h" @@ -10,19 +10,13 @@ #include "helper.h" #include -void WebApiNetworkClass::init(AsyncWebServer* server) +void WebApiNetworkClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); - _server->on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); - _server->on("/api/network/config", HTTP_POST, std::bind(&WebApiNetworkClass::onNetworkAdminPost, this, _1)); -} - -void WebApiNetworkClass::loop() -{ + server.on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); + server.on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); + server.on("/api/network/config", HTTP_POST, std::bind(&WebApiNetworkClass::onNetworkAdminPost, this, _1)); } void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) @@ -32,7 +26,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); root["sta_status"] = ((WiFi.getMode() & WIFI_STA) != 0); root["sta_ssid"] = WiFi.SSID(); @@ -52,8 +46,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) root["ap_mac"] = WiFi.softAPmacAddress(); root["ap_stationnum"] = WiFi.softAPgetStationNum(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) @@ -63,23 +56,22 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["hostname"] = config.WiFi_Hostname; - root["dhcp"] = config.WiFi_Dhcp; - root["ipaddress"] = IPAddress(config.WiFi_Ip).toString(); - root["netmask"] = IPAddress(config.WiFi_Netmask).toString(); - root["gateway"] = IPAddress(config.WiFi_Gateway).toString(); - root["dns1"] = IPAddress(config.WiFi_Dns1).toString(); - root["dns2"] = IPAddress(config.WiFi_Dns2).toString(); - root["ssid"] = config.WiFi_Ssid; - root["password"] = config.WiFi_Password; - root["aptimeout"] = config.WiFi_ApTimeout; - root["mdnsenabled"] = config.Mdns_Enabled; - - response->setLength(); - request->send(response); + root["hostname"] = config.WiFi.Hostname; + root["dhcp"] = config.WiFi.Dhcp; + root["ipaddress"] = IPAddress(config.WiFi.Ip).toString(); + root["netmask"] = IPAddress(config.WiFi.Netmask).toString(); + root["gateway"] = IPAddress(config.WiFi.Gateway).toString(); + root["dns1"] = IPAddress(config.WiFi.Dns1).toString(); + root["dns2"] = IPAddress(config.WiFi.Dns2).toString(); + root["ssid"] = config.WiFi.Ssid; + root["password"] = config.WiFi.Password; + root["aptimeout"] = config.WiFi.ApTimeout; + root["mdnsenabled"] = config.Mdns.Enabled; + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) @@ -89,37 +81,12 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("ssid") && root.containsKey("password") @@ -133,8 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) && root.containsKey("aptimeout"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -142,111 +108,98 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (!ipaddress.fromString(root["ipaddress"].as())) { retMsg["message"] = "IP address is invalid!"; retMsg["code"] = WebApiError::NetworkIpInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress netmask; if (!netmask.fromString(root["netmask"].as())) { retMsg["message"] = "Netmask is invalid!"; retMsg["code"] = WebApiError::NetworkNetmaskInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress gateway; if (!gateway.fromString(root["gateway"].as())) { retMsg["message"] = "Gateway is invalid!"; retMsg["code"] = WebApiError::NetworkGatewayInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns1; if (!dns1.fromString(root["dns1"].as())) { retMsg["message"] = "DNS Server IP 1 is invalid!"; retMsg["code"] = WebApiError::NetworkDns1Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns2; if (!dns2.fromString(root["dns2"].as())) { retMsg["message"] = "DNS Server IP 2 is invalid!"; retMsg["code"] = WebApiError::NetworkDns2Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["hostname"].as().length() == 0 || root["hostname"].as().length() > WIFI_MAX_HOSTNAME_STRLEN) { retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (NetworkSettings.NetworkMode() == network_mode::WiFi) { if (root["ssid"].as().length() == 0 || root["ssid"].as().length() > WIFI_MAX_SSID_STRLEN) { retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } if (root["password"].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["aptimeout"].as() > 99999) { retMsg["message"] = "ApTimeout must be a number between 0 and 99999!"; retMsg["code"] = WebApiError::NetworkApTimeoutInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); 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)); + config.WiFi.Ip[0] = ipaddress[0]; + config.WiFi.Ip[1] = ipaddress[1]; + config.WiFi.Ip[2] = ipaddress[2]; + config.WiFi.Ip[3] = ipaddress[3]; + config.WiFi.Netmask[0] = netmask[0]; + config.WiFi.Netmask[1] = netmask[1]; + config.WiFi.Netmask[2] = netmask[2]; + config.WiFi.Netmask[3] = netmask[3]; + config.WiFi.Gateway[0] = gateway[0]; + config.WiFi.Gateway[1] = gateway[1]; + config.WiFi.Gateway[2] = gateway[2]; + config.WiFi.Gateway[3] = gateway[3]; + config.WiFi.Dns1[0] = dns1[0]; + config.WiFi.Dns1[1] = dns1[1]; + config.WiFi.Dns1[2] = dns1[2]; + config.WiFi.Dns1[3] = dns1[3]; + config.WiFi.Dns2[0] = dns2[0]; + config.WiFi.Dns2[1] = dns2[1]; + config.WiFi.Dns2[2] = dns2[2]; + config.WiFi.Dns2[3] = dns2[3]; + strlcpy(config.WiFi.Ssid, root["ssid"].as().c_str(), sizeof(config.WiFi.Ssid)); + strlcpy(config.WiFi.Password, root["password"].as().c_str(), sizeof(config.WiFi.Password)); + strlcpy(config.WiFi.Hostname, root["hostname"].as().c_str(), sizeof(config.WiFi.Hostname)); if (root["dhcp"].as()) { - config.WiFi_Dhcp = true; + config.WiFi.Dhcp = true; } else { - config.WiFi_Dhcp = false; + config.WiFi.Dhcp = false; } - config.WiFi_ApTimeout = root["aptimeout"].as(); - config.Mdns_Enabled = root["mdnsenabled"].as(); - Configuration.write(); + config.WiFi.ApTimeout = root["aptimeout"].as(); + config.Mdns.Enabled = root["mdnsenabled"].as(); - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NetworkSettings.enableAdminMode(); NetworkSettings.applyConfig(); -} \ No newline at end of file +} diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index c0cfaa441..d50e0f02f 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_ntp.h" #include "Configuration.h" @@ -11,21 +11,15 @@ #include "helper.h" #include -void WebApiNtpClass::init(AsyncWebServer* server) +void WebApiNtpClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); - _server->on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); - _server->on("/api/ntp/config", HTTP_POST, std::bind(&WebApiNtpClass::onNtpAdminPost, this, _1)); - _server->on("/api/ntp/time", HTTP_GET, std::bind(&WebApiNtpClass::onNtpTimeGet, this, _1)); - _server->on("/api/ntp/time", HTTP_POST, std::bind(&WebApiNtpClass::onNtpTimePost, this, _1)); -} - -void WebApiNtpClass::loop() -{ + server.on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); + server.on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); + server.on("/api/ntp/config", HTTP_POST, std::bind(&WebApiNtpClass::onNtpAdminPost, this, _1)); + server.on("/api/ntp/time", HTTP_GET, std::bind(&WebApiNtpClass::onNtpTimeGet, this, _1)); + server.on("/api/ntp/time", HTTP_POST, std::bind(&WebApiNtpClass::onNtpTimePost, this, _1)); } void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) @@ -35,12 +29,12 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["ntp_server"] = config.Ntp_Server; - root["ntp_timezone"] = config.Ntp_Timezone; - root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr; + root["ntp_server"] = config.Ntp.Server; + root["ntp_timezone"] = config.Ntp.Timezone; + root["ntp_timezone_descr"] = config.Ntp.TimezoneDescr; struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { @@ -69,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable(); root["sun_isDayPeriod"] = SunPosition.isDayPeriod(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) @@ -80,18 +73,17 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["ntp_server"] = config.Ntp_Server; - root["ntp_timezone"] = config.Ntp_Timezone; - root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr; - root["longitude"] = config.Ntp_Longitude; - root["latitude"] = config.Ntp_Latitude; - root["sunsettype"] = config.Ntp_SunsetType; + root["ntp_server"] = config.Ntp.Server; + root["ntp_timezone"] = config.Ntp.Timezone; + root["ntp_timezone_descr"] = config.Ntp.TimezoneDescr; + root["longitude"] = config.Ntp.Longitude; + root["latitude"] = config.Ntp.Latitude; + root["sunsettype"] = config.Ntp.SunsetType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) @@ -101,37 +93,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") @@ -140,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) && root.containsKey("sunsettype"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -149,8 +115,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpServerLength; retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -158,8 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -167,26 +131,21 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); 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(); - Configuration.write(); + 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(); - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NtpSettings.setServer(); NtpSettings.setTimezone(); @@ -201,7 +160,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { @@ -217,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) root["minute"] = timeinfo.tm_min; root["second"] = timeinfo.tm_sec; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) @@ -228,37 +186,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("year") && root.containsKey("month") @@ -268,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) && root.containsKey("second"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -278,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpYearInvalid; retMsg["param"]["min"] = 2022; retMsg["param"]["max"] = 2100; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -288,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMonthInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 12; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -298,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpDayInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 31; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -308,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpHourInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 23; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -318,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMinuteInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -328,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpSecondInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -350,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["message"] = "Time updated!"; retMsg["code"] = WebApiError::NtpTimeUpdated; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index ca7923642..b2b2ce42e 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_power.h" #include "WebApi.h" @@ -8,18 +8,12 @@ #include #include -void WebApiPowerClass::init(AsyncWebServer* server) +void WebApiPowerClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); - _server->on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); -} - -void WebApiPowerClass::loop() -{ + server.on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); + server.on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); } void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) @@ -29,7 +23,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -46,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) root[inv->serialString()]["power_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) @@ -57,63 +50,37 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && (root.containsKey("power") || root.containsKey("restart")))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - if (root["serial"].as() == 0) { + // 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); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::PowerSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::PowerInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -130,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 6178f5330..ad95aacbf 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_prometheus.h" #include "Configuration.h" @@ -9,18 +9,13 @@ #include "NetworkSettings.h" #include "WebApi.h" #include +#include "__compiled_constants.h" -void WebApiPrometheusClass::init(AsyncWebServer* server) +void WebApiPrometheusClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); -} - -void WebApiPrometheusClass::loop() -{ + server.on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); } void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* request) @@ -35,7 +30,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_build Build info\n"); stream->print("# TYPE opendtu_build gauge\n"); stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", - NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); + NetworkSettings.getHostname().c_str(), __COMPILED_GIT_HASH__, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); stream->print("# HELP opendtu_platform Platform info\n"); stream->print("# TYPE opendtu_platform gauge\n"); @@ -53,6 +48,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# TYPE opendtu_free_heap_size gauge\n"); stream->printf("opendtu_free_heap_size %zu\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->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->print("# HELP wifi_rssi WiFi RSSI\n"); stream->print("# TYPE wifi_rssi gauge\n"); stream->printf("wifi_rssi %d\n", WiFi.RSSI()); @@ -73,13 +76,29 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\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", + serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0); + + if (inv->DevInfo()->getMaxPower() > 0) { + if (i == 0) { + 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", + serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0); + } + // Loop all channels if Statistics have been updated at least once since DTU boot if (inv->Statistics()->getLastUpdate() > 0) { for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& c : inv->Statistics()->getChannelsByType(t)) { addPanelInfo(stream, serial, i, inv, t, c); for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(_publishFields[0]); f++) { - if (t == TYPE_AC && _publishFields[f].field == FLD_PDC) { + if (t == TYPE_INV && _publishFields[f].field == FLD_PDC) { addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type], "PowerDC"); } else { addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type]); @@ -99,10 +118,10 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques } } -void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName) +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, const char* metricName, const char* channelName) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { - const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; + const char* chanName = (channelName == nullptr) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; if (idx == 0 && type == TYPE_AC && channel == 0) { stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); @@ -118,13 +137,13 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial } } -void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel) +void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel) { if (type != TYPE_DC) { return; } - const CONFIG_T& config = Configuration.get(); + const auto& config = Configuration.getInverterConfig(inv->serial()); const bool printHelp = (idx == 0 && channel == 0); if (printHelp) { @@ -136,7 +155,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se idx, inv->name(), channel, - config.Inverter[idx].channel[channel].Name); + config->channel[channel].Name); if (printHelp) { stream->print("# HELP opendtu_MaxPower panel maximum output power\n"); @@ -147,7 +166,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se idx, inv->name(), channel, - config.Inverter[idx].channel[channel].MaxChannelPower); + config->channel[channel].MaxChannelPower); if (printHelp) { stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"); @@ -158,5 +177,5 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se idx, inv->name(), channel, - config.Inverter[idx].channel[channel].YieldTotalOffset); + config->channel[channel].YieldTotalOffset); } diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index a2221f9b6..eb0f27d20 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_security.h" #include "Configuration.h" @@ -9,19 +9,13 @@ #include "helper.h" #include -void WebApiSecurityClass::init(AsyncWebServer* server) +void WebApiSecurityClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); - _server->on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); - _server->on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); -} - -void WebApiSecurityClass::loop() -{ + server.on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); + server.on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); + server.on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); } void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) @@ -31,14 +25,13 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); - root["password"] = config.Security_Password; - root["allow_readonly"] = config.Security_AllowReadonly; + root["password"] = config.Security.Password; + root["allow_readonly"] = config.Security.AllowReadonly; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) @@ -48,44 +41,18 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!root.containsKey("password") && root.containsKey("allow_readonly")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -93,22 +60,17 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; retMsg["code"] = WebApiError::SecurityPasswordLength; retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); 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(); - Configuration.write(); + strlcpy(config.Security.Password, root["password"].as().c_str(), sizeof(config.Security.Password)); + config.Security.AllowReadonly = root["allow_readonly"].as(); - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) @@ -118,11 +80,10 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); + auto& retMsg = response->getRoot(); retMsg["type"] = "success"; retMsg["message"] = "Authentication successful!"; retMsg["code"] = WebApiError::SecurityAuthSuccess; - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index fbb392d9f..646922a60 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -1,32 +1,24 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_sysstatus.h" #include "Configuration.h" #include "NetworkSettings.h" #include "PinMapping.h" #include "WebApi.h" +#include "__compiled_constants.h" #include +#include #include #include #include -#ifndef AUTO_GIT_HASH -#define AUTO_GIT_HASH "" -#endif - -void WebApiSysstatusClass::init(AsyncWebServer* server) +void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = server; - - _server->on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); -} - -void WebApiSysstatusClass::loop() -{ + server.on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); } void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) @@ -36,15 +28,20 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); + auto& root = response->getRoot(); root["hostname"] = NetworkSettings.getHostname(); root["sdkversion"] = ESP.getSdkVersion(); root["cpufreq"] = ESP.getCpuFreqMHz(); + root["cputemp"] = CpuTemperature.read(); root["heap_total"] = ESP.getHeapSize(); root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap(); + root["heap_max_block"] = ESP.getMaxAllocHeap(); + root["heap_min_free"] = ESP.getMinFreeHeap(); + root["psram_total"] = ESP.getPsramSize(); + root["psram_used"] = ESP.getPsramSize() - ESP.getFreePsram(); root["sketch_total"] = ESP.getFreeSketchSpace(); root["sketch_used"] = ESP.getSketchSize(); root["littlefs_total"] = LittleFS.totalBytes(); @@ -53,20 +50,21 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["chiprevision"] = ESP.getChipRevision(); root["chipmodel"] = ESP.getChipModel(); root["chipcores"] = ESP.getChipCores(); + root["flashsize"] = ESP.getFlashChipSize(); String reason; - reason = ResetReason.get_reset_reason_verbose(0); + reason = ResetReason::get_reset_reason_verbose(0); root["resetreason_0"] = reason; - reason = ResetReason.get_reset_reason_verbose(1); + reason = ResetReason::get_reset_reason_verbose(1); root["resetreason_1"] = reason; - root["cfgsavecount"] = Configuration.get().Cfg_SaveCount; + root["cfgsavecount"] = Configuration.get().Cfg.SaveCount; char version[16]; snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); root["config_version"] = version; - root["git_hash"] = AUTO_GIT_HASH; + root["git_hash"] = __COMPILED_GIT_HASH__; root["pioenv"] = PIOENV; root["uptime"] = esp_timer_get_time() / 1000000; @@ -78,6 +76,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["cmt_configured"] = PinMapping.isValidCmt2300Config(); root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); - response->setLength(); - request->send(response); -} \ No newline at end of file + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_webapp.cpp b/src/WebApi_webapp.cpp index 90516ad6c..b8b813853 100644 --- a/src/WebApi_webapp.cpp +++ b/src/WebApi_webapp.cpp @@ -1,89 +1,96 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_webapp.h" +#include extern const uint8_t file_index_html_start[] asm("_binary_webapp_dist_index_html_gz_start"); extern const uint8_t file_favicon_ico_start[] asm("_binary_webapp_dist_favicon_ico_start"); extern const uint8_t file_favicon_png_start[] asm("_binary_webapp_dist_favicon_png_start"); extern const uint8_t file_zones_json_start[] asm("_binary_webapp_dist_zones_json_gz_start"); extern const uint8_t file_app_js_start[] asm("_binary_webapp_dist_js_app_js_gz_start"); +extern const uint8_t file_site_webmanifest_start[] asm("_binary_webapp_dist_site_webmanifest_start"); extern const uint8_t file_index_html_end[] asm("_binary_webapp_dist_index_html_gz_end"); extern const uint8_t file_favicon_ico_end[] asm("_binary_webapp_dist_favicon_ico_end"); extern const uint8_t file_favicon_png_end[] asm("_binary_webapp_dist_favicon_png_end"); extern const uint8_t file_zones_json_end[] asm("_binary_webapp_dist_zones_json_gz_end"); extern const uint8_t file_app_js_end[] asm("_binary_webapp_dist_js_app_js_gz_end"); +extern const uint8_t file_site_webmanifest_end[] asm("_binary_webapp_dist_site_webmanifest_end"); -void WebApiWebappClass::init(AsyncWebServer* server) +void WebApiWebappClass::responseBinaryDataWithETagCache(AsyncWebServerRequest *request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len) { - _server = server; + auto md5 = MD5Builder(); + md5.begin(); + md5.add(const_cast(content), len); + md5.calculate(); - _server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + String expectedEtag; + expectedEtag = "\""; + expectedEtag += md5.toString(); + 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_P(200, contentType, content, len); + if (contentEncoding.length() > 0) { + response->addHeader("Content-Encoding", contentEncoding); + } + } + + // 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); +} + +void WebApiWebappClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + /* + We don't validate the request header "Accept-Encoding" if gzip compression is supported! + We just have the gzipped data available - so we ship them! + */ + + server.on("/", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->onNotFound([](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.onNotFound([&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->on("/index.html", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.on("/index.html", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "image/x-icon", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); - request->send(response); + server.on("/favicon.ico", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "image/x-icon", "", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); }); - _server->on("/favicon.png", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "image/png", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start); - request->send(response); + server.on("/favicon.png", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "image/png", "", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start); }); - _server->on("/zones.json", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_zones_json_start, file_zones_json_end - file_zones_json_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.on("/zones.json", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "application/json", "gzip", file_zones_json_start, file_zones_json_end - file_zones_json_start); }); - _server->on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) { -#ifdef AUTO_GIT_HASH - // check client If-None-Match header vs ETag/AUTO_GIT_HASH - bool eTagMatch = false; - if (request->hasHeader("If-None-Match")) { - AsyncWebHeader* h = request->getHeader("If-None-Match"); - if (strncmp(AUTO_GIT_HASH, h->value().c_str(), strlen(AUTO_GIT_HASH)) == 0) { - eTagMatch = true; - } - } + server.on("/site.webmanifest", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "application/json", "", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start); + }); - // begin response 200 or 304 - AsyncWebServerResponse* response; - if (eTagMatch) { - response = request->beginResponse(304); - } else { - response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); - response->addHeader("Content-Encoding", "gzip"); - } - // HTTP requires cache headers in 200 and 304 to be identical - response->addHeader("Cache-Control", "public, must-revalidate"); - response->addHeader("ETag", AUTO_GIT_HASH); -#else - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); - response->addHeader("Content-Encoding", "gzip"); -#endif - request->send(response); + server.on("/js/app.js", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/javascript", "gzip", file_app_js_start, file_app_js_end - file_app_js_start); }); } - -void WebApiWebappClass::loop() -{ -} \ No newline at end of file diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp index 2837fc39d..1f1efcb20 100644 --- a/src/WebApi_ws_console.cpp +++ b/src/WebApi_ws_console.cpp @@ -1,37 +1,36 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "WebApi_ws_console.h" -#include "Configuration.h" -#include "MessageOutput.h" -#include "WebApi.h" -#include "defaults.h" - -WebApiWsConsoleClass::WebApiWsConsoleClass() - : _ws("/console") -{ -} - -void WebApiWsConsoleClass::init(AsyncWebServer* server) -{ - _server = server; - _server->addHandler(&_ws); - MessageOutput.register_ws_output(&_ws); -} - -void WebApiWsConsoleClass::loop() -{ - // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients - if (millis() - _lastWsCleanup > 1000) { - _ws.cleanupClients(); - - if (Configuration.get().Security_AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); - } - - _lastWsCleanup = millis(); - } -} \ No newline at end of file +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_ws_console.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "defaults.h" + +WebApiWsConsoleClass::WebApiWsConsoleClass() + : _ws("/console") + , _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsConsoleClass::wsCleanupTaskCb, this)) +{ +} + +void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + server.addHandler(&_ws); + MessageOutput.register_ws_output(&_ws); + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.enable(); +} + +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 42f79b5a3..f1d3b2662 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -1,21 +1,23 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_ws_live.h" -#include "Configuration.h" #include "Datastore.h" #include "MessageOutput.h" +#include "Utils.h" #include "WebApi.h" #include "defaults.h" #include WebApiWsLiveClass::WebApiWsLiveClass() : _ws("/livedata") + , _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::wsCleanupTaskCb, this)) + , _sendDataTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::sendDataTaskCb, this)) { } -void WebApiWsLiveClass::init(AsyncWebServer* server) +void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; using std::placeholders::_2; @@ -24,162 +26,123 @@ void WebApiWsLiveClass::init(AsyncWebServer* server) using std::placeholders::_5; using std::placeholders::_6; +<<<<<<< HEAD _server = server; _server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); _server->on("/api/livedata/totals", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataTotals, this, _1)); +======= + server.on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 - _server->addHandler(&_ws); + server.addHandler(&_ws); _ws.onEvent(std::bind(&WebApiWsLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.enable(); + + scheduler.addTask(_sendDataTask); + _sendDataTask.enable(); } -void WebApiWsLiveClass::loop() +void WebApiWsLiveClass::wsCleanupTaskCb() { // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients - if (millis() - _lastWsCleanup > 1000) { - _ws.cleanupClients(); - _lastWsCleanup = millis(); + _ws.cleanupClients(); + + if (Configuration.get().Security.AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); } +} +void WebApiWsLiveClass::sendDataTaskCb() +{ // do nothing if no WS client is connected if (_ws.count() == 0) { return; } - if (millis() - _lastInvUpdateCheck < 1000) { - return; - } - _lastInvUpdateCheck = millis(); - - uint32_t maxTimeStamp = 0; + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } - if (inv->Statistics()->getLastUpdate() > maxTimeStamp) { - maxTimeStamp = inv->Statistics()->getLastUpdate(); + const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (!((lastUpdateInternal > 0 && lastUpdateInternal > _lastPublishStats[i]) || (millis() - _lastPublishStats[i] > (10 * 1000)))) { + continue; } - } - // Update on every inverter change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { + _lastPublishStats[i] = millis(); try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(4096 * INV_MAX_COUNT); + JsonDocument root; JsonVariant var = root; - generateJsonResponse(var); - String buffer; - if (buffer) { - serializeJson(root, buffer); + auto invArray = var["inverters"].to(); + auto invObject = invArray.add(); - if (Configuration.get().Security_AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); - } + generateCommonJsonResponse(var); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); - _ws.textAll(buffer); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + continue; } + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); + } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } catch (const std::exception& exc) { MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what()); } - - _lastWsPublish = millis(); } } -void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) { - JsonArray invArray = root.createNestedArray("inverters"); - - // Loop all inverters - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - if (inv == nullptr) { - continue; - } - - JsonObject invObject = invArray.createNestedObject(); - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg == nullptr) { - continue; - } - - invObject["serial"] = inv->serialString(); - invObject["name"] = inv->name(); - invObject["order"] = inv_cfg->Order; - invObject["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; - invObject["poll_enabled"] = inv->getEnablePolling(); - invObject["reachable"] = inv->isReachable(); - invObject["producing"] = inv->isProducing(); - invObject["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); - if (inv->DevInfo()->getMaxPower() > 0) { - invObject["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; - } else { - invObject["limit_absolute"] = -1; - } - - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - JsonObject chanTypeObj = invObject.createNestedObject(inv->Statistics()->getChannelTypeName(t)); - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - if (t == TYPE_DC) { - chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; - } - addField(chanTypeObj, i, inv, t, c, FLD_PAC); - addField(chanTypeObj, i, inv, t, c, FLD_UAC); - addField(chanTypeObj, i, inv, t, c, FLD_IAC); - if (t == TYPE_AC) { - addField(chanTypeObj, i, inv, t, c, FLD_PDC, "Power DC"); - } else { - addField(chanTypeObj, i, inv, t, c, FLD_PDC); - } - addField(chanTypeObj, i, inv, t, c, FLD_UDC); - addField(chanTypeObj, i, inv, t, c, FLD_IDC); - addField(chanTypeObj, i, inv, t, c, FLD_YD); - addField(chanTypeObj, i, inv, t, c, FLD_YT); - addField(chanTypeObj, i, inv, t, c, FLD_F); - addField(chanTypeObj, i, inv, t, c, FLD_T); - addField(chanTypeObj, i, inv, t, c, FLD_PF); - addField(chanTypeObj, i, inv, t, c, FLD_Q); - addField(chanTypeObj, i, inv, t, c, FLD_EFF); - if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { - addField(chanTypeObj, i, inv, t, c, FLD_IRR); - chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); - } - } - } - - if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { - invObject["events"] = inv->EventLog()->getEntryCount(); - } else { - invObject["events"] = -1; - } - - if (inv->Statistics()->getLastUpdate() > _newestInverterTimestamp) { - _newestInverterTimestamp = inv->Statistics()->getLastUpdate(); - } - } - - JsonObject totalObj = root.createNestedObject("total"); + auto totalObj = root["total"].to(); addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits()); - JsonObject hintObj = root.createNestedObject("hints"); + JsonObject hintObj = root["hints"].to(); struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); - if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) { - hintObj["default_password"] = true; + hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; +} + +void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + root["serial"] = inv->serialString(); + root["name"] = inv->name(); + root["order"] = inv_cfg->Order; + root["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; + root["poll_enabled"] = inv->getEnablePolling(); + root["reachable"] = inv->isReachable(); + root["producing"] = inv->isProducing(); + root["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); + if (inv->DevInfo()->getMaxPower() > 0) { + root["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; } else { - hintObj["default_password"] = false; + root["limit_absolute"] = -1; } } +<<<<<<< HEAD void WebApiWsLiveClass::generateJsonResponseTotals(JsonVariant& root) { JsonObject totalObj = root.createNestedObject("total"); @@ -190,6 +153,54 @@ void WebApiWsLiveClass::generateJsonResponseTotals(JsonVariant& root) } void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic) +======= +void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + // Loop all channels + for (auto& t : inv->Statistics()->getChannelTypes()) { + auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to(); + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; + } + addField(chanTypeObj, inv, t, c, FLD_PAC); + addField(chanTypeObj, inv, t, c, FLD_UAC); + addField(chanTypeObj, inv, t, c, FLD_IAC); + if (t == TYPE_INV) { + addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC"); + } else { + addField(chanTypeObj, inv, t, c, FLD_PDC); + } + addField(chanTypeObj, inv, t, c, FLD_UDC); + addField(chanTypeObj, inv, t, c, FLD_IDC); + addField(chanTypeObj, inv, t, c, FLD_YD); + addField(chanTypeObj, inv, t, c, FLD_YT); + addField(chanTypeObj, inv, t, c, FLD_F); + addField(chanTypeObj, inv, t, c, FLD_T); + addField(chanTypeObj, inv, t, c, FLD_PF); + addField(chanTypeObj, inv, t, c, FLD_Q); + addField(chanTypeObj, inv, t, c, FLD_EFF); + if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { + addField(chanTypeObj, inv, t, c, FLD_IRR); + chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); + } + } + } + + if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { + root["events"] = inv->EventLog()->getEntryCount(); + } else { + root["events"] = -1; + } +} + +void WebApiWsLiveClass::addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic) +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { String chanName; @@ -206,7 +217,7 @@ void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr< } } -void WebApiWsLiveClass::addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits) +void WebApiWsLiveClass::addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits) { root[name]["v"] = value; root[name]["u"] = unit; @@ -230,13 +241,34 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096 * INV_MAX_COUNT); - JsonVariant root = response->getRoot(); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + auto invArray = root["inverters"].to(); + auto serial = WebApi.parseSerialFromRequest(request); + + if (serial > 0) { + auto inv = Hoymiles.getInverterBySerial(serial); + if (inv != nullptr) { + JsonObject invObject = invArray.add(); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); + } + } else { + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } - generateJsonResponse(root); + JsonObject invObject = invArray.add(); + generateInverterCommonJsonResponse(invObject, inv); + } + } - response->setLength(); - request->send(response); + generateCommonJsonResponse(root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); @@ -246,6 +278,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) WebApi.sendTooManyRequests(request); } } +<<<<<<< HEAD void WebApiWsLiveClass::onLivedataTotals(AsyncWebServerRequest* request) { @@ -267,4 +300,6 @@ void WebApiWsLiveClass::onLivedataTotals(AsyncWebServerRequest* request) WebApi.sendTooManyRequests(request); } -} \ No newline at end of file +} +======= +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 diff --git a/src/main.cpp b/src/main.cpp index 8d86c5eee..123bebcf4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +<<<<<<< HEAD // SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) 2022 Thomas Basler and others @@ -197,4 +198,168 @@ void loop() yield(); WatchDogDtu.loop(); yield(); -} \ No newline at end of file +} +======= +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "Configuration.h" +#include "Datastore.h" +#include "Display_Graphic.h" +#include "InverterSettings.h" +#include "Led_Single.h" +#include "MessageOutput.h" +#include "MqttHandleDtu.h" +#include "MqttHandleHass.h" +#include "MqttHandleInverter.h" +#include "MqttHandleInverterTotal.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include "NtpSettings.h" +#include "PinMapping.h" +#include "Scheduler.h" +#include "SunPosition.h" +#include "Utils.h" +#include "WebApi.h" +#include "defaults.h" +#include +#include +#include +#include + +void setup() +{ + // Move all dynamic allocations >512byte to psram (if available) + heap_caps_malloc_extmem_enable(512); + + // Initialize serial output + Serial.begin(SERIAL_BAUDRATE); +#if ARDUINO_USB_CDC_ON_BOOT + Serial.setTxTimeoutMs(0); + delay(100); +#else + while (!Serial) + yield(); +#endif + MessageOutput.init(scheduler); + MessageOutput.println(); + MessageOutput.println("Starting OpenDTU"); + + // Initialize file system + MessageOutput.print("Initialize FS... "); + if (!LittleFS.begin(false)) { // Do not format if mount failed + MessageOutput.print("failed... trying to format..."); + if (!LittleFS.begin(true)) { + MessageOutput.print("success"); + } else { + MessageOutput.print("failed"); + } + } else { + MessageOutput.println("done"); + } + + // Read configuration values + MessageOutput.print("Reading configuration... "); + if (!Configuration.read()) { + MessageOutput.print("initializing... "); + Configuration.init(); + if (Configuration.write()) { + MessageOutput.print("written... "); + } else { + MessageOutput.print("failed... "); + } + } + if (Configuration.get().Cfg.Version != CONFIG_VERSION) { + MessageOutput.print("migrated... "); + Configuration.migrate(); + } + auto& config = Configuration.get(); + MessageOutput.println("done"); + + // Load PinMapping + MessageOutput.print("Reading PinMapping... "); + if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { + MessageOutput.print("found valid mapping "); + } else { + MessageOutput.print("using default config "); + } + const auto& pin = PinMapping.get(); + MessageOutput.println("done"); + + // Initialize WiFi + MessageOutput.print("Initialize Network... "); + NetworkSettings.init(scheduler); + MessageOutput.println("done"); + NetworkSettings.applyConfig(); + + // Initialize NTP + MessageOutput.print("Initialize NTP... "); + NtpSettings.init(); + MessageOutput.println("done"); + + // Initialize SunPosition + MessageOutput.print("Initialize SunPosition... "); + SunPosition.init(scheduler); + MessageOutput.println("done"); + + // Initialize MqTT + MessageOutput.print("Initialize MqTT... "); + MqttSettings.init(); + MqttHandleDtu.init(scheduler); + MqttHandleInverter.init(scheduler); + MqttHandleInverterTotal.init(scheduler); + MqttHandleHass.init(scheduler); + MessageOutput.println("done"); + + // Initialize WebApi + MessageOutput.print("Initialize WebApi... "); + WebApi.init(scheduler); + MessageOutput.println("done"); + + // Initialize Display + MessageOutput.print("Initialize Display... "); + Display.init( + scheduler, + static_cast(pin.display_type), + pin.display_data, + pin.display_clk, + pin.display_cs, + pin.display_reset); + Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); + Display.setOrientation(config.Display.Rotation); + Display.enablePowerSafe = config.Display.PowerSafe; + Display.enableScreensaver = config.Display.ScreenSaver; + Display.setContrast(config.Display.Contrast); + Display.setLanguage(config.Display.Language); + Display.setStartupDisplay(); + MessageOutput.println("done"); + + // Initialize Single LEDs + MessageOutput.print("Initialize LEDs... "); + LedSingle.init(scheduler); + MessageOutput.println("done"); + + // Check for default DTU serial + MessageOutput.print("Check for default DTU serial... "); + if (config.Dtu.Serial == DTU_SERIAL) { + MessageOutput.print("generate serial based on ESP chip id: "); + const uint64_t dtuId = Utils::generateDtuSerial(); + MessageOutput.printf("%0x%08x... ", + ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), + ((uint32_t)(dtuId & 0xFFFFFFFF))); + config.Dtu.Serial = dtuId; + Configuration.write(); + } + MessageOutput.println("done"); + + InverterSettings.init(scheduler); + + Datastore.init(scheduler); +} + +void loop() +{ + scheduler.execute(); +} +>>>>>>> e541a885f51dcd3b88195ff4fa01a0f413889807 diff --git a/webapp/.eslintrc.cjs b/webapp/.eslintrc.cjs deleted file mode 100644 index ade85716e..000000000 --- a/webapp/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - 'extends': [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/eslint-config-typescript' - ], - parserOptions: { - ecmaVersion: 'latest' - } -} diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js new file mode 100644 index 000000000..9a2aaecb7 --- /dev/null +++ b/webapp/eslint.config.js @@ -0,0 +1,36 @@ +/* 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, +}); + +export default [ + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + ...compat.extends("@vue/eslint-config-typescript/recommended"), + { + files: [ + "**/*.vue", + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs", + "**/*.ts", + "**/*.tsx", + "**/*.cts", + "**/*.mts", + ], + languageOptions: { + ecmaVersion: 2022 + }, + } + ] diff --git a/webapp/index.html b/webapp/index.html index 39a94a039..2d84d6f53 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -5,6 +5,7 @@ + OpenDTU diff --git a/webapp/package.json b/webapp/package.json index c781b53ea..cadcee66d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,39 +9,40 @@ "preview": "vite preview --port 4173", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint ." }, "dependencies": { "@popperjs/core": "^2.11.8", - "bootstrap": "^5.3.2", - "bootstrap-icons-vue": "^1.11.1", + "bootstrap": "^5.3.3", + "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", - "sortablejs": "^1.15.0", + "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.3.8", - "vue-i18n": "^9.7.0", - "vue-router": "^4.2.5" + "vue": "^3.4.31", + "vue-i18n": "^9.13.1", + "vue-router": "^4.4.0" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^1.5.0", - "@rushstack/eslint-patch": "^1.5.1", - "@tsconfig/node18": "^18.2.2", - "@types/bootstrap": "^5.2.9", - "@types/node": "^20.9.0", - "@types/sortablejs": "^1.15.5", + "@intlify/unplugin-vue-i18n": "^4.0.0", + "@tsconfig/node18": "^18.2.4", + "@types/bootstrap": "^5.2.10", + "@types/node": "^20.14.9", + "@types/pulltorefreshjs": "^0.1.7", + "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^4.5.0", - "@vue/eslint-config-typescript": "^12.0.0", - "@vue/tsconfig": "^0.4.0", - "eslint": "^8.53.0", - "eslint-plugin-vue": "^9.18.1", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^9.6.0", + "eslint-plugin-vue": "^9.26.0", "npm-run-all": "^4.1.5", - "sass": "^1.69.5", - "terser": "^5.24.0", - "typescript": "^5.2.2", - "vite": "^5.0.0", + "pulltorefreshjs": "^0.1.22", + "sass": "^1.77.6", + "terser": "^5.31.1", + "typescript": "^5.5.2", + "vite": "^5.3.2", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.3.0", - "vue-tsc": "^1.8.22" + "vite-plugin-css-injected-by-js": "^3.5.1", + "vue-tsc": "^2.0.22" } } diff --git a/webapp/public/favicon.png b/webapp/public/favicon.png index 3378b6613..278aac84f 100644 Binary files a/webapp/public/favicon.png and b/webapp/public/favicon.png differ diff --git a/webapp/public/site.webmanifest b/webapp/public/site.webmanifest new file mode 100644 index 000000000..3be246091 --- /dev/null +++ b/webapp/public/site.webmanifest @@ -0,0 +1,13 @@ +{ + "name": "OpenDTU", + "short_name": "OpenDTU", + "display": "standalone", + "orientation": "portrait", + "icons": [ + { + "src": "/favicon.png", + "sizes": "144x144", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/webapp/public/zones.json b/webapp/public/zones.json index 28bfa0dcf..ad90ea014 100644 --- a/webapp/public/zones.json +++ b/webapp/public/zones.json @@ -11,7 +11,7 @@ "Africa/Blantyre":"CAT-2", "Africa/Brazzaville":"WAT-1", "Africa/Bujumbura":"CAT-2", -"Africa/Cairo":"EET-2", +"Africa/Cairo":"EET-2EEST,M4.5.5/0,M10.5.4/24", "Africa/Casablanca":"<+01>-1", "Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3", "Africa/Conakry":"GMT0", @@ -72,7 +72,7 @@ "America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0", "America/Atikokan":"EST5", "America/Bahia":"<-03>3", -"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0", +"America/Bahia_Banderas":"CST6", "America/Barbados":"AST4", "America/Belem":"<-03>3", "America/Belize":"CST6", @@ -87,7 +87,7 @@ "America/Cayenne":"<-03>3", "America/Cayman":"EST5", "America/Chicago":"CST6CDT,M3.2.0,M11.1.0", -"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0", +"America/Chihuahua":"CST6", "America/Costa_Rica":"CST6", "America/Creston":"MST7", "America/Cuiaba":"<-04>4", @@ -104,7 +104,7 @@ "America/Fort_Nelson":"MST7", "America/Fortaleza":"<-03>3", "America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0", -"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1", +"America/Godthab":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0", "America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0", "America/Grenada":"AST4", @@ -140,14 +140,14 @@ "America/Marigot":"AST4", "America/Martinique":"AST4", "America/Matamoros":"CST6CDT,M3.2.0,M11.1.0", -"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0", +"America/Mazatlan":"MST7", "America/Menominee":"CST6CDT,M3.2.0,M11.1.0", -"America/Merida":"CST6CDT,M4.1.0,M10.5.0", +"America/Merida":"CST6", "America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0", -"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0", +"America/Mexico_City":"CST6", "America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0", "America/Moncton":"AST4ADT,M3.2.0,M11.1.0", -"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0", +"America/Monterrey":"CST6", "America/Montevideo":"<-03>3", "America/Montreal":"EST5EDT,M3.2.0,M11.1.0", "America/Montserrat":"AST4", @@ -159,8 +159,8 @@ "America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0", "America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0", "America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0", -"America/Nuuk":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1", -"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0", +"America/Nuuk":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", +"America/Ojinaga":"CST6CDT,M3.2.0,M11.1.0", "America/Panama":"EST5", "America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0", "America/Paramaribo":"<-03>3", @@ -180,7 +180,7 @@ "America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24", "America/Santo_Domingo":"AST4", "America/Sao_Paulo":"<-03>3", -"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1", +"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0", "America/St_Barthelemy":"AST4", "America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0", @@ -200,7 +200,7 @@ "America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0", "America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0", "America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0", -"Antarctica/Casey":"<+11>-11", +"Antarctica/Casey":"<+08>-8", "Antarctica/Davis":"<+07>-7", "Antarctica/DumontDUrville":"<+10>-10", "Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3", @@ -210,11 +210,11 @@ "Antarctica/Rothera":"<-03>3", "Antarctica/Syowa":"<+03>-3", "Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3", -"Antarctica/Vostok":"<+06>-6", +"Antarctica/Vostok":"<+05>-5", "Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3", "Asia/Aden":"<+03>-3", -"Asia/Almaty":"<+06>-6", -"Asia/Amman":"EET-2EEST,M2.5.4/24,M10.5.5/1", +"Asia/Almaty":"<+05>-5", +"Asia/Amman":"<+03>-3", "Asia/Anadyr":"<+12>-12", "Asia/Aqtau":"<+05>-5", "Asia/Aqtobe":"<+05>-5", @@ -231,14 +231,14 @@ "Asia/Chita":"<+09>-9", "Asia/Choibalsan":"<+08>-8", "Asia/Colombo":"<+0530>-5:30", -"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0", +"Asia/Damascus":"<+03>-3", "Asia/Dhaka":"<+06>-6", "Asia/Dili":"<+09>-9", "Asia/Dubai":"<+04>-4", "Asia/Dushanbe":"<+05>-5", "Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Asia/Gaza":"EET-2EEST,M3.4.4/48,M10.5.5/1", -"Asia/Hebron":"EET-2EEST,M3.4.4/48,M10.5.5/1", +"Asia/Gaza":"EET-2EEST,M3.4.4/50,M10.4.4/50", +"Asia/Hebron":"EET-2EEST,M3.4.4/50,M10.4.4/50", "Asia/Ho_Chi_Minh":"<+07>-7", "Asia/Hong_Kong":"HKT-8", "Asia/Hovd":"<+07>-7", @@ -281,7 +281,7 @@ "Asia/Taipei":"CST-8", "Asia/Tashkent":"<+05>-5", "Asia/Tbilisi":"<+04>-4", -"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24", +"Asia/Tehran":"<+0330>-3:30", "Asia/Thimphu":"<+06>-6", "Asia/Tokyo":"JST-9", "Asia/Tomsk":"<+07>-7", @@ -373,7 +373,7 @@ "Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0", "Europe/Kaliningrad":"EET-2", "Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Europe/Kirov":"<+03>-3", +"Europe/Kirov":"MSK-3", "Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0", "Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/London":"GMT0BST,M3.5.0/1,M10.5.0", @@ -406,7 +406,7 @@ "Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4", -"Europe/Volgograd":"<+03>-3", +"Europe/Volgograd":"MSK-3", "Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3", "Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4", @@ -431,7 +431,7 @@ "Pacific/Efate":"<+11>-11", "Pacific/Enderbury":"<+13>-13", "Pacific/Fakaofo":"<+13>-13", -"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99", +"Pacific/Fiji":"<+12>-12", "Pacific/Funafuti":"<+12>-12", "Pacific/Galapagos":"<-06>6", "Pacific/Gambier":"<-09>9", diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 8ace89a17..6fa6eeaa5 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -1,8 +1,6 @@ \ No newline at end of file + + + diff --git a/webapp/src/components/BootstrapAlert.vue b/webapp/src/components/BootstrapAlert.vue index df96fb620..a629863db 100644 --- a/webapp/src/components/BootstrapAlert.vue +++ b/webapp/src/components/BootstrapAlert.vue @@ -52,7 +52,7 @@ export default defineComponent({ _countDownTimeout = undefined; }; - var countDown = ref(); + const countDown = ref(); watch(() => props.modelValue, () => { countDown.value = parseCountDown(props.modelValue); }); @@ -116,4 +116,4 @@ export default defineComponent({ }; }, }); - \ No newline at end of file + diff --git a/webapp/src/components/CardElement.vue b/webapp/src/components/CardElement.vue index 9605858c1..20bce75fd 100644 --- a/webapp/src/components/CardElement.vue +++ b/webapp/src/components/CardElement.vue @@ -1,7 +1,7 @@ \ No newline at end of file + diff --git a/webapp/src/components/HardwareInfo.vue b/webapp/src/components/HardwareInfo.vue index 34672132c..372eeff0d 100644 --- a/webapp/src/components/HardwareInfo.vue +++ b/webapp/src/components/HardwareInfo.vue @@ -19,6 +19,17 @@ {{ $t('hardwareinfo.CpuFrequency') }} {{ systemStatus.cpufreq }} {{ $t('hardwareinfo.Mhz') }} + + {{ $t('hardwareinfo.CpuTemperature') }} + {{ $n(systemStatus.cputemp, 'celsius') }} + + + {{ $t('hardwareinfo.FlashSize') }} + + {{ $n(systemStatus.flashsize, 'byte') }} + ({{ $n(systemStatus.flashsize / 1024 / 1024, 'megabyte') }}) + + diff --git a/webapp/src/components/HeapDetails.vue b/webapp/src/components/HeapDetails.vue new file mode 100644 index 000000000..04c589319 --- /dev/null +++ b/webapp/src/components/HeapDetails.vue @@ -0,0 +1,55 @@ + + + diff --git a/webapp/src/components/InputElement.vue b/webapp/src/components/InputElement.vue index eff8e9f66..f12a11720 100644 --- a/webapp/src/components/InputElement.vue +++ b/webapp/src/components/InputElement.vue @@ -83,10 +83,12 @@ export default defineComponent({ }, computed: { model: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any get(): any { if (this.type === 'checkbox') return !!this.modelValue; return this.modelValue; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any set(value: any) { this.$emit('update:modelValue', value); }, @@ -112,4 +114,4 @@ export default defineComponent({ } }, }); - \ No newline at end of file + diff --git a/webapp/src/components/InputSerial.vue b/webapp/src/components/InputSerial.vue new file mode 100644 index 000000000..3669da622 --- /dev/null +++ b/webapp/src/components/InputSerial.vue @@ -0,0 +1,116 @@ + + + diff --git a/webapp/src/components/InverterChannelInfo.vue b/webapp/src/components/InverterChannelInfo.vue index c41829ad0..7f7a2316b 100644 --- a/webapp/src/components/InverterChannelInfo.vue +++ b/webapp/src/components/InverterChannelInfo.vue @@ -2,7 +2,7 @@
+ }" style="overflow: hidden">
{{ $t('inverterchannelinfo.General') }}
@@ -16,32 +16,24 @@ {{ $t('inverterchannelinfo.Phase', { num: channelNumber + 1 }) }}
-
-
- - - - - - - - - - - - - -
{{ $t('inverterchannelinfo.Property') }}{{ $t('inverterchannelinfo.Value') }}{{ $t('inverterchannelinfo.Unit') }}
-
+
+ + + + + + +
diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index 5952c24cd..6b5b83f5e 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -1,57 +1,52 @@ \ No newline at end of file + diff --git a/webapp/src/components/MemoryInfo.vue b/webapp/src/components/MemoryInfo.vue index 132d63ca1..b9153dac6 100644 --- a/webapp/src/components/MemoryInfo.vue +++ b/webapp/src/components/MemoryInfo.vue @@ -14,6 +14,8 @@ + + + + + diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 79328fd76..e6eb58f27 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -1,24 +1,23 @@ \ No newline at end of file + diff --git a/webapp/src/views/ConsoleInfoView.vue b/webapp/src/views/ConsoleInfoView.vue index fb17f62de..eba1d533f 100644 --- a/webapp/src/views/ConsoleInfoView.vue +++ b/webapp/src/views/ConsoleInfoView.vue @@ -1,154 +1,154 @@ - - - - - \ No newline at end of file + + + + + diff --git a/webapp/src/views/DeviceAdminView.vue b/webapp/src/views/DeviceAdminView.vue index 2af0e50e4..82b461ab2 100644 --- a/webapp/src/views/DeviceAdminView.vue +++ b/webapp/src/views/DeviceAdminView.vue @@ -13,6 +13,8 @@ }} + +
+
+
+ +
+
+ @@ -56,6 +67,24 @@ v-model="deviceConfigList.display.screensaver" type="checkbox" :tooltip="$t('deviceadmin.ScreensaverHint')" /> +
+ +
+ +
+
+ + +
+ + - + @@ -107,9 +159,10 @@ \ No newline at end of file + diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 1a5fc079d..7d882908c 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -47,6 +47,20 @@ +
+ +
+ +
+
+
- + @@ -75,6 +89,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { DtuConfig } from "@/types/DtuConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -86,6 +101,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, BIconInfoCircle, }, @@ -109,13 +125,30 @@ export default defineComponent({ }, computed: { cmtFrequencyText() { - return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") }); + return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000000, "decimalTwoDigits") }); }, cmtPaLevelText() { return this.$t("dtuadmin.dBm", { dbm: this.$n(this.dtuConfigList.cmt_palevel * 1) }); }, - cmtIsOutOfEu() { - return this.dtuConfigList.cmt_frequency < 863000 || this.dtuConfigList.cmt_frequency > 870000; + cmtMinFrequency() { + return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_min; + }, + cmtMaxFrequency() { + return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_max; + }, + cmtIsOutOfLegalRange() { + return this.dtuConfigList.cmt_frequency < this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_min + || this.dtuConfigList.cmt_frequency > this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_max; + } + }, + watch: { + 'dtuConfigList.cmt_country'(newValue, oldValue) { + // Don't do anything on initial load (then oldValue equals undefined) + if (oldValue != undefined) { + this.$nextTick(() => { + this.dtuConfigList.cmt_frequency = this.dtuConfigList.country_def[newValue].freq_default; + }); + } } }, methods: { @@ -152,4 +185,4 @@ export default defineComponent({ }, }, }); - \ No newline at end of file + diff --git a/webapp/src/views/ErrorView.vue b/webapp/src/views/ErrorView.vue new file mode 100644 index 000000000..e9cd84d84 --- /dev/null +++ b/webapp/src/views/ErrorView.vue @@ -0,0 +1,18 @@ + + + diff --git a/webapp/src/views/FirmwareUpgradeView.vue b/webapp/src/views/FirmwareUpgradeView.vue index 964e2ebe4..f7bf0d75f 100644 --- a/webapp/src/views/FirmwareUpgradeView.vue +++ b/webapp/src/views/FirmwareUpgradeView.vue @@ -37,9 +37,11 @@ {{ $t('firmwareupgrade.OtaSuccess') }}

- +
+
+ +
+
{ + // Check if the response status is OK (200-299 range) + if (response.ok) { + console.log('Remote host is available. Reloading page...'); + clearInterval(this.hostCheckInterval); + this.hostCheckInterval = 0; + // Perform a page reload + window.location.replace("/"); + } else { + console.log('Remote host is not reachable. Do something else if needed.'); + } + }) + .catch(error => { + console.error('Error checking remote host:', error); + }); + } else { + console.log('Browser is offline. Cannot check remote host.'); + } + }, }, mounted() { if (!isLoggedIn()) { @@ -188,5 +218,8 @@ export default defineComponent({ } this.loading = false; }, + unmounted() { + clearInterval(this.hostCheckInterval); + } }); - \ No newline at end of file + diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 0fdf96071..d137b05a5 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -1,18 +1,24 @@