mirror of
https://github.com/lumapu/ahoy.git
synced 2025-07-19 17:27:12 +02:00
Merge remote-tracking branch 'refs/remotes/origin/development03' into development03
This commit is contained in:
commit
e7b29a1f42
27 changed files with 381 additions and 410 deletions
2
.github/workflows/compile_development.yml
vendored
2
.github/workflows/compile_development.yml
vendored
|
@ -47,7 +47,7 @@ jobs:
|
||||||
run: python convert.py
|
run: python convert.py
|
||||||
|
|
||||||
- name: Run PlatformIO
|
- name: Run PlatformIO
|
||||||
run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus
|
run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release
|
||||||
|
|
||||||
- name: Rename Binary files
|
- name: Rename Binary files
|
||||||
id: rename-binary-files
|
id: rename-binary-files
|
||||||
|
|
2
.github/workflows/compile_release.yml
vendored
2
.github/workflows/compile_release.yml
vendored
|
@ -51,7 +51,7 @@ jobs:
|
||||||
run: python convert.py
|
run: python convert.py
|
||||||
|
|
||||||
- name: Run PlatformIO
|
- name: Run PlatformIO
|
||||||
run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus
|
run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release
|
||||||
|
|
||||||
- name: Rename Binary files
|
- name: Rename Binary files
|
||||||
id: rename-binary-files
|
id: rename-binary-files
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ src/web/html/tmp/*
|
||||||
*.db
|
*.db
|
||||||
*.suo
|
*.suo
|
||||||
*.ipch
|
*.ipch
|
||||||
|
src/output.map
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.<br/>
|
||||||
|
Further information will help you to communicate to the compatible inverters.
|
||||||
|
|
||||||
|
You find the full [User_Manual here](User_Manual.md)
|
||||||
|
|
||||||
|
## Compatiblity
|
||||||
|
|
||||||
|
For now the following Inverters should work out of the box:
|
||||||
|
|
||||||
|
Hoymiles Inverters
|
||||||
|
|
||||||
|
| Status | Serie | Model | comment |
|
||||||
|
| ----- | ----- | ------ | ------- |
|
||||||
|
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
|
||||||
|
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
|
||||||
|
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
|
@ -26,45 +45,10 @@
|
||||||
- [HTTP based Pages](#http-based-pages)
|
- [HTTP based Pages](#http-based-pages)
|
||||||
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
|
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
|
||||||
- [Used Libraries](#used-libraries)
|
- [Used Libraries](#used-libraries)
|
||||||
- [Contact](#contact)
|
|
||||||
- [ToDo](#todo)
|
- [ToDo](#todo)
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.<br/>
|
|
||||||
Further information will help you to communicate to the compatible inverters.
|
|
||||||
|
|
||||||
You find the full [User_Manual here](User_Manual.md)
|
|
||||||
|
|
||||||
## Compatiblity
|
|
||||||
|
|
||||||
For now the following Inverters should work out of the box:
|
|
||||||
|
|
||||||
Hoymiles Inverters
|
|
||||||
|
|
||||||
- HM300
|
|
||||||
- HM350
|
|
||||||
- HM400
|
|
||||||
- HM600
|
|
||||||
- HM700
|
|
||||||
- HM800
|
|
||||||
- HM1000?
|
|
||||||
- HM1200
|
|
||||||
- HM1500
|
|
||||||
- MI-300* [For MI inverters see remarks here](User_Manual.md#mi-inverters)
|
|
||||||
- MI-600*
|
|
||||||
- MI-700*
|
|
||||||
- MI-1500* (2nd gen. still untested)
|
|
||||||
|
|
||||||
TSUN Inverters:
|
|
||||||
|
|
||||||
- [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400)
|
|
||||||
- [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400)
|
|
||||||
- [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800)
|
|
||||||
- others may work as well (need to be verified).
|
|
||||||
|
|
||||||
Solenso Inverters:
|
Solenso Inverters:
|
||||||
|
|
||||||
- SOL-H350
|
- SOL-H350
|
||||||
|
@ -178,12 +162,27 @@ CE D2 (GPIO4)
|
||||||
IRQ D0 (GPIO16 - no IRQ!)
|
IRQ D0 (GPIO16 - no IRQ!)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ATTENTION: From development version 108 onwards, also MISO, MOSI and SCLK
|
||||||
|
are configurable. Their defaults are correct for 'standard' ESP32 boards
|
||||||
|
and non-settable for ESP8266 (as this chip cannot move them elsewhere).
|
||||||
|
If you have an existing install though, you might see '0' in the web GUI.
|
||||||
|
|
||||||
|
Set MISO=19, MOSI=23, SCLK=18 in GUI and save for existing installs, this is the old
|
||||||
|
correct default for most ESP32 boards, for ESP82xx, a simple settings save should suffice.
|
||||||
|
Reboot afterwards.
|
||||||
|
|
||||||
|
|
||||||
## Flash the Firmware on your Ahoy DTU Hardware
|
## Flash the Firmware on your Ahoy DTU Hardware
|
||||||
|
|
||||||
Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board.
|
Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board.
|
||||||
You can either build your own using your own configuration or use one of our pre-compiled generic builds.
|
You can either build your own using your own configuration or use one of our pre-compiled generic builds.
|
||||||
|
|
||||||
#### Compiling your own Version
|
### Flash from your browser (easy)
|
||||||
|
|
||||||
|
The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required.
|
||||||
|
[Here you go](https://ahoydtu.de/web_install/)
|
||||||
|
|
||||||
|
### Compiling your own Version
|
||||||
|
|
||||||
This information suits you if you want to configure and build your own firmware.
|
This information suits you if you want to configure and build your own firmware.
|
||||||
|
|
||||||
|
@ -292,12 +291,6 @@ When everything is wired up and the firmware is flashed, it is time to connect t
|
||||||
| `ArduinoJson` | 6.19.4 | MIT |
|
| `ArduinoJson` | 6.19.4 | MIT |
|
||||||
| `ESP Async WebServer` | 4.3.0 | ? |
|
| `ESP Async WebServer` | 4.3.0 | ? |
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
We run a Discord Server that can be used to get in touch with the Developers and Users.
|
|
||||||
|
|
||||||
<https://discord.gg/WzhxEY62mB>
|
|
||||||
|
|
||||||
## ToDo
|
## ToDo
|
||||||
|
|
||||||
[See this post](https://github.com/lumapu/ahoy/issues/142)
|
[See this post](https://github.com/lumapu/ahoy/issues/142)
|
||||||
|
|
22
README.md
22
README.md
|
@ -18,23 +18,27 @@ This work is licensed under a
|
||||||
|
|
||||||
**Communicate with Hoymiles inverters via radio**. Get actual values like power, current, daily energy and set parameters like the power limit via web interface or MQTT. In this repository you will find different approaches means Hardware / Software to realize the described functionalities.
|
**Communicate with Hoymiles inverters via radio**. Get actual values like power, current, daily energy and set parameters like the power limit via web interface or MQTT. In this repository you will find different approaches means Hardware / Software to realize the described functionalities.
|
||||||
|
|
||||||
List of approaches
|
Table of approaches:
|
||||||
|
|
||||||
- [ESP8266/ESP32, C++](Getting_Started.md) 👈 the most effort is spent here
|
| Board | MI | HM | HMS/HMT | comment | HowTo start |
|
||||||
- [Arduino Nano, C++](tools/nano/NRF24_SendRcv/)
|
| ------ | -- | -- | ------- | ------- | ---------- |
|
||||||
- [Raspberry Pi, Python](tools/rpi/)
|
| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | coming soon✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
|
||||||
- [Others, C/C++](tools/nano/NRF24_SendRcv/)
|
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
|
||||||
|
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
|
||||||
|
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
|
||||||
|
|
||||||
## Quick Start with ESP8266
|
## Getting Started
|
||||||
- [Go here ✨](Getting_Started.md#things-needed)
|
[Guide how to start with a ESP module](Getting_Started.md)
|
||||||
- [Our Website](https://ahoydtu.de)
|
[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install)
|
||||||
|
|
||||||
|
## Our Website
|
||||||
|
[https://ahoydtu.de](https://ahoydtu.de)
|
||||||
|
|
||||||
## Success Stories
|
## Success Stories
|
||||||
- [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)
|
- [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)
|
||||||
|
|
||||||
## Support, Feedback, Information and Discussion
|
## Support, Feedback, Information and Discussion
|
||||||
- [Discord Server (~ 1200 Users)](https://discord.gg/WzhxEY62mB)
|
- [Discord Server (~ 3.800 Users)](https://discord.gg/WzhxEY62mB)
|
||||||
- [The root of development](https://www.mikrocontroller.net/topic/525778)
|
- [The root of development](https://www.mikrocontroller.net/topic/525778)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
|
@ -78,6 +78,11 @@ def readVersion(path, infile):
|
||||||
dst = path + "firmware/" + versionout
|
dst = path + "firmware/" + versionout
|
||||||
os.rename(src, dst)
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
versionout = version[:-1] + "_" + sha + "_esp32s3.bin"
|
||||||
|
src = path + ".pio/build/opendtufusionv1-release/firmware.bin"
|
||||||
|
dst = path + "firmware/" + versionout
|
||||||
|
os.rename(src, dst)
|
||||||
|
|
||||||
# other ESP32 bin files
|
# other ESP32 bin files
|
||||||
src = path + ".pio/build/esp32-wroom32-release/"
|
src = path + ".pio/build/esp32-wroom32-release/"
|
||||||
dst = path + "firmware/"
|
dst = path + "firmware/"
|
||||||
|
|
5
src/.vscode/settings.json
vendored
5
src/.vscode/settings.json
vendored
|
@ -4,20 +4,16 @@
|
||||||
"workbench.colorCustomizations": {
|
"workbench.colorCustomizations": {
|
||||||
"editorLineNumber.foreground": "#00ff00"
|
"editorLineNumber.foreground": "#00ff00"
|
||||||
},
|
},
|
||||||
|
|
||||||
"editor.wordWrap": "off",
|
"editor.wordWrap": "off",
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true,
|
||||||
|
|
||||||
"diffEditor.ignoreTrimWhitespace": true,
|
"diffEditor.ignoreTrimWhitespace": true,
|
||||||
"files.autoSave": "afterDelay",
|
"files.autoSave": "afterDelay",
|
||||||
|
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
// `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.
|
// `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.
|
||||||
// Set to false to keep the values you've explicitly set, above.
|
// Set to false to keep the values you've explicitly set, above.
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
|
|
||||||
// https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
// https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||||
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}",
|
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}",
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
|
@ -86,4 +82,5 @@
|
||||||
"thread": "cpp"
|
"thread": "cpp"
|
||||||
},
|
},
|
||||||
"cmake.configureOnOpen": false,
|
"cmake.configureOnOpen": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
}
|
}
|
280
src/CHANGES.md
280
src/CHANGES.md
|
@ -1,253 +1,33 @@
|
||||||
# Changelog
|
Changelog v0.6.0
|
||||||
|
|
||||||
(starting from release version `0.5.66`)
|
## General
|
||||||
|
* improved night time calculation time to 1 minute after last communication pause #515
|
||||||
|
* refactored code for better readability
|
||||||
|
* improved Hoymiles commuinication (retransmits, immediate power limit transmission, timing at all)
|
||||||
|
* renamed firmware binaries
|
||||||
|
* add login / logout to menu
|
||||||
|
* add display support for `SH1106`, `SSD1306`, `Nokia` and `ePaper 1.54"` (ESP32 only)
|
||||||
|
* add yield total correction - move your yield to a new inverter or correct an already used inverter
|
||||||
|
* added import / export feature
|
||||||
|
* added `Prometheus` endpoints
|
||||||
|
* improved wifi connection and stability (connect to strongest AP)
|
||||||
|
* addded Hoymiles alarm IDs to log
|
||||||
|
* improved `System` information page (eg. radio statitistics)
|
||||||
|
* improved UI (repsonsive design, (optional) dark mode)
|
||||||
|
* improved system stability (reduced `heap-fragmentation`, don't break settings on failure) #644, #645
|
||||||
|
* added support for 2nd generation of Hoymiles inverters, MI series
|
||||||
|
* improved JSON API for more stable WebUI
|
||||||
|
* added option to disable input display in `/live` (`max-power` has to be set to `0`)
|
||||||
|
* updated documentation
|
||||||
|
* improved settings on ESP32 devices while setting SPI pins (for `NRF24` radio)
|
||||||
|
|
||||||
## 0.5.107
|
## MqTT
|
||||||
* fix: show save message
|
* added `comm_disabled` #529
|
||||||
* fix: removed serial newline for `enqueueCmd`
|
* added fixed interval option #542, #523
|
||||||
* Merged improved Prometheus #808
|
* improved communication, only required publishes
|
||||||
|
* improved retained flags
|
||||||
## 0.5.106
|
|
||||||
* merged MI and debug message changes #804
|
|
||||||
* fixed MQTT autodiscover #794, #632
|
|
||||||
|
|
||||||
## 0.5.105
|
|
||||||
* merged MI, thx @rejoe2 #788
|
|
||||||
* fixed reboot message #793
|
|
||||||
|
|
||||||
## 0.5.104
|
|
||||||
* further improved save settings
|
|
||||||
* removed `#` character from ePaper
|
|
||||||
* fixed saving pinout for `Nokia-Display`
|
|
||||||
* removed `Reset` Pin for monochrome displays
|
|
||||||
* improved wifi connection #652
|
|
||||||
|
|
||||||
## 0.5.103
|
|
||||||
* merged MI improvements, thx @rejoe2 #778
|
|
||||||
* changed display inverter online message
|
|
||||||
* merged heap improvements #772
|
|
||||||
|
|
||||||
## 0.5.102
|
|
||||||
* Warning: old exports are not compatible any more!
|
|
||||||
* fix JSON import #775
|
|
||||||
* fix save settings, at least already stored settings are not lost #771
|
|
||||||
* further save settings improvements (only store inverters which are existing)
|
|
||||||
* improved display of settings save return value
|
|
||||||
* made save settings asynchronous (more heap memory is free)
|
|
||||||
|
|
||||||
## 0.5.101
|
|
||||||
* fix SSD1306
|
|
||||||
* update documentation
|
|
||||||
* Update miPayload.h
|
|
||||||
* Update README.md
|
|
||||||
* MI - remarks to user manual
|
|
||||||
* MI - fix AC calc
|
|
||||||
* MI - fix status msg. analysis
|
|
||||||
|
|
||||||
## 0.5.100
|
|
||||||
* fix add inverter `setup.html` #766
|
|
||||||
* fix MQTT retained flag for total values #726
|
|
||||||
* renamed buttons for import and export `setup.html`
|
|
||||||
* added serial message `settings saved`
|
|
||||||
|
|
||||||
## 0.5.99
|
|
||||||
* fix limit in [User_Manual.md](../User_Manual.md)
|
|
||||||
* changed `contrast` to `luminance` in `setup.html`
|
|
||||||
* try to fix SSD1306 display #759
|
|
||||||
* only show necessary display pins depending on setting
|
|
||||||
|
|
||||||
## 0.5.98
|
|
||||||
* fix SH1106 rotation and turn off during night #756
|
|
||||||
* removed MQTT subscription `sync_ntp`, `set_time` with a value of `0` does the same #696
|
|
||||||
* simplified MQTT subscription for `limit`. Check [User_Manual.md](../User_Manual.md) for new syntax #696, #713
|
|
||||||
* repaired inverter wise limit control
|
|
||||||
* fix upload settings #686
|
|
||||||
|
|
||||||
## 0.5.97
|
|
||||||
* Attention: re-ordered display types, check your settings! #746
|
|
||||||
* improved saving settings of display #747, #746
|
|
||||||
* disabled contrast for Nokia display #746
|
|
||||||
* added Prometheus as compile option #719, #615
|
|
||||||
* update MQTT lib to v1.4.1
|
|
||||||
* limit decimal places to 2 in `live`
|
|
||||||
* added `-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48` to esp8266 debug build #657
|
|
||||||
* a `max-module-power` of `0` disables channel in live view `setup`
|
|
||||||
* merge MI improvements, get firmware information #753
|
|
||||||
|
|
||||||
## 0.5.96
|
|
||||||
* added Nokia display again for ESP8266 #764
|
|
||||||
* changed `var` / `VAr` to SI unit `var` #732
|
|
||||||
* fix MQTT retained flags for totals (P_AC, P_DC) #726, #721
|
|
||||||
|
|
||||||
## 0.5.95
|
|
||||||
* merged #742 MI Improvments
|
|
||||||
* merged #736 remove obsolete JSON Endpoint
|
|
||||||
|
|
||||||
## 0.5.94
|
|
||||||
* added ePaper (for ESP32 only!), thx @dAjaY85 #735
|
|
||||||
* improved `/live` margins #732
|
|
||||||
* renamed `var` to `VAr` #732
|
|
||||||
|
|
||||||
## 0.5.93
|
|
||||||
* improved web API for `live`
|
|
||||||
* added dark mode option
|
|
||||||
* converted all forms to reponsive design
|
|
||||||
* repaired menu with password protection #720, #716, #709
|
|
||||||
* merged MI series fixes #729
|
|
||||||
|
|
||||||
## 0.5.92
|
|
||||||
* fix mobile menu
|
|
||||||
* fix inverters in select `serial.html` #709
|
|
||||||
|
|
||||||
## 0.5.91
|
|
||||||
* improved html and navi, navi is visible even when API dies #660
|
|
||||||
* reduced maximum allowed JSON size for API to 6000Bytes #660
|
|
||||||
* small fix: output command at `prepareDevInformCmd` #692
|
|
||||||
* improved inverter handling #671
|
|
||||||
|
|
||||||
## 0.5.90
|
|
||||||
* merged PR #684, #698, #705
|
|
||||||
* webserial minor overflow fix #660
|
|
||||||
* web `index.html` improve version information #701
|
|
||||||
* fix MQTT sets power limit to zero (0) #692
|
|
||||||
* changed `reset at midnight` with timezone #697
|
|
||||||
|
|
||||||
## 0.5.89
|
|
||||||
* reduced heap fragmentation (removed `strtok` completely) #644, #645, #682
|
|
||||||
* added part of mac address to MQTT client ID to seperate multiple ESPs in same network
|
|
||||||
* added dictionary for MQTT to reduce heap-fragmentation
|
|
||||||
* removed `last Alarm` from Live view, because it showed always the same alarm - will change in future
|
|
||||||
|
|
||||||
## 0.5.88
|
|
||||||
* MQTT Yield Day zero, next try to fix #671, thx @beegee3
|
|
||||||
* added Solenso inverter to supported devices
|
|
||||||
* improved reconnection of MQTT #650
|
|
||||||
|
|
||||||
## 0.5.87
|
|
||||||
* fix yield total correction as module (inverter input) value #570
|
|
||||||
* reneabled instant start communication (once NTP is synced) #674
|
|
||||||
|
|
||||||
## 0.5.86
|
|
||||||
* prevent send devcontrol request during disabled night communication
|
|
||||||
* changed yield total correction as module (inverter input) value #570
|
|
||||||
* MQTT Yield Day zero, next try to fix #671
|
|
||||||
|
|
||||||
## 0.5.85
|
|
||||||
* fix power-limit was not checked for max retransmits #667
|
|
||||||
* fix blue LED lights up all the time #672
|
|
||||||
* fix installing schedulers if NTP server isn't available
|
|
||||||
* improved zero values on triggers #671
|
|
||||||
* hardcoded MQTT subtopics, because wildcard `#` leads to errors
|
|
||||||
* rephrased some messages on webif, thx to @Argafal #638
|
|
||||||
* fixed 'polling stop message' on `index.html` #639
|
|
||||||
|
|
||||||
## 0.5.84
|
|
||||||
* fix blue LED lights up all the time #672
|
|
||||||
* added an instant start communication (once NTP is synced)
|
|
||||||
* add MI 3rd generation inverters (10x2 serial numbers)
|
|
||||||
* first decodings of messages from MI 2nd generation inverters
|
|
||||||
|
|
||||||
## 0.5.83
|
|
||||||
* fix MQTT publishing, `callback` was set but reset by following `setup()`
|
|
||||||
|
|
||||||
## 0.5.82
|
|
||||||
* fixed communication error #652
|
|
||||||
* reset values is no bound to MQTT any more, setting moved to `inverter` #649
|
|
||||||
* fixed wording on `index.hmtl` #661
|
|
||||||
|
|
||||||
## 0.5.81
|
|
||||||
* started implementation of MI inverters (setup.html, own processing `MiPayload.h`)
|
|
||||||
|
|
||||||
## 0.5.80
|
|
||||||
* fixed communication #656
|
|
||||||
|
|
||||||
## 0.5.79
|
|
||||||
* fixed mixed reset flags #648
|
|
||||||
* fixed `mCbAlarm` if MQTT is not used #653
|
|
||||||
* fixed MQTT `autodiscover` #630 thanks to @antibill51
|
|
||||||
* next changes from @beegee many thanks for your contribution!
|
|
||||||
* replaced `CircularBuffer` by `std::queue`
|
|
||||||
* reworked `hmRadio.h` completely (interrupts, packaging)
|
|
||||||
* fix exception while `reboot`
|
|
||||||
* cleanup MQTT coding
|
|
||||||
|
|
||||||
## 0.5.78
|
|
||||||
* further improvements regarding wifi #611, fix connection if only one AP with same SSID is there
|
|
||||||
* fix endless loop in `zerovalues` #564
|
|
||||||
* fix auto discover again #565
|
|
||||||
* added total values to autodiscover #630
|
|
||||||
* improved zero at midnight #625
|
|
||||||
|
|
||||||
## 0.5.77
|
|
||||||
* fix wrong filename for automatically created manifest (online installer) #620
|
|
||||||
* added rotate display feature #619
|
|
||||||
* improved Prometheus endpoint #615, thx to @fsck-block
|
|
||||||
* improved wifi to connect always to strongest RSSI, thx to @beegee3 #611
|
|
||||||
|
|
||||||
## 0.5.76
|
|
||||||
* reduce MQTT retry interval from maximum speed to one second
|
|
||||||
* fixed homeassistant autodiscovery #565
|
|
||||||
* implemented `getNTPTime` improvements #609 partially #611
|
|
||||||
* added alarm messages to MQTT #177, #600, #608
|
|
||||||
|
|
||||||
## 0.5.75
|
|
||||||
* fix wakeup issue, once wifi was lost during night the communication didn't start in the morning
|
|
||||||
* reenabled FlashStringHelper because of lacking RAM
|
|
||||||
* complete rewrite of monochrome display class, thx to @dAjaY85 -> displays are now configurable in setup
|
|
||||||
* fix power limit not possible #607
|
|
||||||
|
|
||||||
## 0.5.74
|
|
||||||
* improved payload handling (retransmit all fragments on CRC error)
|
|
||||||
* improved `isAvailable`, checkes all record structs, inverter becomes available more early because version is check first
|
|
||||||
* fix tickers were not set if NTP is not available
|
|
||||||
* disabled annoying `FlashStringHelper` it gives randomly Expeptions during development, feels more stable since then
|
|
||||||
* moved erase button to the bottom in settings, not nice but more functional
|
|
||||||
* split `tx_count` to `tx_cnt` and `retransmits` in `system.html`
|
|
||||||
* fix mqtt retransmit IP address #602
|
|
||||||
* added debug infos for `scheduler` (web -> `/debug` as trigger prints list of tickers to serial console)
|
|
||||||
|
|
||||||
## 0.5.73
|
|
||||||
* improved payload handling (request / retransmit) #464
|
|
||||||
* included alarm ID parse to serial console (in development)
|
|
||||||
|
|
||||||
## 0.5.72
|
|
||||||
* repaired system, scheduler was not called any more #596
|
|
||||||
|
|
||||||
## 0.5.71
|
|
||||||
* improved wifi handling and tickers, many thanks to @beegee3 #571
|
|
||||||
* fixed YieldTotal correction calculation #589
|
|
||||||
* fixed serial output of power limit acknowledge #569
|
|
||||||
* reviewed `sendDiscoveryConfig` #565
|
|
||||||
* merged PR `Monodisplay`, many thanks to @dAjaY85 #566, Note: (settings are introduced but not able to be modified, will be included in next version)
|
|
||||||
|
|
||||||
## 0.5.70
|
|
||||||
* corrected MQTT `comm_disabled` #529
|
|
||||||
* fix Prometheus and JSON endpoints (`config_override.h`) #561
|
|
||||||
* publish MQTT with fixed interval even if inverter is not available #542
|
|
||||||
* added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551
|
|
||||||
* MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman
|
|
||||||
* improved potential issue with `checkTicker`, thx @cbscpe
|
|
||||||
* MQTT option for reset values on midnight / not avail / communication stop #539
|
|
||||||
* small fix in `tickIVCommunication` #534
|
|
||||||
* add `YieldTotal` correction, eg. to have the option to zero at year start #512
|
|
||||||
|
|
||||||
## 0.5.69
|
|
||||||
* merged SH1106 1.3" Display, thx @dAjaY85
|
|
||||||
* added SH1106 to automatic build
|
|
||||||
* added IP address to MQTT (version, device and IP are retained and only transmitted once after boot) #556
|
|
||||||
* added `set_power_limit` acknowledge MQTT publish #553
|
* added `set_power_limit` acknowledge MQTT publish #553
|
||||||
* changed: version, device name are only published via MQTT once after boot
|
* added feature to reset values on midnight, communication pause or if the inverters are not available
|
||||||
* added `Login` to menu if admin password is set #554
|
* partially added Hoymiles alarm ID
|
||||||
* added `development` to second changelog link in `index.html` #543
|
* improved autodiscover (added total values on multi-inverter setup)
|
||||||
* added interval for MQTT (as option). With this settings MQTT live data is published in a fixed timing (only if inverter is available) #542, #523
|
* improved `clientID` a part of the MAC address is added to have an unique name
|
||||||
* added MQTT `comm_disabled` #529
|
|
||||||
* changed name of binaries, moved GIT-Sha to the front #538
|
|
||||||
|
|
||||||
## 0.5.68
|
|
||||||
* repaired receive payload
|
|
||||||
* Powerlimit is transfered immediately to inverter
|
|
||||||
|
|
||||||
## 0.5.67
|
|
||||||
* changed calculation of start / stop communication to 1 min after last comm. stop #515
|
|
||||||
* moved payload send to `payload.h`, function `ivSend` #515
|
|
||||||
* payload: if last frame is missing, request all frames again
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ void app::setup() {
|
||||||
DBGPRINTLN(F("false"));
|
DBGPRINTLN(F("false"));
|
||||||
|
|
||||||
mSys.enableDebug();
|
mSys.enableDebug();
|
||||||
mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs);
|
mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
|
||||||
|
|
||||||
#if defined(AP_ONLY)
|
#if defined(AP_ONLY)
|
||||||
mInnerLoopCb = std::bind(&app::loopStandard, this);
|
mInnerLoopCb = std::bind(&app::loopStandard, this);
|
||||||
|
|
|
@ -44,13 +44,25 @@
|
||||||
|
|
||||||
// default pinout (GPIO Number)
|
// default pinout (GPIO Number)
|
||||||
#if defined(ESP32)
|
#if defined(ESP32)
|
||||||
|
// this is the default ESP32 (son-S) pinout on the WROOM modules for VSPI,
|
||||||
|
// for the ESP32-S3 there is no sane 'default', as it has full flexibility
|
||||||
|
// to map its two HW SPIs anywhere and PCBs differ materially,
|
||||||
|
// so it has to be selected in the Web UI
|
||||||
#define DEF_CS_PIN 5
|
#define DEF_CS_PIN 5
|
||||||
#define DEF_CE_PIN 4
|
#define DEF_CE_PIN 4
|
||||||
#define DEF_IRQ_PIN 16
|
#define DEF_IRQ_PIN 16
|
||||||
|
#define DEF_MISO_PIN 19
|
||||||
|
#define DEF_MOSI_PIN 23
|
||||||
|
#define DEF_SCLK_PIN 18
|
||||||
#else
|
#else
|
||||||
#define DEF_CS_PIN 15
|
#define DEF_CS_PIN 15
|
||||||
#define DEF_CE_PIN 2
|
#define DEF_CE_PIN 2
|
||||||
#define DEF_IRQ_PIN 0
|
#define DEF_IRQ_PIN 0
|
||||||
|
// these are given to relay the correct values via API
|
||||||
|
// they cannot actually be moved for ESP82xx models
|
||||||
|
#define DEF_MISO_PIN 12
|
||||||
|
#define DEF_MOSI_PIN 13
|
||||||
|
#define DEF_SCLK_PIN 14
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// default NRF24 power, possible values (0 - 3)
|
// default NRF24 power, possible values (0 - 3)
|
||||||
|
|
|
@ -17,14 +17,19 @@
|
||||||
#undef FB_WIFI_PWD
|
#undef FB_WIFI_PWD
|
||||||
#define FB_WIFI_PWD "MY_WIFI_KEY"
|
#define FB_WIFI_PWD "MY_WIFI_KEY"
|
||||||
|
|
||||||
// ESP32 default pinout
|
// ESP32-S3 example pinout
|
||||||
#undef DEF_RF24_CS_PIN
|
#undef DEF_CS_PIN
|
||||||
#define DEF_RF24_CS_PIN 5
|
#define DEF_CS_PIN 37
|
||||||
#undef DEF_RF24_CE_PIN
|
#undef DEF_CE_PIN
|
||||||
#define DEF_RF24_CE_PIN 4
|
#define DEF_CE_PIN 38
|
||||||
#undef DEF_RF24_IRQ_PIN
|
#undef DEF_IRQ_PIN
|
||||||
#define DEF_RF24_IRQ_PIN 16
|
#define DEF_IRQ_PIN 47
|
||||||
|
#undef DEF_MISO_PIN
|
||||||
|
#define DEF_MISO_PIN 48
|
||||||
|
#undef DEF_MOSI_PIN
|
||||||
|
#define DEF_MOSI_PIN 35
|
||||||
|
#undef DEF_SCLK_PIN
|
||||||
|
#define DEF_SCLK_PIN 36
|
||||||
|
|
||||||
// Offset for midnight Ticker Example: 1 second before midnight (local time)
|
// Offset for midnight Ticker Example: 1 second before midnight (local time)
|
||||||
#undef MIDNIGHTTICKER_OFFSET
|
#undef MIDNIGHTTICKER_OFFSET
|
||||||
|
|
|
@ -73,6 +73,9 @@ typedef struct {
|
||||||
uint8_t pinCs;
|
uint8_t pinCs;
|
||||||
uint8_t pinCe;
|
uint8_t pinCe;
|
||||||
uint8_t pinIrq;
|
uint8_t pinIrq;
|
||||||
|
uint8_t pinMiso;
|
||||||
|
uint8_t pinMosi;
|
||||||
|
uint8_t pinSclk;
|
||||||
uint8_t amplifierPower;
|
uint8_t amplifierPower;
|
||||||
} cfgNrf24_t;
|
} cfgNrf24_t;
|
||||||
|
|
||||||
|
@ -344,6 +347,10 @@ class settings {
|
||||||
mCfg.nrf.pinCs = DEF_CS_PIN;
|
mCfg.nrf.pinCs = DEF_CS_PIN;
|
||||||
mCfg.nrf.pinCe = DEF_CE_PIN;
|
mCfg.nrf.pinCe = DEF_CE_PIN;
|
||||||
mCfg.nrf.pinIrq = DEF_IRQ_PIN;
|
mCfg.nrf.pinIrq = DEF_IRQ_PIN;
|
||||||
|
mCfg.nrf.pinMiso = DEF_MISO_PIN;
|
||||||
|
mCfg.nrf.pinMosi = DEF_MOSI_PIN;
|
||||||
|
mCfg.nrf.pinSclk = DEF_SCLK_PIN;
|
||||||
|
|
||||||
mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03;
|
mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03;
|
||||||
|
|
||||||
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME);
|
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME);
|
||||||
|
@ -426,6 +433,9 @@ class settings {
|
||||||
obj[F("cs")] = mCfg.nrf.pinCs;
|
obj[F("cs")] = mCfg.nrf.pinCs;
|
||||||
obj[F("ce")] = mCfg.nrf.pinCe;
|
obj[F("ce")] = mCfg.nrf.pinCe;
|
||||||
obj[F("irq")] = mCfg.nrf.pinIrq;
|
obj[F("irq")] = mCfg.nrf.pinIrq;
|
||||||
|
obj[F("sclk")] = mCfg.nrf.pinSclk;
|
||||||
|
obj[F("mosi")] = mCfg.nrf.pinMosi;
|
||||||
|
obj[F("miso")] = mCfg.nrf.pinMiso;
|
||||||
obj[F("pwr")] = mCfg.nrf.amplifierPower;
|
obj[F("pwr")] = mCfg.nrf.amplifierPower;
|
||||||
} else {
|
} else {
|
||||||
mCfg.nrf.sendInterval = obj[F("intvl")];
|
mCfg.nrf.sendInterval = obj[F("intvl")];
|
||||||
|
@ -433,11 +443,17 @@ class settings {
|
||||||
mCfg.nrf.pinCs = obj[F("cs")];
|
mCfg.nrf.pinCs = obj[F("cs")];
|
||||||
mCfg.nrf.pinCe = obj[F("ce")];
|
mCfg.nrf.pinCe = obj[F("ce")];
|
||||||
mCfg.nrf.pinIrq = obj[F("irq")];
|
mCfg.nrf.pinIrq = obj[F("irq")];
|
||||||
|
mCfg.nrf.pinSclk = obj[F("sclk")];
|
||||||
|
mCfg.nrf.pinMosi = obj[F("mosi")];
|
||||||
|
mCfg.nrf.pinMiso = obj[F("miso")];
|
||||||
mCfg.nrf.amplifierPower = obj[F("pwr")];
|
mCfg.nrf.amplifierPower = obj[F("pwr")];
|
||||||
if((obj[F("cs")] == obj[F("ce")])) {
|
if((obj[F("cs")] == obj[F("ce")])) {
|
||||||
mCfg.nrf.pinCs = DEF_CS_PIN;
|
mCfg.nrf.pinCs = DEF_CS_PIN;
|
||||||
mCfg.nrf.pinCe = DEF_CE_PIN;
|
mCfg.nrf.pinCe = DEF_CE_PIN;
|
||||||
mCfg.nrf.pinIrq = DEF_IRQ_PIN;
|
mCfg.nrf.pinIrq = DEF_IRQ_PIN;
|
||||||
|
mCfg.nrf.pinSclk = DEF_SCLK_PIN;
|
||||||
|
mCfg.nrf.pinMosi = DEF_MOSI_PIN;
|
||||||
|
mCfg.nrf.pinMiso = DEF_MISO_PIN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
// VERSION
|
// VERSION
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 5
|
#define VERSION_MINOR 6
|
||||||
#define VERSION_PATCH 107
|
#define VERSION_PATCH 0
|
||||||
|
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
@ -304,7 +304,7 @@ class Inverter {
|
||||||
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
|
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
|
||||||
actPowerLimit = rec->record[pos];
|
actPowerLimit = rec->record[pos];
|
||||||
DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
|
DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
|
||||||
DBGPRINTLN(String(actPowerLimit, 1));
|
DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (rec->assign == AlarmDataAssignment) {
|
else if (rec->assign == AlarmDataAssignment) {
|
||||||
|
|
|
@ -168,7 +168,7 @@ class HmPayload {
|
||||||
DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored"));
|
DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored"));
|
||||||
} else {
|
} else {
|
||||||
DPRINT(DBG_DEBUG, F("PID: 0x"));
|
DPRINT(DBG_DEBUG, F("PID: 0x"));
|
||||||
DBGHEXLN(*pid);
|
DPRINTLN(DBG_DEBUG, String(*pid, HEX));
|
||||||
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {
|
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {
|
||||||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11);
|
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11);
|
||||||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11;
|
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11;
|
||||||
|
@ -285,7 +285,7 @@ class HmPayload {
|
||||||
DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
|
DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
|
||||||
DBGHEXLN(mPayload[iv->id].txId);
|
DBGHEXLN(mPayload[iv->id].txId);
|
||||||
DPRINT(DBG_DEBUG, F("procPyld: max: "));
|
DPRINT(DBG_DEBUG, F("procPyld: max: "));
|
||||||
DBGPRINTLN(String(mPayload[iv->id].maxPackId));
|
DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId));
|
||||||
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
|
||||||
mPayload[iv->id].complete = true;
|
mPayload[iv->id].complete = true;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <RF24.h>
|
#include <RF24.h>
|
||||||
#include "../utils/crc.h"
|
#include "../utils/crc.h"
|
||||||
#include "../config/config.h"
|
#include "../config/config.h"
|
||||||
|
#include "SPI.h"
|
||||||
|
|
||||||
#define SPI_SPEED 1000000
|
#define SPI_SPEED 1000000
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// HM Radio class
|
// HM Radio class
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
template <uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW>
|
template <uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_SCLK_PIN, uint8_t MOSI_PIN = DEF_MOSI_PIN, uint8_t MISO_PIN = DEF_MISO_PIN>
|
||||||
class HmRadio {
|
class HmRadio {
|
||||||
public:
|
public:
|
||||||
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
|
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
|
||||||
|
@ -78,7 +79,7 @@ class HmRadio {
|
||||||
}
|
}
|
||||||
~HmRadio() {}
|
~HmRadio() {}
|
||||||
|
|
||||||
void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) {
|
void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) {
|
||||||
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
|
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
|
||||||
pinMode(irq, INPUT_PULLUP);
|
pinMode(irq, INPUT_PULLUP);
|
||||||
|
|
||||||
|
@ -100,7 +101,19 @@ class HmRadio {
|
||||||
// change the byte order of the DTU serial number and append the required 0x01 at the end
|
// change the byte order of the DTU serial number and append the required 0x01 at the end
|
||||||
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01;
|
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01;
|
||||||
|
|
||||||
mNrf24.begin(ce, cs);
|
#ifdef ESP32
|
||||||
|
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
|
||||||
|
mSpi = new SPIClass(FSPI);
|
||||||
|
#else
|
||||||
|
mSpi = new SPIClass(VSPI);
|
||||||
|
#endif
|
||||||
|
mSpi->begin(sclk, miso, mosi, cs);
|
||||||
|
#else
|
||||||
|
//the old ESP82xx cannot freely place their SPI pins
|
||||||
|
mSpi = new SPIClass();
|
||||||
|
mSpi->begin();
|
||||||
|
#endif
|
||||||
|
mNrf24.begin(mSpi, ce, cs);
|
||||||
mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
|
mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
|
||||||
|
|
||||||
mNrf24.setChannel(mRfChLst[mRxChIdx]);
|
mNrf24.setChannel(mRfChLst[mRxChIdx]);
|
||||||
|
@ -350,6 +363,7 @@ class HmRadio {
|
||||||
uint8_t mTxChIdx;
|
uint8_t mTxChIdx;
|
||||||
uint8_t mRxChIdx;
|
uint8_t mRxChIdx;
|
||||||
|
|
||||||
|
SPIClass* mSpi;
|
||||||
RF24 mNrf24;
|
RF24 mNrf24;
|
||||||
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
|
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,9 +21,9 @@ class HmSystem {
|
||||||
Radio.setup();
|
Radio.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) {
|
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) {
|
||||||
mNumInv = 0;
|
mNumInv = 0;
|
||||||
Radio.setup(ampPwr, irqPin, cePin, csPin);
|
Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addInverters(cfgInst_t *config) {
|
void addInverters(cfgInst_t *config) {
|
||||||
|
|
|
@ -448,8 +448,8 @@ const byteAssign_t InfoAssignment[] = {
|
||||||
} else {
|
} else {
|
||||||
bool change = false;
|
bool change = false;
|
||||||
if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command
|
if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command
|
||||||
cmd++; // just request the next channel
|
//cmd++; // just request the next channel
|
||||||
change = true;
|
//change = true;
|
||||||
} else if ( cmd == 0x09 ) {//MI single or dual channel device
|
} else if ( cmd == 0x09 ) {//MI single or dual channel device
|
||||||
if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) {
|
if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) {
|
||||||
if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {}
|
if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {}
|
||||||
|
@ -472,6 +472,7 @@ const byteAssign_t InfoAssignment[] = {
|
||||||
if (change) {
|
if (change) {
|
||||||
DBGPRINT(F("next request is"));
|
DBGPRINT(F("next request is"));
|
||||||
//mPayload[iv->id].skipfirstrepeat = 0;
|
//mPayload[iv->id].skipfirstrepeat = 0;
|
||||||
|
mPayload[iv->id].txCmd = cmd;
|
||||||
} else {
|
} else {
|
||||||
DBGPRINT(F("sth."));
|
DBGPRINT(F("sth."));
|
||||||
DBGPRINT(F(" missing: Request Retransmit"));
|
DBGPRINT(F(" missing: Request Retransmit"));
|
||||||
|
@ -480,7 +481,6 @@ const byteAssign_t InfoAssignment[] = {
|
||||||
DBGHEXLN(cmd);
|
DBGHEXLN(cmd);
|
||||||
//mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true);
|
//mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true);
|
||||||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd);
|
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd);
|
||||||
mPayload[iv->id].txCmd = cmd;
|
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -682,11 +682,13 @@ const byteAssign_t InfoAssignment[] = {
|
||||||
}*/
|
}*/
|
||||||
miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]);
|
miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]);
|
||||||
|
|
||||||
|
|
||||||
if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
|
if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
|
||||||
/*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1;
|
/*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1;
|
||||||
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd);
|
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd);
|
||||||
mPayload[iv->id].txCmd = cmd;*/
|
mPayload[iv->id].txCmd = cmd;*/
|
||||||
|
mPayload[iv->id].txCmd++;
|
||||||
|
if (mPayload[iv->id].retransmits)
|
||||||
|
mPayload[iv->id].retransmits--; // reserve retransmissions for each response
|
||||||
mPayload[iv->id].complete = false;
|
mPayload[iv->id].complete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ lib_deps =
|
||||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||||
nrf24/RF24 @ ^1.4.5
|
nrf24/RF24 @ ^1.4.5
|
||||||
paulstoffregen/Time @ ^1.6.1
|
paulstoffregen/Time @ ^1.6.1
|
||||||
https://github.com/bertmelis/espMqttClient#v1.4.1
|
https://github.com/bertmelis/espMqttClient#v1.4.2
|
||||||
bblanchon/ArduinoJson @ ^6.21.0
|
bblanchon/ArduinoJson @ ^6.21.0
|
||||||
https://github.com/JChristensen/Timezone @ ^1.2.4
|
https://github.com/JChristensen/Timezone @ ^1.2.4
|
||||||
olikraus/U8g2 @ ^2.34.16
|
olikraus/U8g2 @ ^2.34.16
|
||||||
|
@ -48,7 +48,8 @@ lib_deps =
|
||||||
platform = espressif8266
|
platform = espressif8266
|
||||||
board = esp12e
|
board = esp12e
|
||||||
board_build.f_cpu = 80000000L
|
board_build.f_cpu = 80000000L
|
||||||
build_flags = -D RELEASE -Wl,-Map,output.map
|
build_flags = -D RELEASE
|
||||||
|
;-Wl,-Map,output.map
|
||||||
monitor_filters =
|
monitor_filters =
|
||||||
;default ; Remove typical terminal control codes from input
|
;default ; Remove typical terminal control codes from input
|
||||||
;time ; Add timestamp with milliseconds for each new line
|
;time ; Add timestamp with milliseconds for each new line
|
||||||
|
@ -133,3 +134,13 @@ monitor_filters =
|
||||||
;default ; Remove typical terminal control codes from input
|
;default ; Remove typical terminal control codes from input
|
||||||
time ; Add timestamp with milliseconds for each new line
|
time ; Add timestamp with milliseconds for each new line
|
||||||
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
||||||
|
|
||||||
|
[env:opendtufusionv1-release]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
build_flags = -D RELEASE -std=gnu++14
|
||||||
|
build_unflags = -std=gnu++11
|
||||||
|
monitor_filters =
|
||||||
|
;default ; Remove typical terminal control codes from input
|
||||||
|
time ; Add timestamp with milliseconds for each new line
|
||||||
|
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
|
||||||
|
|
|
@ -146,28 +146,28 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl
|
||||||
|
|
||||||
_display->setFont(&FreeSans12pt7b);
|
_display->setFont(&FreeSans12pt7b);
|
||||||
y = _display->height() / 2;
|
y = _display->height() / 2;
|
||||||
_display->setCursor(0, y);
|
_display->setCursor(5, y);
|
||||||
_display->print("today:");
|
_display->print("today:");
|
||||||
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay);
|
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay);
|
||||||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
|
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||||||
x = ((_display->width() - tbw) / 2) - tbx;
|
x = ((_display->width() - tbw) / 2) - tbx;
|
||||||
_display->setCursor(x, y);
|
_display->setCursor(x, y);
|
||||||
_display->print(_fmtText);
|
_display->print(_fmtText);
|
||||||
_display->setCursor(_display->width() - 33, y);
|
_display->setCursor(_display->width() - 38, y);
|
||||||
_display->println("Wh");
|
_display->println("Wh");
|
||||||
|
|
||||||
y = y + tbh + 7;
|
y = y + tbh + 7;
|
||||||
_display->setCursor(0, y);
|
_display->setCursor(5, y);
|
||||||
_display->print("total:");
|
_display->print("total:");
|
||||||
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal);
|
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal);
|
||||||
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
|
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||||||
x = ((_display->width() - tbw) / 2) - tbx;
|
x = ((_display->width() - tbw) / 2) - tbx;
|
||||||
_display->setCursor(x, y);
|
_display->setCursor(x, y);
|
||||||
_display->print(_fmtText);
|
_display->print(_fmtText);
|
||||||
_display->setCursor(_display->width() - 45, y);
|
_display->setCursor(_display->width() - 50, y);
|
||||||
_display->println("kWh");
|
_display->println("kWh");
|
||||||
|
|
||||||
_display->setCursor(0, _display->height() - (mHeadFootPadding + 10));
|
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10));
|
||||||
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod);
|
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod);
|
||||||
_display->println(_fmtText);
|
_display->println(_fmtText);
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ typedef struct {
|
||||||
bool running;
|
bool running;
|
||||||
uint8_t lastIvId;
|
uint8_t lastIvId;
|
||||||
uint8_t sub;
|
uint8_t sub;
|
||||||
|
uint8_t foundIvCnt;
|
||||||
} discovery_t;
|
} discovery_t;
|
||||||
|
|
||||||
template<class HMSYSTEM>
|
template<class HMSYSTEM>
|
||||||
|
@ -100,7 +101,7 @@ class PubMqtt {
|
||||||
if (mIntervalTimeout > 0)
|
if (mIntervalTimeout > 0)
|
||||||
mIntervalTimeout--;
|
mIntervalTimeout--;
|
||||||
|
|
||||||
if(!mClient.connected()) {
|
if(mClient.disconnected()) {
|
||||||
mClient.connect();
|
mClient.connect();
|
||||||
return; // next try in a second
|
return; // next try in a second
|
||||||
}
|
}
|
||||||
|
@ -117,9 +118,8 @@ class PubMqtt {
|
||||||
}
|
}
|
||||||
|
|
||||||
void tickerMinute() {
|
void tickerMinute() {
|
||||||
char val[12];
|
snprintf(mVal, 40, "%ld", millis() / 1000);
|
||||||
snprintf(val, 12, "%ld", millis() / 1000);
|
publish(subtopics[MQTT_UPTIME], mVal);
|
||||||
publish(subtopics[MQTT_UPTIME], val);
|
|
||||||
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
|
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
|
||||||
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
|
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
|
||||||
#ifndef ESP32
|
#ifndef ESP32
|
||||||
|
@ -151,12 +151,10 @@ class PubMqtt {
|
||||||
}
|
}
|
||||||
|
|
||||||
void tickerMidnight() {
|
void tickerMidnight() {
|
||||||
char topic[7 + MQTT_TOPIC_LEN], val[4];
|
|
||||||
|
|
||||||
// set Total YieldDay to zero
|
// set Total YieldDay to zero
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]);
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]);
|
||||||
snprintf(val, 2, "0");
|
snprintf(mVal, 2, "0");
|
||||||
publish(topic, val, true);
|
publish(mSubTopic, mVal, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void payloadEventListener(uint8_t cmd) {
|
void payloadEventListener(uint8_t cmd) {
|
||||||
|
@ -176,7 +174,6 @@ class PubMqtt {
|
||||||
if(!mClient.connected())
|
if(!mClient.connected())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
memset(mTopic, 0, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1);
|
|
||||||
if(addTopic){
|
if(addTopic){
|
||||||
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic);
|
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic);
|
||||||
} else {
|
} else {
|
||||||
|
@ -224,14 +221,13 @@ class PubMqtt {
|
||||||
mDiscovery.running = true;
|
mDiscovery.running = true;
|
||||||
mDiscovery.lastIvId = 0;
|
mDiscovery.lastIvId = 0;
|
||||||
mDiscovery.sub = 0;
|
mDiscovery.sub = 0;
|
||||||
|
mDiscovery.foundIvCnt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPowerLimitAck(Inverter<> *iv) {
|
void setPowerLimitAck(Inverter<> *iv) {
|
||||||
if (NULL != iv) {
|
if (NULL != iv) {
|
||||||
char topic[7 + MQTT_TOPIC_LEN];
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
|
||||||
|
publish(mSubTopic, "true", true);
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
|
|
||||||
publish(topic, "true", true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,12 +241,11 @@ class PubMqtt {
|
||||||
tickerMinute();
|
tickerMinute();
|
||||||
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false);
|
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false);
|
||||||
|
|
||||||
char sub[20];
|
|
||||||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
|
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
|
||||||
snprintf(sub, 20, "ctrl/limit/%d", i);
|
snprintf(mVal, 20, "ctrl/limit/%d", i);
|
||||||
subscribe(sub);
|
subscribe(mVal);
|
||||||
snprintf(sub, 20, "ctrl/restart/%d", i);
|
snprintf(mVal, 20, "ctrl/restart/%d", i);
|
||||||
subscribe(sub);
|
subscribe(mVal);
|
||||||
}
|
}
|
||||||
subscribe(subscr[MQTT_SUBS_SET_TIME]);
|
subscribe(subscr[MQTT_SUBS_SET_TIME]);
|
||||||
}
|
}
|
||||||
|
@ -350,14 +345,16 @@ class PubMqtt {
|
||||||
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
|
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
|
||||||
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
|
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
|
||||||
|
|
||||||
String node_mac = WiFi.macAddress().substring(12,14)+ WiFi.macAddress().substring(15,17);
|
String node_id = String(mDevName) + "_TOTAL";
|
||||||
String node_id = "AHOY_DTU_" + node_mac;
|
|
||||||
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS);
|
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS);
|
||||||
|
|
||||||
Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId);
|
Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId);
|
||||||
record_t<> *rec;
|
record_t<> *rec = NULL;
|
||||||
if (NULL != iv)
|
if (NULL != iv) {
|
||||||
rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
||||||
|
if(0 == mDiscovery.sub)
|
||||||
|
mDiscovery.foundIvCnt++;
|
||||||
|
}
|
||||||
|
|
||||||
if ((NULL != iv) || total) {
|
if ((NULL != iv) || total) {
|
||||||
if (!total) {
|
if (!total) {
|
||||||
|
@ -420,18 +417,25 @@ class PubMqtt {
|
||||||
|
|
||||||
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
|
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
|
||||||
mDiscovery.sub = 0;
|
mDiscovery.sub = 0;
|
||||||
if(++mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1))
|
checkDiscoveryEnd();
|
||||||
mDiscovery.running = false;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mDiscovery.sub = 0;
|
mDiscovery.sub = 0;
|
||||||
if(++mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1))
|
checkDiscoveryEnd();
|
||||||
mDiscovery.running = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void checkDiscoveryEnd(void) {
|
||||||
|
if(++mDiscovery.lastIvId == MAX_NUM_INVERTERS) {
|
||||||
|
// check if only one inverter was found, then don't create 'total' sensor
|
||||||
|
if(mDiscovery.foundIvCnt == 1)
|
||||||
|
mDiscovery.running = false;
|
||||||
|
} else if(mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1))
|
||||||
|
mDiscovery.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
const char *getFieldDeviceClass(uint8_t fieldId) {
|
const char *getFieldDeviceClass(uint8_t fieldId) {
|
||||||
uint8_t pos = 0;
|
uint8_t pos = 0;
|
||||||
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) {
|
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) {
|
||||||
|
@ -455,7 +459,6 @@ class PubMqtt {
|
||||||
bool allAvail = true; // shows if all enabled inverters are available
|
bool allAvail = true; // shows if all enabled inverters are available
|
||||||
bool anyAvail = false; // shows if at least one enabled inverter is available
|
bool anyAvail = false; // shows if at least one enabled inverter is available
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
char topic[7 + MQTT_TOPIC_LEN], val[40];
|
|
||||||
Inverter<> *iv;
|
Inverter<> *iv;
|
||||||
record_t<> *rec;
|
record_t<> *rec;
|
||||||
|
|
||||||
|
@ -485,19 +488,19 @@ class PubMqtt {
|
||||||
mLastIvState[id] = status;
|
mLastIvState[id] = status;
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
|
||||||
snprintf(val, 40, "%d", status);
|
snprintf(mVal, 40, "%d", status);
|
||||||
publish(topic, val, true);
|
publish(mSubTopic, mVal, true);
|
||||||
|
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
|
||||||
snprintf(val, 40, "%d", iv->getLastTs(rec));
|
snprintf(mVal, 40, "%d", iv->getLastTs(rec));
|
||||||
publish(topic, val, true);
|
publish(mSubTopic, mVal, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(changed) {
|
if(changed) {
|
||||||
snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
|
snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
|
||||||
publish("status", val, true);
|
publish("status", mVal, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return anyAvail;
|
return anyAvail;
|
||||||
|
@ -517,7 +520,6 @@ class PubMqtt {
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendData(Inverter<> *iv, uint8_t curInfoCmd) {
|
void sendData(Inverter<> *iv, uint8_t curInfoCmd) {
|
||||||
char topic[7 + MQTT_TOPIC_LEN], val[40];
|
|
||||||
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
|
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
|
||||||
|
|
||||||
if (iv->getLastTs(rec) > 0) {
|
if (iv->getLastTs(rec) > 0) {
|
||||||
|
@ -534,9 +536,9 @@ class PubMqtt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
|
||||||
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
|
snprintf(mVal, 40, "%g", ah::round3(iv->getValue(i, rec)));
|
||||||
publish(topic, val, retained);
|
publish(mSubTopic, mVal, retained);
|
||||||
|
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
|
@ -551,7 +553,6 @@ class PubMqtt {
|
||||||
if(mSendList.empty())
|
if(mSendList.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
char topic[7 + MQTT_TOPIC_LEN], val[40];
|
|
||||||
float total[4];
|
float total[4];
|
||||||
bool RTRDataHasBeenSent = false;
|
bool RTRDataHasBeenSent = false;
|
||||||
|
|
||||||
|
@ -623,9 +624,9 @@ class PubMqtt {
|
||||||
retained = false;
|
retained = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
|
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
|
||||||
snprintf(val, 40, "%g", ah::round3(total[i]));
|
snprintf(mVal, 40, "%g", ah::round3(total[i]));
|
||||||
publish(topic, val, retained);
|
publish(mSubTopic, mVal, retained);
|
||||||
}
|
}
|
||||||
RTRDataHasBeenSent = true;
|
RTRDataHasBeenSent = true;
|
||||||
yield();
|
yield();
|
||||||
|
@ -660,7 +661,8 @@ class PubMqtt {
|
||||||
char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT
|
char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT
|
||||||
// global buffer for mqtt topic. Used when publishing mqtt messages.
|
// global buffer for mqtt topic. Used when publishing mqtt messages.
|
||||||
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1];
|
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1];
|
||||||
|
char mSubTopic[32 + MAX_NAME_LENGTH + 1];
|
||||||
|
char mVal[40];
|
||||||
discovery_t mDiscovery;
|
discovery_t mDiscovery;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -386,6 +386,9 @@ class RestApi {
|
||||||
obj[F("cs")] = mConfig->nrf.pinCs;
|
obj[F("cs")] = mConfig->nrf.pinCs;
|
||||||
obj[F("ce")] = mConfig->nrf.pinCe;
|
obj[F("ce")] = mConfig->nrf.pinCe;
|
||||||
obj[F("irq")] = mConfig->nrf.pinIrq;
|
obj[F("irq")] = mConfig->nrf.pinIrq;
|
||||||
|
obj[F("sclk")] = mConfig->nrf.pinSclk;
|
||||||
|
obj[F("mosi")] = mConfig->nrf.pinMosi;
|
||||||
|
obj[F("miso")] = mConfig->nrf.pinMiso;
|
||||||
obj[F("led0")] = mConfig->led.led0;
|
obj[F("led0")] = mConfig->led.led0;
|
||||||
obj[F("led1")] = mConfig->led.led1;
|
obj[F("led1")] = mConfig->led.led1;
|
||||||
}
|
}
|
||||||
|
|
57
src/web/html/about.html
Normal file
57
src/web/html/about.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>About</title>
|
||||||
|
{#HTML_HEADER}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{#HTML_NAV}
|
||||||
|
<div id="wrapper">
|
||||||
|
<div id="content">
|
||||||
|
<div class="my-3"><h2>About AhoyDTU</h2></div>
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="row my-3 head">
|
||||||
|
<div class="p-2">Used Libraries</div>
|
||||||
|
</div>
|
||||||
|
<div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/yubox-node-org/ESPAsyncWebServer" target="_blank">yubox-node-org/ESPAsyncWebServer</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/olikraus/U8g2" target="_blank">olikraus/U8g2</a></div>
|
||||||
|
<div class="row"><a href="https://github.com/zinggjm/GxEPD2" target="_blank">zinggjm/GxEPD2</a></div>
|
||||||
|
|
||||||
|
<div class="row my-3 head">
|
||||||
|
<div class="p-2">Contact Information</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 col-sm-3">Github Repository</div>
|
||||||
|
<div class="col-7 col-sm-9"><a href="https://github.com/lumapu/ahoy">https://github.com/lumapu/ahoy</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 col-sm-3">Discord Chat</div>
|
||||||
|
<div class="col-7 col-sm-9"><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 col-sm-3">E-Mail</div>
|
||||||
|
<div class="col-7 col-sm-9"><a href="mailto:contact@ahoydtu.de">contact@ahoydtu.de</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#HTML_FOOTER}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function parseGeneric(obj) {
|
||||||
|
parseNav(obj);
|
||||||
|
parseESP(obj);
|
||||||
|
parseRssi(obj);
|
||||||
|
}
|
||||||
|
function parse(obj) {
|
||||||
|
if(null != obj) {
|
||||||
|
parseGeneric(obj["generic"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getAjax("/api/html/save", parse);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -74,7 +74,7 @@ function topnav() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNav(obj) {
|
function parseNav(obj) {
|
||||||
for(i = 0; i < 10; i++) {
|
for(i = 0; i < 11; i++) {
|
||||||
if(i == 2)
|
if(i == 2)
|
||||||
continue;
|
continue;
|
||||||
var l = document.getElementById("nav"+i);
|
var l = document.getElementById("nav"+i);
|
||||||
|
|
|
@ -15,11 +15,10 @@
|
||||||
<span class="seperator"></span>
|
<span class="seperator"></span>
|
||||||
<a id="nav8" href="/api" target="_blank">REST API</a>
|
<a id="nav8" href="/api" target="_blank">REST API</a>
|
||||||
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
|
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
|
||||||
|
<a id="nav10" href="/about">About</a>
|
||||||
<span class="seperator"></span>
|
<span class="seperator"></span>
|
||||||
<a id="nav0" class="hide" href="/login">Login</a>
|
<a id="nav0" class="hide" href="/login">Login</a>
|
||||||
<a id="nav1" class="hide" href="/logout">Logout</a>
|
<a id="nav1" class="hide" href="/logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="wifiicon" class="info"></div>
|
<div id="wifiicon" class="info"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -363,6 +363,54 @@
|
||||||
[36, "VP (GPIO36)"],
|
[36, "VP (GPIO36)"],
|
||||||
[39, "VN (GPIO39)"]
|
[39, "VN (GPIO39)"]
|
||||||
];
|
];
|
||||||
|
var esp32s3pins = [
|
||||||
|
[255, "off / default"],
|
||||||
|
[0, "GPIO0 (DONT USE - BOOT)"],
|
||||||
|
[1, "GPIO1"],
|
||||||
|
[2, "GPIO2"],
|
||||||
|
[3, "GPIO3"],
|
||||||
|
[4, "GPIO4"],
|
||||||
|
[5, "GPIO5"],
|
||||||
|
[6, "GPIO6"],
|
||||||
|
[7, "GPIO7"],
|
||||||
|
[8, "GPIO8"],
|
||||||
|
[9, "GPIO9"],
|
||||||
|
[10, "GPIO10"],
|
||||||
|
[11, "GPIO11"],
|
||||||
|
[12, "GPIO12"],
|
||||||
|
[13, "GPIO13"],
|
||||||
|
[14, "GPIO14"],
|
||||||
|
[15, "GPIO15"],
|
||||||
|
[16, "GPIO16"],
|
||||||
|
[17, "GPIO17"],
|
||||||
|
[18, "GPIO18"],
|
||||||
|
[19, "GPIO19 (DONT USE - USB-)"],
|
||||||
|
[20, "GPIO20 (DONT USE - USB+)"],
|
||||||
|
[21, "GPIO21"],
|
||||||
|
[26, "GPIO26 (PSRAM - not available)"],
|
||||||
|
[27, "GPIO27 (FLASH - not available)"],
|
||||||
|
[28, "GPIO28 (FLASH - not available)"],
|
||||||
|
[29, "GPIO29 (FLASH - not available)"],
|
||||||
|
[30, "GPIO30 (FLASH - not available)"],
|
||||||
|
[31, "GPIO31 (FLASH - not available)"],
|
||||||
|
[32, "GPIO32 (FLASH - not available)"],
|
||||||
|
[33, "GPIO33 (not exposed on WROOM modules)"],
|
||||||
|
[34, "GPIO34 (not exposed on WROOM modules)"],
|
||||||
|
[35, "GPIO35"],
|
||||||
|
[36, "GPIO36"],
|
||||||
|
[37, "GPIO37"],
|
||||||
|
[38, "GPIO38"],
|
||||||
|
[39, "GPIO39"],
|
||||||
|
[40, "GPIO40"],
|
||||||
|
[41, "GPIO41"],
|
||||||
|
[42, "GPIO42"],
|
||||||
|
[43, "GPIO43"],
|
||||||
|
[44, "GPIO44"],
|
||||||
|
[45, "GPIO45 (DONT USE - STRAPPING PIN)"],
|
||||||
|
[46, "GPIO46 (DONT USE - STRAPPING PIN)"],
|
||||||
|
[47, "GPIO47"],
|
||||||
|
[48, "GPIO48"],
|
||||||
|
];
|
||||||
|
|
||||||
const re = /11[2,4,6]1.*/;
|
const re = /11[2,4,6]1.*/;
|
||||||
|
|
||||||
|
@ -605,15 +653,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePinout(obj, type) {
|
function parsePinout(obj, type, system) {
|
||||||
var e = document.getElementById("pinout");
|
var e = document.getElementById("pinout");
|
||||||
|
if ("ESP8266" == type) {
|
||||||
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
|
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
|
||||||
|
} else {
|
||||||
|
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
|
||||||
|
}
|
||||||
for(p of pins) {
|
for(p of pins) {
|
||||||
e.append(
|
e.append(
|
||||||
ml("div", {class: "row mb-3"}, [
|
ml("div", {class: "row mb-3"}, [
|
||||||
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
|
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
|
||||||
ml("div", {class: "col-12 col-sm-9"},
|
ml("div", {class: "col-12 col-sm-9"},
|
||||||
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])
|
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]])
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
@ -642,7 +694,7 @@
|
||||||
document.getElementsByName("serIntvl")[0].value = obj["interval"];
|
document.getElementsByName("serIntvl")[0].value = obj["interval"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDisplay(obj, type) {
|
function parseDisplay(obj, type, system) {
|
||||||
for(var i of ["disp_pwr", "disp_pxshift"])
|
for(var i of ["disp_pwr", "disp_pxshift"])
|
||||||
document.getElementsByName(i)[0].checked = obj[i];
|
document.getElementsByName(i)[0].checked = obj[i];
|
||||||
|
|
||||||
|
@ -655,7 +707,7 @@
|
||||||
ml("div", {class: "row mb-3", id: "row_" + p[1]}, [
|
ml("div", {class: "row mb-3", id: "row_" + p[1]}, [
|
||||||
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
|
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
|
||||||
ml("div", {class: "col-12 col-sm-9"},
|
ml("div", {class: "col-12 col-sm-9"},
|
||||||
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])
|
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[1]])
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
@ -720,10 +772,10 @@
|
||||||
parseMqtt(root["mqtt"]);
|
parseMqtt(root["mqtt"]);
|
||||||
parseNtp(root["ntp"]);
|
parseNtp(root["ntp"]);
|
||||||
parseSun(root["sun"]);
|
parseSun(root["sun"]);
|
||||||
parsePinout(root["pinout"], root["system"]["esp_type"]);
|
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
|
||||||
parseRadio(root["radio"]);
|
parseRadio(root["radio"]);
|
||||||
parseSerial(root["serial"]);
|
parseSerial(root["serial"]);
|
||||||
parseDisplay(root["display"], root["system"]["esp_type"]);
|
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
|
||||||
getAjax("/api/inverter/list", parseIv);
|
getAjax("/api/inverter/list", parseIv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,11 @@
|
||||||
#include "html/h/save_html.h"
|
#include "html/h/save_html.h"
|
||||||
#include "html/h/update_html.h"
|
#include "html/h/update_html.h"
|
||||||
#include "html/h/visualization_html.h"
|
#include "html/h/visualization_html.h"
|
||||||
|
#include "html/h/about_html.h"
|
||||||
|
|
||||||
#define WEB_SERIAL_BUF_SIZE 2048
|
#define WEB_SERIAL_BUF_SIZE 2048
|
||||||
|
|
||||||
const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"};
|
const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1"};
|
||||||
|
|
||||||
template <class HMSYSTEM>
|
template <class HMSYSTEM>
|
||||||
class Web {
|
class Web {
|
||||||
|
@ -83,6 +84,7 @@ class Web {
|
||||||
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1),
|
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1),
|
||||||
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
|
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
|
||||||
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
|
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
|
||||||
|
mWeb.on("/about", HTTP_GET, std::bind(&Web::onAbout, this, std::placeholders::_1));
|
||||||
mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1));
|
mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1));
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,18 +143,16 @@ class Web {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Update.hasError()) {
|
if (!Update.hasError()) {
|
||||||
if (Update.write(data, len) != len) {
|
if (Update.write(data, len) != len)
|
||||||
Update.printError(Serial);
|
Update.printError(Serial);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (final) {
|
if (final) {
|
||||||
if (Update.end(true)) {
|
if (Update.end(true))
|
||||||
Serial.printf("Update Success: %uB\n", index + len);
|
Serial.printf("Update Success: %uB\n", index + len);
|
||||||
} else {
|
else
|
||||||
Update.printError(Serial);
|
Update.printError(Serial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||||
if (!index) {
|
if (!index) {
|
||||||
|
@ -245,7 +245,7 @@ class Web {
|
||||||
}
|
}
|
||||||
|
|
||||||
void showUpdate(AsyncWebServerRequest *request) {
|
void showUpdate(AsyncWebServerRequest *request) {
|
||||||
bool reboot = !Update.hasError();
|
bool reboot = (!Update.hasError());
|
||||||
|
|
||||||
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
|
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
|
||||||
if (reboot)
|
if (reboot)
|
||||||
|
@ -521,14 +521,17 @@ class Web {
|
||||||
|
|
||||||
// pinout
|
// pinout
|
||||||
uint8_t pin;
|
uint8_t pin;
|
||||||
for (uint8_t i = 0; i < 5; i++) {
|
for (uint8_t i = 0; i < 8; i++) {
|
||||||
pin = request->arg(String(pinArgNames[i])).toInt();
|
pin = request->arg(String(pinArgNames[i])).toInt();
|
||||||
switch(i) {
|
switch(i) {
|
||||||
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
|
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
|
||||||
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break;
|
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break;
|
||||||
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
|
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
|
||||||
case 3: mConfig->led.led0 = pin; break;
|
case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_SCLK_PIN); break;
|
||||||
case 4: mConfig->led.led1 = pin; break;
|
case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_MOSI_PIN); break;
|
||||||
|
case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_MISO_PIN); break;
|
||||||
|
case 6: mConfig->led.led0 = pin; break;
|
||||||
|
case 7: mConfig->led.led1 = pin; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,6 +618,21 @@ class Web {
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onAbout(AsyncWebServerRequest *request) {
|
||||||
|
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
|
||||||
|
if (mProtected) {
|
||||||
|
checkRedirect(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len);
|
||||||
|
response->addHeader(F("Content-Encoding"), "gzip");
|
||||||
|
response->addHeader(F("content-type"), "text/html; charset=UTF-8");
|
||||||
|
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
void onDebug(AsyncWebServerRequest *request) {
|
void onDebug(AsyncWebServerRequest *request) {
|
||||||
mApp->getSchedulerNames();
|
mApp->getSchedulerNames();
|
||||||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
|
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue