Compare commits
322 Commits
1.4.0-rc.1
...
1.5.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d694ae18c0 | ||
|
|
0f4fa2e406 | ||
|
|
df2af928a5 | ||
|
|
3e6a2d93d4 | ||
|
|
062468c6b1 | ||
|
|
530892dc09 | ||
|
|
c73f74ad45 | ||
|
|
a67ff046b0 | ||
|
|
0f496fee42 | ||
|
|
e63a0dc7eb | ||
|
|
792c6a6668 | ||
|
|
94f6f14795 | ||
|
|
47696a0721 | ||
|
|
5c4ad8cd07 | ||
|
|
f6cfdf3263 | ||
|
|
58b0c18448 | ||
|
|
b985275309 | ||
|
|
1eee184887 | ||
|
|
ba03c9cda3 | ||
|
|
14b1eac732 | ||
|
|
d16f56d280 | ||
|
|
8b50ed48c1 | ||
|
|
f212d9d9a8 | ||
|
|
0e78e71493 | ||
|
|
655313562d | ||
|
|
c8e7724da8 | ||
|
|
f986129c72 | ||
|
|
7b31315242 | ||
|
|
6872cad8ce | ||
|
|
4b1b7f5857 | ||
|
|
a667317412 | ||
|
|
612b17b86f | ||
|
|
7c032ddc2f | ||
|
|
9dd5ee8da5 | ||
|
|
7dbd503e1e | ||
|
|
85932fdc1d | ||
|
|
77d80225ad | ||
|
|
dc68315166 | ||
|
|
5d0ca68dc0 | ||
|
|
d50b70c211 | ||
|
|
dd53d1ef3e | ||
|
|
3bd8010b74 | ||
|
|
6a26e27d39 | ||
|
|
8fa440810c | ||
|
|
95b18385ba | ||
|
|
4457e16a8f | ||
|
|
1965ca671e | ||
|
|
0d1873ec77 | ||
|
|
38ec56fb33 | ||
|
|
bb7c3eeba3 | ||
|
|
0c778d4c7f | ||
|
|
2e5e5e59a8 | ||
|
|
e1623e7b63 | ||
|
|
80b91d9a01 | ||
|
|
25b70e4db5 | ||
|
|
1903ee2bc7 | ||
|
|
916a710064 | ||
|
|
38acae417d | ||
|
|
3bc9fa81a8 | ||
|
|
cc2d6ef385 | ||
|
|
fe93c00204 | ||
|
|
05a2d080be | ||
|
|
664bd7938c | ||
|
|
a78f35328f | ||
|
|
eab47af0e1 | ||
|
|
c524abd959 | ||
|
|
666786fd65 | ||
|
|
8475833dce | ||
|
|
afe710abd3 | ||
|
|
1982843624 | ||
|
|
bf161c1200 | ||
|
|
57f1129cee | ||
|
|
0425cdc499 | ||
|
|
53ff69f03a | ||
|
|
e7cae4b950 | ||
|
|
3ff8f40654 | ||
|
|
d2499a2727 | ||
|
|
5b7da4ed2a | ||
|
|
8d516c7f95 | ||
|
|
d756716497 | ||
|
|
9a2f9d64ec | ||
|
|
0d0926cdac | ||
|
|
3ce3ce5016 | ||
|
|
6ca6d3cab7 | ||
|
|
e7f3c66e05 | ||
|
|
17bd31b2a2 | ||
|
|
8662b9dc8f | ||
|
|
6efa3a52fe | ||
|
|
7e31de6c71 | ||
|
|
b53dae6a43 | ||
|
|
de2318bc6a | ||
|
|
081209420a | ||
|
|
75bc4d5c4a | ||
|
|
527e9cc1d6 | ||
|
|
60b7caf4bc | ||
|
|
e8d0ad0a4e | ||
|
|
afe269aeff | ||
|
|
4702909043 | ||
|
|
4c32ccc450 | ||
|
|
2e3b38e14f | ||
|
|
b6c80f355f | ||
|
|
65b2a3c2bd | ||
|
|
0cb361d243 | ||
|
|
c7f54ca4fb | ||
|
|
1d46176b5e | ||
|
|
5e2f6c9cea | ||
|
|
f439f8c5ba | ||
|
|
bae7770371 | ||
|
|
4e5a3e9da5 | ||
|
|
412e1594e9 | ||
|
|
9701e8c97b | ||
|
|
2fe546812c | ||
|
|
7efcbaa57e | ||
|
|
43c065b97a | ||
|
|
5c1e967fdc | ||
|
|
105a79f72c | ||
|
|
50280f6db3 | ||
|
|
c97e50669c | ||
|
|
43fd095714 | ||
|
|
1bb9b61017 | ||
|
|
1eb10563ed | ||
|
|
222ea4feaa | ||
|
|
ea1406c509 | ||
|
|
b20b450736 | ||
|
|
e8c7f58e67 | ||
|
|
2589020428 | ||
|
|
2bb771a4a7 | ||
|
|
f4af237472 | ||
|
|
5473034421 | ||
|
|
a5996cc93d | ||
|
|
9d6b6c18ab | ||
|
|
e6119dc7ee | ||
|
|
19feb85230 | ||
|
|
0d71a674b6 | ||
|
|
34eabca64a | ||
|
|
d3b28c5bfb | ||
|
|
640935a2b7 | ||
|
|
c0c631aab4 | ||
|
|
861db33765 | ||
|
|
b91266063b | ||
|
|
b087e6e6d3 | ||
|
|
b205f14bae | ||
|
|
7482eb8898 | ||
|
|
2ff84cbddf | ||
|
|
8b6e6be670 | ||
|
|
636872f72d | ||
|
|
c6df74f06e | ||
|
|
24a46f4c16 | ||
|
|
ddc9cf7c90 | ||
|
|
2d74d0c0ad | ||
|
|
e04462811b | ||
|
|
99d82657c0 | ||
|
|
ed50208546 | ||
|
|
fda18cdb13 | ||
|
|
6a9bd9673a | ||
|
|
5aea80e630 | ||
|
|
fcf7d61ca5 | ||
|
|
e71f3868fd | ||
|
|
c3d0d94806 | ||
|
|
e4211c872c | ||
|
|
467cfea449 | ||
|
|
0e3473e065 | ||
|
|
8780e5245a | ||
|
|
94e8288d76 | ||
|
|
261a53207c | ||
|
|
1dbc895cdb | ||
|
|
747d8841bc | ||
|
|
7ed47a4eca | ||
|
|
0ccea290cb | ||
|
|
c03df67900 | ||
|
|
f86857c279 | ||
|
|
acd8348a5b | ||
|
|
cdde3c30af | ||
|
|
392242ef3e | ||
|
|
a6e8953807 | ||
|
|
11b1277d79 | ||
|
|
f62e687d3f | ||
|
|
42fa95969f | ||
|
|
56a0d1322f | ||
|
|
45762967ee | ||
|
|
10f9cde17a | ||
|
|
351a884685 | ||
|
|
98db62cc9e | ||
|
|
355d983437 | ||
|
|
3d11d13631 | ||
|
|
6c4f8a78a0 | ||
|
|
3fb5eb32c3 | ||
|
|
c1447098da | ||
|
|
87b222e7bc | ||
|
|
0eea1b8121 | ||
|
|
7f701a74e7 | ||
|
|
57cf98ca19 | ||
|
|
c32c643442 | ||
|
|
5553a13cc0 | ||
|
|
a9e97c15ad | ||
|
|
dc62f99b7d | ||
|
|
05ff426b28 | ||
|
|
45af7a30d8 | ||
|
|
8aab541afa | ||
|
|
7672c4b927 | ||
|
|
b0a9460257 | ||
|
|
3c69f1295e | ||
|
|
282a02ecdb | ||
|
|
8503ef966f | ||
|
|
a4ee4c5224 | ||
|
|
4478e8f204 | ||
|
|
a50c13fd8a | ||
|
|
ee1e7f92b2 | ||
|
|
5704075682 | ||
|
|
52e4933923 | ||
|
|
00a82ca3e5 | ||
|
|
7658aeaa8c | ||
|
|
790ff5a011 | ||
|
|
935cf27139 | ||
|
|
503068f6e7 | ||
|
|
5c868d589d | ||
|
|
1cca8ffd5d | ||
|
|
f291eb33ac | ||
|
|
f0c505c332 | ||
|
|
7eafe4a90b | ||
|
|
d23527a48b | ||
|
|
c341f86e5c | ||
|
|
939ed6cdab | ||
|
|
2da41707a9 | ||
|
|
460cb01146 | ||
|
|
1b2bc8e200 | ||
|
|
d5acb44648 | ||
|
|
c64cf41757 | ||
|
|
9250bb26f2 | ||
|
|
b2c6eca2d5 | ||
|
|
5cec043015 | ||
|
|
7c9a483677 | ||
|
|
1bfe7a688a | ||
|
|
05104aa8eb | ||
|
|
ca3c318d8f | ||
|
|
2648918dda | ||
|
|
8b50fdec21 | ||
|
|
5b6e23251a | ||
|
|
769daa0857 | ||
|
|
76979531b8 | ||
|
|
7779076498 | ||
|
|
45e2e0334e | ||
|
|
b631b496cb | ||
|
|
36328a1db5 | ||
|
|
a28c7f67b5 | ||
|
|
bb9b6a5f4c | ||
|
|
ce7bd7e23b | ||
|
|
249d32ce35 | ||
|
|
baf8adfb02 | ||
|
|
018a1c5188 | ||
|
|
b600c130f0 | ||
|
|
59eb05726a | ||
|
|
f245f37dfd | ||
|
|
a825412f37 | ||
|
|
935f8bd0a8 | ||
|
|
f78d2f38b8 | ||
|
|
ef083991e3 | ||
|
|
3c0f846335 | ||
|
|
85011ce4ea | ||
|
|
8687e122ca | ||
|
|
d35ea81080 | ||
|
|
cca8ec58b4 | ||
|
|
301b14bbd4 | ||
|
|
41ce9b268e | ||
|
|
646939179e | ||
|
|
73dddd18f0 | ||
|
|
f069de0415 | ||
|
|
f4a4afeb29 | ||
|
|
f9824337dc | ||
|
|
1cd8c6a336 | ||
|
|
63228baebd | ||
|
|
6bb261dfd7 | ||
|
|
a026a962f0 | ||
|
|
db2faad741 | ||
|
|
fbc43dc535 | ||
|
|
31dfc21d69 | ||
|
|
96289cb0f7 | ||
|
|
73da3ee07a | ||
|
|
a14281924f | ||
|
|
3dec390cce | ||
|
|
9a29819d4f | ||
|
|
2af159d566 | ||
|
|
92ca257d32 | ||
|
|
44b6620431 | ||
|
|
b89f61ed58 | ||
|
|
ab1566bd45 | ||
|
|
86734ab622 | ||
|
|
0a8dd2a076 | ||
|
|
a7a561622e | ||
|
|
b0e0f6fd7d | ||
|
|
53eaa1d7f1 | ||
|
|
a7d796e0cc | ||
|
|
4490b38130 | ||
|
|
0ede2240a2 | ||
|
|
0cff35ee12 | ||
|
|
14aef20234 | ||
|
|
560f8fbd51 | ||
|
|
946414ad31 | ||
|
|
39a29042e1 | ||
|
|
f544f01caa | ||
|
|
41cca76bfa | ||
|
|
942bc53043 | ||
|
|
1bad689b6b | ||
|
|
2f4dbcc205 | ||
|
|
9e3ef7a465 | ||
|
|
a5f6749101 | ||
|
|
b07dd46f55 | ||
|
|
07ab121788 | ||
|
|
7cbc52a8b0 | ||
|
|
e090be380c | ||
|
|
0bf49d2249 | ||
|
|
a83d94d361 | ||
|
|
358980da4c | ||
|
|
f91e39d067 | ||
|
|
1d53f21d46 | ||
|
|
c225e7c2a8 | ||
|
|
6831c4331f | ||
|
|
8fb62ce8ae | ||
|
|
e829a00355 | ||
|
|
bee720386a | ||
|
|
c4b6eadb81 | ||
|
|
a5d2b9fcfa |
11
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
22
.github/workflows/pio-dependabot.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: PlatformIO Dependabot
|
||||
on:
|
||||
workflow_dispatch: # option to manually trigger the workflow
|
||||
schedule:
|
||||
# Runs every day at 00:00
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
name: run PlatformIO Dependabot
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: run PlatformIO Dependabot
|
||||
uses: peterus/platformio_dependabot@v1.2.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
11
.gitignore
vendored
@@ -1,5 +1,14 @@
|
||||
.pio
|
||||
.vscode
|
||||
build/*.bin
|
||||
.PVS-Studio
|
||||
build/*
|
||||
data/*
|
||||
managed_components/*
|
||||
node_modules/*
|
||||
secrets.ini
|
||||
package-lock.json
|
||||
*.lock
|
||||
sdkconfig.*
|
||||
CMakeLists.txt
|
||||
!sdkconfig.defaults
|
||||
!.gitkeep
|
||||
83
README.md
@@ -1,50 +1,55 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||
<br>
|
||||
[](https://github.com/Laxilef/OTGateway/releases)
|
||||
|
||||
[](https://github.com/Laxilef/OTGateway/releases)
|
||||
[](https://github.com/Laxilef/OTGateway/releases/latest)
|
||||
[](LICENSE.txt)
|
||||
[](https://t.me/otgateway)
|
||||
|
||||
|
||||
</div>
|
||||
<hr />
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
- Hot water temperature control
|
||||
- DHW temperature control
|
||||
- Heating temperature control
|
||||
- Smart heating temperature control modes:
|
||||
- PID
|
||||
- Equithermic curves - adjusts the temperature based on indoor and outdoor temperatures
|
||||
- Hysteresis setting (for accurate maintenance of room temperature)
|
||||
- Ability to connect an external sensors to monitor outdoor and indoor temperature ([compatible sensors](#compatible-temperature-sensors))
|
||||
- Emergency mode. If the Wi-Fi connection is lost or the gateway cannot connect to the MQTT server, the mode will turn on. This mode will automatically maintain the set temperature and prevent your home from freezing. In this mode it is also possible to use equithermal curves (weather-compensated control).
|
||||
- Automatic error reset (not with all boilers)
|
||||
- Hysteresis setting _(for accurate maintenance of room temperature)_
|
||||
- Ability to connect [additional (external) sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#temperature-sensors): Dallas (1-wire), NTC 10k, Bluetooth (BLE). Makes it possible to monitor indoor and outdoor temperatures, temperatures on pipes/heat exchangers/etc.
|
||||
- Emergency mode. In any dangerous situation _(loss of connection with Wifi, MQTT, sensors, etc)_ it will not let you and your home freeze.
|
||||
- Ability of remote fault reset _(not with all boilers)_
|
||||
- Diagnostics:
|
||||
- The process of heating: works/does not work
|
||||
- The process of heating water for hot water: working/not working
|
||||
- Display of boiler errors
|
||||
- Burner status (flame): on/off
|
||||
- Burner modulation level in percent
|
||||
- Pressure in the heating system
|
||||
- Gateway status (depending on errors and connection status)
|
||||
- Boiler connection status via OpenTherm interface
|
||||
- The current temperature of the heat carrier (usually the return heat carrier)
|
||||
- Set heat carrier temperature (depending on the selected mode)
|
||||
- Current hot water temperature
|
||||
- Auto tuning of PID and Equitherm parameters *(in development)*
|
||||
- Displaying gateway status
|
||||
- Displaying the connection status to the boiler via OpenTherm
|
||||
- Displaying the fault status and fault code
|
||||
- Displaying the diagnostic status & diagnostic code
|
||||
- Display of the process of heating: works/does not work
|
||||
- Display of burner (flame) status: on/off
|
||||
- Display of burner modulation level in percent
|
||||
- Display of pressure in the heating system
|
||||
- Display of current temperature of the heat carrier
|
||||
- Display of return temperature of the heat carrier
|
||||
- Display of setpoint heat carrier temperature (useful when using PID or Equitherm)
|
||||
- Display of the process of DHW: working/not working
|
||||
- Display of current DHW temperature
|
||||
- _And other information..._
|
||||
- [Home Assistant](https://www.home-assistant.io/) integration via MQTT. The ability to create any automation for the boiler!
|
||||
|
||||

|
||||
|
||||
## Documentation
|
||||
All available information and instructions can be found in the wiki:
|
||||
|
||||
* [Home](https://github.com/Laxilef/OTGateway/wiki)
|
||||
* [Quick Start](https://github.com/Laxilef/OTGateway/wiki#quick-start)
|
||||
* [Build firmware](https://github.com/Laxilef/OTGateway/wiki#build-firmware)
|
||||
* [Flash firmware via ESP Flash Download Tool](https://github.com/Laxilef/OTGateway/wiki#flash-firmware-via-esp-flash-download-tool)
|
||||
* [HomeAsssistant settings](https://github.com/Laxilef/OTGateway/wiki#homeasssistant-settings)
|
||||
* [Flashing via Web Flasher](https://github.com/Laxilef/OTGateway/wiki#flashing-via-web-flasher)
|
||||
* [Flashing via ESP Flash Download Tool](https://github.com/Laxilef/OTGateway/wiki#flashing-via-esp-flash-download-tool)
|
||||
* [Settings](https://github.com/Laxilef/OTGateway/wiki#settings)
|
||||
* [External temperature sensors](https://github.com/Laxilef/OTGateway/wiki#external-temperature-sensors)
|
||||
* [Other external sensors](https://github.com/Laxilef/OTGateway/wiki#other-external-sensors)
|
||||
* [Reporting indoor/outdoor temperature from any Home Assistant sensor](https://github.com/Laxilef/OTGateway/wiki#reporting-indooroutdoor-temperature-from-any-home-assistant-sensor)
|
||||
* [Reporting outdoor temperature from Home Assistant weather integration](https://github.com/Laxilef/OTGateway/wiki#reporting-outdoor-temperature-from-home-assistant-weather-integration)
|
||||
* [DHW meter](https://github.com/Laxilef/OTGateway/wiki#dhw-meter)
|
||||
@@ -53,6 +58,7 @@ All available information and instructions can be found in the wiki:
|
||||
* [Ratios](https://github.com/Laxilef/OTGateway/wiki#ratios)
|
||||
* [Fit coefficients](https://github.com/Laxilef/OTGateway/wiki#fit-coefficients)
|
||||
* [PID mode](https://github.com/Laxilef/OTGateway/wiki#pid-mode)
|
||||
* [Logs and debug](https://github.com/Laxilef/OTGateway/wiki#logs-and-debug)
|
||||
* [Compatibility](https://github.com/Laxilef/OTGateway/wiki/Compatibility)
|
||||
* [Boilers](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boilers)
|
||||
* [Boards](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boards)
|
||||
@@ -65,21 +71,10 @@ All available information and instructions can be found in the wiki:
|
||||
* [Connection](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#connection)
|
||||
* [Leds on board](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#leds-on-board)
|
||||
|
||||
|
||||
|
||||
## Dependencies
|
||||
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler) (for ESP8266)
|
||||
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
|
||||
- [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient) /
|
||||
- [ESPTelnet](https://github.com/LennartHennigs/ESPTelnet)
|
||||
- [FileData](https://github.com/GyverLibs/FileData)
|
||||
- [GyverPID](https://github.com/GyverLibs/GyverPID)
|
||||
- [GyverBlinker](https://github.com/GyverLibs/GyverBlinker)
|
||||
- [DallasTemperature](https://github.com/milesburton/Arduino-Temperature-Control-Library)
|
||||
- [TinyLogger](https://github.com/laxilef/TinyLogger)
|
||||
|
||||
## Debug
|
||||
To display DEBUG messages you must enable debug in settings (switch is disabled by default).
|
||||
You can connect via Telnet to read messages. IP: ESP8266 ip, port: 23
|
||||
## Gratitude
|
||||
* To the developers of the libraries used: [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library), [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler), [ArduinoJson](https://github.com/bblanchon/ArduinoJson), [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino), [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient), [ESPTelnet](https://github.com/LennartHennigs/ESPTelnet), [FileData](https://github.com/GyverLibs/FileData), [GyverPID](https://github.com/GyverLibs/GyverPID), [GyverBlinker](https://github.com/GyverLibs/GyverBlinker), [FileData](https://github.com/GyverLibs/FileData), [OneWireNg](https://github.com/pstolarz/OneWireNg) & [OneWire](https://github.com/PaulStoffregen/OneWire)
|
||||
* To the [PlatformIO](https://platformio.org/) Team
|
||||
* To the team and contributors of the [pioarduino](https://github.com/pioarduino/platform-espressif32) project
|
||||
* To the [BrowserStack](https://www.browserstack.com/) team. This project is tested with BrowserStack.
|
||||
* To the [PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code.
|
||||
* And of course to the contributors for their contribution to the development of the project!
|
||||
BIN
assets/2D_PCB_bottom.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
assets/2D_PCB_top.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
BIN
assets/3D_PCB.png
Normal file
|
After Width: | Height: | Size: 849 KiB |
BIN
assets/BOM.xlsx
BIN
assets/CPL.csv
24409
assets/Schematic.pdf
12
assets/blueprint_import.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="40" aria-label="Import blueprint to My Home Assistant" style="border-radius:24px;width:auto" viewBox="0 0 592 96">
|
||||
<rect width="592" height="96" fill="#18BCF2" rx="48"/>
|
||||
<path fill="#fff" d="M42.55 60.2h4.59V36.75h-4.59Zm10.35 0h4.59V43.99l7.23 10.58 7.24-10.62V60.2h4.56V36.75h-4.56v.03l-.4-.03-6.84 10.12-6.83-10.12-.4.03v-.03H52.9Zm29.38 0h4.59v-7.77h4.49c4.72 0 8.07-3.29 8.07-7.87 0-4.59-3.48-7.88-8.44-7.84l-4.66.03h-4.05Zm8.61-19.26c2.27-.04 3.88 1.47 3.88 3.62 0 2.14-1.47 3.65-3.51 3.65h-4.39v-7.27Zm22.98 19.66c6.97 0 11.92-5.02 11.92-12.09 0-7.1-4.95-12.13-12.02-12.13-7.04 0-12 4.99-12 12.13 0 7.07 5 12.09 12.1 12.09m0-4.19c-4.36 0-7.41-3.28-7.41-7.9 0-4.66 3.02-7.94 7.31-7.94 4.32 0 7.33 3.28 7.33 7.94 0 4.62-2.98 7.9-7.23 7.9m16.31 3.79h4.59v-8.31h3.52l4.79 8.31h5.19l-5.42-9.11c2.71-1.21 4.48-3.69 4.48-6.77 0-4.45-3.48-7.64-8.44-7.6l-4.65.03h-4.06Zm8.51-19.26c2.28 0 3.89 1.37 3.89 3.38 0 1.98-1.61 3.38-3.65 3.38h-4.16v-6.76Zm18.33 19.26h4.59V40.94h7v-4.19h-18.63v4.19h7.04Zm23.91 0h9.02c4.69 0 7.77-2.41 7.77-6.7 0-2.65-1.24-4.42-3.42-5.53 1.64-1.17 2.61-2.78 2.61-4.55 0-4.43-3.21-6.7-8.04-6.7l-3.45.03h-4.49Zm7.68-19.8c2.21-.03 3.61 1.07 3.61 2.95s-1.27 2.95-3.25 2.95h-3.55v-5.9Zm.53 9.65c2.41-.03 3.89 1.17 3.89 3.08 0 1.81-1.34 3.02-3.52 3.02h-4.09v-6.1h.24Zm12.97 10.15h14.9v-4.19H206.7V36.75h-4.59Zm18.32-8.74c0 5.62 3.69 9.21 9.51 9.21 5.9 0 9.65-3.59 9.65-9.21V36.75H235v14.71c0 3.01-1.97 4.95-4.99 4.95-3.01 0-4.99-1.94-4.99-4.95V36.75h-4.59Zm24.59 8.74h14.97v-4.19h-10.38v-5.66h8.84v-4.09h-8.84v-5.32h10.25v-4.19h-14.84Zm20.4 0h4.59v-7.77h4.49c4.72 0 8.07-3.29 8.07-7.87 0-4.59-3.48-7.88-8.44-7.84l-4.66.03h-4.05Zm8.61-19.26c2.28-.04 3.89 1.47 3.89 3.62 0 2.14-1.48 3.65-3.52 3.65h-4.39v-7.27Zm12.26 19.26h4.59v-8.31h3.52l4.79 8.31h5.19l-5.43-9.11c2.72-1.21 4.49-3.69 4.49-6.77 0-4.45-3.48-7.64-8.44-7.6l-4.65.03h-4.06Zm8.51-19.26c2.28 0 3.89 1.37 3.89 3.38 0 1.98-1.61 3.38-3.65 3.38h-4.16v-6.76Zm13.77 19.26h4.59V36.75h-4.59Zm10.35 0 4.59.03V44.12l11.66 16.08h4.55V36.75h-4.55v15.81l-11.46-15.81h-4.79Zm31.52 0h4.59V40.94h7.01v-4.19h-18.63v4.19h7.03Zm28.88 0h4.59V40.94h7v-4.19h-18.62v4.19h7.03Zm26 .4c6.97 0 11.92-5.02 11.92-12.09 0-7.1-4.95-12.13-12.02-12.13-7.04 0-12 4.99-12 12.13 0 7.07 5 12.09 12.1 12.09m0-4.19c-4.36 0-7.41-3.28-7.41-7.9 0-4.66 3.02-7.94 7.31-7.94 4.32 0 7.33 3.28 7.33 7.94 0 4.62-2.98 7.9-7.23 7.9"/>
|
||||
<g style="transform:translate(95px,0)">
|
||||
<rect width="137" height="64" x="344" y="16" fill="#F2F4F9" rx="32"/>
|
||||
<path fill="#18BCF2" d="M394.419 37.047V60.5h-4.297V46.797L384.716 60.5h-4.157l-5.343-13.594V60.5h-4.188V37.047h4.188l7.422 18.36 7.484-18.36zm9.365 0 5.344 9.89 5.344-9.89h4.766l-7.969 14.14V60.5h-4.391v-9.312l-8.031-14.141zM457 60c0 1.65-1.35 3-3 3h-24c-1.65 0-3-1.35-3-3v-9c0-1.65.95-3.95 2.12-5.12l10.76-10.76a3 3 0 0 1 4.24 0l10.76 10.76c1.17 1.17 2.12 3.47 2.12 5.12z"/>
|
||||
<path fill="#F2F4F9" stroke="#F2F4F9" d="M442 45.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
|
||||
<path fill="#F2F4F9" stroke="#F2F4F9" stroke-miterlimit="10" d="M449.5 53.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM434.5 57.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
|
||||
<path fill="none" stroke="#F2F4F9" stroke-miterlimit="10" stroke-width="2.25" d="M442 43.48V63l-7.5-7.5M449.5 51.46l-7.41 7.41"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/ha.png
|
Before Width: | Height: | Size: 190 KiB |
@@ -1,3 +1,6 @@
|
||||
# Package for Home Assistant Packages
|
||||
# More info: https://www.home-assistant.io/docs/configuration/packages/
|
||||
|
||||
dhw_meter:
|
||||
sensor:
|
||||
- platform: integration
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Script for reporting outdoor temperature to the controller from home assistant weather integration
|
||||
# Updated: 07.12.2023
|
||||
|
||||
alias: Report outdoor temp to controller from weather
|
||||
description: ""
|
||||
variables:
|
||||
# The source weather from which we take the temperature
|
||||
source_entity: "weather.home"
|
||||
|
||||
# Target entity number where we set the temperature
|
||||
# If the prefix has not changed, then you do not need to change it
|
||||
target_entity: "number.opentherm_outdoor_temp"
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ state_attr(source_entity, 'temperature')|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: "{{ target_entity }}"
|
||||
mode: single
|
||||
@@ -1,30 +0,0 @@
|
||||
# Script for reporting indoor/outdoor temperature to the controller from any home assistant sensor
|
||||
# Updated: 07.12.2023
|
||||
|
||||
alias: Report temp to controller
|
||||
description: ""
|
||||
variables:
|
||||
# The source sensor from which we take the temperature
|
||||
source_entity: "sensor.livingroom_temperature"
|
||||
|
||||
# Target entity number where we set the temperature
|
||||
# To report indoor temperature: number.opentherm_indoor_temp
|
||||
# To report outdoor temperature: number.opentherm_outdoor_temp
|
||||
target_entity: "number.opentherm_indoor_temp"
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ states(source_entity)|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: "{{ target_entity }}"
|
||||
mode: single
|
||||
48
assets/ha/report_temp_to_otgateway.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
# Blueprint for reporting indoor/outdoor temperature to OpenTherm Gateway from any home assistant sensor
|
||||
# Updated: 03.09.2024
|
||||
|
||||
blueprint:
|
||||
name: Report temp to OpenTherm Gateway
|
||||
domain: automation
|
||||
author: "Laxilef"
|
||||
|
||||
input:
|
||||
source_entity:
|
||||
name: Source entity
|
||||
description: "Temperature data source"
|
||||
selector:
|
||||
entity:
|
||||
multiple: false
|
||||
filter:
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
target_entity:
|
||||
name: Target entity
|
||||
description: "Usually ``number.opentherm_indoor_temp`` or ``number.opentherm_outdoor_temp``"
|
||||
default: "number.opentherm_indoor_temp"
|
||||
selector:
|
||||
entity:
|
||||
multiple: false
|
||||
filter:
|
||||
- domain: number
|
||||
|
||||
mode: single
|
||||
variables:
|
||||
source_entity: !input source_entity
|
||||
target_entity: !input target_entity
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ states(source_entity)|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: "{{ target_entity }}"
|
||||
47
assets/ha/report_temp_to_otgateway_from_weather.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Blueprint for reporting temperature to OpenTherm Gateway from home assistant weather integration
|
||||
# Updated: 03.09.2024
|
||||
|
||||
blueprint:
|
||||
name: Report temp to OpenTherm Gateway from Weather
|
||||
domain: automation
|
||||
author: "Laxilef"
|
||||
|
||||
input:
|
||||
source_entity:
|
||||
name: Source entity
|
||||
description: "Temperature data source"
|
||||
selector:
|
||||
entity:
|
||||
multiple: false
|
||||
filter:
|
||||
- domain: weather
|
||||
target_entity:
|
||||
name: Target entity
|
||||
description: "Usually ``number.opentherm_outdoor_temp``"
|
||||
default: "number.opentherm_outdoor_temp"
|
||||
selector:
|
||||
entity:
|
||||
multiple: false
|
||||
filter:
|
||||
- domain: number
|
||||
|
||||
mode: single
|
||||
variables:
|
||||
source_entity: !input source_entity
|
||||
target_entity: !input target_entity
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ state_attr(source_entity, 'temperature')|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: "{{ target_entity }}"
|
||||
|
Before Width: | Height: | Size: 675 KiB |
|
Before Width: | Height: | Size: 154 KiB |
BIN
assets/poster-1.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/poster-2.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/poster-3.png
Normal file
|
After Width: | Height: | Size: 973 KiB |
0
data/.gitkeep
Normal file
230
data/index.html
@@ -1,230 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenTherm Gateway</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/app.css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Network</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div class="main-busy" aria-busy="true"></div>
|
||||
<table class="main-table hidden">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Hostname:</th>
|
||||
<td><b class="network-hostname"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MAC:</th>
|
||||
<td><b class="network-mac"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Connected:</th>
|
||||
<td><input type="radio" class="network-connected" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">SSID:</th>
|
||||
<td><b class="network-ssid"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Signal:</th>
|
||||
<td><b class="network-signal"></b> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">IP:</th>
|
||||
<td><b class="network-ip"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Subnet:</th>
|
||||
<td><b class="network-subnet"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Gateway:</th>
|
||||
<td><b class="network-gateway"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">DNS:</th>
|
||||
<td><b class="network-dns"></b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="grid">
|
||||
<a href="/network.html" role="button">Network settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>System</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div class="system-busy" aria-busy="true"></div>
|
||||
<table class="system-table hidden">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Version:</th>
|
||||
<td><b class="version"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Build date:</th>
|
||||
<td><b class="build-date"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Uptime:</th>
|
||||
<td><b class="uptime-days"></b> days, <b class="uptime-hours"></b> hours, <b class="uptime-min"></b> min., <b class="uptime-sec"></b> sec.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Free memory:</th>
|
||||
<td><b class="free-heap"></b> of <b class="total-heap"></b> bytes (min: <b class="min-free-heap"></b> bytes)<br>max free block: <b class="max-free-block-heap"></b> bytes (min: <b class="min-max-free-block-heap"></b> bytes)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last reset reason:</th>
|
||||
<td><b class="reset-reason"></b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="grid">
|
||||
<a href="/settings.html" role="button">Settings</a>
|
||||
<a href="/upgrade.html" role="button">Upgrade</a>
|
||||
<a href="/restart.html" role="button" class="secondary restart">Restart</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>States and sensors</h2>
|
||||
<p>More information and settings can be found in your home assistant after setting up network and MQTT</p>
|
||||
</hgroup>
|
||||
|
||||
<div class="ot-busy" aria-busy="true"></div>
|
||||
<table class="ot-table hidden">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">OpenTherm connected:</th>
|
||||
<td><input type="radio" class="ot-connected" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MQTT connected:</th>
|
||||
<td><input type="radio" class="mqtt-connected" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Emergency:</th>
|
||||
<td><input type="radio" class="ot-emergency" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Heating:</th>
|
||||
<td><input type="radio" class="ot-heating" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">DHW:</th>
|
||||
<td><input type="radio" class="ot-dhw" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Flame:</th>
|
||||
<td><input type="radio" class="ot-flame" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Fault:</th>
|
||||
<td><input type="radio" class="ot-fault" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Diagnostic:</th>
|
||||
<td><input type="radio" class="ot-diagnostic" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">External pump:</th>
|
||||
<td><input type="radio" class="ot-external-pump" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Modulation:</th>
|
||||
<td><b class="ot-modulation"></b> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Pressure:</th>
|
||||
<td><b class="ot-pressure"></b> bar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">DHW flow rate:</th>
|
||||
<td><b class="ot-dhw-flow-rate"></b> l/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Fault code:</th>
|
||||
<td><b class="ot-fault-code"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Indoor temp:</th>
|
||||
<td><b class="indoor-temp"></b> C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Outdoor temp:</th>
|
||||
<td><b class="outdoor-temp"></b> C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Heating temp:</th>
|
||||
<td><b class="heating-temp"></b> C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Heating setpoint temp:</th>
|
||||
<td><b class="heating-setpoint-temp"></b> C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">DHW temp:</th>
|
||||
<td><b class="dhw-temp"></b> C</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
setTimeout(async function onLoadPage() {
|
||||
await loadNetworkStatus();
|
||||
await loadVars();
|
||||
|
||||
setTimeout(onLoadPage, 10000);
|
||||
}, 1000);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,166 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Network - OpenTherm Gateway</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/app.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Network settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="network-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="network-settings" class="hidden">
|
||||
<label for="hostname">
|
||||
Hostname
|
||||
<input type="text" class="network-hostname" name="hostname" maxlength="24" pattern="[A-Za-z0-9]+[A-Za-z0-9\-]+[A-Za-z0-9]+" required>
|
||||
</label>
|
||||
<label for="network-use-dhcp">
|
||||
<input type="checkbox" class="network-use-dhcp" name="useDhcp" value="true">
|
||||
Use DHCP
|
||||
</label>
|
||||
<br>
|
||||
<hr>
|
||||
<label for="network-static-ip">
|
||||
Static IP:
|
||||
<input type="text" class="network-static-ip" name="staticConfig[ip]" value="true" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
<label for="network-static-gateway">
|
||||
Static gateway:
|
||||
<input type="text" class="network-static-gateway" name="staticConfig[gateway]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
<label for="network-static-subnet">
|
||||
Static subnet:
|
||||
<input type="text" class="network-static-subnet" name="staticConfig[subnet]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
<label for="network-static-dns">
|
||||
Static DNS:
|
||||
<input type="text" class="network-static-dns" name="staticConfig[dns]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h3>Available networks</h3>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/network/scan" id="network-scan">
|
||||
<figure style="max-height: 25em;">
|
||||
<table id="networks" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">SSID</th>
|
||||
<th scope="col">Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<button type="submit">Refresh</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>WiFi settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
<div id="sta-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="sta-settings" class="hidden">
|
||||
<label for="sta-ssid">
|
||||
SSID:
|
||||
<input type="text" class="sta-ssid" name="sta[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
<label for="sta-password">
|
||||
Password:
|
||||
<input type="password" class="sta-password" name="sta[password]" maxlength="64" required>
|
||||
</label>
|
||||
<label for="sta-channel">
|
||||
Channel:
|
||||
<input type="number" inputmode="numeric" class="sta-channel" name="sta[channel]" min="0" max="12" step="1" required>
|
||||
<small>set 0 for auto select</small>
|
||||
</label>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>AP settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
<div id="ap-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="ap-settings" class="hidden">
|
||||
<label for="ap-ssid">
|
||||
SSID:
|
||||
<input type="text" class="ap-ssid" name="ap[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
<label for="ap-password">
|
||||
Password:
|
||||
<input type="text" class="ap-password" name="ap[password]" maxlength="64" required>
|
||||
</label>
|
||||
<label for="ap-channel">
|
||||
Channel:
|
||||
<input type="number" inputmode="numeric" class="ap-channel" name="ap[channel]" min="1" max="12" step="1" required>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
await loadNetworkSettings();
|
||||
|
||||
setupForm('#network-settings');
|
||||
setupNetworkScanForm('#network-scan', '#networks');
|
||||
setupForm('#sta-settings');
|
||||
setupForm('#ap-settings');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,357 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Settings - OpenTherm Gateway</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/app.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Portal settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="portal-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="portal-settings" class="hidden">
|
||||
<div class="grid">
|
||||
<label for="portal-login">
|
||||
Login
|
||||
<input type="text" class="portal-login" name="portal[login]" maxlength="12" required>
|
||||
</label>
|
||||
<label for="portal-password">
|
||||
Password
|
||||
<input type="password" class="portal-password" name="portal[password]" maxlength="32" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label for="portal-use-auth">
|
||||
<input type="checkbox" class="portal-use-auth" name="portal[useAuth]" value="true">
|
||||
Use auth
|
||||
</label>
|
||||
<br>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>OpenTherm settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="opentherm-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="opentherm-settings" class="hidden">
|
||||
<div class="grid">
|
||||
<label for="opentherm-in-pin">
|
||||
In GPIO
|
||||
<input type="number" inputmode="numeric" class="opentherm-in-pin" name="opentherm[inPin]" min="0" max="99" step="1" required>
|
||||
</label>
|
||||
<label for="opentherm-in-pin">
|
||||
Out GPIO
|
||||
<input type="number" inputmode="numeric" class="opentherm-out-pin" name="opentherm[outPin]" min="0" max="99" step="1" required>
|
||||
</label>
|
||||
<label for="opentherm-member-id-code">
|
||||
Master MemberID code
|
||||
<input type="number" inputmode="numeric" class="opentherm-member-id-code" name="opentherm[memberIdCode]" min="0" max="65535" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<mark>After changing GPIO, the ESP must be restarted for the changes to take effect.</mark>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Options</legend>
|
||||
<label for="opentherm-dhw-present">
|
||||
<input type="checkbox" class="opentherm-dhw-present" name="opentherm[dhwPresent]" value="true">
|
||||
DHW present
|
||||
</label>
|
||||
<label for="opentherm-sw-mode">
|
||||
<input type="checkbox" class="opentherm-sw-mode" name="opentherm[summerWinterMode]" value="true">
|
||||
Summer/winter mode
|
||||
</label>
|
||||
<label for="opentherm-heating-ch2-enabled">
|
||||
<input type="checkbox" class="opentherm-heating-ch2-enabled" name="opentherm[heatingCh2Enabled]" value="true">
|
||||
Heating CH2 always enabled
|
||||
</label>
|
||||
<label for="opentherm-heating-ch1-to-ch2">
|
||||
<input type="checkbox" class="opentherm-heating-ch1-to-ch2" name="opentherm[heatingCh1ToCh2]" value="true">
|
||||
Duplicate heating CH1 to CH2
|
||||
</label>
|
||||
<label for="opentherm-dhw-to-ch2">
|
||||
<input type="checkbox" class="opentherm-dhw-to-ch2" name="opentherm[dhwToCh2]" value="true">
|
||||
Duplicate DHW to CH2
|
||||
</label>
|
||||
<label for="opentherm-dhw-blocking">
|
||||
<input type="checkbox" class="opentherm-dhw-blocking" name="opentherm[dhwBlocking]" value="true">
|
||||
DHW blocking
|
||||
</label>
|
||||
<label for="opentherm-sync-modulation-with-heating">
|
||||
<input type="checkbox" class="opentherm-sync-modulation-with-heating" name="opentherm[modulationSyncWithHeating]" value="true">
|
||||
Sync modulation with heating
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>MQTT settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="mqtt-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="mqtt-settings" class="hidden">
|
||||
<div class="grid">
|
||||
<label for="mqtt-server">
|
||||
Server
|
||||
<input type="text" class="mqtt-server" name="mqtt[server]" maxlength="80" required>
|
||||
</label>
|
||||
<label for="mqtt-port">
|
||||
Port
|
||||
<input type="number" inputmode="numeric" class="mqtt-port" name="mqtt[port]" min="1" max="65535" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="mqtt-user">
|
||||
User
|
||||
<input type="text" class="mqtt-user" name="mqtt[user]" maxlength="32" required>
|
||||
</label>
|
||||
<label for="mqtt-password">
|
||||
Password
|
||||
<input type="password" class="mqtt-password" name="mqtt[password]" maxlength="32">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="mqtt-prefix">
|
||||
Prefix
|
||||
<input type="text" class="mqtt-prefix" name="mqtt[prefix]" maxlength="32" required>
|
||||
</label>
|
||||
<label for="mqtt-interval">
|
||||
Publish interval <small>(sec)</small>
|
||||
<input type="number" inputmode="numeric" class="mqtt-interval" name="mqtt[interval]" min="3" max="60" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Outdoor sensor settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="outdoor-sensor-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="outdoor-sensor-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend>Source type</legend>
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="0" />
|
||||
From boiler via OpenTherm
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="1" />
|
||||
Manual via MQTT/API
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" />
|
||||
External (DS18B20)
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="outdoor-sensor-pin">
|
||||
GPIO
|
||||
<input type="number" inputmode="numeric" class="outdoor-sensor-pin" name="sensors[outdoor][pin]" min="0" max="99" step="1" required>
|
||||
</label>
|
||||
<label for="outdoor-sensor-offset">
|
||||
Temp offset (calibration)
|
||||
<input type="number" inputmode="numeric" class="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-20" max="20" step="0.01" required>
|
||||
</label>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Indoor sensor settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="indoor-sensor-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="indoor-sensor-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend>Source type</legend>
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="1" />
|
||||
Manual via MQTT/API
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="2" />
|
||||
External (DS18B20)
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="3" />
|
||||
BLE device <i>(ONLY for some ESP32 which support BLE)</i>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="indoor-sensor-pin">
|
||||
GPIO
|
||||
<input type="number" inputmode="numeric" class="indoor-sensor-pin" name="sensors[indoor][pin]" min="0" max="99" step="1" required>
|
||||
</label>
|
||||
|
||||
<div class="grid">
|
||||
<label for="indoor-sensor-offset">
|
||||
Temp offset (calibration)
|
||||
<input type="number" inputmode="numeric" class="indoor-sensor-offset" name="sensors[indoor][offset]" min="-20" max="20" step="0.01" required>
|
||||
</label>
|
||||
<label for="indoor-sensor-ble-addresss">
|
||||
BLE addresss
|
||||
<input type="text" class="indoor-sensor-ble-addresss" name="sensors[indoor][bleAddresss]" pattern="([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}">
|
||||
<small>ONLY for some ESP32 which support BLE</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>External pump settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="extpump-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="extpump-settings" class="hidden">
|
||||
<label for="extpump-use">
|
||||
<input type="checkbox" class="extpump-use" name="externalPump[use]" value="true">
|
||||
Use external pump
|
||||
</label>
|
||||
<br>
|
||||
|
||||
<div class="grid">
|
||||
<label for="extpump-pin">
|
||||
Relay GPIO
|
||||
<input type="number" inputmode="numeric" class="extpump-pin" name="externalPump[pin]" min="0" max="99" step="1" required>
|
||||
</label>
|
||||
<label for="extpump-pc-time">
|
||||
Post circulation time <small>(min)</small>
|
||||
<input type="number" inputmode="numeric" class="extpump-pc-time" name="externalPump[postCirculationTime]" min="1" max="120" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="extpump-as-interval">
|
||||
Anti stuck interval <small>(days)</small>
|
||||
<input type="number" inputmode="numeric" class="extpump-as-interval" name="externalPump[antiStuckInterval]" min="1" max="366" step="1" required>
|
||||
</label>
|
||||
<label for="extpump-as-time">
|
||||
Anti stuck time <small>(min)</small>
|
||||
<input type="number" inputmode="numeric" class="extpump-as-time" name="externalPump[antiStuckTime]" min="1" max="20" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>System settings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="system-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="system-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="system-debug">
|
||||
<input type="checkbox" class="system-debug" name="system[debug]" value="true">
|
||||
Debug mode
|
||||
</label>
|
||||
<label for="system-use-serial">
|
||||
<input type="checkbox" class="system-use-serial" name="system[useSerial]" value="true">
|
||||
Enable serial port
|
||||
</label>
|
||||
<label for="system-use-telnet">
|
||||
<input type="checkbox" class="system-use-telnet" name="system[useTelnet]" value="true">
|
||||
Enable telnet
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<mark>After changing this settings, the ESP must be restarted for the changes to take effect.</mark>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
await loadSettings();
|
||||
|
||||
setupForm('#portal-settings');
|
||||
setupForm('#opentherm-settings');
|
||||
setupForm('#mqtt-settings');
|
||||
setupForm('#outdoor-sensor-settings');
|
||||
setupForm('#indoor-sensor-settings');
|
||||
setupForm('#extpump-settings');
|
||||
setupForm('#system-settings');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
0
data/static/.gitkeep
Normal file
@@ -1,108 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Upgrade - OpenTherm Gateway</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Backup & restore</h2>
|
||||
<p>
|
||||
In this section you can save and restore a backup of ALL settings.
|
||||
</p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/backup/restore" id="restore">
|
||||
<label for="restore-file">
|
||||
Settings file:
|
||||
<input type="file" name="settings" id="restore-file" accept=".json">
|
||||
</label>
|
||||
|
||||
<div class="grid">
|
||||
<button type="submit">Restore</button>
|
||||
<button type="button" class="secondary" onclick="window.location='/api/backup/save';">Backup</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Upgrade</h2>
|
||||
<p>
|
||||
In this section you can upgrade the firmware and filesystem of your device.<br>
|
||||
Latest releases can be downloaded from the <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank">Releases page</a> of the project repository.
|
||||
</p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/upgrade" id="upgrade">
|
||||
<fieldset class="primary">
|
||||
<label for="firmware-file">
|
||||
Firmware:
|
||||
<div class="grid">
|
||||
<input type="file" name="firmware" id="firmware-file" accept=".bin">
|
||||
<button type="button" class="upgrade-firmware-result hidden" disabled></button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="filesystem-file">
|
||||
Filesystem:
|
||||
<div class="grid">
|
||||
<input type="file" name="filesystem" id="filesystem-file" accept=".bin">
|
||||
<button type="button" class="upgrade-filesystem-result hidden" disabled></button>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<ul>
|
||||
<li><mark>After a successful upgrade the filesystem, ALL settings will be reset to default values! Save backup before upgrading.</mark></li>
|
||||
<li><mark>After a successful upgrade, the device will automatically reboot after 10 seconds.</mark></li>
|
||||
</ul>
|
||||
|
||||
<button type="submit">Upgrade</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
setupRestoreBackupForm('#restore');
|
||||
setupUpgradeForm('#upgrade');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
167
gulpfile.js
Normal file
@@ -0,0 +1,167 @@
|
||||
const { src, dest, series, parallel } = require('gulp');
|
||||
const concat = require('gulp-concat');
|
||||
const gzip = require('gulp-gzip');
|
||||
const postcss = require('gulp-postcss');
|
||||
const cssnano = require('cssnano');
|
||||
const terser = require('gulp-terser');
|
||||
const jsonminify = require('gulp-jsonminify');
|
||||
const htmlmin = require('gulp-html-minifier-terser');
|
||||
const replace = require('gulp-replace');
|
||||
|
||||
// Paths for tasks
|
||||
let paths = {
|
||||
styles: {
|
||||
dest: 'data/static/',
|
||||
bundles: {
|
||||
'app.css': [
|
||||
'src_data/styles/pico.min.css',
|
||||
'src_data/styles/iconly.css',
|
||||
'src_data/styles/app.css'
|
||||
]
|
||||
}
|
||||
},
|
||||
scripts: {
|
||||
dest: 'data/static/',
|
||||
bundles: {
|
||||
'app.js': [
|
||||
'src_data/scripts/i18n.min.js',
|
||||
'src_data/scripts/lang.js',
|
||||
'src_data/scripts/utils.js'
|
||||
]
|
||||
}
|
||||
},
|
||||
json: [
|
||||
{
|
||||
src: 'src_data/locales/*.json',
|
||||
dest: 'data/static/locales/'
|
||||
},
|
||||
{
|
||||
src: 'src_data/*.json',
|
||||
dest: 'data/static/'
|
||||
}
|
||||
],
|
||||
static: [
|
||||
{
|
||||
src: 'src_data/fonts/*.*',
|
||||
dest: 'data/static/fonts/'
|
||||
},
|
||||
{
|
||||
src: 'src_data/images/*.*',
|
||||
dest: 'data/static/images/'
|
||||
},
|
||||
{
|
||||
src: 'src_data/*.txt',
|
||||
dest: 'data/static/'
|
||||
}
|
||||
],
|
||||
pages: {
|
||||
src: 'src_data/pages/*.html',
|
||||
dest: 'data/pages/'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Tasks
|
||||
const styles = (cb) => {
|
||||
for (let name in paths.styles.bundles) {
|
||||
const items = paths.styles.bundles[name];
|
||||
|
||||
src(items)
|
||||
.pipe(replace(
|
||||
"{BUILD_TIME}",
|
||||
Math.floor(Date.now() / 1000)
|
||||
))
|
||||
.pipe(postcss([
|
||||
cssnano({ preset: 'advanced' })
|
||||
]))
|
||||
.pipe(concat(name))
|
||||
.pipe(gzip({
|
||||
append: true
|
||||
}))
|
||||
.pipe(dest(paths.styles.dest));
|
||||
}
|
||||
|
||||
cb();
|
||||
}
|
||||
|
||||
const scripts = (cb) => {
|
||||
for (let name in paths.scripts.bundles) {
|
||||
const items = paths.scripts.bundles[name];
|
||||
|
||||
src(items)
|
||||
.pipe(replace(
|
||||
"{BUILD_TIME}",
|
||||
Math.floor(Date.now() / 1000)
|
||||
))
|
||||
.pipe(terser().on('error', console.error))
|
||||
.pipe(concat(name))
|
||||
.pipe(gzip({
|
||||
append: true
|
||||
}))
|
||||
.pipe(dest(paths.scripts.dest));
|
||||
}
|
||||
|
||||
cb();
|
||||
}
|
||||
|
||||
const jsonFiles = (cb) => {
|
||||
for (let i in paths.json) {
|
||||
const item = paths.json[i];
|
||||
|
||||
src(item.src)
|
||||
.pipe(replace(
|
||||
"{BUILD_TIME}",
|
||||
Math.floor(Date.now() / 1000)
|
||||
))
|
||||
.pipe(jsonminify())
|
||||
.pipe(gzip({
|
||||
append: true
|
||||
}))
|
||||
.pipe(dest(item.dest));
|
||||
}
|
||||
|
||||
cb();
|
||||
}
|
||||
|
||||
const staticFiles = (cb) => {
|
||||
for (let i in paths.static) {
|
||||
const item = paths.static[i];
|
||||
|
||||
src(item.src, { encoding: false })
|
||||
.pipe(gzip({
|
||||
append: true
|
||||
}))
|
||||
.pipe(dest(item.dest));
|
||||
}
|
||||
|
||||
cb();
|
||||
}
|
||||
|
||||
const pages = () => {
|
||||
return src(paths.pages.src)
|
||||
.pipe(replace(
|
||||
"{BUILD_TIME}",
|
||||
Math.floor(Date.now() / 1000)
|
||||
))
|
||||
.pipe(htmlmin({
|
||||
html5: true,
|
||||
caseSensitive: true,
|
||||
collapseWhitespace: true,
|
||||
collapseInlineTagWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
removeComments: true,
|
||||
minifyJS: true
|
||||
}))
|
||||
.pipe(gzip({
|
||||
append: true
|
||||
}))
|
||||
.pipe(dest(paths.pages.dest));
|
||||
}
|
||||
|
||||
exports.build_styles = styles;
|
||||
exports.build_scripts = scripts;
|
||||
exports.build_json = jsonFiles;
|
||||
exports.build_static = staticFiles;
|
||||
exports.build_pages = pages;
|
||||
exports.build_all = parallel(styles, scripts, jsonFiles, staticFiles, pages);
|
||||
@@ -10,21 +10,29 @@ public:
|
||||
free(this->buffer);
|
||||
}
|
||||
|
||||
void send(int code, const char* contentType, JsonDocument& content) {
|
||||
template <class T>
|
||||
void send(int code, T contentType, const JsonVariantConst content, bool pretty = false) {
|
||||
auto contentLength = pretty ? measureJsonPretty(content) : measureJson(content);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (!this->webServer->chunkedResponseModeStart(code, contentType)) {
|
||||
this->webServer->send(505, F("text/html"), F("HTTP1.1 required"));
|
||||
return;
|
||||
}
|
||||
|
||||
this->webServer->setContentLength(measureJson(content));
|
||||
this->webServer->setContentLength(contentLength);
|
||||
#else
|
||||
this->webServer->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
this->webServer->sendHeader(F("Content-Length"), String(measureJson(content)));
|
||||
this->webServer->setContentLength(contentLength);
|
||||
this->webServer->send(code, contentType, emptyString);
|
||||
#endif
|
||||
|
||||
serializeJson(content, *this);
|
||||
if (pretty) {
|
||||
serializeJsonPretty(content, *this);
|
||||
|
||||
} else {
|
||||
serializeJson(content, *this);
|
||||
}
|
||||
|
||||
this->flush();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
@@ -70,10 +78,19 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
this->webServer->sendContent((const char*)this->buffer, this->bufferPos);
|
||||
this->bufferPos = 0;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
#endif
|
||||
|
||||
auto& client = this->webServer->client();
|
||||
if (client.connected()) {
|
||||
this->webServer->sendContent((const char*)this->buffer, this->bufferPos);
|
||||
}
|
||||
|
||||
this->bufferPos = 0;
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::optimistic_yield(1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
|
||||
class CustomOpenTherm : public OpenTherm {
|
||||
public:
|
||||
typedef std::function<void()> YieldCallback;
|
||||
typedef std::function<void(unsigned int)> DelayCallback;
|
||||
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
|
||||
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
|
||||
|
||||
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
|
||||
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false, bool alwaysReceive = false) : OpenTherm(inPin, outPin, isSlave, alwaysReceive) {}
|
||||
~CustomOpenTherm() {}
|
||||
|
||||
CustomOpenTherm* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
this->yieldCallback = callback;
|
||||
CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) {
|
||||
this->delayCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -27,163 +28,130 @@ public:
|
||||
return this;
|
||||
}
|
||||
|
||||
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
||||
_attempt++;
|
||||
unsigned long sendRequest(unsigned long request) override {
|
||||
this->sendRequestAttempt++;
|
||||
|
||||
while (!this->isReady()) {
|
||||
if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
} else {
|
||||
::yield();
|
||||
if (this->delayCallback) {
|
||||
this->delayCallback(150);
|
||||
}
|
||||
|
||||
this->process();
|
||||
}
|
||||
|
||||
if (this->beforeSendRequestCallback) {
|
||||
this->beforeSendRequestCallback(request, _attempt);
|
||||
this->beforeSendRequestCallback(request, this->sendRequestAttempt);
|
||||
}
|
||||
|
||||
unsigned long _response;
|
||||
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
|
||||
if (!this->sendRequestAync(request)) {
|
||||
_response = 0;
|
||||
|
||||
} else {
|
||||
while (true) {
|
||||
this->process();
|
||||
|
||||
if (this->status == OpenThermStatus::READY || this->status == OpenThermStatus::DELAY) {
|
||||
break;
|
||||
} else if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
} else {
|
||||
::yield();
|
||||
if (this->sendRequestAsync(request)) {
|
||||
do {
|
||||
if (this->delayCallback) {
|
||||
this->delayCallback(150);
|
||||
}
|
||||
}
|
||||
|
||||
_response = this->getLastResponse();
|
||||
_responseStatus = this->getLastResponseStatus();
|
||||
this->process();
|
||||
} while (this->status != OpenThermStatus::READY && this->status != OpenThermStatus::DELAY);
|
||||
}
|
||||
|
||||
if (this->afterSendRequestCallback) {
|
||||
this->afterSendRequestCallback(request, _response, _responseStatus, _attempt);
|
||||
this->afterSendRequestCallback(request, this->response, this->responseStatus, this->sendRequestAttempt);
|
||||
}
|
||||
|
||||
if (_responseStatus == OpenThermResponseStatus::SUCCESS || _responseStatus == OpenThermResponseStatus::INVALID || _attempt >= attempts) {
|
||||
return _response;
|
||||
if (this->responseStatus == OpenThermResponseStatus::SUCCESS || this->responseStatus == OpenThermResponseStatus::INVALID) {
|
||||
this->sendRequestAttempt = 0;
|
||||
return this->response;
|
||||
|
||||
} else if (this->sendRequestAttempt >= this->sendRequestMaxAttempts) {
|
||||
this->sendRequestAttempt = 0;
|
||||
return this->response;
|
||||
|
||||
} else {
|
||||
return this->sendRequest(request, attempts, _attempt);
|
||||
return this->sendRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking) {
|
||||
unsigned int data = enableCentralHeating
|
||||
| (enableHotWater << 1)
|
||||
| (enableCooling << 2)
|
||||
| (enableOutsideTemperatureCompensation << 3)
|
||||
| (enableCentralHeating2 << 4)
|
||||
| (summerWinterMode << 5)
|
||||
| (dhwBlocking << 6);
|
||||
data <<= 8;
|
||||
|
||||
return this->sendRequest(this->buildRequest(
|
||||
OpenThermMessageType::READ_DATA,
|
||||
OpenThermMessageID::Status,
|
||||
data
|
||||
));
|
||||
}
|
||||
|
||||
bool setHeatingCh1Temp(float temperature) {
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TSet,
|
||||
this->temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setHeatingCh2Temp(float temperature) {
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TsetCH2,
|
||||
this->temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setDhwTemp(float temperature) {
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TdhwSet,
|
||||
this->temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool sendBoilerReset() {
|
||||
unsigned int data = 1;
|
||||
data <<= 8;
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::Command,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
|
||||
}
|
||||
|
||||
bool sendServiceReset() {
|
||||
unsigned int data = 10;
|
||||
data <<= 8;
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::Command,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
|
||||
}
|
||||
|
||||
bool sendWaterFilling() {
|
||||
unsigned int data = 2;
|
||||
data <<= 8;
|
||||
unsigned long response = this->sendRequest(this->buildRequest(
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::Command,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
|
||||
}
|
||||
|
||||
static bool isCh2Active(unsigned long response)
|
||||
{
|
||||
return response & 0x20;
|
||||
}
|
||||
|
||||
static bool isValidResponseId(unsigned long response, OpenThermMessageID id) {
|
||||
byte responseId = (response >> 16) & 0xFF;
|
||||
|
||||
return (byte)id == responseId;
|
||||
}
|
||||
|
||||
static uint8_t getResponseMessageTypeId(unsigned long response) {
|
||||
return (response << 1) >> 29;
|
||||
}
|
||||
|
||||
static const char* getResponseMessageTypeString(unsigned long response) {
|
||||
uint8_t msgType = getResponseMessageTypeId(response);
|
||||
|
||||
switch (msgType) {
|
||||
case (uint8_t) OpenThermMessageType::READ_ACK:
|
||||
case (uint8_t) OpenThermMessageType::WRITE_ACK:
|
||||
case (uint8_t) OpenThermMessageType::DATA_INVALID:
|
||||
case (uint8_t) OpenThermMessageType::UNKNOWN_DATA_ID:
|
||||
return CustomOpenTherm::messageTypeToString(
|
||||
static_cast<OpenThermMessageType>(msgType)
|
||||
);
|
||||
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
// converters
|
||||
float fromF88(unsigned long response) {
|
||||
const byte valueLB = response & 0xFF;
|
||||
const byte valueHB = (response >> 8) & 0xFF;
|
||||
|
||||
float value = (int8_t)valueHB;
|
||||
return value + (float)valueLB / 256.0;
|
||||
}
|
||||
|
||||
template <class T> unsigned int toF88(T val) {
|
||||
template <class T>
|
||||
static unsigned int toFloat(const T val) {
|
||||
return (unsigned int)(val * 256);
|
||||
}
|
||||
|
||||
int16_t fromS16(unsigned long response) {
|
||||
const byte valueLB = response & 0xFF;
|
||||
const byte valueHB = (response >> 8) & 0xFF;
|
||||
|
||||
int16_t value = valueHB;
|
||||
return ((value << 8) + valueLB);
|
||||
static short getInt(const unsigned long response) {
|
||||
return response & 0xffff;
|
||||
}
|
||||
|
||||
protected:
|
||||
YieldCallback yieldCallback;
|
||||
const uint8_t sendRequestMaxAttempts = 5;
|
||||
uint8_t sendRequestAttempt = 0;
|
||||
DelayCallback delayCallback;
|
||||
BeforeSendRequestCallback beforeSendRequestCallback;
|
||||
AfterSendRequestCallback afterSendRequestCallback;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
float Kk = 0.0;
|
||||
float Kt = 0.0;
|
||||
|
||||
Equitherm() {}
|
||||
Equitherm() = default;
|
||||
|
||||
// kn, kk, kt
|
||||
Equitherm(float new_kn, float new_kk, float new_kt) {
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
}
|
||||
|
||||
// лимит выходной величины
|
||||
void setLimits(int min_output, int max_output) {
|
||||
void setLimits(unsigned short min_output, unsigned short max_output) {
|
||||
_minOut = min_output;
|
||||
_maxOut = max_output;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
int _minOut = 20, _maxOut = 90;
|
||||
unsigned short _minOut = 20, _maxOut = 90;
|
||||
|
||||
// температура контура отопления в зависимости от наружной температуры
|
||||
datatype getResultN() {
|
||||
@@ -58,6 +58,6 @@ private:
|
||||
|
||||
// Расчет поправки (ошибки) термостата
|
||||
datatype getResultT() {
|
||||
return constrain((targetTemp - indoorTemp), -2, 2) * Kt;
|
||||
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ class HomeAssistantHelper {
|
||||
public:
|
||||
typedef std::function<void(const char*, bool)> PublishEventCallback;
|
||||
|
||||
HomeAssistantHelper() {}
|
||||
HomeAssistantHelper() = default;
|
||||
|
||||
void setWriter() {
|
||||
this->writer = nullptr;
|
||||
@@ -100,8 +100,8 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
String getTopic(T category, T name, char nameSeparator = '/') {
|
||||
template <class CT, class NT>
|
||||
String makeConfigTopic(CT category, NT name, char nameSeparator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(this->prefix);
|
||||
topic.concat('/');
|
||||
@@ -110,21 +110,45 @@ public:
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(nameSeparator);
|
||||
topic.concat(name);
|
||||
topic.concat("/config");
|
||||
topic.concat(F("/config"));
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
String getDeviceTopic(T value, char separator = '/') {
|
||||
String getDeviceTopic(T value, char dpvSeparator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(separator);
|
||||
topic.concat(dpvSeparator);
|
||||
topic.concat(value);
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class CT, class NT>
|
||||
String getDeviceTopic(CT category, NT name, char dpcSeparator = '/', char cnSeparator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(dpcSeparator);
|
||||
topic.concat(category);
|
||||
topic.concat(cnSeparator);
|
||||
topic.concat(name);
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class CT, class NT, class ST>
|
||||
String getDeviceTopic(CT category, NT name, ST suffix, char dpcSeparator = '/', char cnSeparator = '/', char nsSeparator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(dpcSeparator);
|
||||
topic.concat(category);
|
||||
topic.concat(cnSeparator);
|
||||
topic.concat(name);
|
||||
topic.concat(nsSeparator);
|
||||
topic.concat(suffix);
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
String getObjectId(T value, char separator = '_') {
|
||||
String getObjectIdWithPrefix(T value, char separator = '_') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(separator);
|
||||
|
||||
@@ -25,6 +25,8 @@ const char HA_ENABLED_BY_DEFAULT[] PROGMEM = "enabled_by_default";
|
||||
const char HA_UNIQUE_ID[] PROGMEM = "unique_id";
|
||||
const char HA_OBJECT_ID[] PROGMEM = "object_id";
|
||||
const char HA_ENTITY_CATEGORY[] PROGMEM = "entity_category";
|
||||
const char HA_ENTITY_CATEGORY_DIAGNOSTIC[] PROGMEM = "diagnostic";
|
||||
const char HA_ENTITY_CATEGORY_CONFIG[] PROGMEM = "config";
|
||||
const char HA_STATE_TOPIC[] PROGMEM = "state_topic";
|
||||
const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template";
|
||||
const char HA_OPTIONS[] PROGMEM = "options";
|
||||
@@ -33,16 +35,23 @@ const char HA_AVAILABILITY_MODE[] PROGMEM = "availability_mode";
|
||||
const char HA_TOPIC[] PROGMEM = "topic";
|
||||
const char HA_DEVICE_CLASS[] PROGMEM = "device_class";
|
||||
const char HA_UNIT_OF_MEASUREMENT[] PROGMEM = "unit_of_measurement";
|
||||
const char HA_UNIT_OF_MEASUREMENT_C[] PROGMEM = "°C";
|
||||
const char HA_UNIT_OF_MEASUREMENT_F[] PROGMEM = "°F";
|
||||
const char HA_UNIT_OF_MEASUREMENT_PERCENT[] PROGMEM = "%";
|
||||
const char HA_UNIT_OF_MEASUREMENT_L_MIN[] PROGMEM = "L/min";
|
||||
const char HA_UNIT_OF_MEASUREMENT_GAL_MIN[] PROGMEM = "gal/min";
|
||||
const char HA_ICON[] PROGMEM = "icon";
|
||||
const char HA_MIN[] PROGMEM = "min";
|
||||
const char HA_MAX[] PROGMEM = "max";
|
||||
const char HA_STEP[] PROGMEM = "step";
|
||||
const char HA_MODE[] PROGMEM = "mode";
|
||||
const char HA_MODE_BOX[] PROGMEM = "box";
|
||||
const char HA_STATE_ON[] PROGMEM = "state_on";
|
||||
const char HA_STATE_OFF[] PROGMEM = "state_off";
|
||||
const char HA_PAYLOAD_ON[] PROGMEM = "payload_on";
|
||||
const char HA_PAYLOAD_OFF[] PROGMEM = "payload_off";
|
||||
const char HA_STATE_CLASS[] PROGMEM = "state_class";
|
||||
const char HA_STATE_CLASS_MEASUREMENT[] PROGMEM = "measurement";
|
||||
const char HA_EXPIRE_AFTER[] PROGMEM = "expire_after";
|
||||
const char HA_CURRENT_TEMPERATURE_TOPIC[] PROGMEM = "current_temperature_topic";
|
||||
const char HA_CURRENT_TEMPERATURE_TEMPLATE[] PROGMEM = "current_temperature_template";
|
||||
@@ -50,6 +59,7 @@ const char HA_TEMPERATURE_COMMAND_TOPIC[] PROGMEM = "temperature_command_t
|
||||
const char HA_TEMPERATURE_COMMAND_TEMPLATE[] PROGMEM = "temperature_command_template";
|
||||
const char HA_TEMPERATURE_STATE_TOPIC[] PROGMEM = "temperature_state_topic";
|
||||
const char HA_TEMPERATURE_STATE_TEMPLATE[] PROGMEM = "temperature_state_template";
|
||||
const char HA_TEMPERATURE_UNIT[] PROGMEM = "temperature_unit";
|
||||
const char HA_MODE_COMMAND_TOPIC[] PROGMEM = "mode_command_topic";
|
||||
const char HA_MODE_COMMAND_TEMPLATE[] PROGMEM = "mode_command_template";
|
||||
const char HA_MODE_STATE_TOPIC[] PROGMEM = "mode_state_topic";
|
||||
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
bool publish(const char* topic, JsonDocument& doc, bool retained = false) {
|
||||
bool publish(const char* topic, const JsonVariantConst doc, bool retained = false) {
|
||||
if (!this->client->connected()) {
|
||||
this->bufferPos = 0;
|
||||
return false;
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
#include "NetworkConnection.h"
|
||||
using namespace Network;
|
||||
using namespace NetworkUtils;
|
||||
|
||||
void Connection::setup(bool useDhcp) {
|
||||
void NetworkConnection::setup(bool useDhcp) {
|
||||
setUseDhcp(useDhcp);
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
wifi_set_event_handler_cb(Connection::onEvent);
|
||||
wifi_set_event_handler_cb(NetworkConnection::onEvent);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
WiFi.onEvent(Connection::onEvent);
|
||||
WiFi.onEvent(NetworkConnection::onEvent);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Connection::reset() {
|
||||
void NetworkConnection::reset() {
|
||||
status = Status::NONE;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
}
|
||||
|
||||
void Connection::setUseDhcp(bool value) {
|
||||
void NetworkConnection::setUseDhcp(bool value) {
|
||||
useDhcp = value;
|
||||
}
|
||||
|
||||
Connection::Status Connection::getStatus() {
|
||||
NetworkConnection::Status NetworkConnection::getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
Connection::DisconnectReason Connection::getDisconnectReason() {
|
||||
NetworkConnection::DisconnectReason NetworkConnection::getDisconnectReason() {
|
||||
return disconnectReason;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
void Connection::onEvent(System_Event_t *event) {
|
||||
void NetworkConnection::onEvent(System_Event_t *event) {
|
||||
switch (event->event) {
|
||||
case EVENT_STAMODE_CONNECTED:
|
||||
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
|
||||
break;
|
||||
|
||||
case EVENT_STAMODE_GOT_IP:
|
||||
status = Status::CONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
break;
|
||||
|
||||
case EVENT_STAMODE_DHCP_TIMEOUT:
|
||||
status = Status::DISCONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
|
||||
break;
|
||||
|
||||
case EVENT_STAMODE_DISCONNECTED:
|
||||
status = Status::DISCONNECTED;
|
||||
rawDisconnectReason = event->event_info.disconnected.reason;
|
||||
disconnectReason = convertDisconnectReason(event->event_info.disconnected.reason);
|
||||
|
||||
// https://github.com/esp8266/Arduino/blob/d5eb265f78bff9deb7063d10030a02d021c8c66c/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp#L231
|
||||
@@ -63,6 +67,7 @@ void Connection::onEvent(System_Event_t *event) {
|
||||
auto& src = event->event_info.auth_change;
|
||||
if ((src.old_mode != AUTH_OPEN) && (src.new_mode == AUTH_OPEN)) {
|
||||
status = Status::DISCONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::OTHER;
|
||||
|
||||
wifi_station_disconnect();
|
||||
@@ -75,29 +80,31 @@ void Connection::onEvent(System_Event_t *event) {
|
||||
}
|
||||
}
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
void NetworkConnection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
|
||||
status = Status::CONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
|
||||
status = Status::DISCONNECTED;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
status = Status::DISCONNECTED;
|
||||
rawDisconnectReason = info.wifi_sta_disconnected.reason;
|
||||
disconnectReason = convertDisconnectReason(info.wifi_sta_disconnected.reason);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -106,7 +113,7 @@ void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
}
|
||||
#endif
|
||||
|
||||
Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason) {
|
||||
NetworkConnection::DisconnectReason NetworkConnection::convertDisconnectReason(uint8_t reason) {
|
||||
switch (reason) {
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
case REASON_BEACON_TIMEOUT:
|
||||
@@ -145,6 +152,7 @@ Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool Connection::useDhcp = false;
|
||||
Connection::Status Connection::status = Status::NONE;
|
||||
Connection::DisconnectReason Connection::disconnectReason = DisconnectReason::NONE;
|
||||
bool NetworkConnection::useDhcp = false;
|
||||
NetworkConnection::Status NetworkConnection::status = Status::NONE;
|
||||
NetworkConnection::DisconnectReason NetworkConnection::disconnectReason = DisconnectReason::NONE;
|
||||
uint8_t NetworkConnection::rawDisconnectReason = 0;
|
||||
@@ -5,8 +5,8 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
namespace Network {
|
||||
struct Connection {
|
||||
namespace NetworkUtils {
|
||||
struct NetworkConnection {
|
||||
enum class Status {
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
@@ -27,6 +27,7 @@ namespace Network {
|
||||
|
||||
static Status status;
|
||||
static DisconnectReason disconnectReason;
|
||||
static uint8_t rawDisconnectReason;
|
||||
|
||||
static void setup(bool useDhcp);
|
||||
static void setUseDhcp(bool value);
|
||||
@@ -7,35 +7,35 @@
|
||||
#endif
|
||||
#include <NetworkConnection.h>
|
||||
|
||||
namespace Network {
|
||||
class Manager {
|
||||
namespace NetworkUtils {
|
||||
class NetworkMgr {
|
||||
public:
|
||||
typedef std::function<void()> YieldCallback;
|
||||
typedef std::function<void(unsigned int)> DelayCallback;
|
||||
|
||||
Manager() {
|
||||
Connection::setup(this->useDhcp);
|
||||
NetworkMgr() {
|
||||
NetworkConnection::setup(this->useDhcp);
|
||||
this->resetWifi();
|
||||
}
|
||||
|
||||
Manager* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
NetworkMgr* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
this->yieldCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setDelayCallback(DelayCallback callback = nullptr) {
|
||||
NetworkMgr* setDelayCallback(DelayCallback callback = nullptr) {
|
||||
this->delayCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setHostname(const char* value) {
|
||||
NetworkMgr* setHostname(const char* value) {
|
||||
this->hostname = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
|
||||
NetworkMgr* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
|
||||
this->apName = ssid;
|
||||
this->apPassword = password;
|
||||
this->apChannel = channel;
|
||||
@@ -43,7 +43,7 @@ namespace Network {
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
|
||||
NetworkMgr* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
|
||||
this->staSsid = ssid;
|
||||
this->staPassword = password;
|
||||
this->staChannel = channel;
|
||||
@@ -51,14 +51,14 @@ namespace Network {
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setUseDhcp(bool value) {
|
||||
NetworkMgr* setUseDhcp(bool value) {
|
||||
this->useDhcp = value;
|
||||
Connection::setup(this->useDhcp);
|
||||
NetworkConnection::setup(this->useDhcp);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
|
||||
NetworkMgr* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
|
||||
this->staticIp.fromString(ip);
|
||||
this->staticGateway.fromString(gateway);
|
||||
this->staticSubnet.fromString(subnet);
|
||||
@@ -67,7 +67,7 @@ namespace Network {
|
||||
return this;
|
||||
}
|
||||
|
||||
Manager* setStaticConfig(IPAddress &ip, IPAddress &gateway, IPAddress &subnet, IPAddress &dns) {
|
||||
NetworkMgr* setStaticConfig(IPAddress& ip, IPAddress& gateway, IPAddress& subnet, IPAddress& dns) {
|
||||
this->staticIp = ip;
|
||||
this->staticGateway = gateway;
|
||||
this->staticSubnet = subnet;
|
||||
@@ -81,11 +81,11 @@ namespace Network {
|
||||
}
|
||||
|
||||
bool isConnected() {
|
||||
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTED;
|
||||
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTED;
|
||||
}
|
||||
|
||||
bool isConnecting() {
|
||||
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTING;
|
||||
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTING;
|
||||
}
|
||||
|
||||
bool isStaEnabled() {
|
||||
@@ -147,16 +147,19 @@ namespace Network {
|
||||
bool resetWifi() {
|
||||
// set policy manual for work 13 ch
|
||||
{
|
||||
wifi_country_t country = {"CN", 1, 13, WIFI_COUNTRY_POLICY_MANUAL};
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
wifi_country_t country = {"JP", 1, 14, WIFI_COUNTRY_POLICY_MANUAL};
|
||||
wifi_set_country(&country);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
const wifi_country_t country = {"JP", 1, 14, CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER, WIFI_COUNTRY_POLICY_MANUAL};
|
||||
esp_wifi_set_country(&country);
|
||||
#endif
|
||||
}
|
||||
|
||||
WiFi.persistent(false);
|
||||
#if !defined(ESP_ARDUINO_VERSION_MAJOR) || ESP_ARDUINO_VERSION_MAJOR < 3
|
||||
WiFi.setAutoConnect(false);
|
||||
#endif
|
||||
WiFi.setAutoReconnect(false);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
@@ -180,8 +183,13 @@ namespace Network {
|
||||
|
||||
wifi_station_dhcpc_set_maxtry(5);
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && ESP_ARDUINO_VERSION_MAJOR < 3
|
||||
// Nothing. Because memory leaks when turn off WiFi on ESP32 SDK < 3.0.0
|
||||
return true;
|
||||
#else
|
||||
return WiFi.mode(WIFI_OFF);
|
||||
#endif
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
@@ -195,6 +203,7 @@ namespace Network {
|
||||
|
||||
if (force && !this->isApEnabled()) {
|
||||
this->resetWifi();
|
||||
NetworkConnection::reset();
|
||||
|
||||
} else {
|
||||
/*#ifdef ARDUINO_ARCH_ESP8266
|
||||
@@ -203,14 +212,14 @@ namespace Network {
|
||||
}
|
||||
#endif*/
|
||||
|
||||
WiFi.disconnect(false, true);
|
||||
this->disconnect();
|
||||
}
|
||||
|
||||
if (!this->hasStaCredentials()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->delayCallback(200);
|
||||
this->delayCallback(250);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (this->setWifiHostname(this->hostname)) {
|
||||
@@ -225,7 +234,7 @@ namespace Network {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->delayCallback(200);
|
||||
this->delayCallback(250);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (this->setWifiHostname(this->hostname)) {
|
||||
@@ -235,36 +244,66 @@ namespace Network {
|
||||
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
|
||||
}
|
||||
|
||||
this->delayCallback(200);
|
||||
this->delayCallback(250);
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
|
||||
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
|
||||
#endif
|
||||
|
||||
if (!this->useDhcp) {
|
||||
WiFi.begin(this->staSsid, this->staPassword, this->staChannel, nullptr, false);
|
||||
WiFi.config(this->staticIp, this->staticGateway, this->staticSubnet, this->staticDns);
|
||||
}
|
||||
WiFi.reconnect();
|
||||
|
||||
WiFi.begin(this->staSsid, this->staPassword, this->staChannel);
|
||||
} else {
|
||||
WiFi.begin(this->staSsid, this->staPassword, this->staChannel, nullptr, true);
|
||||
}
|
||||
|
||||
unsigned long beginConnectionTime = millis();
|
||||
while (millis() - beginConnectionTime < timeout) {
|
||||
this->delayCallback(100);
|
||||
|
||||
Connection::Status status = Connection::getStatus();
|
||||
if (status != Connection::Status::CONNECTING && status != Connection::Status::NONE) {
|
||||
return status == Connection::Status::CONNECTED;
|
||||
NetworkConnection::Status status = NetworkConnection::getStatus();
|
||||
if (status != NetworkConnection::Status::CONNECTING && status != NetworkConnection::Status::NONE) {
|
||||
return status == NetworkConnection::Status::CONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
WiFi.disconnectAsync(false, true);
|
||||
|
||||
const unsigned long start = millis();
|
||||
while (WiFi.isConnected() && (millis() - start) < 5000) {
|
||||
this->delayCallback(100);
|
||||
}
|
||||
#else
|
||||
WiFi.disconnect(false, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (this->isConnected() && !this->hasStaCredentials()) {
|
||||
if (this->reconnectFlag) {
|
||||
this->delayCallback(5000);
|
||||
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Reconnecting..."));
|
||||
this->reconnectFlag = false;
|
||||
this->disconnect();
|
||||
NetworkConnection::reset();
|
||||
this->delayCallback(1000);
|
||||
|
||||
} else if (this->isConnected() && !this->hasStaCredentials()) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Reset"));
|
||||
this->resetWifi();
|
||||
Connection::reset();
|
||||
this->delayCallback(200);
|
||||
NetworkConnection::reset();
|
||||
this->delayCallback(1000);
|
||||
|
||||
} else if (this->isConnected() && !this->reconnectFlag) {
|
||||
} else if (this->isConnected()) {
|
||||
if (!this->connected) {
|
||||
this->connectedTime = millis();
|
||||
this->connected = true;
|
||||
@@ -279,7 +318,7 @@ namespace Network {
|
||||
}
|
||||
|
||||
if (this->isApEnabled() && millis() - this->connectedTime > this->reconnectInterval && !this->hasApClients()) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because connected, start only STA"));
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because STA connected"));
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
return;
|
||||
@@ -300,7 +339,7 @@ namespace Network {
|
||||
Log.sinfoln(
|
||||
FPSTR(L_NETWORK),
|
||||
F("Disconnected, reason: %d, uptime: %lu s."),
|
||||
Connection::getDisconnectReason(),
|
||||
NetworkConnection::getDisconnectReason(),
|
||||
(millis() - this->connectedTime) / 1000
|
||||
);
|
||||
}
|
||||
@@ -322,18 +361,17 @@ namespace Network {
|
||||
} else if (this->isConnecting() && millis() - this->prevReconnectingTime > this->resetConnectionTimeout) {
|
||||
Log.swarningln(FPSTR(L_NETWORK), F("Connection timeout, reset wifi..."));
|
||||
this->resetWifi();
|
||||
Connection::reset();
|
||||
this->delayCallback(200);
|
||||
NetworkConnection::reset();
|
||||
this->delayCallback(250);
|
||||
|
||||
} else if (!this->isConnecting() && this->hasStaCredentials() && (!this->prevReconnectingTime || millis() - this->prevReconnectingTime > this->reconnectInterval)) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Try connect..."));
|
||||
|
||||
this->reconnectFlag = false;
|
||||
Connection::reset();
|
||||
NetworkConnection::reset();
|
||||
if (!this->connect(true, this->connectionTimeout)) {
|
||||
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d"), Connection::getStatus(), Connection::getDisconnectReason());
|
||||
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d, raw reason: %d"), NetworkConnection::getStatus(), NetworkConnection::getDisconnectReason(), NetworkConnection::rawDisconnectReason);
|
||||
}
|
||||
|
||||
|
||||
this->prevReconnectingTime = millis();
|
||||
}
|
||||
}
|
||||
@@ -344,10 +382,10 @@ namespace Network {
|
||||
}
|
||||
|
||||
protected:
|
||||
const unsigned int reconnectInterval = 5000;
|
||||
const unsigned int failedConnectTimeout = 120000;
|
||||
const unsigned int connectionTimeout = 15000;
|
||||
const unsigned int resetConnectionTimeout = 30000;
|
||||
const unsigned int reconnectInterval = 15000;
|
||||
const unsigned int failedConnectTimeout = 185000;
|
||||
const unsigned int connectionTimeout = 10000;
|
||||
const unsigned int resetConnectionTimeout = 90000;
|
||||
|
||||
YieldCallback yieldCallback = []() {
|
||||
::yield();
|
||||
@@ -33,18 +33,16 @@ public:
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
|
||||
return this->canHandle(method, uri);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
return uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
if (!this->canHandle(method, uri)) {
|
||||
return false;
|
||||
}
|
||||
@@ -63,7 +61,7 @@ public:
|
||||
}
|
||||
|
||||
if (this->cacheHeader != nullptr) {
|
||||
server.sendHeader("Cache-Control", this->cacheHeader);
|
||||
server.sendHeader(F("Cache-Control"), this->cacheHeader);
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
@@ -177,7 +175,6 @@ public:
|
||||
|
||||
} else {
|
||||
sizeArgName = length - size_t(argStartPos - currentBuf) - 1;
|
||||
Serial.printf("sizeArgName: %d\r\n", sizeArgName);
|
||||
|
||||
// send all content if arg len > space
|
||||
if (sizeArgName >= sizeof(argName)) {
|
||||
@@ -221,7 +218,6 @@ protected:
|
||||
CanHandleCallback canHandleCallback;
|
||||
BeforeSendCallback beforeSendCallback;
|
||||
TemplateCallback templateCallback;
|
||||
String eTag;
|
||||
const char* uri = nullptr;
|
||||
const char* path = nullptr;
|
||||
const char* cacheHeader = nullptr;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include <FS.h>
|
||||
#include <detail/mimetable.h>
|
||||
|
||||
using namespace mime;
|
||||
|
||||
class StaticPage : public RequestHandler {
|
||||
public:
|
||||
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
|
||||
typedef std::function<bool()> BeforeSendCallback;
|
||||
|
||||
StaticPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) {
|
||||
template <class T>
|
||||
StaticPage(const char* uri, FS* fs, T path, const char* cacheHeader = nullptr) {
|
||||
this->uri = uri;
|
||||
this->fs = fs;
|
||||
this->path = path;
|
||||
@@ -25,18 +29,16 @@ public:
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
|
||||
return this->canHandle(method, uri);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
return method == HTTP_GET && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
if (!this->canHandle(method, uri)) {
|
||||
return false;
|
||||
}
|
||||
@@ -45,21 +47,26 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
if (server._eTagEnabled) {
|
||||
if (server._eTagFunction) {
|
||||
this->eTag = (server._eTagFunction)(*this->fs, this->path);
|
||||
|
||||
} else if (this->eTag.isEmpty()) {
|
||||
this->eTag = esp8266webserver::calcETag(*this->fs, this->path);
|
||||
if (this->eTag.isEmpty()) {
|
||||
if (server._eTagFunction) {
|
||||
this->eTag = (server._eTagFunction)(*this->fs, this->path);
|
||||
}
|
||||
}
|
||||
|
||||
if (server.header("If-None-Match").equals(this->eTag.c_str())) {
|
||||
if (!this->eTag.isEmpty() && server.header(F("If-None-Match")).equals(this->eTag.c_str())) {
|
||||
server.send(304);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!this->path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !this->fs->exists(path)) {
|
||||
String pathWithGz = this->path + FPSTR(mimeTable[gz].endsWith);
|
||||
|
||||
if (this->fs->exists(pathWithGz)) {
|
||||
this->path += FPSTR(mimeTable[gz].endsWith);
|
||||
}
|
||||
}
|
||||
|
||||
File file = this->fs->open(this->path, "r");
|
||||
if (!file) {
|
||||
@@ -71,14 +78,14 @@ public:
|
||||
}
|
||||
|
||||
if (this->cacheHeader != nullptr) {
|
||||
server.sendHeader("Cache-Control", this->cacheHeader);
|
||||
server.sendHeader(F("Cache-Control"), this->cacheHeader);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
if (server._eTagEnabled && this->eTag.length() > 0) {
|
||||
server.sendHeader("ETag", this->eTag);
|
||||
if (server._eTagEnabled && !this->eTag.isEmpty()) {
|
||||
server.sendHeader(F("ETag"), this->eTag);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
server.streamFile(file, F("text/html"), method);
|
||||
#else
|
||||
server.streamFile(file, F("text/html"), 200);
|
||||
@@ -93,6 +100,6 @@ protected:
|
||||
BeforeSendCallback beforeSendCallback;
|
||||
String eTag;
|
||||
const char* uri = nullptr;
|
||||
const char* path = nullptr;
|
||||
String path;
|
||||
const char* cacheHeader = nullptr;
|
||||
};
|
||||
@@ -58,26 +58,26 @@ public:
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
|
||||
return this->canHandle(method, uri);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
return method == HTTP_POST && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canUpload(const String uri) override {
|
||||
#else
|
||||
bool canUpload(const String& uri) override {
|
||||
bool canUpload(WebServer &server, const String &uri) override {
|
||||
return this->canUpload(uri);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool canUpload(const String& uri) override {
|
||||
return uri.equals(this->uri) && (!this->canUploadCallback || this->canUploadCallback(uri));
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
if (this->afterUpgradeCallback) {
|
||||
this->afterUpgradeCallback(this->firmwareResult, this->filesystemResult);
|
||||
}
|
||||
@@ -91,16 +91,14 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
void upload(WebServer& server, const String uri, HTTPUpload& upload) override {
|
||||
#else
|
||||
void upload(WebServer& server, const String& uri, HTTPUpload& upload) override {
|
||||
#endif
|
||||
UpgradeResult* result;
|
||||
if (upload.name.equals("firmware")) {
|
||||
if (upload.name.equals(F("firmware"))) {
|
||||
result = &this->firmwareResult;
|
||||
} else if (upload.name.equals("filesystem")) {
|
||||
|
||||
} else if (upload.name.equals(F("filesystem"))) {
|
||||
result = &this->filesystemResult;
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -174,7 +172,7 @@ public:
|
||||
Log.serrorln(
|
||||
FPSTR(L_PORTAL_OTA),
|
||||
F("File '%s', on writing %d bytes: %s"),
|
||||
upload.filename.c_str(), upload.totalSize, result->error
|
||||
upload.filename.c_str(), upload.totalSize, result->error.c_str()
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/*
|
||||
This file is needed by the Arduino IDE because the ino file needs to be named as the directory name.
|
||||
Don't worry, the Arduino compiler will "merge" all files, including src/main.cpp
|
||||
*/
|
||||
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "otgateway",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.0.2",
|
||||
"cssnano-preset-advanced": "^7.0.2",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
"gulp-html-minifier-terser": "^7.1.0",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-postcss": "^10.0.0",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"gulp-replace": "^1.1.4"
|
||||
}
|
||||
}
|
||||
321
platformio.ini
@@ -11,216 +11,365 @@
|
||||
[platformio]
|
||||
;extra_configs = secrets.ini
|
||||
extra_configs = secrets.default.ini
|
||||
core_dir = .pio
|
||||
|
||||
[env]
|
||||
version = 1.5.6
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@^7.0.3
|
||||
;ihormelnyk/OpenTherm Library@^1.1.4
|
||||
https://github.com/Laxilef/opentherm_library/archive/refs/heads/fix_start_bit.zip
|
||||
bblanchon/ArduinoJson@^7.3.0
|
||||
;ihormelnyk/OpenTherm Library@^1.1.5
|
||||
https://github.com/Laxilef/opentherm_library#esp32_timer
|
||||
arduino-libraries/ArduinoMqttClient@^0.1.8
|
||||
lennarthennigs/ESP Telnet@^2.2
|
||||
gyverlibs/FileData@^1.0.2
|
||||
gyverlibs/GyverPID@^3.3.2
|
||||
gyverlibs/GyverBlinker@^1.0
|
||||
milesburton/DallasTemperature@^3.11.0
|
||||
laxilef/TinyLogger@^1.1.0
|
||||
gyverlibs/GyverBlinker@^1.1.1
|
||||
https://github.com/pstolarz/Arduino-Temperature-Control-Library.git#OneWireNg
|
||||
laxilef/TinyLogger@^1.1.1
|
||||
build_type = ${secrets.build_type}
|
||||
build_flags =
|
||||
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
|
||||
-D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305
|
||||
-mtext-section-literals
|
||||
-D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1
|
||||
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_PORT=Serial
|
||||
-D USE_SERIAL=${secrets.use_serial}
|
||||
-D USE_TELNET=${secrets.use_telnet}
|
||||
-D DEBUG_BY_DEFAULT=${secrets.debug}
|
||||
-D HOSTNAME_DEFAULT='"${secrets.hostname}"'
|
||||
-D AP_SSID_DEFAULT='"${secrets.ap_ssid}"'
|
||||
-D AP_PASSWORD_DEFAULT='"${secrets.ap_password}"'
|
||||
-D STA_SSID_DEFAULT='"${secrets.sta_ssid}"'
|
||||
-D STA_PASSWORD_DEFAULT='"${secrets.sta_password}"'
|
||||
-D PORTAL_LOGIN_DEFAULT='"${secrets.portal_login}"'
|
||||
-D PORTAL_PASSWORD_DEFAULT='"${secrets.portal_password}"'
|
||||
-D MQTT_SERVER_DEFAULT='"${secrets.mqtt_server}"'
|
||||
-D MQTT_PORT_DEFAULT=${secrets.mqtt_port}
|
||||
-D MQTT_USER_DEFAULT='"${secrets.mqtt_user}"'
|
||||
-D MQTT_PASSWORD_DEFAULT='"${secrets.mqtt_password}"'
|
||||
-D MQTT_PREFIX_DEFAULT='"${secrets.mqtt_prefix}"'
|
||||
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_HTTP_SERVER -D DEBUG_ESP_PORT=Serial
|
||||
-D BUILD_VERSION='"${this.version}"'
|
||||
-D BUILD_ENV='"$PIOENV"'
|
||||
-D DEFAULT_SERIAL_ENABLED=${secrets.serial_enabled}
|
||||
-D DEFAULT_SERIAL_BAUD=${secrets.serial_baud}
|
||||
-D DEFAULT_TELNET_ENABLED=${secrets.telnet_enabled}
|
||||
-D DEFAULT_TELNET_PORT=${secrets.telnet_port}
|
||||
-D DEFAULT_LOG_LEVEL=${secrets.log_level}
|
||||
-D DEFAULT_HOSTNAME='"${secrets.hostname}"'
|
||||
-D DEFAULT_AP_SSID='"${secrets.ap_ssid}"'
|
||||
-D DEFAULT_AP_PASSWORD='"${secrets.ap_password}"'
|
||||
-D DEFAULT_STA_SSID='"${secrets.sta_ssid}"'
|
||||
-D DEFAULT_STA_PASSWORD='"${secrets.sta_password}"'
|
||||
-D DEFAULT_PORTAL_LOGIN='"${secrets.portal_login}"'
|
||||
-D DEFAULT_PORTAL_PASSWORD='"${secrets.portal_password}"'
|
||||
-D DEFAULT_MQTT_ENABLED=${secrets.mqtt_enabled}
|
||||
-D DEFAULT_MQTT_SERVER='"${secrets.mqtt_server}"'
|
||||
-D DEFAULT_MQTT_PORT=${secrets.mqtt_port}
|
||||
-D DEFAULT_MQTT_USER='"${secrets.mqtt_user}"'
|
||||
-D DEFAULT_MQTT_PASSWORD='"${secrets.mqtt_password}"'
|
||||
-D DEFAULT_MQTT_PREFIX='"${secrets.mqtt_prefix}"'
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
;monitor_filters = direct
|
||||
monitor_filters =
|
||||
esp32_exception_decoder
|
||||
esp8266_exception_decoder
|
||||
board_build.flash_mode = dio
|
||||
board_build.filesystem = littlefs
|
||||
version = 1.4.0-rc.14
|
||||
check_tool = ; pvs-studio
|
||||
check_flags =
|
||||
; pvs-studio:
|
||||
; --analysis-mode=4
|
||||
; --exclude-path=./.pio/libdeps
|
||||
|
||||
; Defaults
|
||||
[esp8266_defaults]
|
||||
platform = espressif8266
|
||||
platform = espressif8266@^4.2.1
|
||||
platform_packages = ${env.platform_packages}
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
nrwiersma/ESP8266Scheduler@^1.1
|
||||
nrwiersma/ESP8266Scheduler@^1.2
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/build.py
|
||||
build_flags = ${env.build_flags}
|
||||
board_build.ldscript = eagle.flash.1m256.ld
|
||||
;board_build.ldscript = eagle.flash.4m1m.ld
|
||||
build_type = ${env.build_type}
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
|
||||
;-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH
|
||||
-D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305
|
||||
board_build.ldscript = eagle.flash.4m1m.ld
|
||||
check_tool = ${env.check_tool}
|
||||
check_flags = ${env.check_flags}
|
||||
|
||||
[esp32_defaults]
|
||||
platform = espressif32
|
||||
;platform = espressif32@^6.7
|
||||
;platform = https://github.com/platformio/platform-espressif32.git
|
||||
;platform_packages =
|
||||
; framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.5
|
||||
; framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.1/esp32-arduino-libs-idf-release_v5.1-33fbade6.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip
|
||||
platform_packages = ${env.platform_packages}
|
||||
board_build.partitions = esp32_partitions.csv
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
laxilef/ESP32Scheduler@^1.0.1
|
||||
nimble_lib = h2zero/NimBLE-Arduino@^2.1.0
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/esp32.py
|
||||
post:tools/build.py
|
||||
build_type = ${env.build_type}
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D CORE_DEBUG_LEVEL=0
|
||||
-Wl,--wrap=esp_panic_handler
|
||||
check_tool = ${env.check_tool}
|
||||
check_flags = ${env.check_flags}
|
||||
|
||||
|
||||
; Boards
|
||||
[env:d1_mini]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
platform_packages = ${esp8266_defaults.platform_packages}
|
||||
board = d1_mini
|
||||
lib_deps = ${esp8266_defaults.lib_deps}
|
||||
lib_ignore = ${esp8266_defaults.lib_ignore}
|
||||
extra_scripts = ${esp8266_defaults.extra_scripts}
|
||||
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
|
||||
build_type = ${esp8266_defaults.build_type}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
-D DEFAULT_OT_IN_GPIO=4
|
||||
-D DEFAULT_OT_OUT_GPIO=5
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=14
|
||||
-D DEFAULT_STATUS_LED_GPIO=13
|
||||
-D DEFAULT_OT_RX_LED_GPIO=15
|
||||
check_tool = ${esp8266_defaults.check_tool}
|
||||
check_flags = ${esp8266_defaults.check_flags}
|
||||
|
||||
[env:d1_mini_lite]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
platform_packages = ${esp8266_defaults.platform_packages}
|
||||
board = d1_mini_lite
|
||||
lib_deps = ${esp8266_defaults.lib_deps}
|
||||
lib_ignore = ${esp8266_defaults.lib_ignore}
|
||||
extra_scripts = ${esp8266_defaults.extra_scripts}
|
||||
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
|
||||
build_type = ${esp8266_defaults.build_type}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
-D DEFAULT_OT_IN_GPIO=4
|
||||
-D DEFAULT_OT_OUT_GPIO=5
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=14
|
||||
-D DEFAULT_STATUS_LED_GPIO=13
|
||||
-D DEFAULT_OT_RX_LED_GPIO=15
|
||||
check_tool = ${esp8266_defaults.check_tool}
|
||||
check_flags = ${esp8266_defaults.check_flags}
|
||||
|
||||
[env:d1_mini_pro]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
platform_packages = ${esp8266_defaults.platform_packages}
|
||||
board = d1_mini_pro
|
||||
lib_deps = ${esp8266_defaults.lib_deps}
|
||||
lib_ignore = ${esp8266_defaults.lib_ignore}
|
||||
extra_scripts = ${esp8266_defaults.extra_scripts}
|
||||
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
|
||||
build_type = ${esp8266_defaults.build_type}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
-D DEFAULT_OT_IN_GPIO=4
|
||||
-D DEFAULT_OT_OUT_GPIO=5
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=14
|
||||
-D DEFAULT_STATUS_LED_GPIO=13
|
||||
-D DEFAULT_OT_RX_LED_GPIO=15
|
||||
check_tool = ${esp8266_defaults.check_tool}
|
||||
check_flags = ${esp8266_defaults.check_flags}
|
||||
|
||||
[env:nodemcu_8266]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
platform_packages = ${esp8266_defaults.platform_packages}
|
||||
board = nodemcuv2
|
||||
lib_deps = ${esp8266_defaults.lib_deps}
|
||||
lib_ignore = ${esp8266_defaults.lib_ignore}
|
||||
extra_scripts = ${esp8266_defaults.extra_scripts}
|
||||
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
|
||||
build_type = ${esp8266_defaults.build_type}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D DEFAULT_OT_IN_GPIO=13
|
||||
-D DEFAULT_OT_OUT_GPIO=15
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=4
|
||||
-D DEFAULT_STATUS_LED_GPIO=2
|
||||
-D DEFAULT_OT_RX_LED_GPIO=16
|
||||
check_tool = ${esp8266_defaults.check_tool}
|
||||
check_flags = ${esp8266_defaults.check_flags}
|
||||
|
||||
[env:s2_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_s2_mini
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=33
|
||||
-D OT_OUT_PIN_DEFAULT=35
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=9
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=7
|
||||
-D LED_STATUS_PIN=11
|
||||
-D LED_OT_RX_PIN=12
|
||||
-D ARDUINO_USB_MODE=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D DEFAULT_OT_IN_GPIO=33
|
||||
-D DEFAULT_OT_OUT_GPIO=35
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=9
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=7
|
||||
-D DEFAULT_STATUS_LED_GPIO=11
|
||||
-D DEFAULT_OT_RX_LED_GPIO=12
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:s3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_s3_mini
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D ARDUINO_USB_MODE=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D CONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-D USE_BLE=1
|
||||
-D OT_IN_PIN_DEFAULT=35
|
||||
-D OT_OUT_PIN_DEFAULT=36
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=13
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=12
|
||||
-D LED_STATUS_PIN=11
|
||||
-D LED_OT_RX_PIN=10
|
||||
-D DEFAULT_OT_IN_GPIO=35
|
||||
-D DEFAULT_OT_OUT_GPIO=36
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=13
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=12
|
||||
-D DEFAULT_STATUS_LED_GPIO=11
|
||||
-D DEFAULT_OT_RX_LED_GPIO=10
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:c3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_c3_mini
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-mtext-section-literals
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D CONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-D USE_BLE=1
|
||||
-D OT_IN_PIN_DEFAULT=8
|
||||
-D OT_OUT_PIN_DEFAULT=10
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=0
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=1
|
||||
-D LED_STATUS_PIN=4
|
||||
-D LED_OT_RX_PIN=5
|
||||
-D DEFAULT_OT_IN_GPIO=8
|
||||
-D DEFAULT_OT_OUT_GPIO=10
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=1
|
||||
-D DEFAULT_STATUS_LED_GPIO=4
|
||||
-D DEFAULT_OT_RX_LED_GPIO=5
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:nodemcu_32s]
|
||||
[env:nodemcu_32]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = nodemcu-32s
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D USE_BLE=1
|
||||
-D OT_IN_PIN_DEFAULT=21
|
||||
-D OT_OUT_PIN_DEFAULT=22
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=13
|
||||
-D LED_STATUS_PIN=2 ; 18
|
||||
-D LED_OT_RX_PIN=19
|
||||
;-D WOKWI=1
|
||||
-D DEFAULT_OT_IN_GPIO=16
|
||||
-D DEFAULT_OT_OUT_GPIO=4
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=15
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=26
|
||||
-D DEFAULT_STATUS_LED_GPIO=2
|
||||
-D DEFAULT_OT_RX_LED_GPIO=19
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:d1_mini32]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = wemos_d1_mini32
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D USE_BLE=1
|
||||
-D OT_IN_PIN_DEFAULT=21
|
||||
-D OT_OUT_PIN_DEFAULT=22
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=18
|
||||
-D LED_STATUS_PIN=2
|
||||
-D LED_OT_RX_PIN=19
|
||||
-D DEFAULT_OT_IN_GPIO=21
|
||||
-D DEFAULT_OT_OUT_GPIO=22
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=18
|
||||
-D DEFAULT_STATUS_LED_GPIO=2
|
||||
-D DEFAULT_OT_RX_LED_GPIO=19
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:esp32_c6]
|
||||
platform = ${esp32_defaults.platform}
|
||||
framework = arduino, espidf
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = esp32-c6-devkitm-1
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
board_build.embed_txtfiles =
|
||||
managed_components/espressif__esp_insights/server_certs/https_server.crt
|
||||
managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt
|
||||
managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt
|
||||
managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
lib_ignore =
|
||||
${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-mtext-section-literals
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D USE_BLE=1
|
||||
-D DEFAULT_OT_IN_GPIO=15
|
||||
-D DEFAULT_OT_OUT_GPIO=23
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=0
|
||||
-D DEFAULT_STATUS_LED_GPIO=11
|
||||
-D DEFAULT_OT_RX_LED_GPIO=10
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
[env:otthing]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_c3_mini
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-mtext-section-literals
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D CONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-D USE_BLE=1
|
||||
-D DEFAULT_OT_IN_GPIO=3
|
||||
-D DEFAULT_OT_OUT_GPIO=1
|
||||
; -D DEFAULT_SENSOR_OUTDOOR_GPIO=0
|
||||
; -D DEFAULT_SENSOR_INDOOR_GPIO=1
|
||||
-D DEFAULT_STATUS_LED_GPIO=8
|
||||
-D DEFAULT_OT_RX_LED_GPIO=2
|
||||
-D OT_BYPASS_RELAY_GPIO=20
|
||||
check_tool = ${esp32_defaults.check_tool}
|
||||
check_flags = ${esp32_defaults.check_flags}
|
||||
|
||||
33
sdkconfig.defaults
Normal file
@@ -0,0 +1,33 @@
|
||||
# Source:
|
||||
# https://github.com/pioarduino/platform-espressif32/tree/main/examples/espidf-arduino-h2zero-BLE_scan
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_MBEDTLS_PSK_MODES=y
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
|
||||
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
|
||||
|
||||
#
|
||||
# BT config
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
|
||||
CONFIG_BTDM_CTRL_MODE_BTDM=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
|
||||
#
|
||||
# Arduino Configuration
|
||||
CONFIG_AUTOSTART_ARDUINO=y
|
||||
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Zigbee=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Matter=n
|
||||
CONFIG_ARDUINO_SELECTIVE_WiFiProv=n
|
||||
CONFIG_ARDUINO_SELECTIVE_BLE=n
|
||||
CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n
|
||||
CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n
|
||||
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
|
||||
CONFIG_ARDUINO_SELECTIVE_OpenThread=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Insights=n
|
||||
@@ -1,7 +1,11 @@
|
||||
[secrets]
|
||||
use_serial = true
|
||||
use_telnet = true
|
||||
debug = true
|
||||
build_type = release
|
||||
|
||||
serial_enabled = true
|
||||
serial_baud = 115200
|
||||
telnet_enabled = true
|
||||
telnet_port = 23
|
||||
log_level = 5
|
||||
hostname = opentherm
|
||||
|
||||
ap_ssid = OpenTherm Gateway
|
||||
@@ -13,6 +17,7 @@ sta_password =
|
||||
portal_login = admin
|
||||
portal_password = admin
|
||||
|
||||
mqtt_enabled = false
|
||||
mqtt_server =
|
||||
mqtt_port = 1883
|
||||
mqtt_user =
|
||||
|
||||
132
src/CrashRecorder.h
Normal file
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "esp_err.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
// https://github.com/espressif/ESP8266_RTOS_SDK/blob/master/components/esp8266/include/esp_attr.h
|
||||
#define _COUNTER_STRINGIFY(COUNTER) #COUNTER
|
||||
#define _SECTION_ATTR_IMPL(SECTION, COUNTER) __attribute__((section(SECTION "." _COUNTER_STRINGIFY(COUNTER))))
|
||||
#define __NOINIT_ATTR _SECTION_ATTR_IMPL(".noinit", __COUNTER__)
|
||||
#endif
|
||||
|
||||
namespace CrashRecorder {
|
||||
typedef struct {
|
||||
unsigned int data[32];
|
||||
uint8_t length;
|
||||
bool continues;
|
||||
} backtrace_t;
|
||||
|
||||
typedef struct {
|
||||
unsigned int data[4];
|
||||
uint8_t length;
|
||||
} epc_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t core;
|
||||
size_t heap;
|
||||
unsigned long uptime;
|
||||
} ext_t;
|
||||
|
||||
|
||||
__NOINIT_ATTR volatile static backtrace_t backtrace;
|
||||
__NOINIT_ATTR volatile static epc_t epc;
|
||||
__NOINIT_ATTR volatile static ext_t ext;
|
||||
|
||||
uint8_t backtraceMaxLength = sizeof(backtrace.data) / sizeof(*backtrace.data);
|
||||
uint8_t epcMaxLength = sizeof(epc.data) / sizeof(*epc.data);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
void IRAM_ATTR panicHandler(arduino_panic_info_t *info, void *arg) {;
|
||||
ext.core = info->core;
|
||||
ext.heap = ESP.getFreeHeap();
|
||||
ext.uptime = millis() / 1000u;
|
||||
|
||||
// Backtrace
|
||||
backtrace.length = info->backtrace_len < backtraceMaxLength ? info->backtrace_len : backtraceMaxLength;
|
||||
backtrace.continues = false;
|
||||
for (unsigned int i = 0; i < info->backtrace_len; i++) {
|
||||
if (i >= backtraceMaxLength) {
|
||||
backtrace.continues = true;
|
||||
break;
|
||||
}
|
||||
|
||||
backtrace.data[i] = info->backtrace[i];
|
||||
}
|
||||
|
||||
// EPC
|
||||
if (info->pc) {
|
||||
epc.data[0] = (unsigned int) info->pc;
|
||||
epc.length = 1;
|
||||
|
||||
} else {
|
||||
epc.length = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void init() {
|
||||
if (backtrace.length > backtraceMaxLength) {
|
||||
backtrace.length = 0;
|
||||
}
|
||||
|
||||
if (epc.length > epcMaxLength) {
|
||||
epc.length = 0;
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
set_arduino_panic_handler(panicHandler, nullptr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
extern "C" void custom_crash_callback(struct rst_info *info, uint32_t stack, uint32_t stack_end) {
|
||||
uint8_t _length = 0;
|
||||
|
||||
CrashRecorder::ext.core = 0;
|
||||
CrashRecorder::ext.heap = ESP.getFreeHeap();
|
||||
CrashRecorder::ext.uptime = millis() / 1000u;
|
||||
|
||||
// Backtrace
|
||||
CrashRecorder::backtrace.continues = false;
|
||||
uint32_t value;
|
||||
for (uint32_t i = stack; i < stack_end; i += 4) {
|
||||
value = *((uint32_t*) i);
|
||||
|
||||
// keep only addresses in code area
|
||||
if ((value >= 0x40000000) && (value < 0x40300000)) {
|
||||
if (_length >= CrashRecorder::backtraceMaxLength) {
|
||||
CrashRecorder::backtrace.continues = true;
|
||||
break;
|
||||
}
|
||||
|
||||
CrashRecorder::backtrace.data[_length++] = value;
|
||||
}
|
||||
}
|
||||
|
||||
CrashRecorder::backtrace.length = _length;
|
||||
|
||||
// EPC
|
||||
_length = 0;
|
||||
if (info->epc1 > 0) {
|
||||
CrashRecorder::epc.data[_length++] = info->epc1;
|
||||
}
|
||||
|
||||
if (info->epc2 > 0) {
|
||||
CrashRecorder::epc.data[_length++] = info->epc2;
|
||||
}
|
||||
|
||||
if (info->epc3 > 0) {
|
||||
CrashRecorder::epc.data[_length++] = info->epc3;
|
||||
}
|
||||
|
||||
CrashRecorder::epc.length = _length;
|
||||
}
|
||||
#endif
|
||||
1910
src/HaHelper.h
585
src/MainTask.h
@@ -1,9 +1,11 @@
|
||||
#include <Blinker.h>
|
||||
|
||||
extern Network::Manager* network;
|
||||
using namespace NetworkUtils;
|
||||
|
||||
extern NetworkMgr* network;
|
||||
extern MqttTask* tMqtt;
|
||||
extern OpenThermTask* tOt;
|
||||
extern FileData fsSettings, fsNetworkSettings;
|
||||
extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings;
|
||||
extern ESPTelnetStream* telnetStream;
|
||||
|
||||
|
||||
@@ -20,106 +22,126 @@ public:
|
||||
}
|
||||
|
||||
~MainTask() {
|
||||
if (this->blinker != nullptr) {
|
||||
delete this->blinker;
|
||||
}
|
||||
delete this->blinker;
|
||||
}
|
||||
|
||||
protected:
|
||||
const static byte REASON_PUMP_START_HEATING = 1;
|
||||
const static byte REASON_PUMP_START_ANTISTUCK = 2;
|
||||
enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
|
||||
|
||||
Blinker* blinker = nullptr;
|
||||
bool blinkerInitialized = false;
|
||||
unsigned long firstFailConnect = 0;
|
||||
unsigned long miscRunned = 0;
|
||||
unsigned long lastHeapInfo = 0;
|
||||
unsigned int minFreeHeap = 0;
|
||||
unsigned int minMaxFreeBlockHeap = 0;
|
||||
unsigned long restartSignalTime = 0;
|
||||
bool restartSignalReceived = false;
|
||||
unsigned long restartSignalReceivedTime = 0;
|
||||
bool heatingEnabled = false;
|
||||
unsigned long heatingDisabledTime = 0;
|
||||
byte externalPumpStartReason;
|
||||
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
|
||||
unsigned long externalPumpStartTime = 0;
|
||||
bool ntpStarted = false;
|
||||
bool telnetStarted = false;
|
||||
bool emergencyDetected = false;
|
||||
unsigned long emergencyFlipTime = 0;
|
||||
bool freezeDetected = false;
|
||||
unsigned long freezeDetectedTime = 0;
|
||||
|
||||
const char* getTaskName() {
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Main";
|
||||
}
|
||||
|
||||
/*int getTaskCore() {
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}*/
|
||||
|
||||
int getTaskPriority() {
|
||||
int getTaskPriority() override {
|
||||
return 3;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
#ifdef LED_STATUS_PIN
|
||||
pinMode(LED_STATUS_PIN, OUTPUT);
|
||||
digitalWrite(LED_STATUS_PIN, false);
|
||||
#endif
|
||||
|
||||
if (settings.externalPump.pin != 0) {
|
||||
pinMode(settings.externalPump.pin, OUTPUT);
|
||||
digitalWrite(settings.externalPump.pin, false);
|
||||
}
|
||||
}
|
||||
void setup() {}
|
||||
|
||||
void loop() {
|
||||
network->loop();
|
||||
|
||||
if (fsSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (fsNetworkSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (fsSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (fsSensorsSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_SENSORS_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (vars.actions.restart) {
|
||||
this->restartSignalReceivedTime = millis();
|
||||
this->restartSignalReceived = true;
|
||||
vars.actions.restart = false;
|
||||
this->restartSignalTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Received restart signal"));
|
||||
}
|
||||
|
||||
if (!vars.states.restarting && this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 5000) {
|
||||
vars.states.restarting = true;
|
||||
|
||||
// save settings
|
||||
fsSettings.updateNow();
|
||||
|
||||
// save sensors settings
|
||||
fsSensorsSettings.updateNow();
|
||||
|
||||
// force save network settings
|
||||
if (fsNetworkSettings.updateNow() == FD_FILE_ERR && LittleFS.begin()) {
|
||||
fsNetworkSettings.write();
|
||||
}
|
||||
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Restart scheduled in 10 sec."));
|
||||
}
|
||||
|
||||
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
|
||||
tOt->enable();
|
||||
vars.mqtt.connected = tMqtt->isConnected();
|
||||
vars.network.connected = network->isConnected();
|
||||
vars.network.rssi = network->isConnected() ? WiFi.RSSI() : 0;
|
||||
|
||||
if (settings.system.logLevel >= TinyLogger::Level::SILENT && settings.system.logLevel <= TinyLogger::Level::VERBOSE) {
|
||||
if (Log.getLevel() != settings.system.logLevel) {
|
||||
Log.setLevel(static_cast<TinyLogger::Level>(settings.system.logLevel));
|
||||
}
|
||||
}
|
||||
|
||||
if (network->isConnected()) {
|
||||
vars.sensors.rssi = WiFi.RSSI();
|
||||
if (!this->ntpStarted) {
|
||||
if (strlen(settings.system.ntp.server)) {
|
||||
configTime(0, 0, settings.system.ntp.server);
|
||||
setenv("TZ", settings.system.ntp.timezone, 1);
|
||||
tzset();
|
||||
|
||||
this->ntpStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->telnetStarted && telnetStream != nullptr) {
|
||||
telnetStream->begin(23, false);
|
||||
this->telnetStarted = true;
|
||||
}
|
||||
|
||||
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
|
||||
if (settings.mqtt.enabled && !tMqtt->isEnabled()) {
|
||||
tMqtt->enable();
|
||||
|
||||
} else if (!settings.mqtt.enabled && tMqtt->isEnabled()) {
|
||||
tMqtt->disable();
|
||||
}
|
||||
|
||||
if (this->firstFailConnect != 0) {
|
||||
this->firstFailConnect = 0;
|
||||
}
|
||||
|
||||
if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.system.debug ) {
|
||||
Log.setLevel(TinyLogger::Level::INFO);
|
||||
|
||||
} else if ( Log.getLevel() != TinyLogger::Level::VERBOSE && settings.system.debug ) {
|
||||
Log.setLevel(TinyLogger::Level::VERBOSE);
|
||||
}
|
||||
Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, !settings.mqtt.enabled || vars.mqtt.connected, false);
|
||||
|
||||
} else {
|
||||
if (this->ntpStarted) {
|
||||
this->ntpStarted = false;
|
||||
}
|
||||
|
||||
if (this->telnetStarted) {
|
||||
telnetStream->stop();
|
||||
this->telnetStarted = false;
|
||||
@@ -129,29 +151,18 @@ protected:
|
||||
tMqtt->disable();
|
||||
}
|
||||
|
||||
if (settings.emergency.enable && !vars.states.emergency) {
|
||||
if (this->firstFailConnect == 0) {
|
||||
this->firstFailConnect = millis();
|
||||
}
|
||||
|
||||
if (millis() - this->firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
|
||||
vars.states.emergency = true;
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
|
||||
}
|
||||
}
|
||||
Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, false, false);
|
||||
}
|
||||
|
||||
this->yield();
|
||||
|
||||
|
||||
#ifdef LED_STATUS_PIN
|
||||
this->ledStatus(LED_STATUS_PIN);
|
||||
#endif
|
||||
this->externalPump();
|
||||
this->yield();
|
||||
|
||||
if (this->misc()) {
|
||||
this->yield();
|
||||
}
|
||||
this->ledStatus();
|
||||
|
||||
// telnet
|
||||
if (this->telnetStarted) {
|
||||
this->yield();
|
||||
telnetStream->loop();
|
||||
this->yield();
|
||||
}
|
||||
@@ -161,32 +172,50 @@ protected:
|
||||
for (Stream* stream : Log.getStreams()) {
|
||||
while (stream->available() > 0) {
|
||||
stream->read();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// heap info
|
||||
this->heap();
|
||||
}
|
||||
|
||||
bool misc() {
|
||||
if (millis() - this->miscRunned < 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// restart if required
|
||||
if (this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 15000) {
|
||||
this->restartSignalReceived = false;
|
||||
|
||||
// restart
|
||||
if (this->restartSignalTime > 0 && millis() - this->restartSignalTime > 10000) {
|
||||
this->restartSignalTime = 0;
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
this->heating();
|
||||
this->emergency();
|
||||
this->cascadeControl();
|
||||
this->externalPump();
|
||||
this->miscRunned = millis();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void heap() {
|
||||
unsigned int freeHeap = getFreeHeap();
|
||||
unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap();
|
||||
|
||||
if (!this->restartSignalTime && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
|
||||
this->restartSignalTime = millis();
|
||||
// critical heap
|
||||
if (!vars.states.restarting && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
|
||||
this->restartSignalReceivedTime = millis();
|
||||
this->restartSignalReceived = true;
|
||||
vars.states.restarting = true;
|
||||
}
|
||||
|
||||
if (!settings.system.debug) {
|
||||
if (settings.system.logLevel < TinyLogger::Level::VERBOSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,31 +243,159 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
void ledStatus(uint8_t ledPin) {
|
||||
void heating() {
|
||||
// freeze protection
|
||||
if (!settings.heating.enabled) {
|
||||
float lowTemp = 255.0f;
|
||||
uint8_t availableSensors = 0;
|
||||
|
||||
if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
|
||||
auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY);
|
||||
if (value < lowTemp) {
|
||||
lowTemp = value;
|
||||
}
|
||||
|
||||
availableSensors++;
|
||||
}
|
||||
|
||||
if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_TEMP)) {
|
||||
auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_TEMP, Sensors::ValueType::PRIMARY);
|
||||
if (value < lowTemp) {
|
||||
lowTemp = value;
|
||||
}
|
||||
|
||||
availableSensors++;
|
||||
}
|
||||
|
||||
if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP)) {
|
||||
auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP, Sensors::ValueType::PRIMARY);
|
||||
if (value < lowTemp) {
|
||||
lowTemp = value;
|
||||
}
|
||||
|
||||
availableSensors++;
|
||||
}
|
||||
|
||||
if (availableSensors && lowTemp <= settings.heating.freezeProtection.lowTemp) {
|
||||
if (!this->freezeDetected) {
|
||||
this->freezeDetected = true;
|
||||
this->freezeDetectedTime = millis();
|
||||
|
||||
} else if (millis() - this->freezeDetectedTime > (settings.heating.freezeProtection.thresholdTime * 1000)) {
|
||||
this->freezeDetected = false;
|
||||
settings.heating.enabled = true;
|
||||
fsSettings.update();
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_MAIN),
|
||||
F("Heating turned on by freeze protection, current low temp: %.2f, threshold: %hhu"),
|
||||
lowTemp, settings.heating.freezeProtection.lowTemp
|
||||
);
|
||||
}
|
||||
|
||||
} else if (this->freezeDetected) {
|
||||
this->freezeDetected = false;
|
||||
}
|
||||
|
||||
} else if (this->freezeDetected) {
|
||||
this->freezeDetected = false;
|
||||
}
|
||||
}
|
||||
|
||||
void emergency() {
|
||||
// flags
|
||||
uint8_t emergencyFlags = 0b00000000;
|
||||
|
||||
// set outdoor sensor flag
|
||||
if (settings.equitherm.enabled) {
|
||||
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP)) {
|
||||
emergencyFlags |= 0b00000001;
|
||||
}
|
||||
}
|
||||
|
||||
// set indoor sensor flags
|
||||
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
|
||||
if (!settings.equitherm.enabled && settings.pid.enabled) {
|
||||
emergencyFlags |= 0b00000010;
|
||||
}
|
||||
|
||||
if (settings.opentherm.options.nativeHeatingControl) {
|
||||
emergencyFlags |= 0b00000100;
|
||||
}
|
||||
}
|
||||
|
||||
// if any flags is true
|
||||
if ((emergencyFlags & 0b00001111) != 0) {
|
||||
if (!this->emergencyDetected) {
|
||||
// flip flag
|
||||
this->emergencyDetected = true;
|
||||
this->emergencyFlipTime = millis();
|
||||
|
||||
} else if (this->emergencyDetected && !vars.emergency.state) {
|
||||
// enable emergency
|
||||
if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
|
||||
vars.emergency.state = true;
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled (%hhu)"), emergencyFlags);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this->emergencyDetected) {
|
||||
// flip flag
|
||||
this->emergencyDetected = false;
|
||||
this->emergencyFlipTime = millis();
|
||||
|
||||
} else if (!this->emergencyDetected && vars.emergency.state) {
|
||||
// disable emergency
|
||||
if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
|
||||
vars.emergency.state = false;
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ledStatus() {
|
||||
uint8_t errors[4];
|
||||
uint8_t errCount = 0;
|
||||
static uint8_t errPos = 0;
|
||||
static unsigned long endBlinkTime = 0;
|
||||
static bool ledOn = false;
|
||||
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
|
||||
if (!this->blinkerInitialized) {
|
||||
this->blinker->init(ledPin);
|
||||
this->blinkerInitialized = true;
|
||||
if (settings.system.statusLedGpio != configuredGpio) {
|
||||
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
}
|
||||
|
||||
if (GPIO_IS_VALID(settings.system.statusLedGpio)) {
|
||||
configuredGpio = settings.system.statusLedGpio;
|
||||
pinMode(configuredGpio, OUTPUT);
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
this->blinker->init(configuredGpio);
|
||||
|
||||
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
configuredGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!network->isConnected()) {
|
||||
errors[errCount++] = 2;
|
||||
}
|
||||
|
||||
if (!vars.states.otStatus) {
|
||||
if (!vars.slave.connected) {
|
||||
errors[errCount++] = 3;
|
||||
}
|
||||
|
||||
if (vars.states.fault) {
|
||||
if (vars.slave.fault.active) {
|
||||
errors[errCount++] = 4;
|
||||
}
|
||||
|
||||
if (vars.states.emergency) {
|
||||
if (vars.emergency.state) {
|
||||
errors[errCount++] = 5;
|
||||
}
|
||||
|
||||
@@ -250,14 +407,14 @@ protected:
|
||||
if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
|
||||
if (errCount == 0) {
|
||||
if (!ledOn) {
|
||||
digitalWrite(ledPin, true);
|
||||
digitalWrite(configuredGpio, HIGH);
|
||||
ledOn = true;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} else if (ledOn) {
|
||||
digitalWrite(ledPin, false);
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
ledOn = false;
|
||||
endBlinkTime = millis();
|
||||
return;
|
||||
@@ -277,68 +434,258 @@ protected:
|
||||
this->blinker->tick();
|
||||
}
|
||||
|
||||
void externalPump() {
|
||||
if (!vars.states.heating && this->heatingEnabled) {
|
||||
this->heatingEnabled = false;
|
||||
this->heatingDisabledTime = millis();
|
||||
void cascadeControl() {
|
||||
static uint8_t configuredInputGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
static uint8_t configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
static bool inputTempValue = false;
|
||||
static unsigned long inputChangedTs = 0;
|
||||
static bool outputTempValue = false;
|
||||
static unsigned long outputChangedTs = 0;
|
||||
|
||||
} else if (vars.states.heating && !this->heatingEnabled) {
|
||||
this->heatingEnabled = true;
|
||||
}
|
||||
|
||||
if (!settings.externalPump.use || settings.externalPump.pin == 0) {
|
||||
if (vars.states.externalPump) {
|
||||
if (settings.externalPump.pin != 0) {
|
||||
digitalWrite(settings.externalPump.pin, false);
|
||||
// input
|
||||
if (settings.cascadeControl.input.enabled) {
|
||||
if (settings.cascadeControl.input.gpio != configuredInputGpio) {
|
||||
if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
pinMode(configuredInputGpio, OUTPUT);
|
||||
digitalWrite(configuredInputGpio, LOW);
|
||||
|
||||
Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Deinitialized on GPIO %hhu"), configuredInputGpio);
|
||||
}
|
||||
|
||||
if (GPIO_IS_VALID(settings.cascadeControl.input.gpio)) {
|
||||
configuredInputGpio = settings.cascadeControl.input.gpio;
|
||||
pinMode(configuredInputGpio, INPUT);
|
||||
|
||||
Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Initialized on GPIO %hhu"), configuredInputGpio);
|
||||
|
||||
} else if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
configuredInputGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
|
||||
Log.swarningln(FPSTR(L_CASCADE_INPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredInputGpio);
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
bool value;
|
||||
if (digitalRead(configuredInputGpio) == HIGH) {
|
||||
value = true ^ settings.cascadeControl.input.invertState;
|
||||
} else {
|
||||
value = false ^ settings.cascadeControl.input.invertState;
|
||||
}
|
||||
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
if (value != vars.cascadeControl.input) {
|
||||
if (value != inputTempValue) {
|
||||
inputTempValue = value;
|
||||
inputChangedTs = millis();
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
|
||||
} else if (millis() - inputChangedTs >= settings.cascadeControl.input.thresholdTime * 1000u) {
|
||||
vars.cascadeControl.input = value;
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_CASCADE_INPUT),
|
||||
F("State changed to %s"),
|
||||
value ? F("TRUE") : F("FALSE")
|
||||
);
|
||||
}
|
||||
|
||||
} else if (value != inputTempValue) {
|
||||
inputTempValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.cascadeControl.input.enabled || configuredInputGpio == GPIO_IS_NOT_CONFIGURED) {
|
||||
if (!vars.cascadeControl.input) {
|
||||
vars.cascadeControl.input = true;
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_CASCADE_INPUT),
|
||||
F("Disabled, state changed to %s"),
|
||||
vars.cascadeControl.input ? F("TRUE") : F("FALSE")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// output
|
||||
if (settings.cascadeControl.output.enabled) {
|
||||
if (settings.cascadeControl.output.gpio != configuredOutputGpio) {
|
||||
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
pinMode(configuredOutputGpio, OUTPUT);
|
||||
digitalWrite(configuredOutputGpio, LOW);
|
||||
|
||||
Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Deinitialized on GPIO %hhu"), configuredOutputGpio);
|
||||
}
|
||||
|
||||
if (GPIO_IS_VALID(settings.cascadeControl.output.gpio)) {
|
||||
configuredOutputGpio = settings.cascadeControl.output.gpio;
|
||||
pinMode(configuredOutputGpio, OUTPUT);
|
||||
digitalWrite(
|
||||
configuredOutputGpio,
|
||||
settings.cascadeControl.output.invertState
|
||||
? HIGH
|
||||
: LOW
|
||||
);
|
||||
|
||||
Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Initialized on GPIO %hhu"), configuredOutputGpio);
|
||||
|
||||
} else if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
|
||||
Log.swarningln(FPSTR(L_CASCADE_OUTPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredOutputGpio);
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
bool value = false;
|
||||
if (settings.cascadeControl.output.onFault && vars.slave.fault.active) {
|
||||
value = true;
|
||||
|
||||
} else if (settings.cascadeControl.output.onLossConnection && !vars.slave.connected) {
|
||||
value = true;
|
||||
|
||||
} else if (settings.cascadeControl.output.onEnabledHeating && settings.heating.enabled && vars.cascadeControl.input) {
|
||||
value = true;
|
||||
}
|
||||
|
||||
if (value != vars.cascadeControl.output) {
|
||||
if (value != outputTempValue) {
|
||||
outputTempValue = value;
|
||||
outputChangedTs = millis();
|
||||
|
||||
} else if (millis() - outputChangedTs >= settings.cascadeControl.output.thresholdTime * 1000u) {
|
||||
vars.cascadeControl.output = value;
|
||||
|
||||
digitalWrite(
|
||||
configuredOutputGpio,
|
||||
vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
|
||||
? HIGH
|
||||
: LOW
|
||||
);
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_CASCADE_OUTPUT),
|
||||
F("State changed to %s"),
|
||||
value ? F("TRUE") : F("FALSE")
|
||||
);
|
||||
}
|
||||
|
||||
} else if (value != outputTempValue) {
|
||||
outputTempValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.cascadeControl.output.enabled || configuredOutputGpio == GPIO_IS_NOT_CONFIGURED) {
|
||||
if (vars.cascadeControl.output) {
|
||||
vars.cascadeControl.output = false;
|
||||
|
||||
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
digitalWrite(
|
||||
configuredOutputGpio,
|
||||
vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
|
||||
? HIGH
|
||||
: LOW
|
||||
);
|
||||
}
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_CASCADE_OUTPUT),
|
||||
F("Disabled, state changed to %s"),
|
||||
vars.cascadeControl.output ? F("TRUE") : F("FALSE")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void externalPump() {
|
||||
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
|
||||
if (settings.externalPump.gpio != configuredGpio) {
|
||||
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
}
|
||||
|
||||
if (GPIO_IS_VALID(settings.externalPump.gpio)) {
|
||||
configuredGpio = settings.externalPump.gpio;
|
||||
pinMode(configuredGpio, OUTPUT);
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
|
||||
configuredGpio = GPIO_IS_NOT_CONFIGURED;
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
|
||||
if (vars.externalPump.state) {
|
||||
vars.externalPump.state = false;
|
||||
vars.externalPump.lastEnabledTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off"));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (vars.states.externalPump && !this->heatingEnabled) {
|
||||
if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
|
||||
digitalWrite(settings.externalPump.pin, false);
|
||||
if (!vars.master.heating.enabled && this->heatingEnabled) {
|
||||
this->heatingEnabled = false;
|
||||
this->heatingDisabledTime = millis();
|
||||
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
} else if (vars.master.heating.enabled && !this->heatingEnabled) {
|
||||
this->heatingEnabled = true;
|
||||
}
|
||||
|
||||
if (!settings.externalPump.use) {
|
||||
if (vars.externalPump.state) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time"));
|
||||
vars.externalPump.state = false;
|
||||
vars.externalPump.lastEnabledTime = millis();
|
||||
|
||||
} else if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) {
|
||||
digitalWrite(settings.externalPump.pin, false);
|
||||
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time"));
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off"));
|
||||
}
|
||||
|
||||
} else if (vars.states.externalPump && this->heatingEnabled && this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK) {
|
||||
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (!vars.states.externalPump && this->heatingEnabled) {
|
||||
vars.states.externalPump = true;
|
||||
if (vars.externalPump.state && !this->heatingEnabled) {
|
||||
if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
vars.externalPump.state = false;
|
||||
vars.externalPump.lastEnabledTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired post circulation time"));
|
||||
|
||||
} else if (this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
vars.externalPump.state = false;
|
||||
vars.externalPump.lastEnabledTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired anti stuck time"));
|
||||
}
|
||||
|
||||
} else if (vars.externalPump.state && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) {
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
|
||||
|
||||
} else if (!vars.externalPump.state && this->heatingEnabled) {
|
||||
vars.externalPump.state = true;
|
||||
this->externalPumpStartTime = millis();
|
||||
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
|
||||
|
||||
digitalWrite(settings.externalPump.pin, true);
|
||||
digitalWrite(configuredGpio, HIGH);
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Enabled: heating on"));
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: heating on"));
|
||||
|
||||
} else if (!vars.states.externalPump && (vars.parameters.extPumpLastEnableTime == 0 || millis() - vars.parameters.extPumpLastEnableTime >= (settings.externalPump.antiStuckInterval * 1000ul))) {
|
||||
vars.states.externalPump = true;
|
||||
} else if (!vars.externalPump.state && (vars.externalPump.lastEnabledTime == 0 || millis() - vars.externalPump.lastEnabledTime >= (settings.externalPump.antiStuckInterval * 1000lu))) {
|
||||
vars.externalPump.state = true;
|
||||
this->externalPumpStartTime = millis();
|
||||
this->externalPumpStartReason = MainTask::REASON_PUMP_START_ANTISTUCK;
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK;
|
||||
|
||||
digitalWrite(settings.externalPump.pin, true);
|
||||
digitalWrite(configuredGpio, HIGH);
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Enabled: anti stuck"));
|
||||
Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: anti stuck"));
|
||||
}
|
||||
}
|
||||
};
|
||||
576
src/MqttTask.h
@@ -1,3 +1,4 @@
|
||||
#include <unordered_map>
|
||||
#include <MqttClient.h>
|
||||
#include <MqttWiFiClient.h>
|
||||
#include <MqttWriter.h>
|
||||
@@ -15,29 +16,33 @@ public:
|
||||
}
|
||||
|
||||
~MqttTask() {
|
||||
if (this->haHelper != nullptr) {
|
||||
delete this->haHelper;
|
||||
}
|
||||
delete this->haHelper;
|
||||
|
||||
if (this->client != nullptr) {
|
||||
if (this->client->connected()) {
|
||||
this->client->stop();
|
||||
|
||||
if (this->connected) {
|
||||
this->onDisconnect();
|
||||
this->connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
delete this->client;
|
||||
}
|
||||
|
||||
if (this->writer != nullptr) {
|
||||
delete this->writer;
|
||||
}
|
||||
|
||||
if (this->wifiClient != nullptr) {
|
||||
delete this->wifiClient;
|
||||
}
|
||||
delete this->writer;
|
||||
delete this->wifiClient;
|
||||
}
|
||||
|
||||
void disable() {
|
||||
this->client->stop();
|
||||
|
||||
if (this->connected) {
|
||||
this->onDisconnect();
|
||||
this->connected = false;
|
||||
}
|
||||
|
||||
Task::disable();
|
||||
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Disabled"));
|
||||
@@ -49,37 +54,59 @@ public:
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Enabled"));
|
||||
}
|
||||
|
||||
bool isConnected() {
|
||||
inline bool isConnected() {
|
||||
return this->connected;
|
||||
}
|
||||
|
||||
inline void resetPublishedSettingsTime() {
|
||||
this->prevPubSettingsTime = 0;
|
||||
}
|
||||
|
||||
inline void resetPublishedSensorTime(uint8_t sensorId) {
|
||||
this->prevPubSensorTime[sensorId] = 0;
|
||||
}
|
||||
|
||||
inline void resetPublishedVarsTime() {
|
||||
this->prevPubVarsTime = 0;
|
||||
}
|
||||
|
||||
inline void reconfigureSensor(uint8_t sensorId, Sensors::Settings& prevSettings) {
|
||||
this->queueReconfigureSensors[sensorId] = prevSettings;
|
||||
}
|
||||
|
||||
protected:
|
||||
MqttWiFiClient* wifiClient = nullptr;
|
||||
MqttClient* client = nullptr;
|
||||
HaHelper* haHelper = nullptr;
|
||||
MqttWriter* writer = nullptr;
|
||||
unsigned short readyForSendTime = 15000;
|
||||
UnitSystem currentUnitSystem = UnitSystem::METRIC;
|
||||
bool currentHomeAssistantDiscovery = false;
|
||||
std::unordered_map<uint8_t, Sensors::Settings> queueReconfigureSensors;
|
||||
unsigned short readyForSendTime = 30000;
|
||||
unsigned long lastReconnectTime = 0;
|
||||
unsigned long connectedTime = 0;
|
||||
unsigned long disconnectedTime = 0;
|
||||
unsigned long prevPubVarsTime = 0;
|
||||
unsigned long prevPubSettingsTime = 0;
|
||||
std::unordered_map<uint8_t, unsigned long> prevPubSensorTime;
|
||||
bool connected = false;
|
||||
bool newConnection = false;
|
||||
|
||||
const char* getTaskName() {
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Mqtt";
|
||||
}
|
||||
|
||||
/*int getTaskCore() {
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}*/
|
||||
|
||||
int getTaskPriority() {
|
||||
return 1;
|
||||
int getTaskPriority() override {
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool isReadyForSend() {
|
||||
inline bool isReadyForSend() {
|
||||
return millis() - this->connectedTime > this->readyForSendTime;
|
||||
}
|
||||
|
||||
@@ -93,7 +120,6 @@ protected:
|
||||
#endif
|
||||
|
||||
// client settings
|
||||
this->client->setKeepAliveInterval(15000);
|
||||
this->client->setTxPayloadSize(256);
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
this->client->setConnectionTimeout(1000);
|
||||
@@ -102,7 +128,7 @@ protected:
|
||||
#endif
|
||||
|
||||
this->client->onMessage([this] (void*, size_t length) {
|
||||
String topic = this->client->messageTopic();
|
||||
const String& topic = this->client->messageTopic();
|
||||
if (!length || length > 2048 || !topic.length()) {
|
||||
return;
|
||||
}
|
||||
@@ -112,7 +138,7 @@ protected:
|
||||
payload[i] = this->client->read();
|
||||
}
|
||||
|
||||
this->onMessage(topic.c_str(), payload, length);
|
||||
this->onMessage(topic, payload, length);
|
||||
});
|
||||
|
||||
// writer settings
|
||||
@@ -126,7 +152,7 @@ protected:
|
||||
Log.straceln(FPSTR(L_MQTT), F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
#endif
|
||||
|
||||
//this->client->poll();
|
||||
@@ -135,19 +161,19 @@ protected:
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
this->writer->setFlushEventCallback([this] (size_t, size_t) {
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
|
||||
if (this->wifiClient->connected()) {
|
||||
this->wifiClient->flush();
|
||||
}
|
||||
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
});
|
||||
#endif
|
||||
|
||||
// ha helper settings
|
||||
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
|
||||
this->haHelper->setDeviceVersion(PROJECT_VERSION);
|
||||
this->haHelper->setDeviceVersion(BUILD_VERSION);
|
||||
this->haHelper->setDeviceModel(PROJECT_NAME);
|
||||
this->haHelper->setDeviceName(PROJECT_NAME);
|
||||
this->haHelper->setWriter(this->writer);
|
||||
@@ -157,9 +183,8 @@ protected:
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (settings.mqtt.interval > 120) {
|
||||
settings.mqtt.interval = 5;
|
||||
fsSettings.update();
|
||||
if (vars.states.restarting || vars.states.upgrading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->connected && !this->client->connected()) {
|
||||
@@ -170,9 +195,15 @@ protected:
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
|
||||
|
||||
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
|
||||
this->haHelper->updateCachedTopics();
|
||||
|
||||
this->client->stop();
|
||||
this->client->setKeepAliveInterval(settings.mqtt.interval * 10000);
|
||||
this->client->setId(networkSettings.hostname);
|
||||
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
|
||||
this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1);
|
||||
this->client->print(F("offline"));
|
||||
this->client->endWill();
|
||||
this->client->connect(settings.mqtt.server, settings.mqtt.port);
|
||||
this->lastReconnectTime = millis();
|
||||
this->yield();
|
||||
@@ -182,15 +213,6 @@ protected:
|
||||
this->onConnect();
|
||||
}
|
||||
|
||||
if (!this->connected && settings.emergency.enable && !vars.states.emergency && millis() - this->disconnectedTime > EMERGENCY_TIME_TRESHOLD) {
|
||||
vars.states.emergency = true;
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
|
||||
|
||||
} else if (this->connected && vars.states.emergency && millis() - this->connectedTime > 10000) {
|
||||
vars.states.emergency = false;
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
|
||||
}
|
||||
|
||||
if (!this->connected) {
|
||||
return;
|
||||
}
|
||||
@@ -203,36 +225,152 @@ protected:
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
::optimistic_yield(1000);
|
||||
#endif
|
||||
|
||||
// publish variables and status
|
||||
if (this->newConnection || millis() - this->prevPubVarsTime > (settings.mqtt.interval * 1000u)) {
|
||||
this->writer->publish(
|
||||
this->haHelper->getDeviceTopic("status").c_str(),
|
||||
!vars.states.otStatus ? "offline" : vars.states.fault ? "fault" : "online",
|
||||
true
|
||||
);
|
||||
|
||||
this->publishVariables(this->haHelper->getDeviceTopic("state").c_str());
|
||||
this->writer->publish(this->haHelper->getDeviceTopic(F("status")).c_str(), "online", false);
|
||||
this->publishVariables(this->haHelper->getDeviceTopic(F("state")).c_str());
|
||||
this->prevPubVarsTime = millis();
|
||||
}
|
||||
|
||||
// publish settings
|
||||
if (this->newConnection || millis() - this->prevPubSettingsTime > (settings.mqtt.interval * 10000u)) {
|
||||
this->publishSettings(this->haHelper->getDeviceTopic("settings").c_str());
|
||||
this->publishSettings(this->haHelper->getDeviceTopic(F("settings")).c_str());
|
||||
this->prevPubSettingsTime = millis();
|
||||
}
|
||||
|
||||
// publish ha entities if not published
|
||||
if (this->newConnection) {
|
||||
this->publishHaEntities();
|
||||
this->publishNonStaticHaEntities(true);
|
||||
this->newConnection = false;
|
||||
// publish sensors
|
||||
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
|
||||
if (!Sensors::hasEnabledAndValid(sensorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} else {
|
||||
// publish non static ha entities
|
||||
this->publishNonStaticHaEntities();
|
||||
auto& rSensor = Sensors::results[sensorId];
|
||||
bool needUpdate = false;
|
||||
if (millis() - this->prevPubSensorTime[sensorId] > ((this->haHelper->getExpireAfter() - 10) * 1000u)) {
|
||||
needUpdate = true;
|
||||
|
||||
} else if (rSensor.activityTime >= this->prevPubSensorTime[sensorId]) {
|
||||
auto estimated = rSensor.activityTime - this->prevPubSensorTime[sensorId];
|
||||
needUpdate = estimated > 1000u;
|
||||
}
|
||||
|
||||
if (this->newConnection || needUpdate) {
|
||||
this->publishSensor(sensorId);
|
||||
this->prevPubSensorTime[sensorId] = millis();
|
||||
}
|
||||
}
|
||||
|
||||
// publish ha entities if not published
|
||||
if (settings.mqtt.homeAssistantDiscovery) {
|
||||
if (this->newConnection || !this->currentHomeAssistantDiscovery || this->currentUnitSystem != settings.system.unitSystem) {
|
||||
this->publishHaEntities();
|
||||
this->publishNonStaticHaEntities(true);
|
||||
this->currentHomeAssistantDiscovery = true;
|
||||
this->currentUnitSystem = settings.system.unitSystem;
|
||||
|
||||
} else {
|
||||
// publish non static ha entities
|
||||
this->publishNonStaticHaEntities();
|
||||
}
|
||||
|
||||
// rebuilding ha configs
|
||||
for (auto& [sensorId, prevSettings] : this->queueReconfigureSensors) {
|
||||
Log.sinfoln(FPSTR(L_MQTT_HA), F("Rebuilding config for sensor #%hhu '%s'"), sensorId, prevSettings.name);
|
||||
|
||||
// delete old config
|
||||
if (strlen(prevSettings.name) && prevSettings.enabled) {
|
||||
switch (prevSettings.type) {
|
||||
case Sensors::Type::BLUETOOTH:
|
||||
this->haHelper->deleteConnectionDynamicSensor(prevSettings);
|
||||
this->haHelper->deleteSignalQualityDynamicSensor(prevSettings);
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE);
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::HUMIDITY);
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::BATTERY);
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::RSSI);
|
||||
break;
|
||||
|
||||
case Sensors::Type::DALLAS_TEMP:
|
||||
this->haHelper->deleteConnectionDynamicSensor(prevSettings);
|
||||
this->haHelper->deleteSignalQualityDynamicSensor(prevSettings);
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE);
|
||||
break;
|
||||
|
||||
default:
|
||||
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::PRIMARY);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Sensors::hasEnabledAndValid(sensorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// make new config
|
||||
auto& sSettings = Sensors::settings[sensorId];
|
||||
switch (sSettings.type) {
|
||||
case Sensors::Type::BLUETOOTH:
|
||||
this->haHelper->publishConnectionDynamicSensor(sSettings);
|
||||
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::HUMIDITY, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::BATTERY, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::RSSI, settings.system.unitSystem, false);
|
||||
break;
|
||||
|
||||
case Sensors::Type::DALLAS_TEMP:
|
||||
this->haHelper->publishConnectionDynamicSensor(sSettings);
|
||||
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
|
||||
break;
|
||||
|
||||
default:
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (this->currentHomeAssistantDiscovery) {
|
||||
this->currentHomeAssistantDiscovery = false;
|
||||
}
|
||||
|
||||
// reconfigure manual sensors
|
||||
for (auto& [sensorId, prevSettings] : this->queueReconfigureSensors) {
|
||||
// unsubscribe from old topic
|
||||
if (strlen(prevSettings.name) && prevSettings.enabled) {
|
||||
if (prevSettings.type == Sensors::Type::MANUAL) {
|
||||
this->client->unsubscribe(
|
||||
this->haHelper->getDeviceTopic(
|
||||
F("sensors"),
|
||||
Sensors::makeObjectId(prevSettings.name).c_str(),
|
||||
F("set")
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Sensors::hasEnabledAndValid(sensorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// subscribe to new topic
|
||||
auto& sSettings = Sensors::settings[sensorId];
|
||||
if (sSettings.type == Sensors::Type::MANUAL) {
|
||||
this->client->subscribe(
|
||||
this->haHelper->getDeviceTopic(
|
||||
F("sensors"),
|
||||
Sensors::makeObjectId(sSettings.name).c_str(),
|
||||
F("set")
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// clear queue
|
||||
this->queueReconfigureSensors.clear();
|
||||
|
||||
if (this->newConnection) {
|
||||
this->newConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,24 +380,44 @@ protected:
|
||||
unsigned long downtime = (millis() - this->disconnectedTime) / 1000;
|
||||
Log.sinfoln(FPSTR(L_MQTT), F("Connected (downtime: %u s.)"), downtime);
|
||||
|
||||
this->client->subscribe(this->haHelper->getDeviceTopic("settings/set").c_str());
|
||||
this->client->subscribe(this->haHelper->getDeviceTopic("state/set").c_str());
|
||||
this->client->subscribe(this->haHelper->getDeviceTopic(F("settings/set")).c_str());
|
||||
this->client->subscribe(this->haHelper->getDeviceTopic(F("state/set")).c_str());
|
||||
|
||||
// subscribe to manual sensors
|
||||
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
|
||||
if (!Sensors::hasEnabledAndValid(sensorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& sSettings = Sensors::settings[sensorId];
|
||||
if (sSettings.type != Sensors::Type::MANUAL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this->client->subscribe(
|
||||
this->haHelper->getDeviceTopic(
|
||||
F("sensors"),
|
||||
Sensors::makeObjectId(sSettings.name).c_str(),
|
||||
F("set")
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onDisconnect() {
|
||||
this->disconnectedTime = millis();
|
||||
|
||||
unsigned long uptime = (millis() - this->connectedTime) / 1000;
|
||||
Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %u s.)"), this->client->connectError(), uptime);
|
||||
Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %lu s.)"), this->client->connectError(), uptime);
|
||||
}
|
||||
|
||||
void onMessage(const char* topic, uint8_t* payload, size_t length) {
|
||||
void onMessage(const String& topic, uint8_t* payload, size_t length) {
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.system.debug) {
|
||||
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
|
||||
if (settings.system.logLevel >= TinyLogger::Level::TRACE) {
|
||||
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic.c_str());
|
||||
if (Log.lock()) {
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
if (payload[i] == 0) {
|
||||
@@ -267,12 +425,12 @@ protected:
|
||||
} else if (payload[i] == 13) {
|
||||
continue;
|
||||
} else if (payload[i] == 10) {
|
||||
Log.print("\r\n> ");
|
||||
Log.print(F("\r\n> "));
|
||||
} else {
|
||||
Log.print((char) payload[i]);
|
||||
}
|
||||
}
|
||||
Log.print("\r\n\n");
|
||||
Log.print(F("\r\n\n"));
|
||||
Log.flush();
|
||||
Log.unlock();
|
||||
}
|
||||
@@ -288,214 +446,160 @@ protected:
|
||||
Log.swarningln(FPSTR(L_MQTT_MSG), F("Not valid json"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->haHelper->getDeviceTopic("state/set").equals(topic)) {
|
||||
this->writer->publish(this->haHelper->getDeviceTopic("state/set").c_str(), nullptr, 0, true);
|
||||
this->updateVariables(doc);
|
||||
|
||||
} else if (this->haHelper->getDeviceTopic("settings/set").equals(topic)) {
|
||||
this->writer->publish(this->haHelper->getDeviceTopic("settings/set").c_str(), nullptr, 0, true);
|
||||
this->updateSettings(doc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool updateSettings(JsonDocument& doc) {
|
||||
bool changed = safeJsonToSettings(doc, settings);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
if (changed) {
|
||||
this->prevPubSettingsTime = 0;
|
||||
fsSettings.update();
|
||||
return true;
|
||||
// delete topic
|
||||
this->writer->publish(topic.c_str(), nullptr, 0, true);
|
||||
|
||||
if (this->haHelper->getDeviceTopic(F("state/set")).equals(topic)) {
|
||||
if (jsonToVars(doc, vars)) {
|
||||
this->resetPublishedVarsTime();
|
||||
}
|
||||
|
||||
} else if (this->haHelper->getDeviceTopic(F("settings/set")).equals(topic)) {
|
||||
if (safeJsonToSettings(doc, settings)) {
|
||||
this->resetPublishedSettingsTime();
|
||||
fsSettings.update();
|
||||
}
|
||||
|
||||
} else {
|
||||
const String& sensorsTopic = this->haHelper->getDeviceTopic(F("sensors/"));
|
||||
auto stLength = sensorsTopic.length();
|
||||
|
||||
if (topic.startsWith(sensorsTopic) && topic.endsWith(F("/set"))) {
|
||||
if (topic.length() > stLength + 4) {
|
||||
const String& name = topic.substring(stLength, topic.indexOf('/', stLength));
|
||||
int16_t id = Sensors::getIdByObjectId(name.c_str());
|
||||
|
||||
if (id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (jsonToSensorResult(id, doc)) {
|
||||
this->resetPublishedSensorTime(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool updateVariables(JsonDocument& doc) {
|
||||
bool changed = jsonToVars(doc, vars);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
if (changed) {
|
||||
this->prevPubVarsTime = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void publishHaEntities() {
|
||||
// main
|
||||
this->haHelper->publishNumberOutdoorSensorOffset(false);
|
||||
this->haHelper->publishNumberIndoorSensorOffset(false);
|
||||
|
||||
// emergency
|
||||
this->haHelper->publishSwitchEmergency();
|
||||
this->haHelper->publishNumberEmergencyTarget();
|
||||
this->haHelper->publishSwitchEmergencyUseEquitherm();
|
||||
this->haHelper->publishSwitchEmergencyUsePid();
|
||||
|
||||
// heating
|
||||
this->haHelper->publishSwitchHeating(false);
|
||||
this->haHelper->publishSwitchHeatingTurbo();
|
||||
this->haHelper->publishNumberHeatingHysteresis();
|
||||
this->haHelper->publishSensorHeatingSetpoint(false);
|
||||
this->haHelper->publishSensorBoilerHeatingMinTemp(false);
|
||||
this->haHelper->publishSensorBoilerHeatingMaxTemp(false);
|
||||
this->haHelper->publishNumberHeatingMinTemp(false);
|
||||
this->haHelper->publishNumberHeatingMaxTemp(false);
|
||||
this->haHelper->publishNumberHeatingMaxModulation(false);
|
||||
this->haHelper->publishSwitchHeatingTurbo(false);
|
||||
this->haHelper->publishInputHeatingHysteresis(settings.system.unitSystem);
|
||||
this->haHelper->publishInputHeatingTurboFactor(false);
|
||||
this->haHelper->publishInputHeatingMinTemp(settings.system.unitSystem);
|
||||
this->haHelper->publishInputHeatingMaxTemp(settings.system.unitSystem);
|
||||
|
||||
// pid
|
||||
this->haHelper->publishSwitchPid();
|
||||
this->haHelper->publishNumberPidFactorP();
|
||||
this->haHelper->publishNumberPidFactorI();
|
||||
this->haHelper->publishNumberPidFactorD();
|
||||
this->haHelper->publishNumberPidDt(false);
|
||||
this->haHelper->publishNumberPidMinTemp(false);
|
||||
this->haHelper->publishNumberPidMaxTemp(false);
|
||||
this->haHelper->publishInputPidFactorP(false);
|
||||
this->haHelper->publishInputPidFactorI(false);
|
||||
this->haHelper->publishInputPidFactorD(false);
|
||||
this->haHelper->publishInputPidDt(false);
|
||||
this->haHelper->publishInputPidMinTemp(settings.system.unitSystem, false);
|
||||
this->haHelper->publishInputPidMaxTemp(settings.system.unitSystem, false);
|
||||
|
||||
// equitherm
|
||||
this->haHelper->publishSwitchEquitherm();
|
||||
this->haHelper->publishNumberEquithermFactorN();
|
||||
this->haHelper->publishNumberEquithermFactorK();
|
||||
this->haHelper->publishNumberEquithermFactorT();
|
||||
|
||||
// tuning
|
||||
this->haHelper->publishSwitchTuning();
|
||||
this->haHelper->publishSelectTuningRegulator();
|
||||
this->haHelper->publishInputEquithermFactorN(false);
|
||||
this->haHelper->publishInputEquithermFactorK(false);
|
||||
this->haHelper->publishInputEquithermFactorT(false);
|
||||
|
||||
// states
|
||||
this->haHelper->publishBinSensorStatus();
|
||||
this->haHelper->publishBinSensorOtStatus();
|
||||
this->haHelper->publishBinSensorHeating();
|
||||
this->haHelper->publishBinSensorFlame();
|
||||
this->haHelper->publishBinSensorFault();
|
||||
this->haHelper->publishBinSensorDiagnostic();
|
||||
this->haHelper->publishStatusState();
|
||||
this->haHelper->publishEmergencyState();
|
||||
this->haHelper->publishOpenthermConnectedState();
|
||||
this->haHelper->publishHeatingState();
|
||||
this->haHelper->publishFlameState();
|
||||
this->haHelper->publishFaultState();
|
||||
this->haHelper->publishDiagState();
|
||||
this->haHelper->publishExternalPumpState(false);
|
||||
|
||||
// sensors
|
||||
this->haHelper->publishSensorModulation(false);
|
||||
this->haHelper->publishSensorPressure(false);
|
||||
this->haHelper->publishSensorFaultCode();
|
||||
this->haHelper->publishSensorRssi(false);
|
||||
this->haHelper->publishSensorUptime(false);
|
||||
|
||||
// temperatures
|
||||
this->haHelper->publishNumberIndoorTemp();
|
||||
this->haHelper->publishSensorHeatingTemp();
|
||||
this->haHelper->publishFaultCode();
|
||||
this->haHelper->publishDiagCode();
|
||||
this->haHelper->publishNetworkRssi(false);
|
||||
this->haHelper->publishUptime(false);
|
||||
|
||||
// buttons
|
||||
this->haHelper->publishButtonRestart(false);
|
||||
this->haHelper->publishButtonResetFault();
|
||||
this->haHelper->publishButtonResetDiagnostic();
|
||||
this->haHelper->publishRestartButton(false);
|
||||
this->haHelper->publishResetFaultButton();
|
||||
this->haHelper->publishResetDiagButton();
|
||||
|
||||
// dynamic sensors
|
||||
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
|
||||
if (!Sensors::hasEnabledAndValid(sensorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& sSettings = Sensors::settings[sensorId];
|
||||
switch (sSettings.type) {
|
||||
case Sensors::Type::BLUETOOTH:
|
||||
this->haHelper->publishConnectionDynamicSensor(sSettings);
|
||||
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::HUMIDITY, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::BATTERY, settings.system.unitSystem);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::RSSI, settings.system.unitSystem, false);
|
||||
break;
|
||||
|
||||
case Sensors::Type::DALLAS_TEMP:
|
||||
this->haHelper->publishConnectionDynamicSensor(sSettings);
|
||||
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
|
||||
break;
|
||||
|
||||
default:
|
||||
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool publishNonStaticHaEntities(bool force = false) {
|
||||
static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp = 0;
|
||||
static bool _isStupidMode, _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent = false;
|
||||
static bool _indoorTempControl, _dhwSupport = false;
|
||||
|
||||
bool published = false;
|
||||
bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable;
|
||||
byte heatingMinTemp = isStupidMode ? settings.heating.minTemp : 10;
|
||||
byte heatingMaxTemp = isStupidMode ? settings.heating.maxTemp : 30;
|
||||
bool editableOutdoorTemp = settings.sensors.outdoor.type == 1;
|
||||
bool editableIndoorTemp = settings.sensors.indoor.type == 1;
|
||||
if (force || _dhwSupport != settings.opentherm.options.dhwSupport) {
|
||||
_dhwSupport = settings.opentherm.options.dhwSupport;
|
||||
|
||||
if (force || _dhwPresent != settings.opentherm.dhwPresent) {
|
||||
_dhwPresent = settings.opentherm.dhwPresent;
|
||||
|
||||
if (_dhwPresent) {
|
||||
this->haHelper->publishSwitchDhw(false);
|
||||
this->haHelper->publishSensorBoilerDhwMinTemp(false);
|
||||
this->haHelper->publishSensorBoilerDhwMaxTemp(false);
|
||||
this->haHelper->publishNumberDhwMinTemp(false);
|
||||
this->haHelper->publishNumberDhwMaxTemp(false);
|
||||
this->haHelper->publishBinSensorDhw();
|
||||
this->haHelper->publishSensorDhwTemp();
|
||||
this->haHelper->publishSensorDhwFlowRate(false);
|
||||
if (_dhwSupport) {
|
||||
this->haHelper->publishInputDhwMinTemp(settings.system.unitSystem);
|
||||
this->haHelper->publishInputDhwMaxTemp(settings.system.unitSystem);
|
||||
this->haHelper->publishDhwState();
|
||||
|
||||
} else {
|
||||
this->haHelper->deleteSwitchDhw();
|
||||
this->haHelper->deleteSensorBoilerDhwMinTemp();
|
||||
this->haHelper->deleteSensorBoilerDhwMaxTemp();
|
||||
this->haHelper->deleteNumberDhwMinTemp();
|
||||
this->haHelper->deleteNumberDhwMaxTemp();
|
||||
this->haHelper->deleteBinSensorDhw();
|
||||
this->haHelper->deleteSensorDhwTemp();
|
||||
this->haHelper->deleteNumberDhwTarget();
|
||||
this->haHelper->deleteInputDhwMinTemp();
|
||||
this->haHelper->deleteInputDhwMaxTemp();
|
||||
this->haHelper->deleteDhwState();
|
||||
this->haHelper->deleteInputDhwTarget();
|
||||
this->haHelper->deleteClimateDhw();
|
||||
this->haHelper->deleteSensorDhwFlowRate();
|
||||
}
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
|
||||
if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) {
|
||||
settings.heating.target = constrain(settings.heating.target, heatingMinTemp, heatingMaxTemp);
|
||||
}
|
||||
if (force || _indoorTempControl != vars.master.heating.indoorTempControl || _heatingMinTemp != vars.master.heating.minTemp || _heatingMaxTemp != vars.master.heating.maxTemp) {
|
||||
_heatingMinTemp = vars.master.heating.minTemp;
|
||||
_heatingMaxTemp = vars.master.heating.maxTemp;
|
||||
_indoorTempControl = vars.master.heating.indoorTempControl;
|
||||
|
||||
_heatingMinTemp = heatingMinTemp;
|
||||
_heatingMaxTemp = heatingMaxTemp;
|
||||
_isStupidMode = isStupidMode;
|
||||
|
||||
this->haHelper->publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false);
|
||||
this->haHelper->publishClimateHeating(
|
||||
heatingMinTemp,
|
||||
heatingMaxTemp,
|
||||
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
|
||||
);
|
||||
|
||||
published = true;
|
||||
|
||||
} else if (_isStupidMode != isStupidMode) {
|
||||
_isStupidMode = isStupidMode;
|
||||
this->haHelper->publishClimateHeating(
|
||||
heatingMinTemp,
|
||||
heatingMaxTemp,
|
||||
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
|
||||
settings.system.unitSystem,
|
||||
vars.master.heating.minTemp,
|
||||
vars.master.heating.maxTemp
|
||||
);
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (_dhwPresent && (force || _dhwMinTemp != settings.dhw.minTemp || _dhwMaxTemp != settings.dhw.maxTemp)) {
|
||||
if (_dhwSupport && (force || _dhwMinTemp != settings.dhw.minTemp || _dhwMaxTemp != settings.dhw.maxTemp)) {
|
||||
_dhwMinTemp = settings.dhw.minTemp;
|
||||
_dhwMaxTemp = settings.dhw.maxTemp;
|
||||
|
||||
this->haHelper->publishNumberDhwTarget(settings.dhw.minTemp, settings.dhw.maxTemp, false);
|
||||
this->haHelper->publishClimateDhw(settings.dhw.minTemp, settings.dhw.maxTemp);
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _editableOutdoorTemp != editableOutdoorTemp) {
|
||||
_editableOutdoorTemp = editableOutdoorTemp;
|
||||
|
||||
if (editableOutdoorTemp) {
|
||||
this->haHelper->deleteSensorOutdoorTemp();
|
||||
this->haHelper->publishNumberOutdoorTemp();
|
||||
} else {
|
||||
this->haHelper->deleteNumberOutdoorTemp();
|
||||
this->haHelper->publishSensorOutdoorTemp();
|
||||
}
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _editableIndoorTemp != editableIndoorTemp) {
|
||||
_editableIndoorTemp = editableIndoorTemp;
|
||||
|
||||
if (editableIndoorTemp) {
|
||||
this->haHelper->deleteSensorIndoorTemp();
|
||||
this->haHelper->publishNumberIndoorTemp();
|
||||
} else {
|
||||
this->haHelper->deleteNumberIndoorTemp();
|
||||
this->haHelper->publishSensorIndoorTemp();
|
||||
}
|
||||
this->haHelper->publishClimateDhw(settings.system.unitSystem, settings.dhw.minTemp, settings.dhw.maxTemp);
|
||||
|
||||
published = true;
|
||||
}
|
||||
@@ -511,6 +615,30 @@ protected:
|
||||
return this->writer->publish(topic, doc, true);
|
||||
}
|
||||
|
||||
bool publishSensor(uint8_t sensorId) {
|
||||
auto& sSettings = Sensors::settings[sensorId];
|
||||
|
||||
if (!Sensors::isValidSensorId(sensorId)) {
|
||||
return false;
|
||||
|
||||
} else if (!strlen(sSettings.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
sensorResultToJson(sensorId, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
return this->writer->publish(
|
||||
this->haHelper->getDeviceTopic(
|
||||
F("sensors"),
|
||||
Sensors::makeObjectId(sSettings.name).c_str()
|
||||
).c_str(),
|
||||
doc,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
bool publishVariables(const char* topic) {
|
||||
JsonDocument doc;
|
||||
varsToJson(vars, doc);
|
||||
|
||||
2577
src/OpenThermTask.h
788
src/PortalTask.h
@@ -1,10 +1,8 @@
|
||||
#include <Equitherm.h>
|
||||
#include <GyverPID.h>
|
||||
#include <PIDtuner.h>
|
||||
|
||||
Equitherm etRegulator;
|
||||
GyverPID pidRegulator(0, 0, 0);
|
||||
PIDtuner pidTuner;
|
||||
|
||||
|
||||
class RegulatorTask : public LeanTask {
|
||||
@@ -12,142 +10,181 @@ public:
|
||||
RegulatorTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {}
|
||||
|
||||
protected:
|
||||
bool tunerInit = false;
|
||||
byte tunerState = 0;
|
||||
byte tunerRegulator = 0;
|
||||
float prevHeatingTarget = 0;
|
||||
float prevEtResult = 0;
|
||||
float prevPidResult = 0;
|
||||
float prevHeatingTarget = 0.0f;
|
||||
float prevEtResult = 0.0f;
|
||||
float prevPidResult = 0.0f;
|
||||
|
||||
const char* getTaskName() {
|
||||
bool indoorSensorsConnected = false;
|
||||
//bool outdoorSensorsConnected = false;
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Regulator";
|
||||
}
|
||||
|
||||
/*int getTaskCore() {
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}*/
|
||||
|
||||
int getTaskPriority() {
|
||||
int getTaskPriority() override {
|
||||
return 4;
|
||||
}
|
||||
#endif
|
||||
|
||||
void loop() {
|
||||
byte newTemp = vars.parameters.heatingSetpoint;
|
||||
if (vars.states.restarting || vars.states.upgrading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vars.states.emergency) {
|
||||
if (settings.heating.turbo) {
|
||||
this->indoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP);
|
||||
//this->outdoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP);
|
||||
|
||||
if (settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeHeatingControl) {
|
||||
vars.master.heating.indoorTempControl = true;
|
||||
vars.master.heating.minTemp = THERMOSTAT_INDOOR_MIN_TEMP;
|
||||
vars.master.heating.maxTemp = THERMOSTAT_INDOOR_MAX_TEMP;
|
||||
|
||||
} else {
|
||||
vars.master.heating.indoorTempControl = false;
|
||||
vars.master.heating.minTemp = settings.heating.minTemp;
|
||||
vars.master.heating.maxTemp = settings.heating.maxTemp;
|
||||
}
|
||||
|
||||
if (!settings.pid.enabled && fabsf(pidRegulator.integral) > 0.01f) {
|
||||
pidRegulator.integral = 0.0f;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
|
||||
this->turbo();
|
||||
this->hysteresis();
|
||||
|
||||
vars.master.heating.targetTemp = settings.heating.target;
|
||||
vars.master.heating.setpointTemp = roundf(constrain(
|
||||
this->getHeatingSetpointTemp(),
|
||||
this->getHeatingMinSetpointTemp(),
|
||||
this->getHeatingMaxSetpointTemp()
|
||||
), 0);
|
||||
|
||||
Sensors::setValueByType(
|
||||
Sensors::Type::HEATING_SETPOINT_TEMP, vars.master.heating.setpointTemp,
|
||||
Sensors::ValueType::PRIMARY, true, true
|
||||
);
|
||||
}
|
||||
|
||||
void turbo() {
|
||||
if (settings.heating.turbo) {
|
||||
if (!settings.heating.enabled || vars.emergency.state || !this->indoorSensorsConnected) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
} else if (!settings.pid.enabled && !settings.equitherm.enabled) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
} else if (fabsf(settings.heating.target - vars.master.heating.indoorTemp) <= 1.0f) {
|
||||
settings.heating.turbo = false;
|
||||
}
|
||||
|
||||
if (!settings.heating.turbo) {
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
|
||||
}
|
||||
|
||||
newTemp = getEmergencyModeTemp();
|
||||
|
||||
} else {
|
||||
if (vars.tuning.enable || tunerInit) {
|
||||
if (settings.heating.turbo) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
|
||||
}
|
||||
|
||||
newTemp = getTuningModeTemp();
|
||||
|
||||
if (newTemp == 0) {
|
||||
vars.tuning.enable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!vars.tuning.enable) {
|
||||
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || !settings.heating.enable || (settings.equitherm.enable && settings.pid.enable))) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
|
||||
}
|
||||
|
||||
newTemp = getNormalModeTemp();
|
||||
}
|
||||
}
|
||||
|
||||
// Ограничиваем, если до этого не ограничило
|
||||
if (newTemp < settings.heating.minTemp || newTemp > settings.heating.maxTemp) {
|
||||
newTemp = constrain(newTemp, settings.heating.minTemp, settings.heating.maxTemp);
|
||||
}
|
||||
|
||||
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
|
||||
vars.parameters.heatingSetpoint = newTemp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
byte getEmergencyModeTemp() {
|
||||
float newTemp = 0;
|
||||
|
||||
// if use equitherm
|
||||
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != 1) {
|
||||
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
|
||||
|
||||
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
|
||||
prevEtResult = etResult;
|
||||
newTemp += etResult;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New emergency result: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevEtResult;
|
||||
}
|
||||
|
||||
} else if(settings.emergency.usePid && settings.sensors.indoor.type != 1) {
|
||||
if (vars.parameters.heatingEnabled) {
|
||||
float pidResult = getPidTemp(
|
||||
settings.heating.minTemp,
|
||||
settings.heating.maxTemp
|
||||
);
|
||||
|
||||
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
|
||||
prevPidResult = pidResult;
|
||||
newTemp += pidResult;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New emergency result: %hhu (%.2f)"), (uint8_t) round(pidResult), pidResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else if (!vars.parameters.heatingEnabled && prevPidResult != 0) {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else {
|
||||
// default temp, manual mode
|
||||
newTemp = settings.emergency.target;
|
||||
void hysteresis() {
|
||||
bool useHyst = false;
|
||||
if (settings.heating.hysteresis > 0.01f && this->indoorSensorsConnected) {
|
||||
useHyst = settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeHeatingControl;
|
||||
}
|
||||
|
||||
return round(newTemp);
|
||||
if (useHyst) {
|
||||
if (!vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target + 0.0001f >= settings.heating.hysteresis) {
|
||||
vars.master.heating.blocking = true;
|
||||
|
||||
} else if (vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target - 0.0001f <= -(settings.heating.hysteresis)) {
|
||||
vars.master.heating.blocking = false;
|
||||
}
|
||||
|
||||
} else if (vars.master.heating.blocking) {
|
||||
vars.master.heating.blocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
byte getNormalModeTemp() {
|
||||
inline float getHeatingMinSetpointTemp() {
|
||||
return settings.opentherm.options.nativeHeatingControl
|
||||
? vars.master.heating.minTemp
|
||||
: settings.heating.minTemp;
|
||||
}
|
||||
|
||||
inline float getHeatingMaxSetpointTemp() {
|
||||
return settings.opentherm.options.nativeHeatingControl
|
||||
? vars.master.heating.maxTemp
|
||||
: settings.heating.maxTemp;
|
||||
}
|
||||
|
||||
float getHeatingSetpointTemp() {
|
||||
float newTemp = 0;
|
||||
|
||||
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
|
||||
if (fabsf(prevHeatingTarget - settings.heating.target) > 0.0001f) {
|
||||
prevHeatingTarget = settings.heating.target;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("New target: %.2f"), settings.heating.target);
|
||||
|
||||
if (settings.equitherm.enable && settings.pid.enable) {
|
||||
pidRegulator.integral = 0;
|
||||
/*if (settings.pid.enabled) {
|
||||
pidRegulator.integral = 0.0f;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
if (vars.emergency.state) {
|
||||
return settings.emergency.target;
|
||||
|
||||
} else if (settings.opentherm.options.nativeHeatingControl) {
|
||||
return settings.heating.target;
|
||||
|
||||
} else if (!settings.equitherm.enabled && !settings.pid.enabled) {
|
||||
return settings.heating.target;
|
||||
}
|
||||
|
||||
// if use equitherm
|
||||
if (settings.equitherm.enable) {
|
||||
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
|
||||
if (settings.equitherm.enabled) {
|
||||
unsigned short minTemp = settings.heating.minTemp;
|
||||
unsigned short maxTemp = settings.heating.maxTemp;
|
||||
float targetTemp = settings.heating.target;
|
||||
float indoorTemp = vars.master.heating.indoorTemp;
|
||||
float outdoorTemp = vars.master.heating.outdoorTemp;
|
||||
|
||||
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
|
||||
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
minTemp = f2c(minTemp);
|
||||
maxTemp = f2c(maxTemp);
|
||||
targetTemp = f2c(targetTemp);
|
||||
indoorTemp = f2c(indoorTemp);
|
||||
outdoorTemp = f2c(outdoorTemp);
|
||||
}
|
||||
|
||||
if (!this->indoorSensorsConnected || settings.pid.enabled) {
|
||||
etRegulator.Kt = 0.0f;
|
||||
etRegulator.indoorTemp = 0.0f;
|
||||
|
||||
} else {
|
||||
etRegulator.Kt = settings.heating.turbo ? 0.0f : settings.equitherm.t_factor;
|
||||
etRegulator.indoorTemp = indoorTemp;
|
||||
}
|
||||
|
||||
etRegulator.setLimits(minTemp, maxTemp);
|
||||
etRegulator.Kn = settings.equitherm.n_factor;
|
||||
etRegulator.Kk = settings.equitherm.k_factor;
|
||||
etRegulator.targetTemp = targetTemp;
|
||||
etRegulator.outdoorTemp = outdoorTemp;
|
||||
float etResult = etRegulator.getResult();
|
||||
|
||||
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
etResult = c2f(etResult);
|
||||
}
|
||||
|
||||
if (fabsf(prevEtResult - etResult) > 0.09f) {
|
||||
prevEtResult = etResult;
|
||||
newTemp += etResult;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %.2f"), etResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevEtResult;
|
||||
@@ -155,192 +192,62 @@ protected:
|
||||
}
|
||||
|
||||
// if use pid
|
||||
if (settings.pid.enable) {
|
||||
if (vars.parameters.heatingEnabled) {
|
||||
float pidResult = getPidTemp(
|
||||
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
|
||||
settings.equitherm.enable ? settings.pid.maxTemp : settings.pid.maxTemp
|
||||
);
|
||||
if (settings.pid.enabled) {
|
||||
//if (vars.parameters.heatingEnabled) {
|
||||
if (settings.heating.enabled && this->indoorSensorsConnected) {
|
||||
pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor;
|
||||
pidRegulator.Ki = settings.pid.i_factor;
|
||||
pidRegulator.Kd = settings.pid.d_factor;
|
||||
|
||||
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
|
||||
pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp);
|
||||
pidRegulator.setDt(settings.pid.dt * 1000u);
|
||||
pidRegulator.input = vars.master.heating.indoorTemp;
|
||||
pidRegulator.setpoint = settings.heating.target;
|
||||
|
||||
/*if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) {
|
||||
pidRegulator.Ki = settings.pid.i_factor;
|
||||
pidRegulator.integral = 0.0f;
|
||||
pidRegulator.getResultNow();
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}*/
|
||||
|
||||
float error = pidRegulator.setpoint - pidRegulator.input;
|
||||
bool hasDeadband = (error > -(settings.pid.deadband.thresholdHigh))
|
||||
&& (error < settings.pid.deadband.thresholdLow);
|
||||
|
||||
if (hasDeadband) {
|
||||
pidRegulator.Kp *= settings.pid.deadband.p_multiplier;
|
||||
pidRegulator.Ki *= settings.pid.deadband.i_multiplier;
|
||||
pidRegulator.Kd *= settings.pid.deadband.d_multiplier;
|
||||
}
|
||||
|
||||
float pidResult = pidRegulator.getResultTimer();
|
||||
if (fabsf(prevPidResult - pidResult) > 0.09f) {
|
||||
prevPidResult = pidResult;
|
||||
newTemp += pidResult;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %hhd (%.2f)"), (int8_t) round(pidResult), pidResult);
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %.2f"), pidResult);
|
||||
Log.straceln(FPSTR(L_REGULATOR_PID), F("Integral: %.2f"), pidRegulator.integral);
|
||||
|
||||
} else {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
}
|
||||
|
||||
// default temp, manual mode
|
||||
if (!settings.equitherm.enable && !settings.pid.enable) {
|
||||
newTemp = settings.heating.target;
|
||||
// Turbo mode
|
||||
if (settings.heating.turbo && (settings.equitherm.enabled || settings.pid.enabled)) {
|
||||
newTemp += constrain(
|
||||
settings.heating.target - vars.master.heating.indoorTemp,
|
||||
-3.0f,
|
||||
3.0f
|
||||
) * settings.heating.turboFactor;
|
||||
}
|
||||
|
||||
newTemp = round(newTemp);
|
||||
newTemp = constrain(newTemp, 0, 100);
|
||||
return newTemp;
|
||||
}
|
||||
|
||||
byte getTuningModeTemp() {
|
||||
if (tunerInit && (!vars.tuning.enable || vars.tuning.regulator != tunerRegulator)) {
|
||||
if (tunerRegulator == 0) {
|
||||
pidTuner.reset();
|
||||
}
|
||||
|
||||
tunerInit = false;
|
||||
tunerRegulator = 0;
|
||||
tunerState = 0;
|
||||
Log.sinfoln("REGULATOR.TUNING", F("Stopped"));
|
||||
}
|
||||
|
||||
if (!vars.tuning.enable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (vars.tuning.regulator == 0) {
|
||||
// @TODO дописать
|
||||
Log.sinfoln("REGULATOR.TUNING.EQUITHERM", F("Not implemented"));
|
||||
return 0;
|
||||
|
||||
} else if (vars.tuning.regulator == 1) {
|
||||
// PID tuner
|
||||
float defaultTemp = settings.equitherm.enable
|
||||
? getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp)
|
||||
: settings.heating.target;
|
||||
|
||||
if (tunerInit && pidTuner.getState() == 3) {
|
||||
Log.sinfoln("REGULATOR.TUNING.PID", F("Finished"));
|
||||
for (Stream* stream : Log.getStreams()) {
|
||||
pidTuner.debugText(stream);
|
||||
}
|
||||
|
||||
pidTuner.reset();
|
||||
tunerInit = false;
|
||||
tunerRegulator = 0;
|
||||
tunerState = 0;
|
||||
|
||||
if (pidTuner.getAccuracy() < 90) {
|
||||
Log.swarningln("REGULATOR.TUNING.PID", F("Bad result, try again..."));
|
||||
|
||||
} else {
|
||||
settings.pid.p_factor = pidTuner.getPID_p();
|
||||
settings.pid.i_factor = pidTuner.getPID_i();
|
||||
settings.pid.d_factor = pidTuner.getPID_d();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tunerInit) {
|
||||
Log.sinfoln("REGULATOR.TUNING.PID", F("Start..."));
|
||||
|
||||
float step;
|
||||
if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) {
|
||||
step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2);
|
||||
} else {
|
||||
step = 5.0f;
|
||||
}
|
||||
|
||||
float startTemp = step;
|
||||
Log.sinfoln("REGULATOR.TUNING.PID", F("Started. Start value: %f, step: %f"), startTemp, step);
|
||||
pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000);
|
||||
tunerInit = true;
|
||||
tunerRegulator = 1;
|
||||
}
|
||||
|
||||
pidTuner.setInput(vars.temperatures.indoor);
|
||||
pidTuner.compute();
|
||||
|
||||
if (tunerState > 0 && pidTuner.getState() != tunerState) {
|
||||
Log.sinfoln("REGULATOR.TUNING.PID", F("Log:"));
|
||||
for (Stream* stream : Log.getStreams()) {
|
||||
pidTuner.debugText(stream);
|
||||
}
|
||||
|
||||
tunerState = pidTuner.getState();
|
||||
}
|
||||
|
||||
return round(defaultTemp + pidTuner.getOutput());
|
||||
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
float getEquithermTemp(int minTemp, int maxTemp) {
|
||||
if (vars.states.emergency) {
|
||||
etRegulator.Kt = 0;
|
||||
etRegulator.indoorTemp = 0;
|
||||
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||
|
||||
} else if (settings.pid.enable) {
|
||||
etRegulator.Kt = 0;
|
||||
etRegulator.indoorTemp = round(vars.temperatures.indoor);
|
||||
etRegulator.outdoorTemp = round(vars.temperatures.outdoor);
|
||||
|
||||
} else {
|
||||
if (settings.heating.turbo) {
|
||||
etRegulator.Kt = 10;
|
||||
} else {
|
||||
etRegulator.Kt = settings.equitherm.t_factor;
|
||||
}
|
||||
etRegulator.indoorTemp = vars.temperatures.indoor;
|
||||
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||
}
|
||||
|
||||
etRegulator.setLimits(minTemp, maxTemp);
|
||||
etRegulator.Kn = settings.equitherm.n_factor;
|
||||
// etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1);
|
||||
etRegulator.Kk = settings.equitherm.k_factor;
|
||||
etRegulator.targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target;
|
||||
|
||||
return etRegulator.getResult();
|
||||
}
|
||||
|
||||
float getPidTemp(int minTemp, int maxTemp) {
|
||||
pidRegulator.Kp = settings.pid.p_factor;
|
||||
pidRegulator.Ki = settings.pid.i_factor;
|
||||
pidRegulator.Kd = settings.pid.d_factor;
|
||||
|
||||
pidRegulator.setLimits(minTemp, maxTemp);
|
||||
pidRegulator.setDt(settings.pid.dt * 1000u);
|
||||
pidRegulator.input = vars.temperatures.indoor;
|
||||
pidRegulator.setpoint = settings.heating.target;
|
||||
|
||||
return pidRegulator.getResultTimer();
|
||||
}
|
||||
|
||||
float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) {
|
||||
static uint32_t _prevIteration = millis();
|
||||
|
||||
if (abs(currentTemp - setTemp) < accurateStepAfter) {
|
||||
if (millis() - _prevIteration < (accurateInterval * 1000)) {
|
||||
return ratio;
|
||||
}
|
||||
|
||||
if (currentTemp - setTemp > 0.1f) {
|
||||
ratio -= accurateStep;
|
||||
|
||||
} else if (currentTemp - setTemp < -0.1f) {
|
||||
ratio += accurateStep;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (millis() - _prevIteration < (dirtyInterval * 1000)) {
|
||||
return ratio;
|
||||
}
|
||||
|
||||
ratio = ratio * (setTemp / currentTemp);
|
||||
}
|
||||
|
||||
_prevIteration = millis();
|
||||
return ratio;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
474
src/Sensors.h
Normal file
@@ -0,0 +1,474 @@
|
||||
#pragma once
|
||||
|
||||
class Sensors {
|
||||
protected:
|
||||
static uint8_t maxSensors;
|
||||
|
||||
public:
|
||||
enum class Type : uint8_t {
|
||||
OT_OUTDOOR_TEMP = 0,
|
||||
OT_HEATING_TEMP = 1,
|
||||
OT_HEATING_RETURN_TEMP = 2,
|
||||
OT_DHW_TEMP = 3,
|
||||
OT_DHW_TEMP2 = 4,
|
||||
OT_DHW_FLOW_RATE = 5,
|
||||
OT_CH2_TEMP = 6,
|
||||
OT_EXHAUST_TEMP = 7,
|
||||
OT_HEAT_EXCHANGER_TEMP = 8,
|
||||
OT_PRESSURE = 9,
|
||||
OT_MODULATION_LEVEL = 10,
|
||||
OT_CURRENT_POWER = 11,
|
||||
OT_EXHAUST_CO2 = 12,
|
||||
OT_EXHAUST_FAN_SPEED = 13,
|
||||
OT_SUPPLY_FAN_SPEED = 14,
|
||||
OT_SOLAR_STORAGE_TEMP = 15,
|
||||
OT_SOLAR_COLLECTOR_TEMP = 16,
|
||||
OT_FAN_SPEED_SETPOINT = 17,
|
||||
OT_FAN_SPEED_CURRENT = 18,
|
||||
|
||||
OT_BURNER_STARTS = 19,
|
||||
OT_DHW_BURNER_STARTS = 20,
|
||||
OT_HEATING_PUMP_STARTS = 21,
|
||||
OT_DHW_PUMP_STARTS = 22,
|
||||
OT_BURNER_HOURS = 23,
|
||||
OT_DHW_BURNER_HOURS = 24,
|
||||
OT_HEATING_PUMP_HOURS = 25,
|
||||
OT_DHW_PUMP_HOURS = 26,
|
||||
|
||||
NTC_10K_TEMP = 50,
|
||||
DALLAS_TEMP = 51,
|
||||
BLUETOOTH = 52,
|
||||
|
||||
HEATING_SETPOINT_TEMP = 253,
|
||||
MANUAL = 254,
|
||||
NOT_CONFIGURED = 255
|
||||
};
|
||||
|
||||
enum class Purpose : uint8_t {
|
||||
OUTDOOR_TEMP = 0,
|
||||
INDOOR_TEMP = 1,
|
||||
HEATING_TEMP = 2,
|
||||
HEATING_RETURN_TEMP = 3,
|
||||
DHW_TEMP = 4,
|
||||
DHW_RETURN_TEMP = 5,
|
||||
DHW_FLOW_RATE = 6,
|
||||
EXHAUST_TEMP = 7,
|
||||
MODULATION_LEVEL = 8,
|
||||
|
||||
NUMBER = 247,
|
||||
POWER_FACTOR = 248,
|
||||
POWER = 249,
|
||||
FAN_SPEED = 250,
|
||||
CO2 = 251,
|
||||
PRESSURE = 252,
|
||||
HUMIDITY = 253,
|
||||
TEMPERATURE = 254,
|
||||
NOT_CONFIGURED = 255
|
||||
};
|
||||
|
||||
enum class ValueType : uint8_t {
|
||||
PRIMARY = 0,
|
||||
TEMPERATURE = 0,
|
||||
HUMIDITY = 1,
|
||||
BATTERY = 2,
|
||||
RSSI = 3
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool enabled = false;
|
||||
char name[33];
|
||||
Purpose purpose = Purpose::NOT_CONFIGURED;
|
||||
Type type = Type::NOT_CONFIGURED;
|
||||
uint8_t gpio = GPIO_IS_NOT_CONFIGURED;
|
||||
uint8_t address[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
float offset = 0.0f;
|
||||
float factor = 1.0f;
|
||||
bool filtering = false;
|
||||
float filteringFactor = 0.15f;
|
||||
} Settings;
|
||||
|
||||
typedef struct {
|
||||
bool connected = false;
|
||||
unsigned long activityTime = 0;
|
||||
uint8_t signalQuality = 100;
|
||||
//float raw[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
float values[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
} Result;
|
||||
|
||||
|
||||
static Settings* settings;
|
||||
static Result* results;
|
||||
|
||||
static inline void setMaxSensors(uint8_t value) {
|
||||
maxSensors = value;
|
||||
}
|
||||
|
||||
static inline uint8_t getMaxSensors() {
|
||||
return maxSensors;
|
||||
}
|
||||
|
||||
static uint8_t getMaxSensorId() {
|
||||
uint8_t maxSensors = getMaxSensors();
|
||||
return maxSensors > 1 ? (maxSensors - 1) : 0;
|
||||
}
|
||||
|
||||
static inline bool isValidSensorId(const uint8_t id) {
|
||||
return id >= 0 && id <= getMaxSensorId();
|
||||
}
|
||||
|
||||
static inline bool isValidValueId(const uint8_t id) {
|
||||
return id >= (uint8_t) ValueType::TEMPERATURE && id <= (uint8_t) ValueType::RSSI;
|
||||
}
|
||||
|
||||
static bool hasEnabledAndValid(const uint8_t id) {
|
||||
if (!isValidSensorId(id) || !settings[id].enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings[id].type == Type::NOT_CONFIGURED || settings[id].purpose == Purpose::NOT_CONFIGURED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t getAmountByType(Type type, bool onlyEnabled = false) {
|
||||
if (settings == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t amount = 0;
|
||||
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
|
||||
if (settings[id].type == type && (!onlyEnabled || settings[id].enabled)) {
|
||||
amount++;
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
static int16_t getIdByName(const char* name) {
|
||||
if (settings == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
|
||||
if (strcmp(settings[id].name, name) == 0) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int16_t getIdByObjectId(const char* objectId) {
|
||||
if (settings == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String refObjectId;
|
||||
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
|
||||
Sensors::makeObjectId(refObjectId, settings[id].name);
|
||||
if (refObjectId.equals(objectId)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool setValueById(const uint8_t sensorId, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) {
|
||||
if (settings == nullptr || results == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t valueId = (uint8_t) valueType;
|
||||
if (!isValidSensorId(sensorId) || !isValidValueId(valueId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sSensor = settings[sensorId];
|
||||
auto& rSensor = results[sensorId];
|
||||
|
||||
float compensatedValue = value;
|
||||
if (sSensor.type == Type::HEATING_SETPOINT_TEMP || sSensor.type == Type::MANUAL) {
|
||||
rSensor.values[valueId] = compensatedValue;
|
||||
|
||||
} else {
|
||||
if (valueType == ValueType::PRIMARY) {
|
||||
if (fabsf(sSensor.factor) > 0.001f) {
|
||||
compensatedValue *= sSensor.factor;
|
||||
}
|
||||
|
||||
if (fabsf(sSensor.offset) > 0.001f) {
|
||||
compensatedValue += sSensor.offset;
|
||||
}
|
||||
|
||||
} else if (valueType == ValueType::RSSI) {
|
||||
if (sSensor.type == Type::BLUETOOTH) {
|
||||
rSensor.signalQuality = Sensors::bluetoothRssiToQuality(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (sSensor.filtering && fabsf(rSensor.values[valueId]) >= 0.1f) {
|
||||
rSensor.values[valueId] += (compensatedValue - rSensor.values[valueId]) * sSensor.filteringFactor;
|
||||
|
||||
} else {
|
||||
rSensor.values[valueId] = compensatedValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateActivityTime) {
|
||||
rSensor.activityTime = millis();
|
||||
}
|
||||
|
||||
if (markConnected && !rSensor.connected) {
|
||||
rSensor.connected = true;
|
||||
|
||||
Log.snoticeln(
|
||||
FPSTR(L_SENSORS), F("#%hhu '%s' new status: CONNECTED"),
|
||||
sensorId, sSensor.name
|
||||
);
|
||||
}
|
||||
|
||||
Log.snoticeln(
|
||||
FPSTR(L_SENSORS), F("#%hhu '%s' new value %hhu: %.2f, compensated: %.2f, raw: %.2f"),
|
||||
sensorId, sSensor.name, valueId, rSensor.values[valueId], compensatedValue, value
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t setValueByType(Type type, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) {
|
||||
if (settings == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t updated = 0;
|
||||
|
||||
// read sensors data for current instance
|
||||
for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) {
|
||||
auto& sSensor = settings[sensorId];
|
||||
|
||||
// only target & valid sensors
|
||||
if (!sSensor.enabled || sSensor.type != type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (setValueById(sensorId, value, valueType, updateActivityTime, markConnected)) {
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
static bool getConnectionStatusById(const uint8_t sensorId) {
|
||||
if (settings == nullptr || results == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidSensorId(sensorId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return results[sensorId].connected;
|
||||
}
|
||||
|
||||
static bool setConnectionStatusById(const uint8_t sensorId, const bool status, const bool updateActivityTime = true) {
|
||||
if (settings == nullptr || results == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidSensorId(sensorId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sSensor = settings[sensorId];
|
||||
auto& rSensor = results[sensorId];
|
||||
|
||||
if (rSensor.connected != status) {
|
||||
Log.snoticeln(
|
||||
FPSTR(L_SENSORS), F("#%hhu '%s' new status: %s"),
|
||||
sensorId, sSensor.name, status ? F("CONNECTED") : F("DISCONNECTED")
|
||||
);
|
||||
|
||||
rSensor.connected = status;
|
||||
}
|
||||
|
||||
if (updateActivityTime) {
|
||||
rSensor.activityTime = millis();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t setConnectionStatusByType(Type type, const bool status, const bool updateActivityTime = true) {
|
||||
if (settings == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t updated = 0;
|
||||
|
||||
// read sensors data for current instance
|
||||
for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) {
|
||||
auto& sSensor = settings[sensorId];
|
||||
|
||||
// only target & valid sensors
|
||||
if (!sSensor.enabled || sSensor.type != type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (setConnectionStatusById(sensorId, status, updateActivityTime)) {
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
static float getMeanValueByPurpose(Purpose purpose, const ValueType valueType, bool onlyConnected = true) {
|
||||
if (settings == nullptr || results == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t valueId = (uint8_t) valueType;
|
||||
if (!isValidValueId(valueId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float value = 0.0f;
|
||||
uint8_t amount = 0;
|
||||
|
||||
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
|
||||
auto& sSensor = settings[id];
|
||||
auto& rSensor = results[id];
|
||||
|
||||
if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
|
||||
value += rSensor.values[valueId];
|
||||
amount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!amount) {
|
||||
return 0.0f;
|
||||
|
||||
} else if (amount == 1) {
|
||||
return value;
|
||||
|
||||
} else {
|
||||
return value / amount;
|
||||
}
|
||||
}
|
||||
|
||||
static bool existsConnectedSensorsByPurpose(Purpose purpose) {
|
||||
if (settings == nullptr || results == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
|
||||
if (settings[id].purpose == purpose && results[id].connected) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static String& cleanName(String& value, char space = ' ') {
|
||||
// only valid symbols
|
||||
for (uint8_t pos = 0; pos < value.length(); pos++) {
|
||||
char symbol = value.charAt(pos);
|
||||
|
||||
// 0..9
|
||||
if (symbol >= 48 && symbol <= 57) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A..Z
|
||||
if (symbol >= 65 && symbol <= 90) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// a..z
|
||||
if (symbol >= 97 && symbol <= 122) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// _-
|
||||
if (symbol == 95 || symbol == 45 || symbol == space) {
|
||||
continue;
|
||||
}
|
||||
|
||||
value.setCharAt(pos, space);
|
||||
}
|
||||
|
||||
value.trim();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static String cleanName(T value, char space = ' ') {
|
||||
String res = value;
|
||||
return cleanName(res, space);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static String& makeObjectId(String& res, T value, char separator = '_') {
|
||||
res = value;
|
||||
cleanName(res);
|
||||
res.toLowerCase();
|
||||
res.replace(' ', separator);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static String makeObjectId(T value, char separator = '_') {
|
||||
String res;
|
||||
makeObjectId(res, value, separator);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class TV, class TS>
|
||||
static String& makeObjectIdWithSuffix(String& res, TV value, TS suffix, char separator = '_') {
|
||||
res.clear();
|
||||
makeObjectId(res, value, separator);
|
||||
res += separator;
|
||||
res += suffix;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class TV, class TS>
|
||||
static String makeObjectIdWithSuffix(TV value, TS suffix, char separator = '_') {
|
||||
String res;
|
||||
makeObjectIdWithSuffix(res, value, suffix, separator);
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class TV, class TP>
|
||||
static String& makeObjectIdWithPrefix(String& res, TV value, TP prefix, char separator = '_') {
|
||||
res = prefix;
|
||||
res += separator;
|
||||
res += makeObjectId(value, separator).c_str();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class TV, class TP>
|
||||
static String makeObjectIdWithPrefix(TV value, TP prefix, char separator = '_') {
|
||||
String res;
|
||||
return makeObjectIdWithPrefix(res, value, prefix, separator);
|
||||
}
|
||||
|
||||
static uint8_t bluetoothRssiToQuality(int rssi) {
|
||||
return constrain(map(rssi, -110, -50, 0, 100), 0, 100);;
|
||||
}
|
||||
};
|
||||
|
||||
uint8_t Sensors::maxSensors = 0;
|
||||
Sensors::Settings* Sensors::settings = nullptr;
|
||||
Sensors::Result* Sensors::results = nullptr;
|
||||
1195
src/SensorsTask.h
454
src/Settings.h
@@ -1,5 +1,5 @@
|
||||
struct NetworkSettings {
|
||||
char hostname[25] = HOSTNAME_DEFAULT;
|
||||
char hostname[25] = DEFAULT_HOSTNAME;
|
||||
bool useDhcp = true;
|
||||
|
||||
struct {
|
||||
@@ -10,178 +10,420 @@ struct NetworkSettings {
|
||||
} staticConfig;
|
||||
|
||||
struct {
|
||||
char ssid[33] = AP_SSID_DEFAULT;
|
||||
char password[65] = AP_PASSWORD_DEFAULT;
|
||||
char ssid[33] = DEFAULT_AP_SSID;
|
||||
char password[65] = DEFAULT_AP_PASSWORD;
|
||||
byte channel = 6;
|
||||
} ap;
|
||||
|
||||
struct {
|
||||
char ssid[33] = STA_SSID_DEFAULT;
|
||||
char password[65] = STA_PASSWORD_DEFAULT;
|
||||
char ssid[33] = DEFAULT_STA_SSID;
|
||||
char password[65] = DEFAULT_STA_PASSWORD;
|
||||
byte channel = 0;
|
||||
} sta;
|
||||
} networkSettings;
|
||||
|
||||
struct Settings {
|
||||
struct {
|
||||
bool debug = DEBUG_BY_DEFAULT;
|
||||
bool useSerial = USE_SERIAL;
|
||||
bool useTelnet = USE_TELNET;
|
||||
uint8_t logLevel = DEFAULT_LOG_LEVEL;
|
||||
|
||||
struct {
|
||||
bool enabled = DEFAULT_SERIAL_ENABLED;
|
||||
unsigned int baudrate = DEFAULT_SERIAL_BAUD;
|
||||
} serial;
|
||||
|
||||
struct {
|
||||
bool enabled = DEFAULT_TELNET_ENABLED;
|
||||
unsigned short port = DEFAULT_TELNET_PORT;
|
||||
} telnet;
|
||||
|
||||
struct {
|
||||
char server[49] = "pool.ntp.org";
|
||||
char timezone[49] = "UTC0";
|
||||
} ntp;
|
||||
|
||||
UnitSystem unitSystem = UnitSystem::METRIC;
|
||||
byte statusLedGpio = DEFAULT_STATUS_LED_GPIO;
|
||||
} system;
|
||||
|
||||
struct {
|
||||
bool useAuth = false;
|
||||
char login[13] = PORTAL_LOGIN_DEFAULT;
|
||||
char password[33] = PORTAL_PASSWORD_DEFAULT;
|
||||
bool auth = false;
|
||||
char login[13] = DEFAULT_PORTAL_LOGIN;
|
||||
char password[33] = DEFAULT_PORTAL_PASSWORD;
|
||||
bool mdns = true;
|
||||
} portal;
|
||||
|
||||
struct {
|
||||
byte inPin = OT_IN_PIN_DEFAULT;
|
||||
byte outPin = OT_OUT_PIN_DEFAULT;
|
||||
unsigned int memberIdCode = 0;
|
||||
bool dhwPresent = true;
|
||||
bool summerWinterMode = false;
|
||||
bool heatingCh2Enabled = true;
|
||||
bool heatingCh1ToCh2 = false;
|
||||
bool dhwToCh2 = false;
|
||||
bool dhwBlocking = false;
|
||||
bool modulationSyncWithHeating = false;
|
||||
UnitSystem unitSystem = UnitSystem::METRIC;
|
||||
byte inGpio = DEFAULT_OT_IN_GPIO;
|
||||
byte outGpio = DEFAULT_OT_OUT_GPIO;
|
||||
byte rxLedGpio = DEFAULT_OT_RX_LED_GPIO;
|
||||
uint8_t memberId = 0;
|
||||
uint8_t flags = 0;
|
||||
float minPower = 0.0f;
|
||||
float maxPower = 0.0f;
|
||||
|
||||
struct {
|
||||
bool dhwSupport = true;
|
||||
bool coolingSupport = false;
|
||||
bool summerWinterMode = false;
|
||||
bool heatingStateToSummerWinterMode = false;
|
||||
bool ch2AlwaysEnabled = true;
|
||||
bool heatingToCh2 = false;
|
||||
bool dhwToCh2 = false;
|
||||
bool dhwBlocking = false;
|
||||
bool dhwStateAsDhwBlocking = false;
|
||||
bool maxTempSyncWithTargetTemp = true;
|
||||
bool getMinMaxTemp = true;
|
||||
bool ignoreDiagState = false;
|
||||
bool autoFaultReset = false;
|
||||
bool autoDiagReset = false;
|
||||
bool setDateAndTime = false;
|
||||
bool nativeHeatingControl = false;
|
||||
bool immergasFix = false;
|
||||
} options;
|
||||
} opentherm;
|
||||
|
||||
struct {
|
||||
char server[81] = MQTT_SERVER_DEFAULT;
|
||||
unsigned short port = MQTT_PORT_DEFAULT;
|
||||
char user[33] = MQTT_USER_DEFAULT;
|
||||
char password[33] = MQTT_PASSWORD_DEFAULT;
|
||||
char prefix[33] = MQTT_PREFIX_DEFAULT;
|
||||
bool enabled = DEFAULT_MQTT_ENABLED;
|
||||
char server[81] = DEFAULT_MQTT_SERVER;
|
||||
unsigned short port = DEFAULT_MQTT_PORT;
|
||||
char user[33] = DEFAULT_MQTT_USER;
|
||||
char password[33] = DEFAULT_MQTT_PASSWORD;
|
||||
char prefix[33] = DEFAULT_MQTT_PREFIX;
|
||||
unsigned short interval = 5;
|
||||
bool homeAssistantDiscovery = true;
|
||||
} mqtt;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
float target = 40.0f;
|
||||
bool useEquitherm = false;
|
||||
bool usePid = false;
|
||||
float target = DEFAULT_HEATING_TARGET_TEMP;
|
||||
unsigned short tresholdTime = 120;
|
||||
} emergency;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
bool enabled = true;
|
||||
bool turbo = false;
|
||||
float target = 40.0f;
|
||||
float target = DEFAULT_HEATING_TARGET_TEMP;
|
||||
float hysteresis = 0.5f;
|
||||
float turboFactor = 7.5f;
|
||||
byte minTemp = DEFAULT_HEATING_MIN_TEMP;
|
||||
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
byte maxModulation = 100;
|
||||
uint8_t maxModulation = 100;
|
||||
|
||||
struct {
|
||||
uint8_t highTemp = 95;
|
||||
uint8_t lowTemp = 90;
|
||||
} overheatProtection;
|
||||
|
||||
struct {
|
||||
uint8_t lowTemp = 10;
|
||||
unsigned short thresholdTime = 600;
|
||||
} freezeProtection;
|
||||
} heating;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
byte target = 40;
|
||||
bool enabled = true;
|
||||
float target = DEFAULT_DHW_TARGET_TEMP;
|
||||
byte minTemp = DEFAULT_DHW_MIN_TEMP;
|
||||
byte maxTemp = DEFAULT_DHW_MAX_TEMP;
|
||||
uint8_t maxModulation = 100;
|
||||
|
||||
struct {
|
||||
uint8_t highTemp = 95;
|
||||
uint8_t lowTemp = 90;
|
||||
} overheatProtection;
|
||||
} dhw;
|
||||
|
||||
struct {
|
||||
bool enable = false;
|
||||
float p_factor = 50;
|
||||
float i_factor = 0.006f;
|
||||
float d_factor = 10000;
|
||||
unsigned short dt = 180;
|
||||
byte minTemp = 0;
|
||||
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
bool enabled = false;
|
||||
float p_factor = 2.0f;
|
||||
float i_factor = 0.002f;
|
||||
float d_factor = 0.0f;
|
||||
unsigned short dt = 300;
|
||||
short minTemp = 0;
|
||||
short maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
|
||||
struct {
|
||||
bool enabled = true;
|
||||
float p_multiplier = 1.0f;
|
||||
float i_multiplier = 0.05f;
|
||||
float d_multiplier = 1.0f;
|
||||
float thresholdHigh = 0.5f;
|
||||
float thresholdLow = 1.0f;
|
||||
} deadband;
|
||||
} pid;
|
||||
|
||||
struct {
|
||||
bool enable = false;
|
||||
bool enabled = false;
|
||||
float n_factor = 0.7f;
|
||||
float k_factor = 3.0f;
|
||||
float t_factor = 2.0f;
|
||||
} equitherm;
|
||||
|
||||
struct {
|
||||
struct {
|
||||
// 0 - boiler, 1 - manual, 2 - ds18b20
|
||||
byte type = 0;
|
||||
byte pin = SENSOR_OUTDOOR_PIN_DEFAULT;
|
||||
float offset = 0.0f;
|
||||
} outdoor;
|
||||
|
||||
struct {
|
||||
// 1 - manual, 2 - ds18b20, 3 - ble
|
||||
byte type = 1;
|
||||
byte pin = SENSOR_INDOOR_PIN_DEFAULT;
|
||||
char bleAddresss[18] = "00:00:00:00:00:00";
|
||||
float offset = 0.0f;
|
||||
} indoor;
|
||||
} sensors;
|
||||
|
||||
struct {
|
||||
bool use = false;
|
||||
byte pin = EXT_PUMP_PIN_DEFAULT;
|
||||
byte gpio = DEFAULT_EXT_PUMP_GPIO;
|
||||
unsigned short postCirculationTime = 600;
|
||||
unsigned int antiStuckInterval = 2592000;
|
||||
unsigned short antiStuckTime = 300;
|
||||
} externalPump;
|
||||
|
||||
struct {
|
||||
struct {
|
||||
bool enabled = false;
|
||||
byte gpio = GPIO_IS_NOT_CONFIGURED;
|
||||
byte invertState = false;
|
||||
unsigned short thresholdTime = 60;
|
||||
} input;
|
||||
|
||||
struct {
|
||||
bool enabled = false;
|
||||
byte gpio = GPIO_IS_NOT_CONFIGURED;
|
||||
byte invertState = false;
|
||||
unsigned short thresholdTime = 60;
|
||||
bool onFault = true;
|
||||
bool onLossConnection = true;
|
||||
bool onEnabledHeating = false;
|
||||
} output;
|
||||
} cascadeControl;
|
||||
|
||||
char validationValue[8] = SETTINGS_VALID_VALUE;
|
||||
} settings;
|
||||
|
||||
Sensors::Settings sensorsSettings[SENSORS_AMOUNT] = {
|
||||
{
|
||||
false,
|
||||
"Outdoor temp",
|
||||
Sensors::Purpose::OUTDOOR_TEMP,
|
||||
Sensors::Type::OT_OUTDOOR_TEMP,
|
||||
DEFAULT_SENSOR_OUTDOOR_GPIO
|
||||
},
|
||||
{
|
||||
false,
|
||||
"Indoor temp",
|
||||
Sensors::Purpose::INDOOR_TEMP,
|
||||
Sensors::Type::DALLAS_TEMP,
|
||||
DEFAULT_SENSOR_INDOOR_GPIO
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Heating temp",
|
||||
Sensors::Purpose::HEATING_TEMP,
|
||||
Sensors::Type::OT_HEATING_TEMP,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Heating return temp",
|
||||
Sensors::Purpose::HEATING_RETURN_TEMP,
|
||||
Sensors::Type::OT_HEATING_RETURN_TEMP,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Heating setpoint temp",
|
||||
Sensors::Purpose::TEMPERATURE,
|
||||
Sensors::Type::HEATING_SETPOINT_TEMP,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"DHW temp",
|
||||
Sensors::Purpose::DHW_TEMP,
|
||||
Sensors::Type::OT_DHW_TEMP,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"DHW flow rate",
|
||||
Sensors::Purpose::DHW_FLOW_RATE,
|
||||
Sensors::Type::OT_DHW_FLOW_RATE,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Exhaust temp",
|
||||
Sensors::Purpose::EXHAUST_TEMP,
|
||||
Sensors::Type::OT_EXHAUST_TEMP,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Pressure",
|
||||
Sensors::Purpose::PRESSURE,
|
||||
Sensors::Type::OT_PRESSURE,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Modulation level",
|
||||
Sensors::Purpose::MODULATION_LEVEL,
|
||||
Sensors::Type::OT_MODULATION_LEVEL,
|
||||
},
|
||||
{
|
||||
true,
|
||||
"Power",
|
||||
Sensors::Purpose::POWER,
|
||||
Sensors::Type::OT_CURRENT_POWER,
|
||||
}
|
||||
};
|
||||
|
||||
struct Variables {
|
||||
struct {
|
||||
bool enable = false;
|
||||
byte regulator = 0;
|
||||
} tuning;
|
||||
|
||||
struct {
|
||||
bool otStatus = false;
|
||||
bool emergency = false;
|
||||
bool heating = false;
|
||||
bool dhw = false;
|
||||
bool flame = false;
|
||||
bool fault = false;
|
||||
bool diagnostic = false;
|
||||
bool externalPump = false;
|
||||
} states;
|
||||
|
||||
struct {
|
||||
float modulation = 0.0f;
|
||||
float pressure = 0.0f;
|
||||
float dhwFlowRate = 0.0f;
|
||||
byte faultCode = 0;
|
||||
bool connected = false;
|
||||
int8_t rssi = 0;
|
||||
} sensors;
|
||||
} network;
|
||||
|
||||
struct {
|
||||
float indoor = 0.0f;
|
||||
float outdoor = 0.0f;
|
||||
float heating = 0.0f;
|
||||
float dhw = 0.0f;
|
||||
} temperatures;
|
||||
bool connected = false;
|
||||
} mqtt;
|
||||
|
||||
struct {
|
||||
bool heatingEnabled = false;
|
||||
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
|
||||
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
byte heatingSetpoint = 0;
|
||||
unsigned long extPumpLastEnableTime = 0;
|
||||
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
|
||||
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
|
||||
byte maxModulation;
|
||||
uint8_t slaveMemberId;
|
||||
uint8_t slaveFlags;
|
||||
uint8_t slaveType;
|
||||
uint8_t slaveVersion;
|
||||
float slaveOtVersion;
|
||||
uint8_t masterMemberId;
|
||||
uint8_t masterFlags;
|
||||
uint8_t masterType;
|
||||
uint8_t masterVersion;
|
||||
float masterOtVersion;
|
||||
} parameters;
|
||||
bool state = false;
|
||||
} emergency;
|
||||
|
||||
struct {
|
||||
bool state = false;
|
||||
unsigned long lastEnabledTime = 0;
|
||||
} externalPump;
|
||||
|
||||
struct {
|
||||
bool input = false;
|
||||
bool output = false;
|
||||
} cascadeControl;
|
||||
|
||||
struct {
|
||||
uint8_t memberId = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t type = 0;
|
||||
uint8_t appVersion = 0;
|
||||
float protocolVersion = 0.0f;
|
||||
|
||||
struct {
|
||||
bool blocking = false;
|
||||
bool enabled = false;
|
||||
bool indoorTempControl = false;
|
||||
bool overheat = false;
|
||||
float setpointTemp = 0.0f;
|
||||
float targetTemp = 0.0f;
|
||||
float currentTemp = 0.0f;
|
||||
float returnTemp = 0.0f;
|
||||
float indoorTemp = 0.0f;
|
||||
float outdoorTemp = 0.0f;
|
||||
float minTemp = 0.0f;
|
||||
float maxTemp = 0.0f;
|
||||
} heating;
|
||||
|
||||
struct {
|
||||
bool enabled = false;
|
||||
bool overheat = false;
|
||||
float targetTemp = 0.0f;
|
||||
float currentTemp = 0.0f;
|
||||
float returnTemp = 0.0f;
|
||||
} dhw;
|
||||
|
||||
struct {
|
||||
bool enabled = false;
|
||||
float targetTemp = 0.0f;
|
||||
} ch2;
|
||||
} master;
|
||||
|
||||
struct {
|
||||
uint8_t memberId = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t type = 0;
|
||||
uint8_t appVersion = 0;
|
||||
float protocolVersion = 0.0f;
|
||||
|
||||
bool connected = false;
|
||||
bool flame = false;
|
||||
bool cooling = false;
|
||||
float pressure = 0.0f;
|
||||
float heatExchangerTemp = 0.0f;
|
||||
|
||||
struct {
|
||||
bool active = false;
|
||||
uint8_t code = 0;
|
||||
} fault;
|
||||
|
||||
struct {
|
||||
bool active = false;
|
||||
uint16_t code = 0;
|
||||
} diag;
|
||||
|
||||
struct {
|
||||
uint8_t current = 0;
|
||||
uint8_t min = 0;
|
||||
uint8_t max = 100;
|
||||
} modulation;
|
||||
|
||||
struct {
|
||||
float current = 0.0f;
|
||||
float min = 0.0f;
|
||||
float max = 0.0f;
|
||||
} power;
|
||||
|
||||
struct {
|
||||
float temp = 0.0f;
|
||||
uint16_t co2 = 0;
|
||||
uint16_t fanSpeed = 0;
|
||||
} exhaust;
|
||||
|
||||
struct {
|
||||
float storage = 0.0f;
|
||||
float collector = 0.0f;
|
||||
} solar;
|
||||
|
||||
struct {
|
||||
uint8_t setpoint = 0;
|
||||
uint8_t current = 0;
|
||||
uint16_t supply = 0;
|
||||
} fanSpeed;
|
||||
|
||||
struct {
|
||||
uint16_t burnerStarts = 0;
|
||||
uint16_t dhwBurnerStarts = 0;
|
||||
uint16_t heatingPumpStarts = 0;
|
||||
uint16_t dhwPumpStarts = 0;
|
||||
uint16_t burnerHours = 0;
|
||||
uint16_t dhwBurnerHours = 0;
|
||||
uint16_t heatingPumpHours = 0;
|
||||
uint16_t dhwPumpHours = 0;
|
||||
} stats;
|
||||
|
||||
struct {
|
||||
bool active = false;
|
||||
bool enabled = false;
|
||||
float targetTemp = 0.0f;
|
||||
float currentTemp = 0.0f;
|
||||
float returnTemp = 0.0f;
|
||||
float indoorTemp = 0.0f;
|
||||
float outdoorTemp = 0.0f;
|
||||
uint8_t minTemp = DEFAULT_HEATING_MIN_TEMP;
|
||||
uint8_t maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
} heating;
|
||||
|
||||
struct {
|
||||
bool active = false;
|
||||
bool enabled = false;
|
||||
float targetTemp = 0.0f;
|
||||
float currentTemp = 0.0f;
|
||||
float currentTemp2 = 0.0f;
|
||||
float returnTemp = 0.0f;
|
||||
float flowRate = 0.0f;
|
||||
uint8_t minTemp = DEFAULT_DHW_MIN_TEMP;
|
||||
uint8_t maxTemp = DEFAULT_DHW_MAX_TEMP;
|
||||
} dhw;
|
||||
|
||||
struct {
|
||||
bool active = false;
|
||||
bool enabled = false;
|
||||
float targetTemp = 0.0f;
|
||||
float currentTemp = 0.0f;
|
||||
float indoorTemp = 0.0f;
|
||||
} ch2;
|
||||
} slave;
|
||||
|
||||
struct {
|
||||
bool restart = false;
|
||||
bool resetFault = false;
|
||||
bool resetDiagnostic = false;
|
||||
} actions;
|
||||
|
||||
struct {
|
||||
bool restarting = false;
|
||||
bool upgrading = false;
|
||||
} states;
|
||||
} vars;
|
||||
167
src/defines.h
@@ -1,115 +1,166 @@
|
||||
#define PROJECT_NAME "OpenTherm Gateway"
|
||||
#define PROJECT_VERSION "1.4.0-rc.14"
|
||||
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
|
||||
#define PROJECT_NAME "OpenTherm Gateway"
|
||||
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
|
||||
|
||||
#define EMERGENCY_TIME_TRESHOLD 120000
|
||||
#define MQTT_RECONNECT_INTERVAL 15000
|
||||
#define MQTT_KEEPALIVE 30
|
||||
#define MQTT_RECONNECT_INTERVAL 15000
|
||||
#define CONFIG_URL "http://%s/"
|
||||
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
|
||||
#define GPIO_IS_NOT_CONFIGURED 0xff
|
||||
|
||||
#define OPENTHERM_OFFLINE_TRESHOLD 10
|
||||
#define DEFAULT_HEATING_TARGET_TEMP 40
|
||||
#define DEFAULT_HEATING_MIN_TEMP 20
|
||||
#define DEFAULT_HEATING_MAX_TEMP 90
|
||||
|
||||
#define EXT_SENSORS_INTERVAL 5000
|
||||
#define EXT_SENSORS_FILTER_K 0.15
|
||||
#define DEFAULT_DHW_TARGET_TEMP 40
|
||||
#define DEFAULT_DHW_MIN_TEMP 30
|
||||
#define DEFAULT_DHW_MAX_TEMP 60
|
||||
|
||||
#define CONFIG_URL "http://%s/"
|
||||
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
|
||||
#define THERMOSTAT_INDOOR_DEFAULT_TEMP 20
|
||||
#define THERMOSTAT_INDOOR_MIN_TEMP 5
|
||||
#define THERMOSTAT_INDOOR_MAX_TEMP 40
|
||||
|
||||
#define DEFAULT_HEATING_MIN_TEMP 20
|
||||
#define DEFAULT_HEATING_MAX_TEMP 90
|
||||
#define DEFAULT_DHW_MIN_TEMP 30
|
||||
#define DEFAULT_DHW_MAX_TEMP 60
|
||||
#define DEFAULT_NTC_NOMINAL_RESISTANCE 10000.0f
|
||||
#define DEFAULT_NTC_NOMINAL_TEMP 25.0f
|
||||
#define DEFAULT_NTC_REF_RESISTANCE 10000.0f
|
||||
#define DEFAULT_NTC_BETA_FACTOR 3950.0f
|
||||
#define DEFAULT_NTC_VREF 3300.0f
|
||||
#define DEFAULT_NTC_VLOW_TRESHOLD 25.0f
|
||||
#define DEFAULT_NTC_VHIGH_TRESHOLD 3298.0f
|
||||
|
||||
|
||||
#ifndef WM_DEBUG_MODE
|
||||
#define WM_DEBUG_MODE WM_DEBUG_NOTIFY
|
||||
#ifndef BUILD_VERSION
|
||||
#define BUILD_VERSION "0.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef USE_SERIAL
|
||||
#define USE_SERIAL true
|
||||
#ifndef BUILD_ENV
|
||||
#define BUILD_ENV "undefined"
|
||||
#endif
|
||||
|
||||
#ifndef USE_TELNET
|
||||
#define USE_TELNET true
|
||||
#ifndef DEFAULT_SERIAL_ENABLED
|
||||
#define DEFAULT_SERIAL_ENABLED true
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_SERIAL_BAUD
|
||||
#define DEFAULT_SERIAL_BAUD 115200
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_TELNET_ENABLED
|
||||
#define DEFAULT_TELNET_ENABLED true
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_TELNET_PORT
|
||||
#define DEFAULT_TELNET_PORT 23
|
||||
#endif
|
||||
|
||||
#ifndef USE_BLE
|
||||
#define USE_BLE false
|
||||
#endif
|
||||
|
||||
#ifndef HOSTNAME_DEFAULT
|
||||
#define HOSTNAME_DEFAULT "opentherm"
|
||||
#ifndef DEFAULT_HOSTNAME
|
||||
#define DEFAULT_HOSTNAME ""
|
||||
#endif
|
||||
|
||||
#ifndef AP_SSID_DEFAULT
|
||||
#define AP_SSID_DEFAULT "OpenTherm Gateway"
|
||||
#ifndef DEFAULT_AP_SSID
|
||||
#define DEFAULT_AP_SSID "OpenTherm Gateway"
|
||||
#endif
|
||||
|
||||
#ifndef AP_PASSWORD_DEFAULT
|
||||
#define AP_PASSWORD_DEFAULT "otgateway123456"
|
||||
#ifndef DEFAULT_AP_PASSWORD
|
||||
#define DEFAULT_AP_PASSWORD "otgateway123456"
|
||||
#endif
|
||||
|
||||
#ifndef STA_SSID_DEFAULT
|
||||
#define STA_SSID_DEFAULT ""
|
||||
#ifndef DEFAULT_STA_SSID
|
||||
#define DEFAULT_STA_SSID ""
|
||||
#endif
|
||||
|
||||
#ifndef STA_PASSWORD_DEFAULT
|
||||
#define STA_PASSWORD_DEFAULT ""
|
||||
#ifndef DEFAULT_STA_PASSWORD
|
||||
#define DEFAULT_STA_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef DEBUG_BY_DEFAULT
|
||||
#define DEBUG_BY_DEFAULT false
|
||||
#ifndef DEFAULT_LOG_LEVEL
|
||||
#define DEFAULT_LOG_LEVEL TinyLogger::Level::VERBOSE
|
||||
#endif
|
||||
|
||||
#ifndef PORTAL_LOGIN_DEFAULT
|
||||
#define PORTAL_LOGIN_DEFAULT ""
|
||||
#ifndef DEFAULT_STATUS_LED_GPIO
|
||||
#define DEFAULT_STATUS_LED_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef PORTAL_PASSWORD_DEFAULT
|
||||
#define PORTAL_PASSWORD_DEFAULT ""
|
||||
#ifndef DEFAULT_PORTAL_LOGIN
|
||||
#define DEFAULT_PORTAL_LOGIN ""
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_SERVER_DEFAULT
|
||||
#define MQTT_SERVER_DEFAULT ""
|
||||
#ifndef DEFAULT_PORTAL_PASSWORD
|
||||
#define DEFAULT_PORTAL_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_PORT_DEFAULT
|
||||
#define MQTT_PORT_DEFAULT 1883
|
||||
#ifndef DEFAULT_MQTT_ENABLED
|
||||
#define DEFAULT_MQTT_ENABLED false
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_USER_DEFAULT
|
||||
#define MQTT_USER_DEFAULT ""
|
||||
#ifndef DEFAULT_MQTT_SERVER
|
||||
#define DEFAULT_MQTT_SERVER ""
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_PASSWORD_DEFAULT
|
||||
#define MQTT_PASSWORD_DEFAULT ""
|
||||
#ifndef DEFAULT_MQTT_PORT
|
||||
#define DEFAULT_MQTT_PORT 1883
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_PREFIX_DEFAULT
|
||||
#define MQTT_PREFIX_DEFAULT "opentherm"
|
||||
#ifndef DEFAULT_MQTT_USER
|
||||
#define DEFAULT_MQTT_USER ""
|
||||
#endif
|
||||
|
||||
#ifndef OT_IN_PIN_DEFAULT
|
||||
#define OT_IN_PIN_DEFAULT 0
|
||||
#ifndef DEFAULT_MQTT_PASSWORD
|
||||
#define DEFAULT_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef OT_OUT_PIN_DEFAULT
|
||||
#define OT_OUT_PIN_DEFAULT 0
|
||||
#ifndef DEFAULT_MQTT_PREFIX
|
||||
#define DEFAULT_MQTT_PREFIX ""
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_OUTDOOR_PIN_DEFAULT
|
||||
#define SENSOR_OUTDOOR_PIN_DEFAULT 0
|
||||
#ifndef DEFAULT_OT_IN_GPIO
|
||||
#define DEFAULT_OT_IN_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_INDOOR_PIN_DEFAULT
|
||||
#define SENSOR_INDOOR_PIN_DEFAULT 0
|
||||
#ifndef DEFAULT_OT_OUT_GPIO
|
||||
#define DEFAULT_OT_OUT_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef EXT_PUMP_PIN_DEFAULT
|
||||
#define EXT_PUMP_PIN_DEFAULT 0
|
||||
#ifndef DEFAULT_OT_RX_LED_GPIO
|
||||
#define DEFAULT_OT_RX_LED_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_OT_FAULT_STATE_GPIO
|
||||
#define DEFAULT_OT_FAULT_STATE_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_SENSOR_OUTDOOR_GPIO
|
||||
#define DEFAULT_SENSOR_OUTDOOR_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_SENSOR_INDOOR_GPIO
|
||||
#define DEFAULT_SENSOR_INDOOR_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef SENSORS_AMOUNT
|
||||
#define SENSORS_AMOUNT 20
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_EXT_PUMP_GPIO
|
||||
#define DEFAULT_EXT_PUMP_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <driver/gpio.h>
|
||||
#elif !defined(GPIO_IS_VALID_GPIO)
|
||||
#define GPIO_IS_VALID_GPIO(gpioNum) (gpioNum >= 0 && gpioNum <= 17)
|
||||
#endif
|
||||
|
||||
#define GPIO_IS_VALID(gpioNum) (gpioNum != GPIO_IS_NOT_CONFIGURED && GPIO_IS_VALID_GPIO(gpioNum))
|
||||
|
||||
enum class UnitSystem : uint8_t {
|
||||
METRIC = 0,
|
||||
IMPERIAL = 1
|
||||
};
|
||||
|
||||
char buffer[255];
|
||||
3
src/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
idf: ">=5.3.2"
|
||||
h2zero/esp-nimble-cpp: ">=2.2.1"
|
||||
132
src/main.cpp
@@ -1,14 +1,21 @@
|
||||
#define ARDUINOJSON_USE_DOUBLE 0
|
||||
#define ARDUINOJSON_USE_LONG_LONG 0
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "strings.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <FileData.h>
|
||||
#include <LittleFS.h>
|
||||
#include "ESPTelnetStream.h"
|
||||
#include <ESPTelnetStream.h>
|
||||
|
||||
#include "defines.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <TinyLogger.h>
|
||||
#include <NetworkManager.h>
|
||||
#include <NetworkMgr.h>
|
||||
#include "CrashRecorder.h"
|
||||
#include "Sensors.h"
|
||||
#include "Settings.h"
|
||||
#include <utils.h>
|
||||
#include "utils.h"
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <ESP32Scheduler.h>
|
||||
@@ -27,11 +34,16 @@
|
||||
#include "PortalTask.h"
|
||||
#include "MainTask.h"
|
||||
|
||||
using namespace NetworkUtils;
|
||||
|
||||
// Vars
|
||||
ESPTelnetStream* telnetStream = nullptr;
|
||||
NetworkMgr* network = nullptr;
|
||||
Sensors::Result sensorsResults[SENSORS_AMOUNT];
|
||||
|
||||
FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000);
|
||||
FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000);
|
||||
ESPTelnetStream* telnetStream = nullptr;
|
||||
Network::Manager* network = nullptr;
|
||||
FileData fsSensorsSettings(&LittleFS, "/sensors.conf", 'e', &sensorsSettings, sizeof(sensorsSettings), 60000);
|
||||
|
||||
// Tasks
|
||||
MqttTask* tMqtt;
|
||||
@@ -43,6 +55,10 @@ MainTask* tMain;
|
||||
|
||||
|
||||
void setup() {
|
||||
CrashRecorder::init();
|
||||
Sensors::setMaxSensors(SENSORS_AMOUNT);
|
||||
Sensors::settings = sensorsSettings;
|
||||
Sensors::results = sensorsResults;
|
||||
LittleFS.begin();
|
||||
|
||||
Log.setLevel(TinyLogger::Level::VERBOSE);
|
||||
@@ -60,10 +76,14 @@ void setup() {
|
||||
});
|
||||
|
||||
Serial.begin(115200);
|
||||
#if ARDUINO_USB_MODE
|
||||
Serial.setTxBufferSize(512);
|
||||
#endif
|
||||
Log.addStream(&Serial);
|
||||
Log.print("\n\n\r");
|
||||
|
||||
// network settings
|
||||
//
|
||||
// Network settings
|
||||
switch (fsNetworkSettings.read()) {
|
||||
case FD_FS_ERR:
|
||||
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default"));
|
||||
@@ -82,7 +102,33 @@ void setup() {
|
||||
break;
|
||||
}
|
||||
|
||||
// settings
|
||||
// generate hostname if it is empty
|
||||
if (!strlen(networkSettings.hostname)) {
|
||||
strcpy(networkSettings.hostname, getChipId("otgateway-").c_str());
|
||||
fsNetworkSettings.update();
|
||||
}
|
||||
|
||||
network = (new NetworkMgr)
|
||||
->setHostname(networkSettings.hostname)
|
||||
->setStaCredentials(
|
||||
strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr,
|
||||
strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr,
|
||||
networkSettings.sta.channel
|
||||
)->setApCredentials(
|
||||
strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr,
|
||||
strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr,
|
||||
networkSettings.ap.channel
|
||||
)
|
||||
->setUseDhcp(networkSettings.useDhcp)
|
||||
->setStaticConfig(
|
||||
networkSettings.staticConfig.ip,
|
||||
networkSettings.staticConfig.gateway,
|
||||
networkSettings.staticConfig.subnet,
|
||||
networkSettings.staticConfig.dns
|
||||
);
|
||||
|
||||
//
|
||||
// Settings
|
||||
switch (fsSettings.read()) {
|
||||
case FD_FS_ERR:
|
||||
Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default"));
|
||||
@@ -108,45 +154,63 @@ void setup() {
|
||||
break;
|
||||
}
|
||||
|
||||
// logs
|
||||
if (!settings.system.useSerial) {
|
||||
Log.clearStreams();
|
||||
Serial.end();
|
||||
// generate mqtt prefix if it is empty
|
||||
if (!strlen(settings.mqtt.prefix)) {
|
||||
strcpy(settings.mqtt.prefix, getChipId("otgateway_").c_str());
|
||||
fsSettings.update();
|
||||
}
|
||||
|
||||
if (settings.system.useTelnet) {
|
||||
// Logs settings
|
||||
if (!settings.system.serial.enabled) {
|
||||
Serial.end();
|
||||
Log.clearStreams();
|
||||
|
||||
} else if (settings.system.serial.baudrate != 115200) {
|
||||
Serial.end();
|
||||
Log.clearStreams();
|
||||
|
||||
Serial.begin(settings.system.serial.baudrate);
|
||||
Log.addStream(&Serial);
|
||||
}
|
||||
|
||||
if (settings.system.telnet.enabled) {
|
||||
telnetStream = new ESPTelnetStream;
|
||||
telnetStream->setKeepAliveInterval(500);
|
||||
Log.addStream(telnetStream);
|
||||
}
|
||||
|
||||
Log.setLevel(settings.system.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
|
||||
if (settings.system.logLevel >= TinyLogger::Level::SILENT && settings.system.logLevel <= TinyLogger::Level::VERBOSE) {
|
||||
Log.setLevel(static_cast<TinyLogger::Level>(settings.system.logLevel));
|
||||
}
|
||||
|
||||
// network
|
||||
network = (new Network::Manager)
|
||||
->setHostname(networkSettings.hostname)
|
||||
->setStaCredentials(
|
||||
#ifdef WOKWI
|
||||
"Wokwi-GUEST", nullptr, 6
|
||||
#else
|
||||
strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr,
|
||||
strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr,
|
||||
networkSettings.sta.channel
|
||||
#endif
|
||||
)->setApCredentials(
|
||||
strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr,
|
||||
strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr,
|
||||
networkSettings.ap.channel
|
||||
);
|
||||
//
|
||||
// Sensors settings
|
||||
switch (fsSensorsSettings.read()) {
|
||||
case FD_FS_ERR:
|
||||
Log.swarningln(FPSTR(L_SENSORS), F("Filesystem error, load default"));
|
||||
break;
|
||||
case FD_FILE_ERR:
|
||||
Log.swarningln(FPSTR(L_SENSORS), F("Bad data, load default"));
|
||||
break;
|
||||
case FD_WRITE:
|
||||
Log.sinfoln(FPSTR(L_SENSORS), F("Not found, load default"));
|
||||
break;
|
||||
case FD_ADD:
|
||||
case FD_READ:
|
||||
Log.sinfoln(FPSTR(L_SENSORS), F("Loaded"));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// tasks
|
||||
//
|
||||
// Make tasks
|
||||
tMqtt = new MqttTask(false, 500);
|
||||
Scheduler.start(tMqtt);
|
||||
|
||||
tOt = new OpenThermTask(false, 750);
|
||||
tOt = new OpenThermTask(true, 750);
|
||||
Scheduler.start(tOt);
|
||||
|
||||
tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL);
|
||||
tSensors = new SensorsTask(true, 1000);
|
||||
Scheduler.start(tSensors);
|
||||
|
||||
tRegulator = new RegulatorTask(true, 10000);
|
||||
|
||||
231
src/strings.h
@@ -3,22 +3,215 @@
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
const char L_SETTINGS[] PROGMEM = "SETTINGS";
|
||||
const char L_NETWORK[] PROGMEM = "NETWORK";
|
||||
const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS";
|
||||
const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER";
|
||||
const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER";
|
||||
const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE";
|
||||
const char L_PORTAL_OTA[] PROGMEM = "PORTAL.OTA";
|
||||
const char L_MAIN[] PROGMEM = "MAIN";
|
||||
const char L_MQTT[] PROGMEM = "MQTT";
|
||||
const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG";
|
||||
const char L_OT[] PROGMEM = "OT";
|
||||
const char L_OT_DHW[] PROGMEM = "OT.DHW";
|
||||
const char L_OT_HEATING[] PROGMEM = "OT.HEATING";
|
||||
const char L_SENSORS_OUTDOOR[] PROGMEM = "SENSORS.OUTDOOR";
|
||||
const char L_SENSORS_INDOOR[] PROGMEM = "SENSORS.INDOOR";
|
||||
const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
|
||||
const char L_REGULATOR[] PROGMEM = "REGULATOR";
|
||||
const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID";
|
||||
const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM";
|
||||
const char L_SETTINGS[] PROGMEM = "SETTINGS";
|
||||
const char L_SETTINGS_OT[] PROGMEM = "SETTINGS.OT";
|
||||
const char L_SETTINGS_DHW[] PROGMEM = "SETTINGS.DHW";
|
||||
const char L_SETTINGS_HEATING[] PROGMEM = "SETTINGS.HEATING";
|
||||
const char L_NETWORK[] PROGMEM = "NETWORK";
|
||||
const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS";
|
||||
const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER";
|
||||
const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER";
|
||||
const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE";
|
||||
const char L_PORTAL_OTA[] PROGMEM = "PORTAL.OTA";
|
||||
const char L_MAIN[] PROGMEM = "MAIN";
|
||||
const char L_MQTT[] PROGMEM = "MQTT";
|
||||
const char L_MQTT_HA[] PROGMEM = "MQTT.HA";
|
||||
const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG";
|
||||
const char L_OT[] PROGMEM = "OT";
|
||||
const char L_OT_DHW[] PROGMEM = "OT.DHW";
|
||||
const char L_OT_HEATING[] PROGMEM = "OT.HEATING";
|
||||
const char L_OT_CH2[] PROGMEM = "OT.CH2";
|
||||
const char L_SENSORS[] PROGMEM = "SENSORS";
|
||||
const char L_SENSORS_SETTINGS[] PROGMEM = "SENSORS.SETTINGS";
|
||||
const char L_SENSORS_DALLAS[] PROGMEM = "SENSORS.DALLAS";
|
||||
const char L_SENSORS_NTC[] PROGMEM = "SENSORS.NTC";
|
||||
const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
|
||||
const char L_REGULATOR[] PROGMEM = "REGULATOR";
|
||||
const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID";
|
||||
const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM";
|
||||
const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT";
|
||||
const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT";
|
||||
const char L_EXTPUMP[] PROGMEM = "EXTPUMP";
|
||||
|
||||
|
||||
const char S_ACTIONS[] PROGMEM = "actions";
|
||||
const char S_ACTIVE[] PROGMEM = "active";
|
||||
const char S_ADDRESS[] PROGMEM = "address";
|
||||
const char S_ANTI_STUCK_INTERVAL[] PROGMEM = "antiStuckInterval";
|
||||
const char S_ANTI_STUCK_TIME[] PROGMEM = "antiStuckTime";
|
||||
const char S_AP[] PROGMEM = "ap";
|
||||
const char S_APP_VERSION[] PROGMEM = "appVersion";
|
||||
const char S_AUTH[] PROGMEM = "auth";
|
||||
const char S_AUTO_DIAG_RESET[] PROGMEM = "autoDiagReset";
|
||||
const char S_AUTO_FAULT_RESET[] PROGMEM = "autoFaultReset";
|
||||
const char S_BACKTRACE[] PROGMEM = "backtrace";
|
||||
const char S_BATTERY[] PROGMEM = "battery";
|
||||
const char S_BAUDRATE[] PROGMEM = "baudrate";
|
||||
const char S_BLOCKING[] PROGMEM = "blocking";
|
||||
const char S_BSSID[] PROGMEM = "bssid";
|
||||
const char S_BUILD[] PROGMEM = "build";
|
||||
const char S_CASCADE_CONTROL[] PROGMEM = "cascadeControl";
|
||||
const char S_CHANNEL[] PROGMEM = "channel";
|
||||
const char S_CH2_ALWAYS_ENABLED[] PROGMEM = "ch2AlwaysEnabled";
|
||||
const char S_CHIP[] PROGMEM = "chip";
|
||||
const char S_CODE[] PROGMEM = "code";
|
||||
const char S_CONNECTED[] PROGMEM = "connected";
|
||||
const char S_CONTINUES[] PROGMEM = "continues";
|
||||
const char S_COOLING[] PROGMEM = "cooling";
|
||||
const char S_COOLING_SUPPORT[] PROGMEM = "coolingSupport";
|
||||
const char S_CORE[] PROGMEM = "core";
|
||||
const char S_CORES[] PROGMEM = "cores";
|
||||
const char S_CRASH[] PROGMEM = "crash";
|
||||
const char S_CURRENT_TEMP[] PROGMEM = "currentTemp";
|
||||
const char S_DATA[] PROGMEM = "data";
|
||||
const char S_DATE[] PROGMEM = "date";
|
||||
const char S_DEADBAND[] PROGMEM = "deadband";
|
||||
const char S_DHW[] PROGMEM = "dhw";
|
||||
const char S_DHW_BLOCKING[] PROGMEM = "dhwBlocking";
|
||||
const char S_DHW_STATE_AS_DHW_BLOCKING[] PROGMEM = "dhwStateAsDhwBlocking";
|
||||
const char S_DHW_SUPPORT[] PROGMEM = "dhwSupport";
|
||||
const char S_DHW_TO_CH2[] PROGMEM = "dhwToCh2";
|
||||
const char S_DIAG[] PROGMEM = "diag";
|
||||
const char S_DNS[] PROGMEM = "dns";
|
||||
const char S_DT[] PROGMEM = "dt";
|
||||
const char S_D_FACTOR[] PROGMEM = "d_factor";
|
||||
const char S_D_MULTIPLIER[] PROGMEM = "d_multiplier";
|
||||
const char S_EMERGENCY[] PROGMEM = "emergency";
|
||||
const char S_ENABLED[] PROGMEM = "enabled";
|
||||
const char S_ENV[] PROGMEM = "env";
|
||||
const char S_EPC[] PROGMEM = "epc";
|
||||
const char S_EQUITHERM[] PROGMEM = "equitherm";
|
||||
const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump";
|
||||
const char S_FACTOR[] PROGMEM = "factor";
|
||||
const char S_FAULT[] PROGMEM = "fault";
|
||||
const char S_FREEZE_PROTECTION[] PROGMEM = "freezeProtection";
|
||||
const char S_FILTERING[] PROGMEM = "filtering";
|
||||
const char S_FILTERING_FACTOR[] PROGMEM = "filteringFactor";
|
||||
const char S_FLAGS[] PROGMEM = "flags";
|
||||
const char S_FLAME[] PROGMEM = "flame";
|
||||
const char S_FLASH[] PROGMEM = "flash";
|
||||
const char S_FREE[] PROGMEM = "free";
|
||||
const char S_FREQ[] PROGMEM = "freq";
|
||||
const char S_GATEWAY[] PROGMEM = "gateway";
|
||||
const char S_GET_MIN_MAX_TEMP[] PROGMEM = "getMinMaxTemp";
|
||||
const char S_GPIO[] PROGMEM = "gpio";
|
||||
const char S_HEAP[] PROGMEM = "heap";
|
||||
const char S_HEATING[] PROGMEM = "heating";
|
||||
const char S_HEATING_TO_CH2[] PROGMEM = "heatingToCh2";
|
||||
const char S_HEATING_STATE_TO_SUMMER_WINTER_MODE[] PROGMEM = "heatingStateToSummerWinterMode";
|
||||
const char S_HIDDEN[] PROGMEM = "hidden";
|
||||
const char S_HIGH_TEMP[] PROGMEM = "highTemp";
|
||||
const char S_HOME_ASSISTANT_DISCOVERY[] PROGMEM = "homeAssistantDiscovery";
|
||||
const char S_HOSTNAME[] PROGMEM = "hostname";
|
||||
const char S_HUMIDITY[] PROGMEM = "humidity";
|
||||
const char S_HYSTERESIS[] PROGMEM = "hysteresis";
|
||||
const char S_ID[] PROGMEM = "id";
|
||||
const char S_IGNORE_DIAG_STATE[] PROGMEM = "ignoreDiagState";
|
||||
const char S_IMMERGAS_FIX[] PROGMEM = "immergasFix";
|
||||
const char S_INDOOR_TEMP[] PROGMEM = "indoorTemp";
|
||||
const char S_INDOOR_TEMP_CONTROL[] PROGMEM = "indoorTempControl";
|
||||
const char S_IN_GPIO[] PROGMEM = "inGpio";
|
||||
const char S_INPUT[] PROGMEM = "input";
|
||||
const char S_INTERVAL[] PROGMEM = "interval";
|
||||
const char S_INVERT_STATE[] PROGMEM = "invertState";
|
||||
const char S_IP[] PROGMEM = "ip";
|
||||
const char S_I_FACTOR[] PROGMEM = "i_factor";
|
||||
const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier";
|
||||
const char S_K_FACTOR[] PROGMEM = "k_factor";
|
||||
const char S_LOGIN[] PROGMEM = "login";
|
||||
const char S_LOG_LEVEL[] PROGMEM = "logLevel";
|
||||
const char S_LOW_TEMP[] PROGMEM = "lowTemp";
|
||||
const char S_MAC[] PROGMEM = "mac";
|
||||
const char S_MASTER[] PROGMEM = "master";
|
||||
const char S_MAX[] PROGMEM = "max";
|
||||
const char S_MAX_FREE_BLOCK[] PROGMEM = "maxFreeBlock";
|
||||
const char S_MAX_MODULATION[] PROGMEM = "maxModulation";
|
||||
const char S_MAX_POWER[] PROGMEM = "maxPower";
|
||||
const char S_MAX_TEMP[] PROGMEM = "maxTemp";
|
||||
const char S_MAX_TEMP_SYNC_WITH_TARGET_TEMP[] PROGMEM = "maxTempSyncWithTargetTemp";
|
||||
const char S_MDNS[] PROGMEM = "mdns";
|
||||
const char S_MEMBER_ID[] PROGMEM = "memberId";
|
||||
const char S_MIN[] PROGMEM = "min";
|
||||
const char S_MIN_FREE[] PROGMEM = "minFree";
|
||||
const char S_MIN_MAX_FREE_BLOCK[] PROGMEM = "minMaxFreeBlock";
|
||||
const char S_MIN_POWER[] PROGMEM = "minPower";
|
||||
const char S_MIN_TEMP[] PROGMEM = "minTemp";
|
||||
const char S_MODEL[] PROGMEM = "model";
|
||||
const char S_MODULATION[] PROGMEM = "modulation";
|
||||
const char S_MQTT[] PROGMEM = "mqtt";
|
||||
const char S_NAME[] PROGMEM = "name";
|
||||
const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl";
|
||||
const char S_NETWORK[] PROGMEM = "network";
|
||||
const char S_NTP[] PROGMEM = "ntp";
|
||||
const char S_N_FACTOR[] PROGMEM = "n_factor";
|
||||
const char S_OFFSET[] PROGMEM = "offset";
|
||||
const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating";
|
||||
const char S_ON_FAULT[] PROGMEM = "onFault";
|
||||
const char S_ON_LOSS_CONNECTION[] PROGMEM = "onLossConnection";
|
||||
const char S_OPENTHERM[] PROGMEM = "opentherm";
|
||||
const char S_OPTIONS[] PROGMEM = "options";
|
||||
const char S_OUTDOOR_TEMP[] PROGMEM = "outdoorTemp";
|
||||
const char S_OUT_GPIO[] PROGMEM = "outGpio";
|
||||
const char S_OUTPUT[] PROGMEM = "output";
|
||||
const char S_OVERHEAT[] PROGMEM = "overheat";
|
||||
const char S_OVERHEAT_PROTECTION[] PROGMEM = "overheatProtection";
|
||||
const char S_PASSWORD[] PROGMEM = "password";
|
||||
const char S_PID[] PROGMEM = "pid";
|
||||
const char S_PORT[] PROGMEM = "port";
|
||||
const char S_PORTAL[] PROGMEM = "portal";
|
||||
const char S_POST_CIRCULATION_TIME[] PROGMEM = "postCirculationTime";
|
||||
const char S_POWER[] PROGMEM = "power";
|
||||
const char S_PREFIX[] PROGMEM = "prefix";
|
||||
const char S_PROTOCOL_VERSION[] PROGMEM = "protocolVersion";
|
||||
const char S_PURPOSE[] PROGMEM = "purpose";
|
||||
const char S_P_FACTOR[] PROGMEM = "p_factor";
|
||||
const char S_P_MULTIPLIER[] PROGMEM = "p_multiplier";
|
||||
const char S_REAL_SIZE[] PROGMEM = "realSize";
|
||||
const char S_REASON[] PROGMEM = "reason";
|
||||
const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic";
|
||||
const char S_RESET_FAULT[] PROGMEM = "resetFault";
|
||||
const char S_RESET_REASON[] PROGMEM = "resetReason";
|
||||
const char S_RESTART[] PROGMEM = "restart";
|
||||
const char S_RETURN_TEMP[] PROGMEM = "returnTemp";
|
||||
const char S_REV[] PROGMEM = "rev";
|
||||
const char S_RSSI[] PROGMEM = "rssi";
|
||||
const char S_RX_LED_GPIO[] PROGMEM = "rxLedGpio";
|
||||
const char S_SDK[] PROGMEM = "sdk";
|
||||
const char S_SENSORS[] PROGMEM = "sensors";
|
||||
const char S_SERIAL[] PROGMEM = "serial";
|
||||
const char S_SERVER[] PROGMEM = "server";
|
||||
const char S_SETTINGS[] PROGMEM = "settings";
|
||||
const char S_SET_DATE_AND_TIME[] PROGMEM = "setDateAndTime";
|
||||
const char S_SIGNAL_QUALITY[] PROGMEM = "signalQuality";
|
||||
const char S_SIZE[] PROGMEM = "size";
|
||||
const char S_SLAVE[] PROGMEM = "slave";
|
||||
const char S_SSID[] PROGMEM = "ssid";
|
||||
const char S_STA[] PROGMEM = "sta";
|
||||
const char S_STATE[] PROGMEM = "state";
|
||||
const char S_STATIC_CONFIG[] PROGMEM = "staticConfig";
|
||||
const char S_STATUS_LED_GPIO[] PROGMEM = "statusLedGpio";
|
||||
const char S_SETPOINT_TEMP[] PROGMEM = "setpointTemp";
|
||||
const char S_SUBNET[] PROGMEM = "subnet";
|
||||
const char S_SUMMER_WINTER_MODE[] PROGMEM = "summerWinterMode";
|
||||
const char S_SYSTEM[] PROGMEM = "system";
|
||||
const char S_TARGET[] PROGMEM = "target";
|
||||
const char S_TARGET_TEMP[] PROGMEM = "targetTemp";
|
||||
const char S_TELNET[] PROGMEM = "telnet";
|
||||
const char S_TEMPERATURE[] PROGMEM = "temperature";
|
||||
const char S_THRESHOLD_HIGH[] PROGMEM = "thresholdHigh";
|
||||
const char S_THRESHOLD_LOW[] PROGMEM = "thresholdLow";
|
||||
const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime";
|
||||
const char S_TIMEZONE[] PROGMEM = "timezone";
|
||||
const char S_TOTAL[] PROGMEM = "total";
|
||||
const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime";
|
||||
const char S_TURBO[] PROGMEM = "turbo";
|
||||
const char S_TURBO_FACTOR[] PROGMEM = "turboFactor";
|
||||
const char S_TYPE[] PROGMEM = "type";
|
||||
const char S_T_FACTOR[] PROGMEM = "t_factor";
|
||||
const char S_UNIT_SYSTEM[] PROGMEM = "unitSystem";
|
||||
const char S_UPTIME[] PROGMEM = "uptime";
|
||||
const char S_USE[] PROGMEM = "use";
|
||||
const char S_USE_DHCP[] PROGMEM = "useDhcp";
|
||||
const char S_USER[] PROGMEM = "user";
|
||||
const char S_VALUE[] PROGMEM = "value";
|
||||
const char S_VERSION[] PROGMEM = "version";
|
||||
|
||||
2174
src/utils.h
BIN
src_data/fonts/iconly.eot
Normal file
96
src_data/fonts/iconly.svg
Normal file
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<font id="iconly" horiz-adv-x="1024">
|
||||
<font-face font-family="iconly"
|
||||
units-per-em="1024" ascent="896"
|
||||
descent="128" />
|
||||
<missing-glyph horiz-adv-x="0" />
|
||||
<glyph glyph-name="plus"
|
||||
unicode=""
|
||||
horiz-adv-x="895.5537099041368" d="M831.585587768127 479.8473854289214H543.729038156083V767.7039350409655C543.729038156083 803.0263324829433 515.0833134620511 831.6720571769752 479.7609160200732 831.6720571769752H415.7927938840635C380.4703964420856 831.6720571769752 351.8246717480538 803.0263324829433 351.8246717480538 767.7039350409655V479.8473854289214H63.9681221360098C28.6457246940319 479.8473854289214 0 451.2016607348895 0 415.8792632929117V351.9111411569019C0 316.588743714924 28.6457246940319 287.9430190208922 63.9681221360098 287.9430190208922H351.8246717480538V0.0864694088482C351.8246717480538 -35.2359280331298 380.4703964420857 -63.8816527271616 415.7927938840635 -63.8816527271616H479.7609160200732C515.0833134620511 -63.8816527271616 543.729038156083 -35.2359280331298 543.729038156083 0.0864694088482V287.9430190208922H831.585587768127C866.9079852101049 287.9430190208922 895.5537099041368 316.588743714924 895.5537099041368 351.9111411569019V415.8792632929117C895.5537099041368 451.2016607348896 866.9079852101049 479.8473854289214 831.585587768127 479.8473854289214z" />
|
||||
<glyph glyph-name="plus-1"
|
||||
unicode="plus"
|
||||
horiz-adv-x="895.5537099041368" d="M831.585587768127 479.8473854289214H543.729038156083V767.7039350409655C543.729038156083 803.0263324829433 515.0833134620511 831.6720571769752 479.7609160200732 831.6720571769752H415.7927938840635C380.4703964420856 831.6720571769752 351.8246717480538 803.0263324829433 351.8246717480538 767.7039350409655V479.8473854289214H63.9681221360098C28.6457246940319 479.8473854289214 0 451.2016607348895 0 415.8792632929117V351.9111411569019C0 316.588743714924 28.6457246940319 287.9430190208922 63.9681221360098 287.9430190208922H351.8246717480538V0.0864694088482C351.8246717480538 -35.2359280331298 380.4703964420857 -63.8816527271616 415.7927938840635 -63.8816527271616H479.7609160200732C515.0833134620511 -63.8816527271616 543.729038156083 -35.2359280331298 543.729038156083 0.0864694088482V287.9430190208922H831.585587768127C866.9079852101049 287.9430190208922 895.5537099041368 316.588743714924 895.5537099041368 351.9111411569019V415.8792632929117C895.5537099041368 451.2016607348896 866.9079852101049 479.8473854289214 831.585587768127 479.8473854289214z" />
|
||||
<glyph glyph-name="minus"
|
||||
unicode=""
|
||||
horiz-adv-x="893.7641930109211" d="M829.9238935101409 480.448527979227H63.8402995007801C28.5884841201931 480.4465329698676 0 451.8580488496745 0 416.6062334690875V352.7659339683073C0 317.5141185877203 28.5884841201931 288.9256344675273 63.8402995007801 288.9256344675273H829.9238935101409C865.1757088907281 288.9256344675273 893.7641930109211 317.5141185877203 893.7641930109211 352.7659339683073V416.6062334690875C893.7641930109211 451.8580488496744 865.1757088907281 480.4465329698675 829.9238935101409 480.4465329698675z" />
|
||||
<glyph glyph-name="minus-1"
|
||||
unicode="minus"
|
||||
horiz-adv-x="893.7641930109211" d="M829.9238935101409 480.448527979227H63.8402995007801C28.5884841201931 480.4465329698676 0 451.8580488496745 0 416.6062334690875V352.7659339683073C0 317.5141185877203 28.5884841201931 288.9256344675273 63.8402995007801 288.9256344675273H829.9238935101409C865.1757088907281 288.9256344675273 893.7641930109211 317.5141185877203 893.7641930109211 352.7659339683073V416.6062334690875C893.7641930109211 451.8580488496744 865.1757088907281 480.4465329698675 829.9238935101409 480.4465329698675z" />
|
||||
<glyph glyph-name="unlocked"
|
||||
unicode=""
|
||||
horiz-adv-x="1024" d="M426.667 341.333C473.6 341.333 512 303.36 512 256S474.027 170.667 426.6670000000001 170.667S341.3330000000001 208.64 341.3330000000001 256S379.733 341.333 426.6670000000001 341.333M768 853.333C650.24 853.333 554.667 757.76 554.667 640V554.667H170.667C123.733 554.667 85.333 516.267 85.333 469.333V42.667C85.333 -4.2669999999999 123.733 -42.6669999999999 170.667 -42.6669999999999H682.667C729.6 -42.6669999999999 768 -4.2669999999999 768 42.6670000000001V469.333C768 516.267 729.6 554.667 682.667 554.667H640V640C640 710.827 697.173 768 768 768S896 710.827 896 640V554.667H981.333V640C981.333 757.76 885.76 853.333 768 853.333M682.667 469.333V42.6669999999999H170.667V469.333H682.667z" />
|
||||
<glyph glyph-name="unlocked-1"
|
||||
unicode="unlocked"
|
||||
horiz-adv-x="1024" d="M426.667 341.333C473.6 341.333 512 303.36 512 256S474.027 170.667 426.6670000000001 170.667S341.3330000000001 208.64 341.3330000000001 256S379.733 341.333 426.6670000000001 341.333M768 853.333C650.24 853.333 554.667 757.76 554.667 640V554.667H170.667C123.733 554.667 85.333 516.267 85.333 469.333V42.667C85.333 -4.2669999999999 123.733 -42.6669999999999 170.667 -42.6669999999999H682.667C729.6 -42.6669999999999 768 -4.2669999999999 768 42.6670000000001V469.333C768 516.267 729.6 554.667 682.667 554.667H640V640C640 710.827 697.173 768 768 768S896 710.827 896 640V554.667H981.333V640C981.333 757.76 885.76 853.333 768 853.333M682.667 469.333V42.6669999999999H170.667V469.333H682.667z" />
|
||||
<glyph glyph-name="locked"
|
||||
unicode=""
|
||||
horiz-adv-x="1024" d="M512 170.667C464.64 170.667 426.6670000000001 209.067 426.6670000000001 256C426.6670000000001 303.36 464.64 341.333 512 341.333A85.333 85.333 0 0 0 597.333 256A85.333 85.333 0 0 0 512 170.667M768 42.667V469.333H256V42.667H768M768 554.667A85.333 85.333 0 0 0 853.333 469.333V42.667A85.333 85.333 0 0 0 768 -42.667H256C208.64 -42.667 170.667 -4.2670000000001 170.667 42.6669999999999V469.333C170.667 516.693 208.64 554.667 256 554.667H298.6670000000001V640A213.333 213.333 0 0 0 512 853.333A213.333 213.333 0 0 0 725.333 640V554.667H768M512 768A128 128 0 0 1 384 640V554.667H640V640A128 128 0 0 1 512 768z" />
|
||||
<glyph glyph-name="locked-1"
|
||||
unicode="locked"
|
||||
horiz-adv-x="1024" d="M512 170.667C464.64 170.667 426.6670000000001 209.067 426.6670000000001 256C426.6670000000001 303.36 464.64 341.333 512 341.333A85.333 85.333 0 0 0 597.333 256A85.333 85.333 0 0 0 512 170.667M768 42.667V469.333H256V42.667H768M768 554.667A85.333 85.333 0 0 0 853.333 469.333V42.667A85.333 85.333 0 0 0 768 -42.667H256C208.64 -42.667 170.667 -4.2670000000001 170.667 42.6669999999999V469.333C170.667 516.693 208.64 554.667 256 554.667H298.6670000000001V640A213.333 213.333 0 0 0 512 853.333A213.333 213.333 0 0 0 725.333 640V554.667H768M512 768A128 128 0 0 1 384 640V554.667H640V640A128 128 0 0 1 512 768z" />
|
||||
<glyph glyph-name="wifi-strength-1"
|
||||
unicode=""
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L665.3592366333123 307.3637631451326C618.0467201032712 329.95509018927 564.7661931477559 341.8891130454328 511.4866651896333 341.8891130454328C458.2071372315108 341.8891130454328 404.9266102759954 329.9550901892699 357.6140937459543 307.7903350319215L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="wifi-strength-1-1"
|
||||
unicode="wifi_strength_1"
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L665.3592366333123 307.3637631451326C618.0467201032712 329.95509018927 564.7661931477559 341.8891130454328 511.4866651896333 341.8891130454328C458.2071372315108 341.8891130454328 404.9266102759954 329.9550901892699 357.6140937459543 307.7903350319215L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="wifi-strength-0"
|
||||
unicode=""
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.4663323732617 768.1283337025917 157.7087214326013 708.4552224295987 17.0498885054521 597.6324456402496C183.283054692083 393.0377795643962 349.5162208787138 184.1813906102245 511.4866651896334 -20.4132754656289C609.5212763518488 103.1956689560682 711.8186093897755 226.8046133777654 809.8542195493837 350.4145567968553V486.8106678482933L511.4866651896334 115.9828355858092L140.6588329271492 580.5835561321903C251.4806107191054 648.7811121592129 379.3522770165138 682.8808891701171 511.4866651896334 682.8808891701171S771.4927196601614 644.5193892808945 882.3144974521176 580.5835561321903L865.2646089466655 555.0092228727087H976.0873857360147C988.8745523657554 567.7963895024495 997.398997119785 584.8452790105086 1010.186163749526 597.6324456402496C865.2646089466655 708.4552224295987 690.5069980060051 768.1283337025917 511.4866651896334 768.1283337025917M895.1016640818584 469.7607793428413V214.0174467480247H980.349108614333V469.7607793428413M895.1016640818584 128.77000221555V43.5225576830754H980.349108614333V128.77000221555" />
|
||||
<glyph glyph-name="wifi-strength-0-1"
|
||||
unicode="wifi_strength_0"
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.4663323732617 768.1283337025917 157.7087214326013 708.4552224295987 17.0498885054521 597.6324456402496C183.283054692083 393.0377795643962 349.5162208787138 184.1813906102245 511.4866651896334 -20.4132754656289C609.5212763518488 103.1956689560682 711.8186093897755 226.8046133777654 809.8542195493837 350.4145567968553V486.8106678482933L511.4866651896334 115.9828355858092L140.6588329271492 580.5835561321903C251.4806107191054 648.7811121592129 379.3522770165138 682.8808891701171 511.4866651896334 682.8808891701171S771.4927196601614 644.5193892808945 882.3144974521176 580.5835561321903L865.2646089466655 555.0092228727087H976.0873857360147C988.8745523657554 567.7963895024495 997.398997119785 584.8452790105086 1010.186163749526 597.6324456402496C865.2646089466655 708.4552224295987 690.5069980060051 768.1283337025917 511.4866651896334 768.1283337025917M895.1016640818584 469.7607793428413V214.0174467480247H980.349108614333V469.7607793428413M895.1016640818584 128.77000221555V43.5225576830754H980.349108614333V128.77000221555" />
|
||||
<glyph glyph-name="wifi-strength-2"
|
||||
unicode=""
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L745.9183864006797 408.8089514068742C693.0644313319532 438.2194346552781 612.505281564586 469.7607793428413 511.4866651896334 469.7607793428413C410.0414769278917 469.7607793428413 329.9088990473136 437.7928627684892 277.0549439785871 408.8089514068743L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="wifi-strength-2-1"
|
||||
unicode="wifi_strength_2"
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L745.9183864006797 408.8089514068742C693.0644313319532 438.2194346552781 612.505281564586 469.7607793428413 511.4866651896334 469.7607793428413C410.0414769278917 469.7607793428413 329.9088990473136 437.7928627684892 277.0549439785871 408.8089514068743L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="error"
|
||||
unicode=""
|
||||
horiz-adv-x="1024" d="M512 810.667C747.605 810.667 938.667 619.605 938.667 384C938.667 148.395 747.6049999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.395 -42.6669999999999 85.333 148.3950000000001 85.333 384.0000000000001C85.333 619.605 276.395 810.667 512 810.667zM512 725.333C323.7970000000001 725.333 170.667 572.203 170.667 384C170.667 195.797 323.797 42.6669999999999 512 42.6669999999999C700.203 42.6669999999999 853.3330000000001 195.7969999999999 853.3330000000001 384C853.3330000000001 572.203 700.2030000000001 725.3330000000001 512 725.3330000000001zM609.835 542.165L670.1650000000001 481.835L572.33 384L670.1650000000001 286.165L609.835 225.8349999999999L512 323.67L414.165 225.8349999999999L353.8350000000001 286.165L451.67 384L353.8350000000001 481.835L414.165 542.165L512 444.33L609.835 542.165z" />
|
||||
<glyph glyph-name="error-1"
|
||||
unicode="error"
|
||||
horiz-adv-x="1024" d="M512 810.667C747.605 810.667 938.667 619.605 938.667 384C938.667 148.395 747.6049999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.395 -42.6669999999999 85.333 148.3950000000001 85.333 384.0000000000001C85.333 619.605 276.395 810.667 512 810.667zM512 725.333C323.7970000000001 725.333 170.667 572.203 170.667 384C170.667 195.797 323.797 42.6669999999999 512 42.6669999999999C700.203 42.6669999999999 853.3330000000001 195.7969999999999 853.3330000000001 384C853.3330000000001 572.203 700.2030000000001 725.3330000000001 512 725.3330000000001zM609.835 542.165L670.1650000000001 481.835L572.33 384L670.1650000000001 286.165L609.835 225.8349999999999L512 323.67L414.165 225.8349999999999L353.8350000000001 286.165L451.67 384L353.8350000000001 481.835L414.165 542.165L512 444.33L609.835 542.165z" />
|
||||
<glyph glyph-name="wifi-strength-3"
|
||||
unicode=""
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L800.0500591349871 474.8756459947375C735.688653096887 512.385001107775 634.2434648351453 555.0092228727087 511.4866651896334 555.0092228727087C383.614998892225 555.0092228727087 284.7272439564316 512.385001107775 222.0701274707015 476.5809345445006L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="wifi-strength-3-1"
|
||||
unicode="wifi_strength_3"
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L800.0500591349871 474.8756459947375C735.688653096887 512.385001107775 634.2434648351453 555.0092228727087 511.4866651896334 555.0092228727087C383.614998892225 555.0092228727087 284.7272439564316 512.385001107775 222.0701274707015 476.5809345445006L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
|
||||
<glyph glyph-name="down"
|
||||
unicode=""
|
||||
horiz-adv-x="893.7641930109211" d="M824.1383663678829 451.520892267936L868.427574146549 407.2306869845902C887.180662124903 388.477599006236 887.180662124903 358.1514617340061 868.427574146549 339.5978746915919L480.79725561525 -48.2279547569284C462.0441676368958 -66.9810427352825 431.7200253740253 -66.9810427352825 413.1664383316111 -48.2279547569284L25.3366188643721 339.6018647103107C6.5835308860179 358.3549526886648 6.5835308860179 388.6790949515354 25.3366188643721 407.2326819939496L69.6258266430383 451.5218897726157C88.5784155573323 470.4744786869098 119.5010606280227 470.07547681503 138.0546476704369 450.723886028856L367.0817221294855 210.325258221231V783.690948112612C367.0817221294855 810.2245725926238 388.4283222750588 831.5711727381971 414.9619467550705 831.5711727381971H478.8022462558506C505.3358707358623 831.5711727381971 526.6824708814356 810.2245725926238 526.6824708814356 783.690948112612V210.325258221231L755.7095453404842 450.723886028856C774.2631323828984 470.2749777509699 805.1857774535888 470.6739796228497 824.1383663678829 451.5218897726157z" />
|
||||
<glyph glyph-name="down-1"
|
||||
unicode="down"
|
||||
horiz-adv-x="893.7641930109211" d="M824.1383663678829 451.520892267936L868.427574146549 407.2306869845902C887.180662124903 388.477599006236 887.180662124903 358.1514617340061 868.427574146549 339.5978746915919L480.79725561525 -48.2279547569284C462.0441676368958 -66.9810427352825 431.7200253740253 -66.9810427352825 413.1664383316111 -48.2279547569284L25.3366188643721 339.6018647103107C6.5835308860179 358.3549526886648 6.5835308860179 388.6790949515354 25.3366188643721 407.2326819939496L69.6258266430383 451.5218897726157C88.5784155573323 470.4744786869098 119.5010606280227 470.07547681503 138.0546476704369 450.723886028856L367.0817221294855 210.325258221231V783.690948112612C367.0817221294855 810.2245725926238 388.4283222750588 831.5711727381971 414.9619467550705 831.5711727381971H478.8022462558506C505.3358707358623 831.5711727381971 526.6824708814356 810.2245725926238 526.6824708814356 783.690948112612V210.325258221231L755.7095453404842 450.723886028856C774.2631323828984 470.2749777509699 805.1857774535888 470.6739796228497 824.1383663678829 451.5218897726157z" />
|
||||
<glyph glyph-name="wifi-strength-4"
|
||||
unicode=""
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917z" />
|
||||
<glyph glyph-name="wifi-strength-4-1"
|
||||
unicode="wifi_strength_4"
|
||||
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917z" />
|
||||
<glyph glyph-name="success"
|
||||
unicode=""
|
||||
horiz-adv-x="1024" d="M512 810.667C276.36 810.667 85.333 619.64 85.333 384C85.333 148.36 276.3590000000001 -42.6669999999999 512 -42.6669999999999C747.64 -42.6669999999999 938.667 148.3590000000002 938.667 384.0000000000001C938.667 619.64 747.64 810.667 512 810.667zM512 42.667C323.79 42.667 170.667 195.789 170.667 384.0000000000001C170.667 572.21 323.789 725.3330000000001 512 725.3330000000001C700.21 725.3330000000001 853.3330000000001 572.211 853.3330000000001 384.0000000000001C853.3330000000001 195.7900000000001 700.211 42.667 512 42.667zM672.672 536.437L733.005 476.104L469.333 211.328L311.167 369.495L371.5 429.828L469.333 331.995L672.673 536.438z" />
|
||||
<glyph glyph-name="success-1"
|
||||
unicode="success"
|
||||
horiz-adv-x="1024" d="M512 810.667C276.36 810.667 85.333 619.64 85.333 384C85.333 148.36 276.3590000000001 -42.6669999999999 512 -42.6669999999999C747.64 -42.6669999999999 938.667 148.3590000000002 938.667 384.0000000000001C938.667 619.64 747.64 810.667 512 810.667zM512 42.667C323.79 42.667 170.667 195.789 170.667 384.0000000000001C170.667 572.21 323.789 725.3330000000001 512 725.3330000000001C700.21 725.3330000000001 853.3330000000001 572.211 853.3330000000001 384.0000000000001C853.3330000000001 195.7900000000001 700.211 42.667 512 42.667zM672.672 536.437L733.005 476.104L469.333 211.328L311.167 369.495L371.5 429.828L469.333 331.995L672.673 536.438z" />
|
||||
<glyph glyph-name="up"
|
||||
unicode=""
|
||||
horiz-adv-x="893.7641930109211" d="M69.6258266430383 317.8562626928574L25.3366188643721 362.1454704715236C6.5835308860179 380.8985584498777 6.5835308860179 411.2227007127483 25.3366188643721 429.7762877551625L412.9669373956711 817.6021172036826C431.7200253740253 836.3552051820368 462.0441676368958 836.3552051820368 480.59775467931 817.6021172036826L868.2280732106092 429.9717986723836C886.9811611889633 411.2187106940295 886.9811611889633 380.894568431159 868.2280732106092 362.3409813887447L823.938865431943 318.0517736100786C804.9862765176489 299.0991846957845 774.0636314469585 299.4981865676644 755.5100444045443 318.8497773538383L526.6824708814356 559.0528942442421V-14.3127956471388C526.6824708814356 -40.8464201271506 505.3358707358623 -62.1930202727239 478.8022462558506 -62.1930202727239H414.9619467550705C388.4283222750588 -62.1930202727239 367.0817221294855 -40.8464201271506 367.0817221294855 -14.3127956471388V559.0528942442422L138.0546476704369 318.6542664366173C119.5010606280227 299.1031747145034 88.5784155573324 298.7041728426234 69.6258266430383 317.8562626928575z" />
|
||||
<glyph glyph-name="up-1"
|
||||
unicode="up"
|
||||
horiz-adv-x="893.7641930109211" d="M69.6258266430383 317.8562626928574L25.3366188643721 362.1454704715236C6.5835308860179 380.8985584498777 6.5835308860179 411.2227007127483 25.3366188643721 429.7762877551625L412.9669373956711 817.6021172036826C431.7200253740253 836.3552051820368 462.0441676368958 836.3552051820368 480.59775467931 817.6021172036826L868.2280732106092 429.9717986723836C886.9811611889633 411.2187106940295 886.9811611889633 380.894568431159 868.2280732106092 362.3409813887447L823.938865431943 318.0517736100786C804.9862765176489 299.0991846957845 774.0636314469585 299.4981865676644 755.5100444045443 318.8497773538383L526.6824708814356 559.0528942442421V-14.3127956471388C526.6824708814356 -40.8464201271506 505.3358707358623 -62.1930202727239 478.8022462558506 -62.1930202727239H414.9619467550705C388.4283222750588 -62.1930202727239 367.0817221294855 -40.8464201271506 367.0817221294855 -14.3127956471388V559.0528942442422L138.0546476704369 318.6542664366173C119.5010606280227 299.1031747145034 88.5784155573324 298.7041728426234 69.6258266430383 317.8562626928575z" />
|
||||
<glyph glyph-name="alarm"
|
||||
unicode=""
|
||||
horiz-adv-x="1024" d="M512 810.667C747.22 810.667 938.667 619.22 938.667 384C938.667 148.78 747.2199999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.78 -42.667 85.333 148.78 85.333 384C85.333 619.22 276.78 810.667 512 810.667zM512 725.333C323.082 725.333 170.667 572.918 170.667 384C170.667 195.082 323.082 42.6669999999999 512 42.6669999999999C700.918 42.6669999999999 853.3330000000001 195.0819999999999 853.3330000000001 384C853.3330000000001 572.918 700.9180000000001 725.3330000000001 512 725.3330000000001zM512 266.581C542.476 266.581 565.333 244.054 565.333 213.3340000000001S542.476 160.085 512 160.085C480.83 160.085 458.667 182.6130000000001 458.667 214.015C458.667 244.053 481.524 266.581 512 266.581zM554.667 640V341.3330000000001H469.333V640H554.667z" />
|
||||
<glyph glyph-name="alarm-1"
|
||||
unicode="alarm"
|
||||
horiz-adv-x="1024" d="M512 810.667C747.22 810.667 938.667 619.22 938.667 384C938.667 148.78 747.2199999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.78 -42.667 85.333 148.78 85.333 384C85.333 619.22 276.78 810.667 512 810.667zM512 725.333C323.082 725.333 170.667 572.918 170.667 384C170.667 195.082 323.082 42.6669999999999 512 42.6669999999999C700.918 42.6669999999999 853.3330000000001 195.0819999999999 853.3330000000001 384C853.3330000000001 572.918 700.9180000000001 725.3330000000001 512 725.3330000000001zM512 266.581C542.476 266.581 565.333 244.054 565.333 213.3340000000001S542.476 160.085 512 160.085C480.83 160.085 458.667 182.6130000000001 458.667 214.015C458.667 244.053 481.524 266.581 512 266.581zM554.667 640V341.3330000000001H469.333V640H554.667z" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 27 KiB |
BIN
src_data/fonts/iconly.ttf
Normal file
BIN
src_data/fonts/iconly.woff
Normal file
BIN
src_data/fonts/iconly.woff2
Normal file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
507
src_data/locales/cn.json
Normal file
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "授权许可",
|
||||
"source": "源代码",
|
||||
"help": "帮助",
|
||||
"issues": "问题与反馈",
|
||||
"releases": "发行版"
|
||||
},
|
||||
"dbm": "dBm",
|
||||
"kw": "kW",
|
||||
"time": {
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"min": "分",
|
||||
"sec": "秒"
|
||||
},
|
||||
|
||||
"button": {
|
||||
"upgrade": "固件升级",
|
||||
"restart": "重启",
|
||||
"save": "保存",
|
||||
"saved": "已保存",
|
||||
"refresh": "刷新",
|
||||
"restore": "恢复",
|
||||
"restored": "已恢复",
|
||||
"backup": "备份",
|
||||
"wait": "请等待...",
|
||||
"uploading": "上传中...",
|
||||
"success": "成功",
|
||||
"error": "错误"
|
||||
},
|
||||
|
||||
"index": {
|
||||
"title": "OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"network": "网络",
|
||||
"system": "系统"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"build": {
|
||||
"title": "Build",
|
||||
"version": "固件版本",
|
||||
"date": "日期",
|
||||
"core": "内核版本",
|
||||
"sdk": "SDK"
|
||||
},
|
||||
"uptime": "运行时间",
|
||||
"memory": {
|
||||
"title": "可用内存",
|
||||
"maxFreeBlock": "max free block",
|
||||
"min": "min"
|
||||
},
|
||||
"board": "开发板",
|
||||
"chip": {
|
||||
"model": "芯片",
|
||||
"cores": "核心数",
|
||||
"freq": "频率"
|
||||
},
|
||||
"flash": {
|
||||
"size": "闪存容量",
|
||||
"realSize": "实际容量"
|
||||
},
|
||||
"lastResetReason": "上次重置原因"
|
||||
}
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"name": "仪表盘",
|
||||
"title": "仪表盘 - OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"control": "调节",
|
||||
"states": "状态",
|
||||
"sensors": "传感器",
|
||||
"diag": "OpenTherm 诊断"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "供暖",
|
||||
"dhw": "生活热水",
|
||||
"temp.current": "当前温度",
|
||||
"enable": "启用",
|
||||
"turbo": "Turbo 模式"
|
||||
},
|
||||
|
||||
"notify": {
|
||||
"fault": {
|
||||
"title": "锅炉报警状态已激活!",
|
||||
"note": "建议检查锅炉并查看说明书对应的报警代码:"
|
||||
},
|
||||
"diag": {
|
||||
"title": "锅炉诊断状态已激活!",
|
||||
"note": "锅炉可能需要检查,建议查看说明书对应的诊断代码:"
|
||||
},
|
||||
"reset": "点击复位"
|
||||
},
|
||||
|
||||
"states": {
|
||||
"mNetworkConnected": "网络连接状态",
|
||||
"mMqttConnected": "MQTT服务器连接状态",
|
||||
"mEmergencyState": "应急模式",
|
||||
"mExtPumpState": "外置循环泵",
|
||||
"mCascadeControlInput": "Cascade 控制 (input)",
|
||||
"mCascadeControlOutput": "Cascade 控制 (output)",
|
||||
|
||||
"sConnected": "OpenTherm 通讯状态",
|
||||
"sFlame": "火焰",
|
||||
"sCooling": "制冷",
|
||||
"sFaultActive": "报警状态",
|
||||
"sFaultCode": "报警代码",
|
||||
"sDiagActive": "诊断状态",
|
||||
"sDiagCode": "诊断代码",
|
||||
|
||||
"mHeatEnabled": "供暖功能已启用",
|
||||
"mHeatBlocking": "供暖",
|
||||
"mHeatOverheat": "供暖超热保护",
|
||||
"sHeatActive": "供暖激活状态",
|
||||
"mHeatSetpointTemp": "供暖供水设定温度",
|
||||
"mHeatTargetTemp": "供暖供水目标温度",
|
||||
"mHeatCurrTemp": "供暖当前供水温度",
|
||||
"mHeatRetTemp": "供暖回水温度",
|
||||
"mHeatIndoorTemp": "供暖,室内温度",
|
||||
"mHeatOutdoorTemp": "供暖,室外温度",
|
||||
|
||||
"mDhwEnabled": "生活热水功能已启用",
|
||||
"mDhwOverheat": "生活热水超热保护",
|
||||
"sDhwActive": "生活热水激活",
|
||||
"mDhwTargetTemp": "生活热水目标温度",
|
||||
"mDhwCurrTemp": "生活热水当前出水温度",
|
||||
"mDhwRetTemp": "生活热水回水温度"
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"values": {
|
||||
"temp": "温度",
|
||||
"humidity": "湿度",
|
||||
"battery": "电量",
|
||||
"rssi": "RSSI"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"title": "网络 - OpenTherm Gateway",
|
||||
"name": "网络设置",
|
||||
|
||||
"section": {
|
||||
"static": "静态设置",
|
||||
"availableNetworks": "可用网络",
|
||||
"staSettings": "WiFi 设置",
|
||||
"apSettings": "AP 设置"
|
||||
},
|
||||
|
||||
"scan": {
|
||||
"pos": "#",
|
||||
"info": "Info"
|
||||
},
|
||||
|
||||
"wifi": {
|
||||
"ssid": "SSID",
|
||||
"password": "密码",
|
||||
"channel": "频道",
|
||||
"signal": "信号强度",
|
||||
"connected": "已连接"
|
||||
},
|
||||
|
||||
"params": {
|
||||
"hostname": "Hostname",
|
||||
"dhcp": "自动 (DHCP)",
|
||||
"mac": "物理地址 (MAC)",
|
||||
"ip": "IP",
|
||||
"subnet": "子网掩码",
|
||||
"gateway": "网关",
|
||||
"dns": "DNS 服务器"
|
||||
},
|
||||
|
||||
"sta": {
|
||||
"channel.note": "自动选择设置为0"
|
||||
}
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"title": "传感器设置 - OpenTherm Gateway",
|
||||
"name": "传感器设置",
|
||||
|
||||
"enabled": "启用",
|
||||
"sensorName": {
|
||||
"title": "传感器名称",
|
||||
"note": "只能包含:a-z、A-Z、0-9、下划线和空格"
|
||||
},
|
||||
"purpose": "用途",
|
||||
"purposes": {
|
||||
"outdoorTemp": "室外温度",
|
||||
"indoorTemp": "室内温度",
|
||||
"heatTemp": "供暖,温度",
|
||||
"heatRetTemp": "供暖回水温度",
|
||||
"dhwTemp": "生活热水温度",
|
||||
"dhwRetTemp": "生活热水回水温度",
|
||||
"dhwFlowRate": "生活热水水流量",
|
||||
"exhaustTemp": "烟气温度",
|
||||
"modLevel": "Modulation level (%)",
|
||||
"number": "Number (raw)",
|
||||
"powerFactor": "功率 (%)",
|
||||
"power": "功率(kW)",
|
||||
"fanSpeed": "风机转速",
|
||||
"co2": "CO2",
|
||||
"pressure": "压力",
|
||||
"humidity": "湿度",
|
||||
"temperature": "温度",
|
||||
"notConfigured": "未配置"
|
||||
},
|
||||
"type": "类型/来源",
|
||||
"types": {
|
||||
"otOutdoorTemp": "OpenTherm, outdoor temp",
|
||||
"otHeatTemp": "OpenTherm, heating, temp",
|
||||
"otHeatRetTemp": "OpenTherm, heating, return temp",
|
||||
"otDhwTemp": "OpenTherm, DHW, temperature",
|
||||
"otDhwTemp2": "OpenTherm, DHW, temperature 2",
|
||||
"otDhwFlowRate": "OpenTherm, DHW, flow rate",
|
||||
"otCh2Temp": "OpenTherm, channel 2, temp",
|
||||
"otExhaustTemp": "OpenTherm, exhaust temp",
|
||||
"otHeatExchangerTemp": "OpenTherm, heat exchanger temp",
|
||||
"otPressure": "OpenTherm, pressure",
|
||||
"otModLevel": "OpenTherm, modulation level",
|
||||
"otCurrentPower": "OpenTherm, current power",
|
||||
"otExhaustCo2": "OpenTherm, exhaust CO2",
|
||||
"otExhaustFanSpeed": "OpenTherm, exhaust fan speed",
|
||||
"otSupplyFanSpeed": "OpenTherm, supply fan speed",
|
||||
"otSolarStorageTemp": "OpenTherm, solar storage temp",
|
||||
"otSolarCollectorTemp": "OpenTherm, solar collector temp",
|
||||
"otFanSpeedSetpoint": "OpenTherm, setpoint fan speed",
|
||||
"otFanSpeedCurrent": "OpenTherm, current fan speed",
|
||||
"otBurnerStarts": "OpenTherm, number of burner starts",
|
||||
"otDhwBurnerStarts": "OpenTherm, number of burner starts (DHW)",
|
||||
"otHeatingPumpStarts": "OpenTherm, number of pump starts (heating)",
|
||||
"otDhwPumpStarts": "OpenTherm, number of pump starts (DHW)",
|
||||
"otBurnerHours": "OpenTherm, number of burner operating hours",
|
||||
"otDhwBurnerHours": "OpenTherm, number of burner operating hours (DHW)",
|
||||
"otHeatingPumpHours": "OpenTherm, number of pump operating hours (heating)",
|
||||
"otDhwPumpHours": "OpenTherm, number of pump operating hours (DHW)",
|
||||
|
||||
"ntcTemp": "NTC 传感器",
|
||||
"dallasTemp": "DALLAS 传感器",
|
||||
"bluetooth": "BLE 传感器",
|
||||
"heatSetpointTemp": "Heating, setpoint temp",
|
||||
"manual": "通过 MQTT/API 手动配置",
|
||||
"notConfigured": "未配置"
|
||||
},
|
||||
"gpio": "GPIO",
|
||||
"address": {
|
||||
"title": "传感器地址",
|
||||
"note": "如需自动检测DALLAS传感器,请保持默认设置;如需连接BLE设备,则需提供MAC地址"
|
||||
},
|
||||
"correction": {
|
||||
"desc": "数值校正",
|
||||
"offset": "补偿值(偏移量)",
|
||||
"factor": "Multiplier"
|
||||
},
|
||||
"filtering": {
|
||||
"desc": "数值滤波",
|
||||
"enabled": {
|
||||
"title": "启用滤波",
|
||||
"note": "如果图表中有大量尖锐的噪声,此功能会很有用。使用的滤波器是 \"滑动平均滤波器\"."
|
||||
},
|
||||
"factor": {
|
||||
"title": "滤波系数",
|
||||
"note": "数值越低,数值变化越平滑且响应越滞后"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "设置 - OpenTherm Gateway",
|
||||
"name": "设置",
|
||||
|
||||
"section": {
|
||||
"portal": "Portal 设置",
|
||||
"system": "系统设置",
|
||||
"diag": "诊断",
|
||||
"heating": "供热设置",
|
||||
"dhw": "生活热水设置",
|
||||
"emergency": "应急模式设置",
|
||||
"equitherm": "气候补偿设置",
|
||||
"pid": "PID 参数设置",
|
||||
"ot": "OpenTherm协议设置",
|
||||
"mqtt": "MQTT 服务器设置",
|
||||
"extPump": "外置循环泵设置",
|
||||
"cascadeControl": "Cascade 级联控制设置"
|
||||
},
|
||||
|
||||
"enable": "启用",
|
||||
"note": {
|
||||
"restart": "更改这些设置后,必须重启设备以使变更生效",
|
||||
"blankNotUse": "空白 - 未使用",
|
||||
"bleDevice": "BLE设备仅支持搭载BLE功能的特定ESP32开发板使用!"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "最低温度",
|
||||
"max": "最高温度"
|
||||
},
|
||||
"maxModulation": "最大调制范围",
|
||||
"ohProtection": {
|
||||
"title": "超温保护",
|
||||
"desc": "<b>注意:</b> 当锅炉内置过热保护失效或工作异常导致系统超温时,此功能可提供额外保护。如需禁用,请将<b>最高温度</b>和<b>最低温度</b>均设为0。",
|
||||
"highTemp": {
|
||||
"title": "高温阈值",
|
||||
"note": "触发燃烧器强制关闭的阈值温度"
|
||||
},
|
||||
"lowTemp": {
|
||||
"title": "低温阈值",
|
||||
"note": "燃烧器重新启动的阈值温度"
|
||||
}
|
||||
},
|
||||
"freezeProtection": {
|
||||
"title": "防冻保护",
|
||||
"desc": "当热媒或室内温度在<b>等待时间</b> 内降至<b>低温阈值</b>以下时,系统将强制启动加热功能。",
|
||||
"lowTemp": "低温阈值",
|
||||
"thresholdTime": "等待时间<small>(秒)</small>"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "登录",
|
||||
"password": "密码",
|
||||
"auth": "需身份验证",
|
||||
"mdns": "使用 mDNS"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "单位",
|
||||
"metric": "公制 <small>(摄氏度、升、巴)</small>",
|
||||
"imperial": "英制 <small>(华氏度、加仑、psi)</small>",
|
||||
"statusLedGpio": "状态指示灯GPIO引脚",
|
||||
"logLevel": "日志级别",
|
||||
"serial": {
|
||||
"enable": "启用串口",
|
||||
"baud": "串口波特率"
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "启用 Telnet",
|
||||
"port": {
|
||||
"title": "Telnet 端口",
|
||||
"note": "默认值:23"
|
||||
}
|
||||
},
|
||||
"ntp": {
|
||||
"server": "NTP服务器",
|
||||
"timezone": "时区",
|
||||
"timezonePresets": "选择预设配置..."
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "滞后值<small>(单位:度)</small>",
|
||||
"turboFactor": "Turbo 模式系数"
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "紧急模式会在以下情况自动激活(当PID或气候补偿无法计算热媒设定值时):<br />启用气候补偿但室外温度传感器断开连接;<br />启用PID或 OpenTherm 选项中启用<i>原生供暖控制</i>但室内温度传感器断开连接。<br /><b>注意:</b> 网络故障或MQTT 服务器连接故障时,类型为<i>通过MQTT/API手动控制<i>的传感器将显示为断开连接状态。",
|
||||
|
||||
"target": {
|
||||
"title": "目标温度",
|
||||
"note": "<b>重要提示:</b> 若启用OpenTherm选项 <i>«原生供暖控制»</i>,此处设定值为<u>目标室内温度</u>;<br />其他所有情况下,此处设定值为 <u>目标热媒出水温度</u>."
|
||||
},
|
||||
"treshold": "阈值时间 <small>(秒)</small>"
|
||||
},
|
||||
|
||||
"equitherm": {
|
||||
"n": "N 系数",
|
||||
"k": "K 系数",
|
||||
"t": {
|
||||
"title": "T 系数",
|
||||
"note": "启用PID时此参数无效"
|
||||
}
|
||||
},
|
||||
|
||||
"pid": {
|
||||
"p": "P 系数",
|
||||
"i": "I 系数",
|
||||
"d": "D 系数",
|
||||
"dt": "DT <small>以秒为单位</small>",
|
||||
"limits": {
|
||||
"title": "Limits",
|
||||
"note": "<b>重要提示:</b> When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.<br />Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from <code>equitherm_result - 15</code> to <code>equitherm_result + 15</code>."
|
||||
},
|
||||
"deadband": {
|
||||
"title": "Deadband",
|
||||
"note": "Deadband is a range around the target temperature where PID regulation becomes less active. Within this range, the algorithm can reduce intensity or pause adjustments to avoid overreacting to small fluctuations.<br /><br />For instance, with a target temperature of 22°, a lower threshold of 1.0, and an upper threshold of 0.5, the deadband operates between 21° and 22.5°. If the I coefficient is 0.0005 and the I multiplier is 0.05, then within the deadband, the I coefficient becomes: <code>0.0005 * 0.05 = 0.000025</code>",
|
||||
"p_multiplier": "Multiplier for P factor",
|
||||
"i_multiplier": "Multiplier for I factor",
|
||||
"d_multiplier": "Multiplier for D factor",
|
||||
"thresholdHigh": "Threshold high",
|
||||
"thresholdLow": "Threshold low"
|
||||
}
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"advanced": "高级设置",
|
||||
"inGpio": "In GPIO",
|
||||
"outGpio": "Out GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberId": "主设备成员 ID",
|
||||
"flags": "主设备标志",
|
||||
"minPower": {
|
||||
"title": "最小锅炉功率 <small>(kW)</small>",
|
||||
"note": "该值对应锅炉0-1%的调制水平,通常在锅炉参数设置中的\"最小有效热输出\"。"
|
||||
},
|
||||
"maxPower": {
|
||||
"title": "最大锅炉功率 <small>(kW)</small>",
|
||||
"note": "<b>0</b> - 自动检测,通常在锅炉参数设置中的\"最大有效热输出\"。 "
|
||||
},
|
||||
|
||||
"options": {
|
||||
"title": "选项(附加设置)",
|
||||
"desc": "附加设置选项可调整锅炉的运行逻辑。由于协议未完整记录所有选项,同一选项在不同锅炉上可能产生不同效果。<br /><b>注意:</b>若系统运行正常,无需修改设置。",
|
||||
"dhwSupport": "生活热水支持",
|
||||
"coolingSupport": "制冷支持",
|
||||
"summerWinterMode": "夏季/冬季模式",
|
||||
"heatingStateToSummerWinterMode": "以供暖状态作为夏季/冬季模式",
|
||||
"ch2AlwaysEnabled": "CH2 始终启用",
|
||||
"heatingToCh2": "将供暖同步至 CH2",
|
||||
"dhwToCh2": "将生活热水同步至 CH2",
|
||||
"dhwBlocking": "生活热水锁定",
|
||||
"dhwStateAsDhwBlocking": "以生活热水状态作为生活热水锁定依据",
|
||||
"maxTempSyncWithTargetTemp": "将最高供暖温度与目标温度同步",
|
||||
"getMinMaxTemp": "从锅炉获取最小/最大温度参数",
|
||||
"ignoreDiagState": "忽略诊断状态",
|
||||
"autoFaultReset": "自动报警复位 <small>(不推荐!)</small>",
|
||||
"autoDiagReset": "自动诊断复位 <small>(不推荐!)</small>",
|
||||
"setDateAndTime": "同步设置锅炉日期与时间",
|
||||
"immergasFix": "针对Immergas锅炉的兼容性修复"
|
||||
},
|
||||
|
||||
"nativeHeating": {
|
||||
"title": "原生锅炉供暖控制",
|
||||
"note": "<u>注意:</u> 仅适用于锅炉需接收目标室温并自主调节载热介质温度的场景,与固件中的PID及Equithermq气候补偿功能不兼容。"
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant 自动发现",
|
||||
"server": "服务器地址",
|
||||
"port": "端口",
|
||||
"user": "用户名",
|
||||
"password": "密码",
|
||||
"prefix": "Prefix 前缀",
|
||||
"interval": "发布间隔 <small>(秒)</small>"
|
||||
},
|
||||
|
||||
"extPump": {
|
||||
"use": "使用外置循环泵",
|
||||
"gpio": "继电器 GPIO引脚",
|
||||
"postCirculationTime": "后循环时间 <small>(分钟)</small>",
|
||||
"antiStuckInterval": "防卡死间隔时间<small>(天)</small>",
|
||||
"antiStuckTime": "防卡死运行时长<small>(分钟)</small>"
|
||||
},
|
||||
|
||||
"cascadeControl": {
|
||||
"input": {
|
||||
"desc": "仅当另一台锅炉发生故障时启用本锅炉加热。另一台锅炉的控制器需在故障发生时切换GPIO输入状态以触发本功能。",
|
||||
"enable": "启用 input",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "切换 GPIO 状态",
|
||||
"thresholdTime": "状态变化阈值时间<small>(秒)</small>"
|
||||
},
|
||||
"output": {
|
||||
"desc": "可通过<u>继电器</u>控制另一台锅炉的启停。",
|
||||
"enable": "启用 output",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "切换 GPIO 状态",
|
||||
"thresholdTime": "状态变化阈值时间 <small>(秒)</small>",
|
||||
"events": {
|
||||
"desc": "事件",
|
||||
"onFault": "当故障状态激活时",
|
||||
"onLossConnection": "当OpenTherm连接中断时",
|
||||
"onEnabledHeating": "当供暖功能启用时"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"title": "固件升级 - OpenTherm Gateway",
|
||||
"name": "固件升级",
|
||||
|
||||
"section": {
|
||||
"backupAndRestore": "备份与恢复",
|
||||
"backupAndRestore.desc": "本功能支持备份和恢复全部设置",
|
||||
"upgrade": "升级",
|
||||
"upgrade.desc": "本模块支持升级设备的固件与系统文件。<br />可从以下地址下载最新版本 <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Releases page</a> 。"
|
||||
},
|
||||
|
||||
"note": {
|
||||
"disclaimer1": "升级系统文件成功后,所有设置将恢复为默认值!升级前请务必备份配置。",
|
||||
"disclaimer2": "升级成功后,设备将在15秒后自动重启。"
|
||||
},
|
||||
|
||||
"settingsFile": "设置文件",
|
||||
"fw": "Firmware",
|
||||
"fs": "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
507
src_data/locales/en.json
Normal file
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "License",
|
||||
"source": "Source code",
|
||||
"help": "Help",
|
||||
"issues": "Issues & questions",
|
||||
"releases": "Releases"
|
||||
},
|
||||
"dbm": "dBm",
|
||||
"kw": "kW",
|
||||
"time": {
|
||||
"days": "d.",
|
||||
"hours": "h.",
|
||||
"min": "min.",
|
||||
"sec": "sec."
|
||||
},
|
||||
|
||||
"button": {
|
||||
"upgrade": "Upgrade",
|
||||
"restart": "Restart",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"refresh": "Refresh",
|
||||
"restore": "Restore",
|
||||
"restored": "Restored",
|
||||
"backup": "Backup",
|
||||
"wait": "Please wait...",
|
||||
"uploading": "Uploading...",
|
||||
"success": "Success",
|
||||
"error": "Error"
|
||||
},
|
||||
|
||||
"index": {
|
||||
"title": "OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"network": "Network",
|
||||
"system": "System"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"build": {
|
||||
"title": "Build",
|
||||
"version": "Version",
|
||||
"date": "Date",
|
||||
"core": "Core",
|
||||
"sdk": "SDK"
|
||||
},
|
||||
"uptime": "Uptime",
|
||||
"memory": {
|
||||
"title": "Free memory",
|
||||
"maxFreeBlock": "max free block",
|
||||
"min": "min"
|
||||
},
|
||||
"board": "Board",
|
||||
"chip": {
|
||||
"model": "Chip",
|
||||
"cores": "Cores",
|
||||
"freq": "frequency"
|
||||
},
|
||||
"flash": {
|
||||
"size": "Flash size",
|
||||
"realSize": "real size"
|
||||
},
|
||||
"lastResetReason": "Last reset reason"
|
||||
}
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"name": "Dashboard",
|
||||
"title": "Dashboard - OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"control": "Control",
|
||||
"states": "States",
|
||||
"sensors": "Sensors",
|
||||
"diag": "OpenTherm diagnostic"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "Heating",
|
||||
"dhw": "DHW",
|
||||
"temp.current": "Current",
|
||||
"enable": "Enable",
|
||||
"turbo": "Turbo mode"
|
||||
},
|
||||
|
||||
"notify": {
|
||||
"fault": {
|
||||
"title": "Boiler Fault state is active!",
|
||||
"note": "It is recommended to inspect the boiler and study the documentation to interpret the fault code:"
|
||||
},
|
||||
"diag": {
|
||||
"title": "Boiler Diagnostic state is active!",
|
||||
"note": "Perhaps your boiler needs inspection? It is recommended study the documentation to interpret the diag code:"
|
||||
},
|
||||
"reset": "Try reset"
|
||||
},
|
||||
|
||||
"states": {
|
||||
"mNetworkConnected": "Network connection",
|
||||
"mMqttConnected": "MQTT connection",
|
||||
"mEmergencyState": "Emergency mode",
|
||||
"mExtPumpState": "External pump",
|
||||
"mCascadeControlInput": "Cascade control (input)",
|
||||
"mCascadeControlOutput": "Cascade control (output)",
|
||||
|
||||
"sConnected": "OpenTherm connection",
|
||||
"sFlame": "Flame",
|
||||
"sCooling": "Cooling",
|
||||
"sFaultActive": "Fault",
|
||||
"sFaultCode": "Fault code",
|
||||
"sDiagActive": "Diagnostic",
|
||||
"sDiagCode": "Diagnostic code",
|
||||
|
||||
"mHeatEnabled": "Heating enabled",
|
||||
"mHeatBlocking": "Heating blocked",
|
||||
"mHeatOverheat": "Heating overheat",
|
||||
"sHeatActive": "Heating active",
|
||||
"mHeatSetpointTemp": "Heating setpoint temp",
|
||||
"mHeatTargetTemp": "Heating target temp",
|
||||
"mHeatCurrTemp": "Heating current temp",
|
||||
"mHeatRetTemp": "Heating return temp",
|
||||
"mHeatIndoorTemp": "Heating, indoor temp",
|
||||
"mHeatOutdoorTemp": "Heating, outdoor temp",
|
||||
|
||||
"mDhwEnabled": "DHW enabled",
|
||||
"mDhwOverheat": "DHW overheat",
|
||||
"sDhwActive": "DHW active",
|
||||
"mDhwTargetTemp": "DHW target temp",
|
||||
"mDhwCurrTemp": "DHW current temp",
|
||||
"mDhwRetTemp": "DHW return temp"
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"values": {
|
||||
"temp": "Temperature",
|
||||
"humidity": "Humidity",
|
||||
"battery": "Battery",
|
||||
"rssi": "RSSI"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"title": "Network - OpenTherm Gateway",
|
||||
"name": "Network settings",
|
||||
|
||||
"section": {
|
||||
"static": "Static settings",
|
||||
"availableNetworks": "Available networks",
|
||||
"staSettings": "WiFi settings",
|
||||
"apSettings": "AP settings"
|
||||
},
|
||||
|
||||
"scan": {
|
||||
"pos": "#",
|
||||
"info": "Info"
|
||||
},
|
||||
|
||||
"wifi": {
|
||||
"ssid": "SSID",
|
||||
"password": "Password",
|
||||
"channel": "Channel",
|
||||
"signal": "Signal",
|
||||
"connected": "Connected"
|
||||
},
|
||||
|
||||
"params": {
|
||||
"hostname": "Hostname",
|
||||
"dhcp": "Use DHCP",
|
||||
"mac": "MAC",
|
||||
"ip": "IP",
|
||||
"subnet": "Subnet",
|
||||
"gateway": "Gateway",
|
||||
"dns": "DNS"
|
||||
},
|
||||
|
||||
"sta": {
|
||||
"channel.note": "set 0 for auto select"
|
||||
}
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"title": "Sensors settings - OpenTherm Gateway",
|
||||
"name": "Sensors settings",
|
||||
|
||||
"enabled": "Enabled",
|
||||
"sensorName": {
|
||||
"title": "Sensor name",
|
||||
"note": "May only contain: a-z, A-Z, 0-9, _ and space"
|
||||
},
|
||||
"purpose": "Purpose",
|
||||
"purposes": {
|
||||
"outdoorTemp": "Outdoor temperature",
|
||||
"indoorTemp": "Indoor temperature",
|
||||
"heatTemp": "Heating, temperature",
|
||||
"heatRetTemp": "Heating, return temperature",
|
||||
"dhwTemp": "DHW, temperature",
|
||||
"dhwRetTemp": "DHW, return temperature",
|
||||
"dhwFlowRate": "DHW, flow rate",
|
||||
"exhaustTemp": "Exhaust temperature",
|
||||
"modLevel": "Modulation level (in percents)",
|
||||
"number": "Number (raw)",
|
||||
"powerFactor": "Power (in percent)",
|
||||
"power": "Power (in kWt)",
|
||||
"fanSpeed": "Fan speed",
|
||||
"co2": "CO2",
|
||||
"pressure": "Pressure",
|
||||
"humidity": "Humidity",
|
||||
"temperature": "Temperature",
|
||||
"notConfigured": "Not configured"
|
||||
},
|
||||
"type": "Type/source",
|
||||
"types": {
|
||||
"otOutdoorTemp": "OpenTherm, outdoor temp",
|
||||
"otHeatTemp": "OpenTherm, heating, temp",
|
||||
"otHeatRetTemp": "OpenTherm, heating, return temp",
|
||||
"otDhwTemp": "OpenTherm, DHW, temperature",
|
||||
"otDhwTemp2": "OpenTherm, DHW, temperature 2",
|
||||
"otDhwFlowRate": "OpenTherm, DHW, flow rate",
|
||||
"otCh2Temp": "OpenTherm, channel 2, temp",
|
||||
"otExhaustTemp": "OpenTherm, exhaust temp",
|
||||
"otHeatExchangerTemp": "OpenTherm, heat exchanger temp",
|
||||
"otPressure": "OpenTherm, pressure",
|
||||
"otModLevel": "OpenTherm, modulation level",
|
||||
"otCurrentPower": "OpenTherm, current power",
|
||||
"otExhaustCo2": "OpenTherm, exhaust CO2",
|
||||
"otExhaustFanSpeed": "OpenTherm, exhaust fan speed",
|
||||
"otSupplyFanSpeed": "OpenTherm, supply fan speed",
|
||||
"otSolarStorageTemp": "OpenTherm, solar storage temp",
|
||||
"otSolarCollectorTemp": "OpenTherm, solar collector temp",
|
||||
"otFanSpeedSetpoint": "OpenTherm, setpoint fan speed",
|
||||
"otFanSpeedCurrent": "OpenTherm, current fan speed",
|
||||
"otBurnerStarts": "OpenTherm, number of burner starts",
|
||||
"otDhwBurnerStarts": "OpenTherm, number of burner starts (DHW)",
|
||||
"otHeatingPumpStarts": "OpenTherm, number of pump starts (heating)",
|
||||
"otDhwPumpStarts": "OpenTherm, number of pump starts (DHW)",
|
||||
"otBurnerHours": "OpenTherm, number of burner operating hours",
|
||||
"otDhwBurnerHours": "OpenTherm, number of burner operating hours (DHW)",
|
||||
"otHeatingPumpHours": "OpenTherm, number of pump operating hours (heating)",
|
||||
"otDhwPumpHours": "OpenTherm, number of pump operating hours (DHW)",
|
||||
|
||||
"ntcTemp": "NTC sensor",
|
||||
"dallasTemp": "DALLAS sensor",
|
||||
"bluetooth": "BLE sensor",
|
||||
"heatSetpointTemp": "Heating, setpoint temp",
|
||||
"manual": "Manual via MQTT/API",
|
||||
"notConfigured": "Not configured"
|
||||
},
|
||||
"gpio": "GPIO",
|
||||
"address": {
|
||||
"title": "Sensor address",
|
||||
"note": "For auto detection of DALLAS sensors leave it at default, for BLE devices need a MAC address"
|
||||
},
|
||||
"correction": {
|
||||
"desc": "Correction of values",
|
||||
"offset": "Compensation (offset)",
|
||||
"factor": "Multiplier"
|
||||
},
|
||||
"filtering": {
|
||||
"desc": "Filtering values",
|
||||
"enabled": {
|
||||
"title": "Enabled filtering",
|
||||
"note": "It can be useful if there is a lot of sharp noise on the charts. The filter used is \"Running Average\"."
|
||||
},
|
||||
"factor": {
|
||||
"title": "Filtration factor",
|
||||
"note": "The lower the value, the smoother and <u>longer</u> the change in numeric values."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Settings - OpenTherm Gateway",
|
||||
"name": "Settings",
|
||||
|
||||
"section": {
|
||||
"portal": "Portal settings",
|
||||
"system": "System settings",
|
||||
"diag": "Diagnostic",
|
||||
"heating": "Heating settings",
|
||||
"dhw": "DHW settings",
|
||||
"emergency": "Emergency mode settings",
|
||||
"equitherm": "Equitherm settings",
|
||||
"pid": "PID settings",
|
||||
"ot": "OpenTherm settings",
|
||||
"mqtt": "MQTT settings",
|
||||
"extPump": "External pump settings",
|
||||
"cascadeControl": "Cascade control settings"
|
||||
},
|
||||
|
||||
"enable": "Enable",
|
||||
"note": {
|
||||
"restart": "After changing these settings, the device must be restarted for the changes to take effect.",
|
||||
"blankNotUse": "blank - not use",
|
||||
"bleDevice": "BLE device can be used <u>only</u> with some ESP32 boards with BLE support!"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "Minimum temperature",
|
||||
"max": "Maximum temperature"
|
||||
},
|
||||
"maxModulation": "Max modulation level",
|
||||
"ohProtection": {
|
||||
"title": "Overheating protection",
|
||||
"desc": "<b>Note:</b> This feature can be useful if the built-in boiler overheating protection does not work or does not work correctly and the heat carrier boils. To disable, set 0 as <b>high</b> and <b>low</b> temperature.",
|
||||
"highTemp": {
|
||||
"title": "High temperature threshold",
|
||||
"note": "Threshold at which the burner will be forcibly switched off"
|
||||
},
|
||||
"lowTemp": {
|
||||
"title": "Low temperature threshold",
|
||||
"note": "Threshold at which the burner can be turned on again"
|
||||
}
|
||||
},
|
||||
"freezeProtection": {
|
||||
"title": "Freeze protection",
|
||||
"desc": "Heating will be forced to turn on if the heat carrier or indoor temperature drops below <b>Low temperature</b> during <b>Waiting time</b>.",
|
||||
"lowTemp": "Low temperature threshold",
|
||||
"thresholdTime": "Waiting time <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "Login",
|
||||
"password": "Password",
|
||||
"auth": "Require authentication",
|
||||
"mdns": "Use mDNS"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "Unit system",
|
||||
"metric": "Metric <small>(celsius, liters, bar)</small>",
|
||||
"imperial": "Imperial <small>(fahrenheit, gallons, psi)</small>",
|
||||
"statusLedGpio": "Status LED GPIO",
|
||||
"logLevel": "Log level",
|
||||
"serial": {
|
||||
"enable": "Enabled Serial port",
|
||||
"baud": "Serial port baud rate"
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "Enabled Telnet",
|
||||
"port": {
|
||||
"title": "Telnet port",
|
||||
"note": "Default: 23"
|
||||
}
|
||||
},
|
||||
"ntp": {
|
||||
"server": "NTP server",
|
||||
"timezone": "Timezone",
|
||||
"timezonePresets": "Select preset..."
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "Hysteresis <small>(in degrees)</small>",
|
||||
"turboFactor": "Turbo mode coeff."
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "Emergency mode is activated automatically when «PID» or «Equitherm» cannot calculate the heat carrier setpoint:<br />- if «Equitherm» is enabled and the outdoor temperature sensor is disconnected;<br />- if «PID» or OT option <i>«Native heating control»</i> is enabled and the indoor temperature sensor is disconnected.<br /><b>Note:</b> On network fault or MQTT fault, sensors with <i>«Manual via MQTT/API»</i> type will be in DISCONNECTED state.",
|
||||
|
||||
"target": {
|
||||
"title": "Target temperature",
|
||||
"note": "<b>Important:</b> <u>Target indoor temperature</u> if OT option <i>«Native heating control»</i> is enabled.<br />In all other cases, the <u>target heat carrier temperature</u>."
|
||||
},
|
||||
"treshold": "Treshold time <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"equitherm": {
|
||||
"n": "N factor",
|
||||
"k": "K factor",
|
||||
"t": {
|
||||
"title": "T factor",
|
||||
"note": "Not used if PID is enabled"
|
||||
}
|
||||
},
|
||||
|
||||
"pid": {
|
||||
"p": "P factor",
|
||||
"i": "I factor",
|
||||
"d": "D factor",
|
||||
"dt": "DT <small>in seconds</small>",
|
||||
"limits": {
|
||||
"title": "Limits",
|
||||
"note": "<b>Important:</b> When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.<br />Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from <code>equitherm_result - 15</code> to <code>equitherm_result + 15</code>."
|
||||
},
|
||||
"deadband": {
|
||||
"title": "Deadband",
|
||||
"note": "Deadband is a range around the target temperature where PID regulation becomes less active. Within this range, the algorithm can reduce intensity or pause adjustments to avoid overreacting to small fluctuations.<br /><br />For instance, with a target temperature of 22°, a lower threshold of 1.0, and an upper threshold of 0.5, the deadband operates between 21° and 22.5°. If the I coefficient is 0.0005 and the I multiplier is 0.05, then within the deadband, the I coefficient becomes: <code>0.0005 * 0.05 = 0.000025</code>",
|
||||
"p_multiplier": "Multiplier for P factor",
|
||||
"i_multiplier": "Multiplier for I factor",
|
||||
"d_multiplier": "Multiplier for D factor",
|
||||
"thresholdHigh": "Threshold high",
|
||||
"thresholdLow": "Threshold low"
|
||||
}
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"advanced": "Advanced Settings",
|
||||
"inGpio": "In GPIO",
|
||||
"outGpio": "Out GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberId": "Master member ID",
|
||||
"flags": "Master flags",
|
||||
"minPower": {
|
||||
"title": "Min boiler power <small>(kW)</small>",
|
||||
"note": "This value is at 0-1% boiler modulation level. Typically found in the boiler specification as \"minimum useful heat output\"."
|
||||
},
|
||||
"maxPower": {
|
||||
"title": "Max boiler power <small>(kW)</small>",
|
||||
"note": "<b>0</b> - try detect automatically. Typically found in the boiler specification as \"maximum useful heat output\"."
|
||||
},
|
||||
|
||||
"options": {
|
||||
"title": "Options (additional settings)",
|
||||
"desc": "Options can change the logic of the boiler. Not all options are documented in the protocol, so the same option can have different effects on different boilers.<br /><b>Note:</b> There is no need to change anything if everything works well.",
|
||||
"dhwSupport": "DHW support",
|
||||
"coolingSupport": "Cooling support",
|
||||
"summerWinterMode": "Summer/winter mode",
|
||||
"heatingStateToSummerWinterMode": "Heating state as summer/winter mode",
|
||||
"ch2AlwaysEnabled": "CH2 always enabled",
|
||||
"heatingToCh2": "Duplicate heating to CH2",
|
||||
"dhwToCh2": "Duplicate DHW to CH2",
|
||||
"dhwBlocking": "DHW blocking",
|
||||
"dhwStateAsDhwBlocking": "DHW state as DHW blocking",
|
||||
"maxTempSyncWithTargetTemp": "Sync max heating temp with target temp",
|
||||
"getMinMaxTemp": "Get min/max temp from boiler",
|
||||
"ignoreDiagState": "Ignore diag state",
|
||||
"autoFaultReset": "Auto fault reset <small>(not recommended!)</small>",
|
||||
"autoDiagReset": "Auto diag reset <small>(not recommended!)</small>",
|
||||
"setDateAndTime": "Set date & time on boiler",
|
||||
"immergasFix": "Fix for Immergas boilers"
|
||||
},
|
||||
|
||||
"nativeHeating": {
|
||||
"title": "Native heating control (boiler)",
|
||||
"note": "Works <u>ONLY</u> if the boiler requires the desired room temperature and regulates the temperature of the coolant itself. Not compatible with PID and Equitherm regulators in firmware."
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant Discovery",
|
||||
"server": "Server",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"password": "Password",
|
||||
"prefix": "Prefix",
|
||||
"interval": "Publish interval <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"extPump": {
|
||||
"use": "Use external pump",
|
||||
"gpio": "Relay GPIO",
|
||||
"postCirculationTime": "Post circulation time <small>(min)</small>",
|
||||
"antiStuckInterval": "Anti stuck interval <small>(days)</small>",
|
||||
"antiStuckTime": "Anti stuck time <small>(min)</small>"
|
||||
},
|
||||
|
||||
"cascadeControl": {
|
||||
"input": {
|
||||
"desc": "Can be used to turn on the heating only if another boiler is faulty. The other boiler controller must change the state of the GPIO input in the event of a fault.",
|
||||
"enable": "Enabled input",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Invert GPIO state",
|
||||
"thresholdTime": "State change threshold time <small>(sec)</small>"
|
||||
},
|
||||
"output": {
|
||||
"desc": "Can be used to switch on another boiler <u>via relay</u>.",
|
||||
"enable": "Enabled output",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Invert GPIO state",
|
||||
"thresholdTime": "State change threshold time <small>(sec)</small>",
|
||||
"events": {
|
||||
"desc": "Events",
|
||||
"onFault": "If the fault state is active",
|
||||
"onLossConnection": "If the connection via Opentherm is lost",
|
||||
"onEnabledHeating": "If heating is enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"title": "Upgrade - OpenTherm Gateway",
|
||||
"name": "Upgrade",
|
||||
|
||||
"section": {
|
||||
"backupAndRestore": "Backup & restore",
|
||||
"backupAndRestore.desc": "In this section you can save and restore a backup of ALL settings.",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade.desc": "In this section you can upgrade the firmware and filesystem of your device.<br />Latest releases can be downloaded from the <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Releases page</a> of the project repository."
|
||||
},
|
||||
|
||||
"note": {
|
||||
"disclaimer1": "After a successful upgrade the filesystem, ALL settings will be reset to default values! Save backup before upgrading.",
|
||||
"disclaimer2": "After a successful upgrade, the device will automatically reboot after 15 seconds."
|
||||
},
|
||||
|
||||
"settingsFile": "Settings file",
|
||||
"fw": "Firmware",
|
||||
"fs": "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
507
src_data/locales/it.json
Normal file
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "Licenza",
|
||||
"source": "Codice",
|
||||
"help": "Aiuto",
|
||||
"issues": "Problemi e domande",
|
||||
"releases": "Versione"
|
||||
},
|
||||
"dbm": "dBm",
|
||||
"kw": "kW",
|
||||
"time": {
|
||||
"days": "d.",
|
||||
"hours": "h.",
|
||||
"min": "min.",
|
||||
"sec": "sec."
|
||||
},
|
||||
|
||||
"button": {
|
||||
"upgrade": "Aggiorna",
|
||||
"restart": "Riavvia",
|
||||
"save": "Salva",
|
||||
"saved": "Salvato",
|
||||
"refresh": "Ricarica",
|
||||
"restore": "Recupera",
|
||||
"restored": "Recuperato",
|
||||
"backup": "Backup",
|
||||
"wait": "Attendi...",
|
||||
"uploading": "caricamento...",
|
||||
"success": "Riuscito",
|
||||
"error": "Errore"
|
||||
},
|
||||
|
||||
"index": {
|
||||
"title": "OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"network": "Rete",
|
||||
"system": "Sistema"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"build": {
|
||||
"title": "Build",
|
||||
"version": "Versione",
|
||||
"date": "Data",
|
||||
"core": "Core",
|
||||
"sdk": "SDK"
|
||||
},
|
||||
"uptime": "Tempo di attività",
|
||||
"memory": {
|
||||
"title": "Memoria libera",
|
||||
"maxFreeBlock": "max free block",
|
||||
"min": "min"
|
||||
},
|
||||
"board": "Scheda",
|
||||
"chip": {
|
||||
"model": "Chip",
|
||||
"cores": "Cores",
|
||||
"freq": "frequenza"
|
||||
},
|
||||
"flash": {
|
||||
"size": "Dimensioni del flash",
|
||||
"realSize": "dimensione reale"
|
||||
},
|
||||
"lastResetReason": "Motivo ultimo Reset"
|
||||
}
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"name": "Pannello",
|
||||
"title": "Pannello - OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"control": "Controlli",
|
||||
"states": "Stato",
|
||||
"sensors": "Sensori",
|
||||
"diag": "Diagnostica OpenTherm"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "Riscaldamento",
|
||||
"dhw": "ACS",
|
||||
"temp.current": "Attuale",
|
||||
"enable": "Attiva",
|
||||
"turbo": "Turbo"
|
||||
},
|
||||
|
||||
"notify": {
|
||||
"fault": {
|
||||
"title": "Rilevamento guasti caldiaia attivo!",
|
||||
"note": "Si consiglia di ispezionare la caldaia e studiare la documentazione per interpretare il codice di errore:"
|
||||
},
|
||||
"diag": {
|
||||
"title": "Stato diagnostica Caldaia attivo!",
|
||||
"note": "Forse la tua caldaia ha bisogno di un'ispezione? Si consiglia di studiare la documentazione per interpretare il codice diagnostico:"
|
||||
},
|
||||
"reset": "Prova a resettare"
|
||||
},
|
||||
|
||||
"states": {
|
||||
"mNetworkConnected": "Connessione Rete",
|
||||
"mMqttConnected": "Connessione MQTT",
|
||||
"mEmergencyState": "Modo Emergenza",
|
||||
"mExtPumpState": "Pompa esterna",
|
||||
"mCascadeControlInput": "Controllo a cascata (input)",
|
||||
"mCascadeControlOutput": "Controllo a cascata (output)",
|
||||
|
||||
"sConnected": "Connessione OpenTherm",
|
||||
"sFlame": "Fiamma",
|
||||
"sCooling": "Raffrescamento",
|
||||
"sFaultActive": "Anomalia",
|
||||
"sFaultCode": "Codice anomalia",
|
||||
"sDiagActive": "Diagnostica",
|
||||
"sDiagCode": "Codice Diagnostica",
|
||||
|
||||
"mHeatEnabled": "Riscaldamento attivato",
|
||||
"mHeatBlocking": "Riscaldamento bloccato",
|
||||
"mHeatOverheat": "Riscaldamento surriscaldamento",
|
||||
"sHeatActive": "Riscaldamento attivo",
|
||||
"mHeatSetpointTemp": "Temp riscaldamento impostato",
|
||||
"mHeatTargetTemp": "Target Temp caldaia",
|
||||
"mHeatCurrTemp": "Temp attuale riscaldamento",
|
||||
"mHeatRetTemp": "Temp ritorno riscaldamento",
|
||||
"mHeatIndoorTemp": "Riscaldamento, temp interna",
|
||||
"mHeatOutdoorTemp": "Riscaldamento, temp esterna",
|
||||
|
||||
"mDhwEnabled": "ACS attivata",
|
||||
"mDhwOverheat": "ACS surriscaldamento",
|
||||
"sDhwActive": "ACS attiva",
|
||||
"mDhwTargetTemp": "ACS temp impostata",
|
||||
"mDhwCurrTemp": "ACS temp attuale",
|
||||
"mDhwRetTemp": "ACS temp ricircolo"
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"values": {
|
||||
"temp": "Temperatura",
|
||||
"humidity": "Umidità",
|
||||
"battery": "Batteria",
|
||||
"rssi": "RSSI"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"title": "Rete - OpenTherm Gateway",
|
||||
"name": "Impostazioni rete",
|
||||
|
||||
"section": {
|
||||
"static": "Impostazioni statico",
|
||||
"availableNetworks": "Reti disponibili",
|
||||
"staSettings": "Impostazioni WiFi",
|
||||
"apSettings": "Impostazioni AP"
|
||||
},
|
||||
|
||||
"scan": {
|
||||
"pos": "#",
|
||||
"info": "Info"
|
||||
},
|
||||
|
||||
"wifi": {
|
||||
"ssid": "SSID",
|
||||
"password": "Password",
|
||||
"channel": "Canale",
|
||||
"signal": "Segnale",
|
||||
"connected": "Connesso"
|
||||
},
|
||||
|
||||
"params": {
|
||||
"hostname": "Hostname",
|
||||
"dhcp": "Usa DHCP",
|
||||
"mac": "MAC",
|
||||
"ip": "IP",
|
||||
"subnet": "Subnet",
|
||||
"gateway": "Gateway",
|
||||
"dns": "DNS"
|
||||
},
|
||||
|
||||
"sta": {
|
||||
"channel.note": "Metti 0 per auto selezione"
|
||||
}
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"title": "Impostazione sensori - OpenTherm Gateway",
|
||||
"name": "Impostazione sensori",
|
||||
|
||||
"enabled": "Attivato",
|
||||
"sensorName": {
|
||||
"title": "Nome sensore",
|
||||
"note": "Può contenere solo: a-z, A-Z, 0-9, _ e spazi"
|
||||
},
|
||||
"purpose": "Funzione",
|
||||
"purposes": {
|
||||
"outdoorTemp": "Temperatura esterna",
|
||||
"indoorTemp": "Temperatura interna",
|
||||
"heatTemp": "Riscaldamento, temperatura mandata",
|
||||
"heatRetTemp": "Riscaldamento, temperatura ritorno",
|
||||
"dhwTemp": "ACS, temperatura",
|
||||
"dhwRetTemp": "ACS, temperatura ritorno",
|
||||
"dhwFlowRate": "ACS, prelievo",
|
||||
"exhaustTemp": "Temperatura fumi",
|
||||
"modLevel": "Livello Modulazione (%)",
|
||||
"number": "Numero (raw)",
|
||||
"powerFactor": "Potenza (%)",
|
||||
"power": "Potenza (in kW)",
|
||||
"fanSpeed": "Velocità ventilatore",
|
||||
"co2": "CO2",
|
||||
"pressure": "Pressione",
|
||||
"humidity": "Umidità",
|
||||
"temperature": "Temperatura",
|
||||
"notConfigured": "Non configurato"
|
||||
},
|
||||
"type": "Tipo/sorgente",
|
||||
"types": {
|
||||
"otOutdoorTemp": "OpenTherm, temp esterna",
|
||||
"otHeatTemp": "OpenTherm, riscaldamento, temp",
|
||||
"otHeatRetTemp": "OpenTherm, riscaldamento, temp ritorno",
|
||||
"otDhwTemp": "OpenTherm, ACS, temperatura",
|
||||
"otDhwTemp2": "OpenTherm, ACS, temperatura 2",
|
||||
"otDhwFlowRate": "OpenTherm, ACS, prelievo",
|
||||
"otCh2Temp": "OpenTherm, canale 2, temp",
|
||||
"otExhaustTemp": "OpenTherm, temp fumi",
|
||||
"otHeatExchangerTemp": "OpenTherm, temp scambiatore",
|
||||
"otPressure": "OpenTherm, pressione",
|
||||
"otModLevel": "OpenTherm, livello modulazione",
|
||||
"otCurrentPower": "OpenTherm, potenza attuale",
|
||||
"otExhaustCo2": "OpenTherm, CO2 fumi",
|
||||
"otExhaustFanSpeed": "OpenTherm, velocità ventola fumi",
|
||||
"otSupplyFanSpeed": "OpenTherm, velocità ventola supporto",
|
||||
"otSolarStorageTemp": "OpenTherm, temp accumulo solare",
|
||||
"otSolarCollectorTemp": "OpenTherm, temp collettore solare",
|
||||
"otFanSpeedSetpoint": "OpenTherm, velocità ventola impostata",
|
||||
"otFanSpeedCurrent": "OpenTherm, velocità ventola attuale",
|
||||
"otBurnerStarts": "OpenTherm, numero di avviamenti del bruciatore",
|
||||
"otDhwBurnerStarts": "OpenTherm, numero di avviamenti del bruciatore (ACS)",
|
||||
"otHeatingPumpStarts": "OpenTherm, numero di avviamenti della pompa (riscaldamento)",
|
||||
"otDhwPumpStarts": "OpenTherm, numero di avviamenti della pompa (ACS)",
|
||||
"otBurnerHours": "OpenTherm, numero di ore di funzionamento del bruciatore",
|
||||
"otDhwBurnerHours": "OpenTherm, numero di ore di funzionamento del bruciatore (ACS)",
|
||||
"otHeatingPumpHours": "OpenTherm, numero di ore di funzionamento della pompa (riscaldamento)",
|
||||
"otDhwPumpHours": "OpenTherm, numero di ore di funzionamento della pompa (ACS)",
|
||||
|
||||
"ntcTemp": "Sensore NTC",
|
||||
"dallasTemp": "Sensore DALLAS",
|
||||
"bluetooth": "Sensore BLE",
|
||||
"heatSetpointTemp": "Riscaldamento, temp impostata",
|
||||
"manual": "Manuale via MQTT/API",
|
||||
"notConfigured": "Non configurato"
|
||||
},
|
||||
"gpio": "GPIO",
|
||||
"address": {
|
||||
"title": "Indirizzo sensore",
|
||||
"note": "Per l'autoriconoscimento del sensore DALLAS lasciare quello di default, per sensore BLE richiede indirizzo MAC"
|
||||
},
|
||||
"correction": {
|
||||
"desc": "Correzione del valore",
|
||||
"offset": "Compensazione (offset)",
|
||||
"factor": "Moltiplicatore"
|
||||
},
|
||||
"filtering": {
|
||||
"desc": "Filtraggio valore",
|
||||
"enabled": {
|
||||
"title": "Filtraggio attivo",
|
||||
"note": "Può servire in caso vi siano molti sbalzi nel grafico. Il filtro usato è \"Running Average\"."
|
||||
},
|
||||
"factor": {
|
||||
"title": "Fattore di filtrazione",
|
||||
"note": "Quanto più basso è il valore, tanto più graduale e prolungata sarà la variazione dei valori numerici."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Impostazioni - OpenTherm Gateway",
|
||||
"name": "Impostazioni",
|
||||
|
||||
"section": {
|
||||
"portal": "Impostazioni Accesso",
|
||||
"system": "Impostazioni sistema",
|
||||
"diag": "Diagnostica",
|
||||
"heating": "Impostazioni riscaldamento",
|
||||
"dhw": "Impostazioni ACS",
|
||||
"emergency": "Impostazioni modo Emergenza",
|
||||
"equitherm": "Impostazioni Equitherm",
|
||||
"pid": "Impostazioni PID",
|
||||
"ot": "Impostazioni OpenTherm",
|
||||
"mqtt": "Impostazioni MQTT",
|
||||
"extPump": "Impostazioni pompa esterna",
|
||||
"cascadeControl": "Impostazioni controllo a cascata"
|
||||
},
|
||||
|
||||
"enable": "Attiva",
|
||||
"note": {
|
||||
"restart": "Dopo aver cambiato queste impostazioni, il sistema sarà riavviato perchè i cambiamenti abbiano effetto.",
|
||||
"blankNotUse": "vuoto - non usare",
|
||||
"bleDevice": "Dispositivi BLE possono essere usati <u>solo</u> con alcune schede ESP32 che supportano il bluetooth!"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "Temperatura minima",
|
||||
"max": "Temperatura massima"
|
||||
},
|
||||
"maxModulation": "Max livello modulazione",
|
||||
"ohProtection": {
|
||||
"title": "Protezione contro il surriscaldamento",
|
||||
"desc": "<b>Nota:</b> questa funzione può essere utile se la protezione contro il surriscaldamento integrata nella caldaia non funziona o non funziona correttamente e il fluido termovettore bolle. Per disattivarla, impostare 0 come temperatura <b>alta</b> e <b>bassa</b>.",
|
||||
"highTemp": {
|
||||
"title": "Soglia di temperatura alta",
|
||||
"note": "Soglia alla quale il bruciatore verrà spento forzatamente"
|
||||
},
|
||||
"lowTemp": {
|
||||
"title": "Soglia di temperatura bassa",
|
||||
"note": "Soglia alla quale il bruciatore può essere riacceso"
|
||||
}
|
||||
},
|
||||
"freezeProtection": {
|
||||
"title": "Protezione antigelo",
|
||||
"desc": "Il riscaldamento verrà attivato forzatamente se la temperatura del vettore di calore o interna scende al di sotto della <b>temperatura minima</b> durante il <b>tempo di attesa</b>.",
|
||||
"lowTemp": "Soglia di temperatura minima",
|
||||
"thresholdTime": "Tempo di attesa <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "Login",
|
||||
"password": "Password",
|
||||
"auth": "Richiede autenticazione",
|
||||
"mdns": "Usa mDNS"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "Unità di misura",
|
||||
"metric": "Metrico <small>(celsius, litri, bar)</small>",
|
||||
"imperial": "Imperiale <small>(fahrenheit, galloni, psi)</small>",
|
||||
"statusLedGpio": "LED di stato GPIO",
|
||||
"logLevel": "Log livello",
|
||||
"serial": {
|
||||
"enable": "Porta seriale attivata",
|
||||
"baud": "Porta seriale baud rate"
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "Telnet attivato",
|
||||
"port": {
|
||||
"title": "Porta Telnet",
|
||||
"note": "Default: 23"
|
||||
}
|
||||
},
|
||||
"ntp": {
|
||||
"server": "NTP server",
|
||||
"timezone": "Zona oraria",
|
||||
"timezonePresets": "Seleziona preimpostato..."
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "Isteresi <small>(in gradi)</small>",
|
||||
"turboFactor": "Turbo mode coeff."
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "Il modo emergenza è attivato automaticamente quando «PID» o «Equitherm» non possono calcolare il setpoint:<br />- se «Equitherm» è attivato e il sensore della temperatura esternare è disconnesso;<br />- se «PID» o l'opzione OT <i>«Impostazioni riscaldamento native»</i> è attiva e il sensore di temperatura interno è disconnesso.<br /><b>Nota:</b> In mancanza di rete o MQTT, sensore di tipo <i>«Manuale via MQTT/API»</i> è in stato Disconnesso.",
|
||||
|
||||
"target": {
|
||||
"title": "Temperatura impostata",
|
||||
"note": "<b>Importante:</b> <u>Temperatura interna impostata</u> se l'opzione OT <i>«Controllo riscaldamento interno»</i> è attivato.<br />In tutti gli altri casi, la <u>target heat carrier temperature</u>."
|
||||
},
|
||||
"treshold": "Tempo di soglia <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"equitherm": {
|
||||
"n": "Fattore N",
|
||||
"k": "Fattore K",
|
||||
"t": {
|
||||
"title": "Fattore T",
|
||||
"note": "Non usato se PID è attivato"
|
||||
}
|
||||
},
|
||||
|
||||
"pid": {
|
||||
"p": "Fattore P",
|
||||
"i": "Fattore I",
|
||||
"d": "Fattore D",
|
||||
"dt": "DT <small>in secondi</small>",
|
||||
"limits": {
|
||||
"title": "Limiti",
|
||||
"note": "<b>Importante:</b> Quando usi «Equitherm» e «PID» allo stesso tempo, i limiti della temperatura min e max influenzano il risultato della temperatura «Equitherm».<br />Thus, se la temperatura minima è impostata a -15 e la massima a 15, il riscaldamento finale sarà impostato fra <code>equitherm_result - 15</code> a <code>equitherm_result + 15</code>."
|
||||
},
|
||||
"deadband": {
|
||||
"title": "Zona morta (Deadband)",
|
||||
"note": "La zona morta è un intervallo intorno alla temperatura target in cui la regolazione PID diventa meno attiva. In questo intervallo, l'algoritmo può ridurre l'intensità o interrompere gli aggiustamenti per evitare di reagire eccessivamente a piccole fluttuazioni.<br /><br />Ad esempio, con una temperatura target di 22°, una soglia inferiore di 1.0 e una soglia superiore di 0.5, la zona morta opera tra 21° e 22.5°. Se il coefficiente I è 0.0005 e il moltiplicatore I è 0.05, allora nella zona morta, il coefficiente I diventa: <code>0.0005 * 0.05 = 0.000025</code>",
|
||||
"p_multiplier": "Moltiplicatore P",
|
||||
"i_multiplier": "Moltiplicatore I",
|
||||
"d_multiplier": "Moltiplicatore D",
|
||||
"thresholdHigh": "Soglia superiore",
|
||||
"thresholdLow": "Soglia inferiore"
|
||||
}
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"inGpio": "In GPIO",
|
||||
"outGpio": "Out GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberId": "Master member ID",
|
||||
"flags": "Master flags",
|
||||
"minPower": {
|
||||
"title": "Potenza minima caldaia <small>(kW)</small>",
|
||||
"note": "Questo valore corrisponde allo livello 0-1% di modulazione della caldaia. Di solito si trova nelle specifiche delle caldaia come \"potenza minima disponibile\"."
|
||||
},
|
||||
"maxPower": {
|
||||
"title": "Potenza massima caldaia <small>(kW)</small>",
|
||||
"note": "<b>0</b> - prova a rilevarla automaticamente. Di solito si trova nelle specifiche delle caldaia come \"potenza massima disponibile\"."
|
||||
},
|
||||
|
||||
"options": {
|
||||
"title": "Opzioni (impostazioni aggiuntive)",
|
||||
"desc": "Le opzioni possono modificare la logica della caldaia. Non tutte le opzioni sono documentate nel protocollo, quindi la stessa opzione può avere effetti diversi su caldaie diverse.<br /><b>Nota:</b> Non è necessario modificare nulla se tutto funziona correttamente.",
|
||||
"dhwSupport": "Supporto ACS",
|
||||
"coolingSupport": "Supporto rafferscamento",
|
||||
"summerWinterMode": "Modalità Estate/inverno",
|
||||
"heatingStateToSummerWinterMode": "Stato di riscaldamento come modalità estate/inverno",
|
||||
"ch2AlwaysEnabled": "CH2 sempre abilitato",
|
||||
"heatingToCh2": "Riproduci riscaldamento su CH2",
|
||||
"dhwToCh2": "Riproduci ACS su CH2",
|
||||
"dhwBlocking": "Bloccare ACS",
|
||||
"dhwStateAsDhwBlocking": "Stato ACS come bloccare ACS",
|
||||
"maxTempSyncWithTargetTemp": "Sincronizza la temperatura massima di riscaldamento con la temperatura target",
|
||||
"getMinMaxTemp": "Prendi temp min/max dalla caldaia",
|
||||
"ignoreDiagState": "Ignora lo stato diagnostico",
|
||||
"autoFaultReset": "Ripristino automatico degli errori <small>(sconsigliato!)</small>",
|
||||
"autoDiagReset": "Ripristino diagnostico automatica <small>(sconsigliato!)</small>",
|
||||
"setDateAndTime": "Imposta data e ora sulla caldaia",
|
||||
"immergasFix": "Fix per caldiaie Immergas"
|
||||
},
|
||||
|
||||
"nativeHeating": {
|
||||
"title": "Controllo del riscaldamento nativo (caldaia)",
|
||||
"note": "Lavora <u>SOLO</u> se la caldaia richiede la temperatura ambiente desiderata e regola autonomamente la temperatura del fluido. Non compatiblile con regolazioni PID e Equitherm del sistema."
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant Discovery",
|
||||
"server": "Server",
|
||||
"port": "Porta",
|
||||
"user": "Utente",
|
||||
"password": "Password",
|
||||
"prefix": "Prefisso",
|
||||
"interval": "Intervallo invio <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"extPump": {
|
||||
"use": "Usa pompa/circolatore esterno",
|
||||
"gpio": "GPIO relè",
|
||||
"postCirculationTime": "Tempo di post circolazione <small>(min)</small>",
|
||||
"antiStuckInterval": "Intervallo antiblocco <small>(days)</small>",
|
||||
"antiStuckTime": "Tempo antiblocco <small>(min)</small>"
|
||||
},
|
||||
|
||||
"cascadeControl": {
|
||||
"input": {
|
||||
"desc": "Può essere attivata la caldaia se un'altra ha fallito. Il controllo dell'altra caldaia cambia lo stato dell'ingresso del GPIO in caso di errore.",
|
||||
"enable": "Ingresso abilitato",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Inverti stato GPIO",
|
||||
"thresholdTime": "Tempo soglia di modifica dello stato <small>(sec)</small>"
|
||||
},
|
||||
"output": {
|
||||
"desc": "Può essere usato per passare ad un'altra caldaia tramite <u>relè</u>.",
|
||||
"enable": "Uscita abilitata",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Invert GPIO state",
|
||||
"thresholdTime": "Tempo soglia di modifica dello stato <small>(sec)</small>",
|
||||
"events": {
|
||||
"desc": "Eventi",
|
||||
"onFault": "Se lo stato di errore è attivo",
|
||||
"onLossConnection": "Se non c'è la connessione via Opentherm",
|
||||
"onEnabledHeating": "Se il riscaldamento è attivato"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"title": "Aggiornamenti - OpenTherm Gateway",
|
||||
"name": "Aggiornamenti",
|
||||
|
||||
"section": {
|
||||
"backupAndRestore": "Backup & restore",
|
||||
"backupAndRestore.desc": "In questa sezione puoi salvare e recuperare un backup di tutte le impostazioni.",
|
||||
"upgrade": "Aggiorna",
|
||||
"upgrade.desc": "In questa sezione puoi aggiornare il firmware il filesystem del tuo dispositivo.<br />L'ultimo aggiornamento può essere scaricato da <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Releases page</a> del progetto."
|
||||
},
|
||||
|
||||
"note": {
|
||||
"disclaimer1": "Dopo un aggiornamento riuscito del filesystem, tutte le impostazioni sono impostate di default! Salva un backup prima di aggiornare.",
|
||||
"disclaimer2": "Dopo un aggiornamento riuscito, il sistema viene automaticamente riavviato dopo 15 secondi."
|
||||
},
|
||||
|
||||
"settingsFile": "Settings file",
|
||||
"fw": "Firmware",
|
||||
"fs": "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
507
src_data/locales/ru.json
Normal file
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "Лицензия",
|
||||
"source": "Исходный код",
|
||||
"help": "Помощь",
|
||||
"issues": "Проблемы и вопросы",
|
||||
"releases": "Релизы"
|
||||
},
|
||||
"dbm": "дБм",
|
||||
"kw": "кВт",
|
||||
"time": {
|
||||
"days": "д.",
|
||||
"hours": "ч.",
|
||||
"min": "мин.",
|
||||
"sec": "сек."
|
||||
},
|
||||
|
||||
"button": {
|
||||
"upgrade": "Обновить",
|
||||
"restart": "Перезагрузка",
|
||||
"save": "Сохранить",
|
||||
"saved": "Сохранено",
|
||||
"refresh": "Обновить",
|
||||
"restore": "Восстановить настройки",
|
||||
"restored": "Восстановлено",
|
||||
"backup": "Сохранить настройки",
|
||||
"wait": "Пожалуйста, подождите...",
|
||||
"uploading": "Загрузка...",
|
||||
"success": "Успешно",
|
||||
"error": "Ошибка"
|
||||
},
|
||||
|
||||
"index": {
|
||||
"title": "OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"network": "Сеть",
|
||||
"system": "Система"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"build": {
|
||||
"title": "Билд",
|
||||
"version": "Версия",
|
||||
"date": "Дата",
|
||||
"core": "Ядро",
|
||||
"sdk": "SDK"
|
||||
},
|
||||
"uptime": "Аптайм",
|
||||
"memory": {
|
||||
"title": "ОЗУ",
|
||||
"maxFreeBlock": "макс. блок",
|
||||
"min": "мин."
|
||||
},
|
||||
"board": "Плата",
|
||||
"chip": {
|
||||
"model": "Чип",
|
||||
"cores": "Кол-во ядер",
|
||||
"freq": "частота"
|
||||
},
|
||||
"flash": {
|
||||
"size": "Размер ПЗУ",
|
||||
"realSize": "реальный размер"
|
||||
},
|
||||
"lastResetReason": "Причина перезагрузки"
|
||||
}
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"name": "Дашборд",
|
||||
"title": "Дашборд - OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"control": "Управление",
|
||||
"states": "Состояние",
|
||||
"sensors": "Сенсоры",
|
||||
"diag": "Диагностика OpenTherm"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "Отопление",
|
||||
"dhw": "ГВС",
|
||||
"temp.current": "Текущая",
|
||||
"enable": "Вкл",
|
||||
"turbo": "Турбо"
|
||||
},
|
||||
|
||||
"notify": {
|
||||
"fault": {
|
||||
"title": "Состояние неисправности котла активно!",
|
||||
"note": "Рекомендуется осмотреть котел и изучить документацию котла для расшифровки кода неисправности:"
|
||||
},
|
||||
"diag": {
|
||||
"title": "Состояние диагностики котла активно!",
|
||||
"note": "Возможно, вашему котлу требуется проверка? Рекомендуется изучить документацию котла для расшифровки кода диагностики:"
|
||||
},
|
||||
"reset": "Сбросить"
|
||||
},
|
||||
|
||||
"states": {
|
||||
"mNetworkConnected": "Подключение к сети",
|
||||
"mMqttConnected": "Подключение к MQTT",
|
||||
"mEmergencyState": "Аварийный режим",
|
||||
"mExtPumpState": "Внешний насос",
|
||||
"mCascadeControlInput": "Каскадное управление (вход)",
|
||||
"mCascadeControlOutput": "Каскадное управление (выход)",
|
||||
|
||||
"sConnected": "Подключение к OpenTherm",
|
||||
"sFlame": "Пламя",
|
||||
"sCooling": "Охлаждение",
|
||||
"sFaultActive": "Ошибка",
|
||||
"sFaultCode": "Код ошибки",
|
||||
"sDiagActive": "Диагностика",
|
||||
"sDiagCode": "Диагностический код",
|
||||
|
||||
"mHeatEnabled": "Отопление",
|
||||
"mHeatBlocking": "Блокировка отопления",
|
||||
"mHeatOverheat": "Отопление, перегрев",
|
||||
"sHeatActive": "Активность отопления",
|
||||
"mHeatSetpointTemp": "Отопление, уставка",
|
||||
"mHeatTargetTemp": "Отопление, целевая температура",
|
||||
"mHeatCurrTemp": "Отопление, текущая температура",
|
||||
"mHeatRetTemp": "Отопление, температура обратки",
|
||||
"mHeatIndoorTemp": "Отопление, внутренняя темп.",
|
||||
"mHeatOutdoorTemp": "Отопление, наружная темп.",
|
||||
|
||||
"mDhwEnabled": "ГВС",
|
||||
"mDhwOverheat": "ГВС, перегрев",
|
||||
"sDhwActive": "Активность ГВС",
|
||||
"mDhwTargetTemp": "ГВС, целевая температура",
|
||||
"mDhwCurrTemp": "ГВС, текущая температура",
|
||||
"mDhwRetTemp": "ГВС, температура обратки"
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"values": {
|
||||
"temp": "Температура",
|
||||
"humidity": "Влажность",
|
||||
"battery": "Уровень заряда",
|
||||
"rssi": "RSSI"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"title": "Сеть - OpenTherm Gateway",
|
||||
"name": "Настройки сети",
|
||||
|
||||
"section": {
|
||||
"static": "Статические параметры",
|
||||
"availableNetworks": "Доступные сети",
|
||||
"staSettings": "Настройки подключения",
|
||||
"apSettings": "Настройки точки доступа"
|
||||
},
|
||||
|
||||
"scan": {
|
||||
"pos": "#",
|
||||
"info": "Инфо"
|
||||
},
|
||||
|
||||
"wifi": {
|
||||
"ssid": "Имя сети",
|
||||
"password": "Пароль",
|
||||
"channel": "Канал",
|
||||
"signal": "Сигнал",
|
||||
"connected": "Подключено"
|
||||
},
|
||||
|
||||
"params": {
|
||||
"hostname": "Имя хоста",
|
||||
"dhcp": "Использовать DHCP",
|
||||
"mac": "MAC адрес",
|
||||
"ip": "IP адрес",
|
||||
"subnet": "Адрес подсети",
|
||||
"gateway": "Адрес шлюза",
|
||||
"dns": "DNS адрес"
|
||||
},
|
||||
|
||||
"sta": {
|
||||
"channel.note": "установите 0 для автоматического выбора"
|
||||
}
|
||||
},
|
||||
|
||||
"sensors": {
|
||||
"title": "Настройки сенсоров - OpenTherm Gateway",
|
||||
"name": "Настройки сенсоров",
|
||||
|
||||
"enabled": "Включить и использовать",
|
||||
"sensorName": {
|
||||
"title": "Имя сенсора",
|
||||
"note": "Может содержать только: a-z, A-Z, 0-9, _ и пробел"
|
||||
},
|
||||
"purpose": "Назначение",
|
||||
"purposes": {
|
||||
"outdoorTemp": "Внешняя температура",
|
||||
"indoorTemp": "Внутренняя температура",
|
||||
"heatTemp": "Отопление, температура",
|
||||
"heatRetTemp": "Отопление, температура обратки",
|
||||
"dhwTemp": "ГВС, температура",
|
||||
"dhwRetTemp": "ГВС, температура обратки",
|
||||
"dhwFlowRate": "ГВС, расход/скорость потока",
|
||||
"exhaustTemp": "Температура выхлопных газов",
|
||||
"modLevel": "Уровень модуляции (в процентах)",
|
||||
"number": "Число (raw)",
|
||||
"powerFactor": "Мощность (в процентах)",
|
||||
"power": "Мощность (в кВт)",
|
||||
"fanSpeed": "Скорость вентилятора",
|
||||
"co2": "CO2",
|
||||
"pressure": "Давление",
|
||||
"humidity": "Влажность",
|
||||
"temperature": "Температура",
|
||||
"notConfigured": "Не сконфигурировано"
|
||||
},
|
||||
"type": "Тип/источник",
|
||||
"types": {
|
||||
"otOutdoorTemp": "OpenTherm, внешняя температура",
|
||||
"otHeatTemp": "OpenTherm, отопление, температура",
|
||||
"otHeatRetTemp": "OpenTherm, отопление, температура обратки",
|
||||
"otDhwTemp": "OpenTherm, ГВС, температура",
|
||||
"otDhwTemp2": "OpenTherm, ГВС, температура 2",
|
||||
"otDhwFlowRate": "OpenTherm, ГВС, расход/скорость потока",
|
||||
"otCh2Temp": "OpenTherm, канал 2, температура",
|
||||
"otExhaustTemp": "OpenTherm, температура выхлопных газов",
|
||||
"otHeatExchangerTemp": "OpenTherm, температура теплообменника",
|
||||
"otPressure": "OpenTherm, давление",
|
||||
"otModLevel": "OpenTherm, уровень модуляции",
|
||||
"otCurrentPower": "OpenTherm, текущая мощность",
|
||||
"otExhaustCo2": "OpenTherm, CO2 вытяжного воздуха",
|
||||
"otExhaustFanSpeed": "OpenTherm, скорость вытяжного вентилятора",
|
||||
"otSupplyFanSpeed": "OpenTherm, скорость приточного вентилятора",
|
||||
"otSolarStorageTemp": "OpenTherm, темп. бойлера солн. коллектора",
|
||||
"otSolarCollectorTemp": "OpenTherm, темп. солн. коллектора",
|
||||
"otFanSpeedSetpoint": "OpenTherm, установленная мощн. вентилятора",
|
||||
"otFanSpeedCurrent": "OpenTherm, текущая мощн. вентилятора",
|
||||
"otBurnerStarts": "OpenTherm, кол-во запусков горелки",
|
||||
"otDhwBurnerStarts": "OpenTherm, кол-во запусков горелки (ГВС)",
|
||||
"otHeatingPumpStarts": "OpenTherm, кол-во запусков насоса (отопление)",
|
||||
"otDhwPumpStarts": "OpenTherm, кол-во запусков насоса (ГВС)",
|
||||
"otBurnerHours": "OpenTherm, кол-во часов работы горелки",
|
||||
"otDhwBurnerHours": "OpenTherm, кол-во часов работы горелки (ГВС)",
|
||||
"otHeatingPumpHours": "OpenTherm, кол-во часов работы насоса (отопление)",
|
||||
"otDhwPumpHours": "OpenTherm, кол-во часов работы насоса (ГВС)",
|
||||
|
||||
"ntcTemp": "NTC датчик",
|
||||
"dallasTemp": "DALLAS датчик",
|
||||
"bluetooth": "BLE датчик",
|
||||
"heatSetpointTemp": "Отопление, температура уставки",
|
||||
"manual": "Вручную через MQTT/API",
|
||||
"notConfigured": "Не сконфигурировано"
|
||||
},
|
||||
"gpio": "GPIO датчика",
|
||||
"address": {
|
||||
"title": "Адрес датчика",
|
||||
"note": "Для DALLAS датчиков оставьте по умолчанию для автоопределения, для BLE устройств необходимо указать MAC адрес"
|
||||
},
|
||||
"correction": {
|
||||
"desc": "Коррекция показаний",
|
||||
"offset": "Компенсация (смещение)",
|
||||
"factor": "Множитель"
|
||||
},
|
||||
"filtering": {
|
||||
"desc": "Фильтрация показаний",
|
||||
"enabled": {
|
||||
"title": "Включить фильтрацию",
|
||||
"note": "Может быть полезно, если на графиках много резкого шума. В качестве фильтра используется \"бегущее среднее\"."
|
||||
},
|
||||
"factor": {
|
||||
"title": "Коэфф. фильтрации",
|
||||
"note": "Чем меньше коэф., тем плавнее и <u>дольше</u> изменение числовых значений."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Настройки - OpenTherm Gateway",
|
||||
"name": "Настройки",
|
||||
|
||||
"section": {
|
||||
"portal": "Настройки портала",
|
||||
"system": "Системные настройки",
|
||||
"diag": "Диагностика",
|
||||
"heating": "Настройки отопления",
|
||||
"dhw": "Настройки ГВС",
|
||||
"emergency": "Настройки аварийного режима",
|
||||
"equitherm": "Настройки ПЗА",
|
||||
"pid": "Настройки ПИД",
|
||||
"ot": "Настройки OpenTherm",
|
||||
"mqtt": "Настройки MQTT",
|
||||
"extPump": "Настройки дополнительного насоса",
|
||||
"cascadeControl": "Настройки каскадного управления"
|
||||
},
|
||||
|
||||
"enable": "Вкл",
|
||||
"note": {
|
||||
"restart": "После изменения этих настроек устройство необходимо перезагрузить, чтобы изменения вступили в силу.",
|
||||
"blankNotUse": "пусто - не использовать",
|
||||
"bleDevice": "BLE устройство можно использовать <u>только</u> с некоторыми платами ESP32, которые поддерживают BLE!"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "Мин. температура",
|
||||
"max": "Макс. температура"
|
||||
},
|
||||
"maxModulation": "Макс. уровень модуляции",
|
||||
"ohProtection": {
|
||||
"title": "Защита от перегрева",
|
||||
"desc": "<b>Примечание:</b> Эта функция может быть полезна, если встроенная защита от перегрева котла не срабатывает или срабатывает некорректно и теплоноситель закипает. Для отключения установите 0 в качестве <b>верхнего</b> и <b>нижнего</b> порога температуры.",
|
||||
"highTemp": {
|
||||
"title": "Верхний порог температуры",
|
||||
"note": "Порог, при котором горелка будет принудительно отключена"
|
||||
},
|
||||
"lowTemp": {
|
||||
"title": "Нижний порог температуры",
|
||||
"note": "Порог, при котором горелка может быть включена снова"
|
||||
}
|
||||
},
|
||||
"freezeProtection": {
|
||||
"title": "Защита от замерзания",
|
||||
"desc": "Отопление будет принудительно включено, если темп. теплоносителя или внутренняя темп. опустится ниже <b>нижнего порога</b> в течение <b>времени ожидания</b>.",
|
||||
"lowTemp": "Нижний порог температуры",
|
||||
"thresholdTime": "Время ожидания <small>(сек)</small>"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "Логин",
|
||||
"password": "Пароль",
|
||||
"auth": "Требовать аутентификацию",
|
||||
"mdns": "Использовать mDNS"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "Система единиц",
|
||||
"metric": "Метрическая <small>(цельсии, литры, бары)</small>",
|
||||
"imperial": "Imperial <small>(фаренгейты, галлоны, psi)</small>",
|
||||
"statusLedGpio": "Статус LED GPIO",
|
||||
"logLevel": "Уровень логирования",
|
||||
"serial": {
|
||||
"enable": "Вкл. Serial порт",
|
||||
"baud": "Скорость Serial порта"
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "Вкл. Telnet",
|
||||
"port": {
|
||||
"title": "Telnet порт",
|
||||
"note": "По умолчанию: 23"
|
||||
}
|
||||
},
|
||||
"ntp": {
|
||||
"server": "NTP сервер",
|
||||
"timezone": "Часовой пояс",
|
||||
"timezonePresets": "Выберите пресет..."
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "Гистерезис <small>(в градусах)</small>",
|
||||
"turboFactor": "Коэфф. турбо режима"
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "Аварийный режим активируется автоматически, если «ПИД» или «ПЗА» не могут рассчитать уставку теплоносителя:<br />- если «ПЗА» включен и датчик наружной температуры отключен;<br />- если включен «ПИД» или OT опция <i>«Передать управление отоплением котлу»</i> и датчик внутренней температуры отключен.<br /><b>Примечание:</b> При сбое сети или MQTT датчики с типом <i>«Вручную через MQTT/API»</i> будут находиться в состоянии ОТКЛЮЧЕН.",
|
||||
|
||||
"target": {
|
||||
"title": "Целевая температура",
|
||||
"note": "<b>Важно:</b> <u>Целевая температура в помещении</u>, если включена ОТ опция <i>«Передать управление отоплением котлу»</i>.<br />Во всех остальных случаях <u>целевая температура теплоносителя</u>."
|
||||
},
|
||||
"treshold": "Пороговое время включения <small>(сек)</small>"
|
||||
},
|
||||
|
||||
"equitherm": {
|
||||
"n": "Коэффициент N",
|
||||
"k": "Коэффициент K",
|
||||
"t": {
|
||||
"title": "Коэффициент T",
|
||||
"note": "Не используется, если ПИД включен"
|
||||
}
|
||||
},
|
||||
|
||||
"pid": {
|
||||
"p": "Коэффициент P",
|
||||
"i": "Коэффициент I",
|
||||
"d": "Коэффициент D",
|
||||
"dt": "DT <small>(сек)</small>",
|
||||
"limits": {
|
||||
"title": "Лимиты",
|
||||
"note": "<b>Важно:</b> При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».<br />Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от <code>equitherm_result - 15</code> до <code>equitherm_result + 15</code>."
|
||||
},
|
||||
"deadband": {
|
||||
"title": "Зона нечувствительности (Deadband)",
|
||||
"note": "Deadband - это зона нечувствительности вокруг целевой температуры, в которой PID-регулирование становится менее активным. В этом диапазоне алгоритм может снижать интенсивность или полностью прекращать корректировку температуры, чтобы избежать излишней чувствительности к небольшим колебаниям.<br /><br />Например, при целевой температуре 22°, нижнем пороге 1.0 и верхнем 0.5, deadband активен в диапазоне от 21° до 22.5°. Если коэфф. I=0.0005, а множитель I=0.05, то при включении зоны нечувствительности коэфф. I будет равен: <code>0.0005 * 0.05 = 0.000025</code>",
|
||||
"p_multiplier": "Множитель для коэф. P",
|
||||
"i_multiplier": "Множитель для коэф. I",
|
||||
"d_multiplier": "Множитель для коэф. D",
|
||||
"thresholdHigh": "Верхний порог",
|
||||
"thresholdLow": "Нижний порог"
|
||||
}
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"advanced": "Дополнительные настройки",
|
||||
"inGpio": "Вход GPIO",
|
||||
"outGpio": "Выход GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberId": "Master member ID",
|
||||
"flags": "Master flags",
|
||||
"minPower": {
|
||||
"title": "Мин. мощность котла <small>(кВт)</small>",
|
||||
"note": "Это значение соответствует уровню модуляции котла 0–1%. Обычно можно найти в спецификации котла как \"минимальная полезная тепловая мощность\"."
|
||||
},
|
||||
"maxPower": {
|
||||
"title": "Макс. мощность котла <small>(кВт)</small>",
|
||||
"note": "<b>0</b> - попробовать определить автоматически. Обычно можно найти в спецификации котла как \"максимальная полезная тепловая мощность\"."
|
||||
},
|
||||
|
||||
"options": {
|
||||
"title": "Опции (дополнительные настройки)",
|
||||
"desc": "Опции могут менять логику работы котла. Не все опции задокументированы в протоколе, поэтому одна и та же опция может иметь разный эффект на разных котлах.<br /><b>Примечание:</b> Нет необходимости что-то менять, если всё работает хорошо.",
|
||||
"dhwSupport": "Поддержка ГВС",
|
||||
"coolingSupport": "Поддержка охлаждения",
|
||||
"summerWinterMode": "Летний/зимний режим",
|
||||
"heatingStateToSummerWinterMode": "Летний/зимний режим в качестве состояния отопления",
|
||||
"ch2AlwaysEnabled": "Канал 2 всегда вкл.",
|
||||
"heatingToCh2": "Дублировать параметры отопления в канал 2",
|
||||
"dhwToCh2": "Дублировать параметры ГВС в канал 2",
|
||||
"dhwBlocking": "DHW blocking",
|
||||
"dhwStateAsDhwBlocking": "DHW blocking в качестве состояния ГВС",
|
||||
"maxTempSyncWithTargetTemp": "Синхронизировать макс. темп. отопления с целевой темп.",
|
||||
"getMinMaxTemp": "Получать мин. и макс. температуру от котла",
|
||||
"ignoreDiagState": "Игнорировать состояние диагностики",
|
||||
"autoFaultReset": "Автоматический сброс ошибок <small>(не рекомендуется!)</small>",
|
||||
"autoDiagReset": "Автоматический сброс диагностики <small>(не рекомендуется!)</small>",
|
||||
"setDateAndTime": "Устанавливать время и дату на котле",
|
||||
"immergasFix": "Фикс для котлов Immergas"
|
||||
},
|
||||
|
||||
"nativeHeating": {
|
||||
"title": "Передать управление отоплением котлу",
|
||||
"note": "Работает <u>ТОЛЬКО</u> если котел требует и принимает целевую температуру в помещении и сам регулирует температуру теплоносителя на основе встроенного режима кривых. Несовместимо с ПИД и ПЗА."
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant Discovery",
|
||||
"server": "Адрес сервера",
|
||||
"port": "Порт",
|
||||
"user": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"prefix": "Префикс",
|
||||
"interval": "Интервал публикации <small>(сек)</small>"
|
||||
},
|
||||
|
||||
"extPump": {
|
||||
"use": "Использовать доп. насос",
|
||||
"gpio": "GPIO реле",
|
||||
"postCirculationTime": "Время постциркуляции <small>(в минутах)</small>",
|
||||
"antiStuckInterval": "Интервал защиты от блокировки <small>(в днях)</small>",
|
||||
"antiStuckTime": "Время работы насоса <small>(в минутах)</small>"
|
||||
},
|
||||
|
||||
"cascadeControl": {
|
||||
"input": {
|
||||
"desc": "Может использоваться для включения отопления только при неисправности другого котла. Контроллер другого котла должен изменить состояние входа GPIO в случае неисправности.",
|
||||
"enable": "Включить вход",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Инвертировать состояние GPIO",
|
||||
"thresholdTime": "Пороговое время изменения состояния <small>(сек)</small>"
|
||||
},
|
||||
"output": {
|
||||
"desc": "Может использоваться для включения другого котла <u>через реле</u>.",
|
||||
"enable": "Включить выход",
|
||||
"gpio": "GPIO",
|
||||
"invertState": "Инвертировать состояние GPIO",
|
||||
"thresholdTime": "Пороговое время изменения состояния <small>(сек)</small>",
|
||||
"events": {
|
||||
"desc": "События",
|
||||
"onFault": "Если состояние fault (ошибки) активно",
|
||||
"onLossConnection": "Если соединение по OpenTherm потеряно",
|
||||
"onEnabledHeating": "Если отопление включено"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"title": "Обновление - OpenTherm Gateway",
|
||||
"name": "Обновление",
|
||||
|
||||
"section": {
|
||||
"backupAndRestore": "Резервное копирование и восстановление",
|
||||
"backupAndRestore.desc": "В этом разделе вы можете сохранить и восстановить резервную копию ВСЕХ настроек.",
|
||||
"upgrade": "Обновление",
|
||||
"upgrade.desc": "В этом разделе вы можете обновить прошивку и файловую систему вашего устройства.<br />Последнюю версию можно загрузить со страницы <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Релизы</a> в репозитории проекта."
|
||||
},
|
||||
|
||||
"note": {
|
||||
"disclaimer1": "После успешного обновления файловой системы ВСЕ настройки будут сброшены на стандартные! Создайте резервную копию ПЕРЕД обновлением.",
|
||||
"disclaimer2": "После успешного обновления устройство автоматически перезагрузится через 15 секунд."
|
||||
},
|
||||
|
||||
"settingsFile": "Файл настроек",
|
||||
"fw": "Прошивка",
|
||||
"fs": "Файловая система"
|
||||
}
|
||||
}
|
||||
}
|
||||
765
src_data/pages/dashboard.html
Normal file
@@ -0,0 +1,765 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>dashboard.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="cn">CN</option>
|
||||
<option value="it">IT</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<hgroup>
|
||||
<h2 data-i18n>dashboard.name</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="dashboard-busy" aria-busy="true"></div>
|
||||
<div id="dashboard-container" class="hidden">
|
||||
<details open>
|
||||
<summary><b data-i18n>dashboard.section.control</b></summary>
|
||||
<div class="grid">
|
||||
<div class="thermostat tHeat" data-purpose="heating" data-min="0" data-max="100" data-step="0.1" data-big-step="1">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tHeatCurrentTemp"></span> <span class="tempUnit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus">
|
||||
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-plus">
|
||||
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="tHeatEnabled" value="true">
|
||||
<label htmlFor="tHeatEnabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
|
||||
<input type="checkbox" role="switch" id="tHeatTurbo" value="true">
|
||||
<label htmlFor="tHeatTurbo" data-i18n>dashboard.thermostat.turbo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="thermostat tDhw" data-purpose="dhw" data-min="0" data-max="100" data-step="1" data-big-step="5">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tDhwCurrentTemp"></span> <span class="tempUnit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus">
|
||||
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-plus">
|
||||
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="tDhwEnabled" value="true">
|
||||
<label htmlFor="tDhwEnabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notify notify-error notify-fault hidden">
|
||||
<div class="notify-icon">
|
||||
<i class="icons-error"></i>
|
||||
</div>
|
||||
<div class="notify-content">
|
||||
<b data-i18n>dashboard.notify.fault.title</b><br />
|
||||
<small>
|
||||
<span data-i18n>dashboard.notify.fault.note</span> <b class="sFaultCode"></b>
|
||||
</small>
|
||||
</div>
|
||||
<div class="notify-actions">
|
||||
<button class="reset" data-i18n>dashboard.notify.reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notify notify-alarm notify-diag hidden">
|
||||
<div class="notify-icon">
|
||||
<i class="icons-alarm"></i>
|
||||
</div>
|
||||
<div class="notify-content">
|
||||
<b data-i18n>dashboard.notify.diag.title</b><br />
|
||||
<small>
|
||||
<span data-i18n>dashboard.notify.diag.note</span> <b class="sDiagCode"></b>
|
||||
</small>
|
||||
</div>
|
||||
<div class="notify-actions">
|
||||
<button class="reset" data-i18n>dashboard.notify.reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>dashboard.section.states</b></summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mNetworkConnected</th>
|
||||
<td><i class="mNetworkConnected"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mMqttConnected</th>
|
||||
<td><i class="mMqttConnected"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mEmergencyState</th>
|
||||
<td><i class="mEmergencyState"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mExtPumpState</th>
|
||||
<td><i class="mExtPumpState"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mCascadeControlInput</th>
|
||||
<td><i class="mCascadeControlInput"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mCascadeControlOutput</th>
|
||||
<td><i class="mCascadeControlOutput"></i></td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sConnected</th>
|
||||
<td><i class="sConnected"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sFlame</th>
|
||||
<td><i class="sFlame"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sCooling</th>
|
||||
<td><i class="sCooling"></i></td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sFaultActive</th>
|
||||
<td><i class="sFaultActive"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sFaultCode</th>
|
||||
<td><b class="sFaultCode"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sDiagActive</th>
|
||||
<td><i class="sDiagActive"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sDiagCode</th>
|
||||
<td><b class="sDiagCode"></b></td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatEnabled</th>
|
||||
<td><i class="mHeatEnabled"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatBlocking</th>
|
||||
<td><i class="mHeatBlocking"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatOverheat</th>
|
||||
<td><i class="mHeatOverheat"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sHeatActive</th>
|
||||
<td><i class="sHeatActive"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatTargetTemp</th>
|
||||
<td><b class="mHeatTargetTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatSetpointTemp</th>
|
||||
<td><b class="mHeatSetpointTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatCurrTemp</th>
|
||||
<td><b class="mHeatCurrTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatRetTemp</th>
|
||||
<td><b class="mHeatRetTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatIndoorTemp</th>
|
||||
<td><b class="mHeatIndoorTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mHeatOutdoorTemp</th>
|
||||
<td><b class="mHeatOutdoorTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mDhwEnabled</th>
|
||||
<td><i class="mDhwEnabled"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mDhwOverheat</th>
|
||||
<td><i class="mDhwOverheat"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.sDhwActive</th>
|
||||
<td><i class="sDhwActive"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mDhwTargetTemp</th>
|
||||
<td><b class="mDhwTargetTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mDhwCurrTemp</th>
|
||||
<td><b class="mDhwCurrTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.states.mDhwRetTemp</th>
|
||||
<td><b class="mDhwRetTemp"></b> <span class="tempUnit"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>dashboard.section.sensors</b></summary>
|
||||
<table>
|
||||
<tbody class="sensors">
|
||||
<tr class="sensor template hidden">
|
||||
<td style="width: 1.35rem">
|
||||
<span class="sStatusContainer">
|
||||
<i class="sStatus"></i>
|
||||
</span>
|
||||
</td>
|
||||
<th scope="row" class="sName"></th>
|
||||
<td class="sValue"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>dashboard.section.diag</b></summary>
|
||||
<pre><b>Vendor:</b> <span class="sVendor"></span>
|
||||
<b>Member ID:</b> <span class="sMemberId"></span>
|
||||
<b>Flags:</b> <span class="sFlags"></span>
|
||||
<b>Type:</b> <span class="sType"></span>
|
||||
<b>AppVersion:</b> <span class="sAppVersion"></span>
|
||||
<b>OT version:</b> <span class="sProtocolVersion"></span>
|
||||
<b>Modulation:</b> min: <span class="sModMin"></span> %, curr. max: <span class="sModMax"></span> %
|
||||
<b>Power limits:</b> <span class="sPowerMin"></span>...<span class="sPowerMax"></span> kW
|
||||
<b>Heating limits:</b> <span class="sHeatMinTemp"></span>...<span class="sHeatMaxTemp"></span> <span class="tempUnit"></span>
|
||||
<b>DHW limits:</b> <span class="sDhwMinTemp"></span>...<span class="sDhwMaxTemp"></span> <span class="tempUnit"></span></pre>
|
||||
</details>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
let modifiedTime = null;
|
||||
let prevSettings;
|
||||
let newSettings = {
|
||||
heating: {
|
||||
enable: false,
|
||||
turbo: false,
|
||||
target: 0
|
||||
},
|
||||
dhw: {
|
||||
enable: false,
|
||||
target: 0
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
let actionTimer = null;
|
||||
let actionLongPress = false;
|
||||
document.querySelectorAll('.tAction').forEach((item) => {
|
||||
const action = item.dataset.action;
|
||||
const tContainer = item.parentNode.parentNode;
|
||||
|
||||
for (const eName of ['pointerup', 'pointercancel']) {
|
||||
item.addEventListener(eName, (event) => {
|
||||
clearInterval(actionTimer);
|
||||
|
||||
const purpose = tContainer.dataset.purpose;
|
||||
const minTemp = parseFloat(tContainer.dataset.min);
|
||||
const maxTemp = parseFloat(tContainer.dataset.max);
|
||||
const step = parseFloat(tContainer.dataset.step);
|
||||
const bigStep = parseFloat(tContainer.dataset.bigStep);
|
||||
|
||||
if (!actionLongPress && prevSettings) {
|
||||
let value = 0;
|
||||
if (action == 'increment') {
|
||||
value = step;
|
||||
|
||||
} else if (action == 'decrement') {
|
||||
value = -(step);
|
||||
}
|
||||
|
||||
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
|
||||
modifiedTime = Date.now();
|
||||
setValue('.targetTemp', newSettings[purpose].target, tContainer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
item.addEventListener('pointerdown', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const purpose = tContainer.dataset.purpose;
|
||||
const minTemp = parseFloat(tContainer.dataset.min);
|
||||
const maxTemp = parseFloat(tContainer.dataset.max);
|
||||
const step = parseFloat(tContainer.dataset.step);
|
||||
const bigStep = parseFloat(tContainer.dataset.bigStep);
|
||||
|
||||
actionLongPress = false;
|
||||
actionTimer = setInterval(() => {
|
||||
if (!actionLongPress) {
|
||||
actionLongPress = true;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
if (action == 'increment') {
|
||||
value = bigStep;
|
||||
|
||||
} else if (action == 'decrement') {
|
||||
value = -(bigStep);
|
||||
}
|
||||
|
||||
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
|
||||
modifiedTime = Date.now();
|
||||
|
||||
setValue('.targetTemp', newSettings[purpose].target, tContainer);
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#tHeatEnabled').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.heating.enabled = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
document.querySelector('#tHeatTurbo').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.heating.turbo = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
document.querySelector('#tDhwEnabled').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.dhw.enabled = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
document.querySelector('.notify-fault .reset').addEventListener('click', async (event) => {
|
||||
const resetBtn = document.querySelector(".notify-fault .reset");
|
||||
if (!resetBtn.disabled) {
|
||||
resetBtn.disabled = true;
|
||||
}
|
||||
|
||||
let response = await fetch("/api/vars", {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"actions": {
|
||||
"resetFault": true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (resetBtn.disabled) {
|
||||
resetBtn.disabled = false;
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
document.querySelector('.notify-diag .reset').addEventListener('click', async (event) => {
|
||||
const resetBtn = document.querySelector(".notify-diag .reset");
|
||||
if (!resetBtn.disabled) {
|
||||
resetBtn.disabled = true;
|
||||
}
|
||||
|
||||
let response = await fetch("/api/vars", {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"actions": {
|
||||
"resetDiagnostic": true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (resetBtn.disabled) {
|
||||
resetBtn.disabled = false;
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
setTimeout(async function onLoadPage() {
|
||||
let unitSystem = null;
|
||||
|
||||
if (modifiedTime) {
|
||||
if ((Date.now() - modifiedTime) < 5000) {
|
||||
setTimeout(onLoadPage, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
modifiedTime = null;
|
||||
}
|
||||
|
||||
// settings
|
||||
try {
|
||||
let modified = prevSettings && (
|
||||
(prevSettings.heating.enabled != newSettings.heating.enabled)
|
||||
|| (prevSettings.heating.turbo != newSettings.heating.turbo)
|
||||
|| (prevSettings.heating.target != newSettings.heating.target)
|
||||
|| (prevSettings.opentherm.options.dhwSupport && prevSettings.dhw.enabled != newSettings.dhw.enabled)
|
||||
|| (prevSettings.opentherm.options.dhwSupport && prevSettings.dhw.target != newSettings.dhw.target)
|
||||
);
|
||||
|
||||
if (modified) {
|
||||
console.log(newSettings);
|
||||
}
|
||||
|
||||
let parameters = {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
if (modified) {
|
||||
parameters.method = "POST";
|
||||
parameters.body = JSON.stringify(newSettings);
|
||||
}
|
||||
|
||||
const response = await fetch("/api/settings", parameters);
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
prevSettings = result;
|
||||
unitSystem = result.system.unitSystem;
|
||||
newSettings.heating.enabled = result.heating.enabled;
|
||||
newSettings.heating.turbo = result.heating.turbo;
|
||||
newSettings.heating.target = result.heating.target;
|
||||
newSettings.dhw.enabled = result.dhw.enabled;
|
||||
newSettings.dhw.target = result.dhw.target;
|
||||
|
||||
if (result.opentherm.options.dhwSupport) {
|
||||
show('.tDhw');
|
||||
} else {
|
||||
hide('.tDhw');
|
||||
}
|
||||
|
||||
setCheckboxValue('#tHeatEnabled', result.heating.enabled);
|
||||
setCheckboxValue('#tHeatTurbo', result.heating.turbo);
|
||||
setValue('.tHeat .targetTemp', result.heating.target);
|
||||
|
||||
setCheckboxValue('#tDhwEnabled', result.dhw.enabled);
|
||||
setValue('.tDhw .targetTemp', result.dhw.target);
|
||||
|
||||
setValue('.tempUnit', temperatureUnit(unitSystem));
|
||||
setValue('.pressureUnit', pressureUnit(unitSystem));
|
||||
setValue('.volumeUnit', volumeUnit(unitSystem));
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// vars
|
||||
try {
|
||||
const response = await fetch("/api/vars", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Graph
|
||||
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
|
||||
? result.master.heating.indoorTemp
|
||||
: result.master.heating.currentTemp
|
||||
);
|
||||
setValue('#tDhwCurrentTemp', result.master.dhw.currentTemp);
|
||||
|
||||
|
||||
// SLAVE
|
||||
setValue('.sMemberId', result.slave.memberId);
|
||||
setValue('.sVendor', memberIdToVendor(result.slave.memberId));
|
||||
setValue('.sFlags', result.slave.flags);
|
||||
setValue('.sType', result.slave.type);
|
||||
setValue('.sAppVersion', result.slave.appVersion);
|
||||
setValue('.sProtocolVersion', result.slave.protocolVersion);
|
||||
|
||||
setStatus(
|
||||
'.sConnected',
|
||||
result.slave.connected ? "success" : "error",
|
||||
result.slave.connected ? "green" : "red"
|
||||
);
|
||||
setState('.sFlame', result.slave.flame);
|
||||
setState('.sCooling', result.slave.cooling);
|
||||
|
||||
setValue('.sModMin', result.slave.modulation.min);
|
||||
setValue('.sModMax', result.slave.modulation.max);
|
||||
|
||||
setValue('.sPowerMin', result.slave.power.min);
|
||||
setValue('.sPowerMax', result.slave.power.max);
|
||||
|
||||
setState('.sHeatActive', result.slave.heating.active);
|
||||
setValue('.sHeatMinTemp', result.slave.heating.minTemp);
|
||||
setValue('.sHeatMaxTemp', result.slave.heating.maxTemp);
|
||||
|
||||
setState('.sDhwActive', result.slave.dhw.active);
|
||||
setValue('.sDhwMinTemp', result.slave.dhw.minTemp);
|
||||
setValue('.sDhwMaxTemp', result.slave.dhw.maxTemp);
|
||||
|
||||
setStatus(
|
||||
'.sFaultActive',
|
||||
result.slave.fault.active ? "success" : "error",
|
||||
result.slave.fault.active ? "red" : "green"
|
||||
);
|
||||
setValue(
|
||||
'.sFaultCode',
|
||||
result.slave.fault.active
|
||||
? `${result.slave.fault.code} (0x${dec2hex(result.slave.fault.code)})`
|
||||
: "-"
|
||||
);
|
||||
|
||||
if (result.slave.fault.active) {
|
||||
show(".notify-fault");
|
||||
|
||||
} else {
|
||||
hide('.notify-fault');
|
||||
}
|
||||
|
||||
setStatus(
|
||||
'.sDiagActive',
|
||||
result.slave.diag.active ? "success" : "error",
|
||||
result.slave.diag.active ? "red" : "green"
|
||||
);
|
||||
setValue(
|
||||
'.sDiagCode',
|
||||
result.slave.diag.active
|
||||
? `${result.slave.diag.code} (0x${dec2hex(result.slave.diag.code)})`
|
||||
: "-"
|
||||
);
|
||||
|
||||
if (result.slave.diag.active) {
|
||||
show(".notify-diag");
|
||||
|
||||
} else {
|
||||
hide('.notify-diag');
|
||||
}
|
||||
|
||||
|
||||
// MASTER
|
||||
setState('.mHeatEnabled', result.master.heating.enabled);
|
||||
setStatus(
|
||||
'.mHeatBlocking',
|
||||
result.master.heating.blocking ? "success" : "error",
|
||||
result.master.heating.blocking ? "red" : "green"
|
||||
);
|
||||
setState('.mHeatIndoorTempControl', result.master.heating.indoorTempControl);
|
||||
setStatus(
|
||||
'.mHeatOverheat',
|
||||
result.master.heating.overheat ? "success" : "error",
|
||||
result.master.heating.overheat ? "red" : "green"
|
||||
);
|
||||
setValue('.mHeatSetpointTemp', result.master.heating.setpointTemp);
|
||||
setValue('.mHeatTargetTemp', result.master.heating.targetTemp);
|
||||
setValue('.mHeatCurrTemp', result.master.heating.currentTemp);
|
||||
setValue('.mHeatRetTemp', result.master.heating.returnTemp);
|
||||
setValue('.mHeatIndoorTemp', result.master.heating.indoorTemp);
|
||||
setValue('.mHeatOutdoorTemp', result.master.heating.outdoorTemp);
|
||||
setValue('.mHeatMinTemp', result.master.heating.minTemp);
|
||||
setValue('.mHeatMaxTemp', result.master.heating.maxTemp);
|
||||
|
||||
setState('.mDhwEnabled', result.master.dhw.enabled);
|
||||
setStatus(
|
||||
'.mDhwOverheat',
|
||||
result.master.dhw.overheat ? "success" : "error",
|
||||
result.master.dhw.overheat ? "red" : "green"
|
||||
);
|
||||
setValue('.mDhwTargetTemp', result.master.dhw.targetTemp);
|
||||
setValue('.mDhwCurrTemp', result.master.dhw.currentTemp);
|
||||
setValue('.mDhwRetTemp', result.master.dhw.returnTemp);
|
||||
setValue('.mDhwMinTemp', result.master.dhw.minTemp);
|
||||
setValue('.mDhwMaxTemp', result.master.dhw.maxTemp);
|
||||
|
||||
setStatus(
|
||||
'.mNetworkConnected',
|
||||
result.master.network.connected ? "success" : "error",
|
||||
result.master.network.connected ? "green" : "red"
|
||||
);
|
||||
setState('.mMqttConnected', result.master.mqtt.connected);
|
||||
setStatus(
|
||||
'.mEmergencyState',
|
||||
result.master.emergency.state ? "success" : "error",
|
||||
result.master.emergency.state ? "red" : "green"
|
||||
);
|
||||
setState('.mExtPumpState', result.master.externalPump.state);
|
||||
setState('.mCascadeControlInput', result.master.cascadeControl.input);
|
||||
setState('.mCascadeControlOutput', result.master.cascadeControl.output);
|
||||
|
||||
const tHeat = document.querySelector('.tHeat');
|
||||
tHeat.dataset.min = result.master.heating.minTemp;
|
||||
tHeat.dataset.max = result.master.heating.maxTemp;
|
||||
|
||||
const tDhw = document.querySelector('.tDhw');
|
||||
tDhw.dataset.min = result.master.dhw.minTemp;
|
||||
tDhw.dataset.max = result.master.dhw.maxTemp;
|
||||
|
||||
setBusy('#dashboard-busy', '#dashboard-container', false);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// sensors
|
||||
try {
|
||||
const response = await fetch("/api/sensors?detailed=1", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Response not valid");
|
||||
}
|
||||
|
||||
const container = document.querySelector(".sensors");
|
||||
const templateNode = container.querySelector(".template");
|
||||
const result = await response.json();
|
||||
|
||||
for (const sensorId in result) {
|
||||
let sensorNode = container.querySelector(`.sensor[data-id='${sensorId}']`);
|
||||
if (sensorNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sensorNode = templateNode.cloneNode(true);
|
||||
sensorNode.dataset.id = sensorId;
|
||||
sensorNode.classList.remove("template");
|
||||
container.appendChild(sensorNode);
|
||||
}
|
||||
|
||||
for (const sensorId in result) {
|
||||
const sensorNode = container.querySelector(`.sensor[data-id='${sensorId}']`);
|
||||
if (!sensorNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sData = result[sensorId];
|
||||
if (!sData.enabled || sData.purpose == 255) {
|
||||
sensorNode.classList.toggle("hidden", true);
|
||||
continue;
|
||||
}
|
||||
|
||||
sensorNode.classList.toggle("hidden", false);
|
||||
|
||||
setStatus(
|
||||
".sStatus",
|
||||
sData.connected ? "success" : "error",
|
||||
sData.connected ? "green" : "red",
|
||||
sensorNode
|
||||
);
|
||||
setValue(".sName", sData.name, sensorNode);
|
||||
setValue(".sValue", "", sensorNode);
|
||||
|
||||
const statusNode = sensorNode.querySelector(`.sStatusContainer`);
|
||||
if (statusNode) {
|
||||
statusNode.dataset.tooltip = `${sData.signalQuality}%`;
|
||||
}
|
||||
|
||||
if (sData.value !== undefined) {
|
||||
const sUnit = purposeUnit(sData.purpose, unitSystem);
|
||||
appendValue(".sValue", `<b>${sData.value.toFixed(2)}</b> ${sUnit !== null ? sUnit : ``}`, `<br />`, sensorNode);
|
||||
}
|
||||
|
||||
if (sData.temperature !== undefined) {
|
||||
const sUnit = temperatureUnit(unitSystem);
|
||||
appendValue(".sValue", `${i18n('dashboard.sensors.values.temp')}: <b>${sData.temperature.toFixed(2)}</b> ${sUnit !== null ? sUnit : ``}`, `<br />`, sensorNode);
|
||||
}
|
||||
|
||||
if (sData.humidity !== undefined) {
|
||||
appendValue(".sValue", `${i18n('dashboard.sensors.values.humidity')}: <b>${sData.humidity.toFixed(2)}</b> %`, `<br />`, sensorNode);
|
||||
}
|
||||
|
||||
if (sData.battery !== undefined) {
|
||||
appendValue(".sValue", `${i18n('dashboard.sensors.values.battery')}: <b>${sData.battery.toFixed(2)}</b> %`, `<br />`, sensorNode);
|
||||
}
|
||||
|
||||
if (sData.rssi !== undefined) {
|
||||
appendValue(".sValue", `${i18n('dashboard.sensors.values.rssi')}: <b>${sData.rssi.toFixed(0)}</b> ${i18n('dbm')}`, `<br />`, sensorNode);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setTimeout(onLoadPage, 10000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
237
src_data/pages/index.html
Normal file
@@ -0,0 +1,237 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>index.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="cn">CN</option>
|
||||
<option value="it">IT</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>index.section.network</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="main-busy" aria-busy="true"></div>
|
||||
<table id="main-table" class="hidden">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.hostname</th>
|
||||
<td><b id="network-hostname"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.mac</th>
|
||||
<td><b id="network-mac"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.wifi.connected</th>
|
||||
<td><i id="network-connected"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.wifi.ssid</th>
|
||||
<td><b id="network-ssid"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.wifi.signal</th>
|
||||
<td><b id="network-signal"></b> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.ip</th>
|
||||
<td><b id="network-ip"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.subnet</th>
|
||||
<td><b id="network-subnet"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.gateway</th>
|
||||
<td><b id="network-gateway"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>network.params.dns</th>
|
||||
<td><b id="network-dns"></b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="grid">
|
||||
<a href="/network.html" role="button" data-i18n>network.name</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>index.section.system</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="system-busy" aria-busy="true"></div>
|
||||
<table id="system-table" class="hidden">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.build.version</th>
|
||||
<td><b id="build-version"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.build.title</th>
|
||||
<td>
|
||||
Env: <b id="build-env"></b><br />
|
||||
<span data-i18n>index.system.build.date</span>: <b id="build-date"></b><br />
|
||||
<span data-i18n>index.system.build.core</span>: <b id="build-core"></b><br />
|
||||
<span data-i18n>index.system.build.sdk</span>: <b id="build-sdk"></b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.uptime</th>
|
||||
<td>
|
||||
<b id="uptime-days"></b> <span data-i18n>time.days</span>,
|
||||
<b id="uptime-hours"></b> <span data-i18n>time.hours</span>,
|
||||
<b id="uptime-min"></b> <span data-i18n>time.min</span>,
|
||||
<b id="uptime-sec"></b> <span data-i18n>time.sec</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.memory.title</th>
|
||||
<td>
|
||||
<b id="heap-free"></b> of <b id="heap-total"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="heap-min-free"></b> bytes)<br />
|
||||
<span data-i18n>index.system.memory.maxFreeBlock</span>: <b id="heap-max-free-block"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="heap-min-max-free-block"></b> bytes)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.board</th>
|
||||
<td>
|
||||
<span data-i18n>index.system.chip.model</span>: <b id="chip-model"></b> (rev. <span id="chip-rev"></span>)<br />
|
||||
<span data-i18n>index.system.chip.cores</span>: <b id="chip-cores"></b>, <span data-i18n>index.system.chip.freq</span>: <b id="chip-freq"></b> mHz<br />
|
||||
<span data-i18n>index.system.flash.size</span>: <b id="flash-size"></b> MB (<span data-i18n>index.system.flash.realSize</span>: <b id="flash-real-size"></b> MB)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.lastResetReason</th>
|
||||
<td>
|
||||
<b id="reset-reason"></b><br />
|
||||
<a href="/api/debug" target="_blank"><small>Save debug data</small></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="grid" style="grid-template-columns: repeat(auto-fit,minmax(12rem,1fr)) !important;">
|
||||
<a href="/dashboard.html" role="button" data-i18n>dashboard.name</a>
|
||||
<a href="/settings.html" role="button" data-i18n>settings.name</a>
|
||||
<a href="/sensors.html" role="button" data-i18n>sensors.name</a>
|
||||
<a href="/upgrade.html" role="button" data-i18n>upgrade.name</a>
|
||||
<a href="/restart.html" role="button" class="secondary restart" data-i18n>button.restart</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
setTimeout(async function onLoadPage() {
|
||||
try {
|
||||
const response = await fetch("/api/info", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setValue('#reset-reason', result.system.resetReason);
|
||||
setValue('#uptime', result.system.uptime);
|
||||
setValue('#uptime-days', Math.floor(result.system.uptime / 86400));
|
||||
setValue('#uptime-hours', Math.floor(result.system.uptime % 86400 / 3600));
|
||||
setValue('#uptime-min', Math.floor(result.system.uptime % 3600 / 60));
|
||||
setValue('#uptime-sec', Math.floor(result.system.uptime % 60));
|
||||
|
||||
setValue('#network-hostname', result.network.hostname);
|
||||
setValue('#network-mac', result.network.mac);
|
||||
setStatus(
|
||||
'#network-connected',
|
||||
result.network.connected ? "success" : "error",
|
||||
result.network.connected ? "green" : "red"
|
||||
);
|
||||
setValue('#network-ssid', result.network.ssid);
|
||||
setValue('#network-signal', result.network.signalQuality);
|
||||
setValue('#network-ip', result.network.ip);
|
||||
setValue('#network-subnet', result.network.subnet);
|
||||
setValue('#network-gateway', result.network.gateway);
|
||||
setValue('#network-dns', result.network.dns);
|
||||
setBusy('#main-busy', '#main-table', false);
|
||||
|
||||
setValue('#build-version', result.build.version);
|
||||
setValue('#build-date', result.build.date);
|
||||
setValue('#build-env', result.build.env);
|
||||
setValue('#build-core', result.build.core);
|
||||
setValue('#build-sdk', result.build.sdk);
|
||||
|
||||
setValue('#heap-total', result.heap.total);
|
||||
setValue('#heap-free', result.heap.free);
|
||||
setValue('#heap-min-free', result.heap.minFree);
|
||||
setValue('#heap-max-free-block', result.heap.maxFreeBlock);
|
||||
setValue('#heap-min-max-free-block', result.heap.minMaxFreeBlock);
|
||||
|
||||
setValue('#chip-model', result.chip.model);
|
||||
setValue('#chip-rev', result.chip.rev);
|
||||
setValue('#chip-cores', result.chip.cores);
|
||||
setValue('#chip-freq', result.chip.freq);
|
||||
setValue('#flash-size', result.flash.size / 1024 / 1024);
|
||||
setValue('#flash-real-size', result.flash.realSize / 1024 / 1024);
|
||||
|
||||
setBusy('#system-busy', '#system-table', false);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setTimeout(onLoadPage, 10000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
227
src_data/pages/network.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>network.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="cn">CN</option>
|
||||
<option value="it">IT</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>network.name</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="network-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="network-settings" class="hidden">
|
||||
<label>
|
||||
<span data-i18n>network.params.hostname</span>
|
||||
<input type="text" name="hostname" maxlength="24" pattern="[A-Za-z0-9]+[A-Za-z0-9\-]+[A-Za-z0-9]+" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="useDhcp" value="true">
|
||||
<span data-i18n>network.params.dhcp</span>
|
||||
</label>
|
||||
<br />
|
||||
<hr />
|
||||
<h4 data-i18n>network.section.static</h4>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.params.ip</span>
|
||||
<input type="text" name="staticConfig[ip]" value="true" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.params.gateway</span>
|
||||
<input type="text" name="staticConfig[gateway]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.params.subnet</span>
|
||||
<input type="text" name="staticConfig[subnet]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.params.dns</span>
|
||||
<input type="text" name="staticConfig[dns]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
|
||||
</label>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h3 data-i18n>network.section.availableNetworks</h3>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/network/scan" id="network-scan">
|
||||
<div style="max-height: 25rem;" class="overflow-auto">
|
||||
<table id="networks" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-i18n>network.scan.pos</th>
|
||||
<th scope="col" data-i18n>network.wifi.ssid</th>
|
||||
<th scope="col" data-i18n>network.scan.info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.refresh</button>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>network.section.staSettings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="sta-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="sta-settings" class="hidden">
|
||||
<label>
|
||||
<span data-i18n>network.wifi.ssid</span>
|
||||
<input type="text" name="sta[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.wifi.password</span>
|
||||
<input type="password" name="sta[password]" maxlength="64" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.wifi.channel</span>
|
||||
<input type="number" inputmode="numeric" name="sta[channel]" min="0" max="12" step="1" required>
|
||||
<small data-i18n>network.sta.channel.note</small>
|
||||
</label>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>network.section.apSettings</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<div id="ap-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/network/settings" id="ap-settings" class="hidden">
|
||||
<label>
|
||||
<span data-i18n>network.wifi.ssid</span>
|
||||
<input type="text" name="ap[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.wifi.password</span>
|
||||
<input type="text" name="ap[password]" maxlength="64" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>network.wifi.channel</span>
|
||||
<input type="number" inputmode="numeric" name="ap[channel]" min="1" max="12" step="1" required>
|
||||
</label>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
const fillData = (data) => {
|
||||
setInputValue("[name='hostname']", data.hostname);
|
||||
setCheckboxValue("[name='useDhcp']", data.useDhcp);
|
||||
setInputValue("[name='staticConfig[ip]']", data.staticConfig.ip);
|
||||
setInputValue("[name='staticConfig[gateway]']", data.staticConfig.gateway);
|
||||
setInputValue("[name='staticConfig[subnet]']", data.staticConfig.subnet);
|
||||
setInputValue("[name='staticConfig[dns]']", data.staticConfig.dns);
|
||||
setBusy('#network-settings-busy', '#network-settings', false);
|
||||
|
||||
setInputValue("[name='sta[ssid]']", data.sta.ssid);
|
||||
setInputValue("[name='sta[password]']", data.sta.password);
|
||||
setInputValue("[name='sta[channel]']", data.sta.channel);
|
||||
setBusy('#sta-settings-busy', '#sta-settings', false);
|
||||
|
||||
setInputValue("[name='ap[ssid]']", data.ap.ssid);
|
||||
setInputValue("[name='ap[password]']", data.ap.password);
|
||||
setInputValue("[name='ap[channel]']", data.ap.channel);
|
||||
setBusy('#ap-settings-busy', '#ap-settings', false);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/network/settings", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
fillData(result);
|
||||
|
||||
setupForm('#network-settings', fillData, ['hostname']);
|
||||
setupNetworkScanForm('#network-scan', '#networks');
|
||||
setupForm('#sta-settings', fillData, ['sta.ssid', 'sta.password']);
|
||||
setupForm('#ap-settings', fillData, ['ap.ssid', 'ap.password']);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
333
src_data/pages/sensors.html
Normal file
@@ -0,0 +1,333 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>sensors.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="cn">CN</option>
|
||||
<option value="it">IT</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<hgroup>
|
||||
<h2 data-i18n>sensors.name</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<details id="template" class="sensor hidden" data-id="" data-preloaded="0">
|
||||
<summary><b>#<span class="pos"></span>: <span class="name"></span></b></summary>
|
||||
|
||||
<div>
|
||||
<div class="form-busy" aria-busy="true"></div>
|
||||
<form action="/api/sensor?id={id}" class="hidden">
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" role="switch" name="enabled" value="true">
|
||||
<span data-i18n>sensors.enabled</span>
|
||||
</label>
|
||||
|
||||
<br />
|
||||
|
||||
<label>
|
||||
<span data-i18n>sensors.sensorName.title</span>
|
||||
<input type="text" name="name" maxlength="32" required>
|
||||
<small data-i18n>sensors.sensorName.note</small>
|
||||
</label>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span data-i18n>sensors.purpose</span>
|
||||
<select name="purpose" required>
|
||||
<option value="0" data-i18n>sensors.purposes.outdoorTemp</option>
|
||||
<option value="1" data-i18n>sensors.purposes.indoorTemp</option>
|
||||
<option value="2" data-i18n>sensors.purposes.heatTemp</option>
|
||||
<option value="3" data-i18n>sensors.purposes.heatRetTemp</option>
|
||||
<option value="4" data-i18n>sensors.purposes.dhwTemp</option>
|
||||
<option value="5" data-i18n>sensors.purposes.dhwRetTemp</option>
|
||||
<option value="6" data-i18n>sensors.purposes.dhwFlowRate</option>
|
||||
<option value="7" data-i18n>sensors.purposes.exhaustTemp</option>
|
||||
<option value="8" data-i18n>sensors.purposes.modLevel</option>
|
||||
<option value="247" data-i18n>sensors.purposes.number</option>
|
||||
<option value="248" data-i18n>sensors.purposes.powerFactor</option>
|
||||
<option value="249" data-i18n>sensors.purposes.power</option>
|
||||
<option value="250" data-i18n>sensors.purposes.fanSpeed</option>
|
||||
<option value="251" data-i18n>sensors.purposes.co2</option>
|
||||
<option value="252" data-i18n>sensors.purposes.pressure</option>
|
||||
<option value="253" data-i18n>sensors.purposes.humidity</option>
|
||||
<option value="254" data-i18n>sensors.purposes.temperature</option>
|
||||
<option value="255" data-i18n>sensors.purposes.notConfigured</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>sensors.type</span>
|
||||
<select name="type" required>
|
||||
<option value="0" data-i18n>sensors.types.otOutdoorTemp</option>
|
||||
<option value="1" data-i18n>sensors.types.otHeatTemp</option>
|
||||
<option value="2" data-i18n>sensors.types.otHeatRetTemp</option>
|
||||
<option value="3" data-i18n>sensors.types.otDhwTemp</option>
|
||||
<option value="4" data-i18n>sensors.types.otDhwTemp2</option>
|
||||
<option value="5" data-i18n>sensors.types.otDhwFlowRate</option>
|
||||
<option value="6" data-i18n>sensors.types.otCh2Temp</option>
|
||||
<option value="7" data-i18n>sensors.types.otExhaustTemp</option>
|
||||
<option value="8" data-i18n>sensors.types.otHeatExchangerTemp</option>
|
||||
<option value="9" data-i18n>sensors.types.otPressure</option>
|
||||
<option value="10" data-i18n>sensors.types.otModLevel</option>
|
||||
<option value="11" data-i18n>sensors.types.otCurrentPower</option>
|
||||
<option value="12" data-i18n>sensors.types.otExhaustCo2</option>
|
||||
<option value="13" data-i18n>sensors.types.otExhaustFanSpeed</option>
|
||||
<option value="14" data-i18n>sensors.types.otSupplyFanSpeed</option>
|
||||
<option value="15" data-i18n>sensors.types.otSolarStorageTemp</option>
|
||||
<option value="16" data-i18n>sensors.types.otSolarCollectorTemp</option>
|
||||
<option value="17" data-i18n>sensors.types.otFanSpeedSetpoint</option>
|
||||
<option value="18" data-i18n>sensors.types.otFanSpeedCurrent</option>
|
||||
<option value="19" data-i18n>sensors.types.otBurnerStarts</option>
|
||||
<option value="20" data-i18n>sensors.types.otDhwBurnerStarts</option>
|
||||
<option value="21" data-i18n>sensors.types.otHeatingPumpStarts</option>
|
||||
<option value="22" data-i18n>sensors.types.otDhwPumpStarts</option>
|
||||
<option value="23" data-i18n>sensors.types.otBurnerHours</option>
|
||||
<option value="24" data-i18n>sensors.types.otDhwBurnerHours</option>
|
||||
<option value="25" data-i18n>sensors.types.otHeatingPumpHours</option>
|
||||
<option value="26" data-i18n>sensors.types.otDhwPumpHours</option>
|
||||
|
||||
<option value="50" data-i18n>sensors.types.ntcTemp</option>
|
||||
<option value="51" data-i18n>sensors.types.dallasTemp</option>
|
||||
<option value="52" data-i18n>sensors.types.bluetooth</option>
|
||||
<option value="253" data-i18n>sensors.types.heatSetpointTemp</option>
|
||||
<option value="254" data-i18n>sensors.types.manual</option>
|
||||
<option value="255" data-i18n>sensors.types.notConfigured</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span data-i18n>sensors.gpio</span>
|
||||
<input type="number" outputmode="numeric" name="gpio" min="0" max="254" step="1">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>sensors.address.title</span>
|
||||
<input type="text" name="address">
|
||||
<small data-i18n>sensors.address.note</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr class="correction" />
|
||||
|
||||
<details class="correction">
|
||||
<summary><b data-i18n>sensors.correction.desc</b></summary>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span data-i18n>sensors.correction.offset</span>
|
||||
<input type="number" inputmode="decimal" name="offset" min="-20" max="20" step="0.01" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>sensors.correction.factor</span>
|
||||
<input type="number" inputmode="decimal" name="factor" min="0.01" max="100" step="0.01" required>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr class="filtering" />
|
||||
|
||||
<details class="filtering">
|
||||
<summary><b data-i18n>sensors.filtering.desc</b></summary>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="filtering" value="true">
|
||||
<span data-i18n>sensors.filtering.enabled.title</span>
|
||||
<br />
|
||||
<small data-i18n>sensors.filtering.enabled.note</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span data-i18n>sensors.filtering.factor.title</span>
|
||||
<input type="number" inputmode="decimal" name="filteringFactor" min="0.01" max="1" step="0.01">
|
||||
<small data-i18n>sensors.filtering.factor.note</small>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const lang = new Lang(document.getElementById("lang"));
|
||||
lang.build();
|
||||
|
||||
const container = document.querySelector("article");
|
||||
const templateNode = container.querySelector("#template");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/sensors", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Response not valid");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
for (const sensorId in result) {
|
||||
const sensorNode = templateNode.cloneNode(true);
|
||||
sensorNode.removeAttribute("id");
|
||||
sensorNode.classList.remove("hidden");
|
||||
sensorNode.dataset.id = sensorId;
|
||||
setValue(".id", sensorId, sensorNode);
|
||||
setValue(".pos", parseInt(sensorId) + 1, sensorNode);
|
||||
setValue(".name", result[sensorId], sensorNode);
|
||||
|
||||
container.appendChild(sensorNode);
|
||||
container.appendChild(document.createElement("hr"));
|
||||
|
||||
const sensorForm = sensorNode.querySelector("form");
|
||||
const fillData = (data) => {
|
||||
setCheckboxValue("[name='enabled']", data.enabled, sensorForm);
|
||||
setInputValue("[name='name']", data.name, {}, sensorForm);
|
||||
setSelectValue("[name='purpose']", data.purpose, sensorForm);
|
||||
setSelectValue("[name='type']", data.type, sensorForm);
|
||||
setInputValue("[name='gpio']", data.gpio < 255 ? data.gpio : "", {}, sensorForm);
|
||||
setInputValue("[name='address']", data.address, {}, sensorForm);
|
||||
setInputValue("[name='offset']", data.offset, {}, sensorForm);
|
||||
setInputValue("[name='factor']", data.factor, {}, sensorForm);
|
||||
setCheckboxValue("[name='filtering']", data.filtering, sensorForm);
|
||||
setInputValue("[name='filteringFactor']", data.filteringFactor, {}, sensorForm);
|
||||
|
||||
sensorForm.querySelector("[name='type']").dispatchEvent(new Event("change"));
|
||||
|
||||
setBusy(".form-busy", "form", false, sensorNode);
|
||||
};
|
||||
|
||||
sensorForm.action = sensorForm.action.replace("{id}", sensorId);
|
||||
sensorForm.querySelector("[name='type']").addEventListener("change", async (event) => {
|
||||
const gpio = sensorForm.querySelector("[name='gpio']");
|
||||
const address = sensorForm.querySelector("[name='address']");
|
||||
const parentGpio = gpio.parentElement;
|
||||
const parentAddress = address.parentElement;
|
||||
|
||||
switch(parseInt(event.target.value)) {
|
||||
// heating setpoint, manual
|
||||
case 253:
|
||||
case 254:
|
||||
hide(".correction", sensorForm);
|
||||
hide(".filtering", sensorForm);
|
||||
break;
|
||||
|
||||
// other
|
||||
default:
|
||||
show(".correction", sensorForm);
|
||||
show(".filtering", sensorForm);
|
||||
}
|
||||
|
||||
switch(parseInt(event.target.value)) {
|
||||
// ntc
|
||||
case 50:
|
||||
parentGpio.classList.remove("hidden");
|
||||
parentAddress.classList.add("hidden");
|
||||
address.removeAttribute("pattern");
|
||||
break;
|
||||
|
||||
// dallas
|
||||
case 51:
|
||||
parentGpio.classList.remove("hidden");
|
||||
parentAddress.classList.remove("hidden");
|
||||
address.setAttribute("pattern", "([A-Fa-f0-9]{2}:){7}[A-Fa-f0-9]{2}");
|
||||
break;
|
||||
|
||||
// ble
|
||||
case 52:
|
||||
parentGpio.classList.add("hidden");
|
||||
parentAddress.classList.remove("hidden");
|
||||
address.setAttribute("pattern", "([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}");
|
||||
break;
|
||||
|
||||
// other
|
||||
default:
|
||||
parentGpio.classList.add("hidden");
|
||||
parentAddress.classList.add("hidden");
|
||||
address.removeAttribute("pattern");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
sensorNode.addEventListener("click", async (event) => {
|
||||
if (parseInt(sensorNode.dataset.preloaded)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(sensorForm.action, {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
fillData(result);
|
||||
sensorNode.dataset.preloaded = 1;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
setupForm(`.sensor[data-id='${sensorId}'] form`, fillData, ['address']);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1096
src_data/pages/settings.html
Normal file
114
src_data/pages/upgrade.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>upgrade.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="cn">CN</option>
|
||||
<option value="it">IT</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>upgrade.section.backupAndRestore</h2>
|
||||
<p data-i18n>upgrade.section.backupAndRestore.desc</p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/backup/restore" id="restore">
|
||||
<label for="restore-file">
|
||||
<span data-i18n>upgrade.settingsFile</span>
|
||||
<input type="file" name="settings" id="restore-file" accept=".json">
|
||||
</label>
|
||||
|
||||
<div role="group">
|
||||
<button type="submit" data-i18n>button.restore</button>
|
||||
<button type="button" class="secondary" onclick="window.location='/api/backup/save';" data-i18n>button.backup</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2 data-i18n>upgrade.section.upgrade</h2>
|
||||
<p data-i18n>upgrade.section.upgrade.desc</p>
|
||||
</hgroup>
|
||||
|
||||
<form action="/api/upgrade" id="upgrade">
|
||||
<fieldset class="primary">
|
||||
<label for="firmware-file">
|
||||
<span data-i18n>upgrade.fw</span>:
|
||||
<div class="grid">
|
||||
<input type="file" name="firmware" id="firmware-file" accept=".bin">
|
||||
<button type="button" class="upgrade-firmware-result hidden" disabled></button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="filesystem-file">
|
||||
<span data-i18n>upgrade.fs</span>:
|
||||
<div class="grid">
|
||||
<input type="file" name="filesystem" id="filesystem-file" accept=".bin">
|
||||
<button type="button" class="upgrade-filesystem-result hidden" disabled></button>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<ul>
|
||||
<li><mark data-i18n>upgrade.note.disclaimer1</mark></li>
|
||||
<li><mark data-i18n>upgrade.note.disclaimer2</mark></li>
|
||||
</ul>
|
||||
|
||||
<button type="submit" data-i18n>button.upgrade</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<small>
|
||||
<b>Made by Laxilef</b>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
|
||||
• <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
setupRestoreBackupForm('#restore');
|
||||
setupUpgradeForm('#upgrade');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
src_data/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
1
src_data/scripts/i18n.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(function(){var e,t,n,r=function(e,t){return function(){return e.apply(t,arguments)}};e=function(){function e(){this.translate=r(this.translate,this);this.data={values:{},contexts:[]};this.globalContext={}}e.prototype.translate=function(e,t,n,r,i){var s,o,u,a;if(i==null){i=this.globalContext}u=function(e){var t;t=typeof e;return t==="function"||t==="object"&&!!e};if(u(t)){s=null;a=null;o=t;i=n||this.globalContext}else{if(typeof t==="number"){s=null;a=t;o=n;i=r||this.globalContext}else{s=t;if(typeof n==="number"){a=n;o=r;i=i}else{a=null;o=n;i=r||this.globalContext}}}if(u(e)){if(u(e["i18n"])){e=e["i18n"]}return this.translateHash(e,i)}else{return this.translateText(e,a,o,i,s)}};e.prototype.add=function(e){var t,n,r,i,s,o,u,a;if(e.values!=null){o=e.values;for(n in o){r=o[n];this.data.values[n]=r}}if(e.contexts!=null){u=e.contexts;a=[];for(i=0,s=u.length;i<s;i++){t=u[i];a.push(this.data.contexts.push(t))}return a}};e.prototype.setContext=function(e,t){return this.globalContext[e]=t};e.prototype.clearContext=function(e){return this.lobalContext[e]=null};e.prototype.reset=function(){this.data={values:{},contexts:[]};return this.globalContext={}};e.prototype.resetData=function(){return this.data={values:{},contexts:[]}};e.prototype.resetContext=function(){return this.globalContext={}};e.prototype.translateHash=function(e,t){var n,r;for(n in e){r=e[n];if(typeof r==="string"){e[n]=this.translateText(r,null,null,t)}}return e};e.prototype.translateText=function(e,t,n,r,i){var s,o;if(r==null){r=this.globalContext}if(this.data==null){return this.useOriginalText(i||e,t,n)}s=this.getContextData(this.data,r);if(s!=null){o=this.findTranslation(e,t,n,s.values,i)}if(o==null){o=this.findTranslation(e,t,n,this.data.values,i)}if(o==null){return this.useOriginalText(i||e,t,n)}return o};e.prototype.findTranslation=function(e,t,n,r){var i,s,o,u,a;o=r[e];if(o==null){return null}if(t==null){if(typeof o==="string"){return this.applyFormatting(o,t,n)}}else{if(o instanceof Array||o.length){for(u=0,a=o.length;u<a;u++){s=o[u];if((t>=s[0]||s[0]===null)&&(t<=s[1]||s[1]===null)){i=this.applyFormatting(s[2].replace("-%n",String(-t)),t,n);return this.applyFormatting(i.replace("%n",String(t)),t,n)}}}}return null};e.prototype.getContextData=function(e,t){var n,r,i,s,o,u,a,f;if(e.contexts==null){return null}a=e.contexts;for(o=0,u=a.length;o<u;o++){n=a[o];r=true;f=n.matches;for(i in f){s=f[i];r=r&&s===t[i]}if(r){return n}}return null};e.prototype.useOriginalText=function(e,t,n){if(t==null){return this.applyFormatting(e,t,n)}return this.applyFormatting(e.replace("%n",String(t)),t,n)};e.prototype.applyFormatting=function(e,t,n){var r,i;for(r in n){i=new RegExp("%{"+r+"}","g");e=e.replace(i,n[r])}return e};return e}();n=new e;t=n.translate;t.translator=n;t.create=function(n){var r;r=new e;if(n!=null){r.add(n)}r.translate.create=t.create;return r.translate};(typeof module!=="undefined"&&module!==null?module.exports=t:void 0)||(this.i18n=t)}).call(this)
|
||||
128
src_data/scripts/lang.js
Normal file
@@ -0,0 +1,128 @@
|
||||
class Lang {
|
||||
constructor(switcher, defaultLocale = null) {
|
||||
if (!(switcher instanceof Object)) {
|
||||
throw new SyntaxError("switcher must be an element object");
|
||||
}
|
||||
|
||||
this.switcher = switcher;
|
||||
this.defaultLocale = defaultLocale;
|
||||
this.supportedLocales = [];
|
||||
this.currentLocale = null;
|
||||
}
|
||||
|
||||
async build() {
|
||||
this.bindSwitcher();
|
||||
|
||||
const userLocale = localStorage.getItem('locale');
|
||||
if (this.localeIsSupported(userLocale)) {
|
||||
await this.setLocale(userLocale);
|
||||
|
||||
} else {
|
||||
const initialLocale = this.getSuitableLocale(this.browserLocales(true));
|
||||
await this.setLocale(initialLocale);
|
||||
}
|
||||
|
||||
this.translatePage();
|
||||
}
|
||||
|
||||
bindSwitcher() {
|
||||
this.supportedLocales = [];
|
||||
for (const option of this.switcher.options) {
|
||||
this.supportedLocales.push(option.value);
|
||||
}
|
||||
|
||||
if (!this.localeIsSupported(this.defaultLocale)) {
|
||||
const selected = this.switcher.selectedIndex ? this.switcher.selectedIndex : 0;
|
||||
this.defaultLocale = this.switcher.options[selected].value;
|
||||
}
|
||||
|
||||
this.switcher.addEventListener('change', async (element) => {
|
||||
await this.setLocale(element.target.value);
|
||||
this.translatePage();
|
||||
});
|
||||
}
|
||||
|
||||
async setLocale(newLocale) {
|
||||
if (this.currentLocale == newLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
i18n.translator.reset();
|
||||
i18n.translator.add(await this.fetchTranslations(newLocale));
|
||||
|
||||
this.currentLocale = newLocale;
|
||||
localStorage.setItem('locale', this.currentLocale);
|
||||
|
||||
if (document.documentElement) {
|
||||
document.documentElement.setAttribute("lang", this.currentLocale);
|
||||
}
|
||||
|
||||
if (this.switcher.value != this.currentLocale) {
|
||||
this.switcher.value = this.currentLocale;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTranslations(locale) {
|
||||
const response = await fetch(`/static/locales/${locale}.json?{BUILD_TIME}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.values instanceof Object) {
|
||||
data.values = this.flattenKeys({keys: data.values, prefix: ''});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
translatePage() {
|
||||
document
|
||||
.querySelectorAll("[data-i18n]")
|
||||
.forEach((element) => this.translateElement(element));
|
||||
}
|
||||
|
||||
translateElement(element) {
|
||||
let key = element.getAttribute("data-i18n");
|
||||
if (!key && element.innerHTML) {
|
||||
key = element.innerHTML;
|
||||
element.setAttribute("data-i18n", key);
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arg = element.getAttribute("data-i18n-arg") || null;
|
||||
const options = JSON.parse(element.getAttribute("data-i18n-options")) || null;
|
||||
|
||||
element.innerHTML = i18n(key, arg, options);
|
||||
}
|
||||
|
||||
|
||||
localeIsSupported(locale) {
|
||||
return locale !== null && this.supportedLocales.indexOf(locale) > -1;
|
||||
}
|
||||
|
||||
getSuitableLocale(locales) {
|
||||
return locales.find(this.localeIsSupported, this) || this.defaultLocale;
|
||||
}
|
||||
|
||||
browserLocales(codeOnly = false) {
|
||||
return navigator.languages.map((locale) =>
|
||||
codeOnly ? locale.split("-")[0] : locale,
|
||||
);
|
||||
}
|
||||
|
||||
flattenKeys({ keys, prefix }) {
|
||||
let result = {};
|
||||
for (let key in keys) {
|
||||
const type = typeof keys[key];
|
||||
if (type === 'string') {
|
||||
result[`${prefix}${key}`] = keys[key];
|
||||
}
|
||||
else if (type === 'object') {
|
||||
result = { ...result, ...this.flattenKeys({ keys: keys[key], prefix: `${prefix}${key}.` }) }
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
856
src_data/scripts/utils.js
Normal file
@@ -0,0 +1,856 @@
|
||||
const setupForm = (formSelector, onResultCallback = null, noCastItems = []) => {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.querySelectorAll('input').forEach(item => {
|
||||
item.addEventListener('change', (e) => {
|
||||
e.target.setAttribute('aria-invalid', !e.target.checkValidity());
|
||||
})
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.textContent = i18n("button.wait");
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = (result) => {
|
||||
if (button) {
|
||||
button.textContent = i18n('button.saved');
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = () => {
|
||||
if (button) {
|
||||
button.textContent = i18n('button.error');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let checkboxes = form.querySelectorAll('input[type="checkbox"]');
|
||||
for (let checkbox of checkboxes) {
|
||||
fd.append(checkbox.getAttribute('name'), checkbox.checked);
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: form2json(fd, noCastItems)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = response.status != 204 ? (await response.json()) : null;
|
||||
onSuccess(result);
|
||||
|
||||
if (onResultCallback instanceof Function) {
|
||||
onResultCallback(result);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
onFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const setupNetworkScanForm = (formSelector, tableSelector) => {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
console.error("form not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
const onSubmitFn = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (button) {
|
||||
defaultText = button.innerHTML;
|
||||
button.innerHTML = i18n('button.wait');
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
let table = document.querySelector(tableSelector);
|
||||
if (!table) {
|
||||
console.error("table not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const onSuccess = async (response) => {
|
||||
let result = await response.json();
|
||||
console.log('networks: ', result);
|
||||
|
||||
let tbody = table.querySelector('tbody');
|
||||
if (!tbody) {
|
||||
tbody = table.createTBody();
|
||||
}
|
||||
|
||||
while (tbody.rows.length > 0) {
|
||||
tbody.rows[0].remove();
|
||||
}
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let row = tbody.insertRow(-1);
|
||||
row.classList.add("network");
|
||||
row.dataset.ssid = result[i].hidden ? '' : result[i].ssid;
|
||||
row.insertCell().textContent = `#${i + 1}`;
|
||||
|
||||
const nameCell = row.insertCell();
|
||||
nameCell.innerHTML = result[i].hidden ? `<i>${result[i].bssid}</i>` : result[i].ssid;
|
||||
nameCell.onclick = (event) => {
|
||||
const input = document.querySelector("[name='sta[ssid]']");
|
||||
const ssid = event.target.parentNode.dataset.ssid;
|
||||
if (!input || !ssid) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = ssid;
|
||||
input.focus();
|
||||
};
|
||||
|
||||
// info cell
|
||||
let infoCell = row.insertCell();
|
||||
|
||||
// signal quality
|
||||
let signalQualityIcon = document.createElement("i");
|
||||
if (result[i].signalQuality > 80) {
|
||||
signalQualityIcon.classList.add('icons-wifi-strength-4');
|
||||
} else if (result[i].signalQuality > 60) {
|
||||
signalQualityIcon.classList.add('icons-wifi-strength-3');
|
||||
} else if (result[i].signalQuality > 40) {
|
||||
signalQualityIcon.classList.add('icons-wifi-strength-2');
|
||||
} else if (result[i].signalQuality > 20) {
|
||||
signalQualityIcon.classList.add('icons-wifi-strength-1');
|
||||
} else {
|
||||
signalQualityIcon.classList.add('icons-wifi-strength-0');
|
||||
}
|
||||
|
||||
let signalQualityContainer = document.createElement("span");
|
||||
signalQualityContainer.dataset.tooltip = `${result[i].signalQuality}%`;
|
||||
signalQualityContainer.appendChild(signalQualityIcon);
|
||||
infoCell.appendChild(signalQualityContainer);
|
||||
|
||||
// auth
|
||||
const authList = {
|
||||
0: "Open",
|
||||
1: "WEP",
|
||||
2: "WPA",
|
||||
3: "WPA2",
|
||||
4: "WPA/WPA2",
|
||||
5: "WPA/WPA2 Enterprise",
|
||||
6: "WPA3",
|
||||
7: "WPA2/WPA3",
|
||||
8: "WAPI",
|
||||
9: "OWE",
|
||||
10: "WPA3 Enterprise"
|
||||
};
|
||||
let authIcon = document.createElement("i");
|
||||
|
||||
if (result[i].auth == 0) {
|
||||
authIcon.classList.add('icons-unlocked');
|
||||
} else {
|
||||
authIcon.classList.add('icons-locked');
|
||||
}
|
||||
|
||||
let authContainer = document.createElement("span");
|
||||
authContainer.dataset.tooltip = (result[i].auth in authList) ? authList[result[i].auth] : "unknown";
|
||||
authContainer.appendChild(authIcon);
|
||||
infoCell.appendChild(authContainer);
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = async (response) => {
|
||||
table.classList.remove('hidden');
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
};
|
||||
|
||||
let attempts = 6;
|
||||
let attemptFn = async () => {
|
||||
attempts--;
|
||||
|
||||
try {
|
||||
let response = await fetch(url, {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (response.status == 200) {
|
||||
await onSuccess(response);
|
||||
|
||||
} else if (attempts <= 0) {
|
||||
await onFailed(response);
|
||||
|
||||
} else {
|
||||
setTimeout(attemptFn, 5000);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (attempts <= 0) {
|
||||
onFailed(err);
|
||||
|
||||
} else {
|
||||
setTimeout(attemptFn, 10000);
|
||||
}
|
||||
}
|
||||
};
|
||||
attemptFn();
|
||||
};
|
||||
|
||||
form.addEventListener('submit', onSubmitFn);
|
||||
onSubmitFn();
|
||||
}
|
||||
|
||||
const setupRestoreBackupForm = (formSelector) => {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.textContent = i18n('button.wait');
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
if (button) {
|
||||
button.textContent = i18n('button.restored');
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = () => {
|
||||
if (button) {
|
||||
button.textContent = i18n('button.error');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const files = form.querySelector('#restore-file').files;
|
||||
if (files.length <= 0) {
|
||||
onFailed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(files[0]);
|
||||
reader.onload = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.target.result);
|
||||
console.log("Backup: ", data);
|
||||
|
||||
if (data.settings != undefined) {
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({"settings": data.settings})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
onFailed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.sensors != undefined) {
|
||||
for (const sensorId in data.sensors) {
|
||||
const payload = {
|
||||
"sensors": {}
|
||||
};
|
||||
payload["sensors"][sensorId] = data.sensors[sensorId];
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
onFailed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.network != undefined) {
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({"network": data.network})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
onFailed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
|
||||
} catch (err) {
|
||||
onFailed();
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.log(reader.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const setupUpgradeForm = (formSelector) => {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
const statusToText = (status) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "None";
|
||||
case 1:
|
||||
return "No file";
|
||||
case 2:
|
||||
return "Success";
|
||||
case 3:
|
||||
return "Prohibited";
|
||||
case 4:
|
||||
return "Aborted";
|
||||
case 5:
|
||||
return "Error on start";
|
||||
case 6:
|
||||
return "Error on write";
|
||||
case 7:
|
||||
return "Error on finish";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const onResult = async (response) => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let resItem = form.querySelector('.upgrade-firmware-result');
|
||||
if (resItem && result.firmware.status > 1) {
|
||||
resItem.textContent = statusToText(result.firmware.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.firmware.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.firmware.error != "") {
|
||||
resItem.textContent += `: ${result.firmware.error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resItem = form.querySelector('.upgrade-filesystem-result');
|
||||
if (resItem && result.filesystem.status > 1) {
|
||||
resItem.textContent = statusToText(result.filesystem.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.filesystem.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.filesystem.error != "") {
|
||||
resItem.textContent += `: ${result.filesystem.error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSuccess = (response) => {
|
||||
onResult(response);
|
||||
|
||||
if (button) {
|
||||
button.textContent = defaultText;
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (response) => {
|
||||
if (button) {
|
||||
button.textContent = i18n('button.error');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
hide('.upgrade-firmware-result');
|
||||
hide('.upgrade-filesystem-result');
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.textContent = i18n('button.uploading');
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
body: fd
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 500) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const setBusy = (busySelector, contentSelector, value, parent = undefined) => {
|
||||
if (!value) {
|
||||
hide(busySelector, parent);
|
||||
show(contentSelector, parent);
|
||||
|
||||
} else {
|
||||
show(busySelector, parent);
|
||||
hide(contentSelector, parent);
|
||||
}
|
||||
}
|
||||
|
||||
const setAriaState = (selector, value, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let item = parent.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setAttribute('aria-invalid', !value);
|
||||
}
|
||||
|
||||
const setStatus = (selector, state, color = undefined, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let item = parent.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.classList.forEach(cName => {
|
||||
if (cName.indexOf("icons-") === 0) {
|
||||
item.classList.remove(cName);
|
||||
}
|
||||
});
|
||||
|
||||
item.classList.add(`icons-${state}`);
|
||||
|
||||
if (color !== undefined) {
|
||||
item.classList.add(`icons-color-${color}`);
|
||||
}
|
||||
}
|
||||
|
||||
const setState = (selector, state, parent = undefined) => {
|
||||
return setStatus(
|
||||
selector,
|
||||
state ? "success" : "error",
|
||||
state ? "green" : "gray",
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
const setValue = (selector, value, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
item.innerHTML = value;
|
||||
}
|
||||
}
|
||||
|
||||
const appendValue = (selector, value, nl = null, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
if (item.innerHTML.trim().length > 0) {
|
||||
if (nl !== null) {
|
||||
item.innerHTML += nl;
|
||||
}
|
||||
|
||||
item.innerHTML += value;
|
||||
|
||||
} else {
|
||||
item.innerHTML = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setCheckboxValue = (selector, value, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let item = parent.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.checked = value;
|
||||
}
|
||||
|
||||
const setRadioValue = (selector, value, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
item.checked = item.value == value;
|
||||
}
|
||||
}
|
||||
|
||||
const setInputValue = (selector, value, attrs = {}, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
item.value = value;
|
||||
|
||||
if (attrs instanceof Object) {
|
||||
for (let attrKey of Object.keys(attrs)) {
|
||||
item.setAttribute(attrKey, attrs[attrKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setSelectValue = (selector, value, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let item = parent.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let option of item.options) {
|
||||
option.selected = option.value == value;
|
||||
}
|
||||
}
|
||||
|
||||
const show = (selector, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
if (item.classList.contains('hidden')) {
|
||||
item.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hide = (selector, parent = undefined) => {
|
||||
if (parent === undefined) {
|
||||
parent = document;
|
||||
}
|
||||
|
||||
let items = parent.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
if (!item.classList.contains('hidden')) {
|
||||
item.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unit2str(unitSystem, units = {}, defaultValue = '?') {
|
||||
return (unitSystem in units)
|
||||
? units[unitSystem]
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
const temperatureUnit = (unitSystem) => {
|
||||
return unit2str(unitSystem, {
|
||||
0: "°C",
|
||||
1: "°F"
|
||||
});
|
||||
}
|
||||
|
||||
const pressureUnit = (unitSystem) => {
|
||||
return unit2str(unitSystem, {
|
||||
0: "bar",
|
||||
1: "psi"
|
||||
});
|
||||
}
|
||||
|
||||
const volumeUnit = (unitSystem) => {
|
||||
return unit2str(unitSystem, {
|
||||
0: "L",
|
||||
1: "gal"
|
||||
});
|
||||
}
|
||||
|
||||
const purposeUnit = (purpose, unitSystem) => {
|
||||
const tUnit = temperatureUnit(unitSystem);
|
||||
|
||||
return unit2str(purpose, {
|
||||
0: tUnit,
|
||||
1: tUnit,
|
||||
2: tUnit,
|
||||
3: tUnit,
|
||||
4: tUnit,
|
||||
5: tUnit,
|
||||
6: `${volumeUnit(unitSystem)}/${i18n('time.min')}`,
|
||||
7: tUnit,
|
||||
8: "%",
|
||||
248: "%",
|
||||
249: i18n('kw'),
|
||||
250: "RPM",
|
||||
251: "ppm",
|
||||
252: pressureUnit(unitSystem),
|
||||
253: "%",
|
||||
254: tUnit
|
||||
}, null);
|
||||
}
|
||||
|
||||
const memberIdToVendor = (memberId) => {
|
||||
// https://github.com/Jeroen88/EasyOpenTherm/blob/main/src/EasyOpenTherm.h
|
||||
// https://github.com/Evgen2/SmartTherm/blob/v0.7/src/Web.cpp
|
||||
const vendorList = {
|
||||
1: "Baxi",
|
||||
2: "AWB/Brink/Viessmann",
|
||||
4: "ATAG/Baxi/Brötje/ELCO/GEMINOX",
|
||||
5: "Itho Daalderop",
|
||||
6: "IDEAL",
|
||||
8: "Buderus/Bosch/Hoval",
|
||||
9: "Ferroli",
|
||||
11: "Remeha/De Dietrich",
|
||||
16: "Unical",
|
||||
24: "Vaillant/Bulex",
|
||||
27: "Baxi",
|
||||
29: "Itho Daalderop",
|
||||
33: "Viessmann",
|
||||
41: "Italtherm/Radiant",
|
||||
56: "Baxi",
|
||||
131: "Nefit",
|
||||
148: "Navien",
|
||||
173: "Intergas",
|
||||
247: "Baxi Ampera",
|
||||
248: "Zota Lux-X"
|
||||
};
|
||||
|
||||
return (memberId in vendorList)
|
||||
? vendorList[memberId]
|
||||
: "unknown vendor";
|
||||
}
|
||||
|
||||
function form2json(data, noCastItems = []) {
|
||||
let method = function (object, pair) {
|
||||
let keys = pair[0].replace(/\]/g, '').split('[');
|
||||
let key = keys[0];
|
||||
let value = pair[1];
|
||||
|
||||
if (!noCastItems.includes(keys.join('.'))) {
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = value === 'true';
|
||||
|
||||
} else if (typeof (value) === 'string' && value.trim() !== '' && !isNaN(value)) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (keys.length > 1) {
|
||||
let i, x, segment;
|
||||
let last = value;
|
||||
let type = isNaN(keys[1]) ? {} : [];
|
||||
value = segment = object[key] || type;
|
||||
|
||||
for (i = 1; i < keys.length; i++) {
|
||||
x = keys[i];
|
||||
if (i == keys.length - 1) {
|
||||
if (Array.isArray(segment)) {
|
||||
segment.push(last);
|
||||
} else {
|
||||
segment[x] = last;
|
||||
}
|
||||
} else if (segment[x] == undefined) {
|
||||
segment[x] = isNaN(keys[i + 1]) ? {} : [];
|
||||
}
|
||||
segment = segment[x];
|
||||
}
|
||||
}
|
||||
|
||||
object[key] = value;
|
||||
return object;
|
||||
}
|
||||
|
||||
let object = Array.from(data).reduce(method, {});
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
function dec2hex(i) {
|
||||
let hex = parseInt(i).toString(16);
|
||||
if (hex.length % 2 != 0) {
|
||||
hex = `0${hex}`;
|
||||
}
|
||||
|
||||
return hex.toUpperCase();
|
||||
}
|
||||
|
||||
function constrain(amt, low, high) {
|
||||
return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header,
|
||||
main,
|
||||
footer {
|
||||
padding-top: 1rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.success {
|
||||
background-color: var(--pico-form-element-valid-border-color);
|
||||
border-color: var(--pico-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
button.failed {
|
||||
background-color: var(--pico-form-element-invalid-border-color);
|
||||
border-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
tr.network:hover {
|
||||
--pico-background-color: var(--pico-primary-focus);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.greatSignal {
|
||||
background-color: var(--pico-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
.normalSignal {
|
||||
background-color: #e48500;
|
||||
}
|
||||
|
||||
.badSignal {
|
||||
background-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: 0.25rem solid var(--pico-form-element-invalid-border-color);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal);
|
||||
vertical-align: baseline;
|
||||
line-height: var(--pico-line-height);
|
||||
background-color: var(--pico-code-kbd-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
color: var(--pico-code-kbd-color);
|
||||
font-weight: bolder;
|
||||
font-size: 1.3rem;
|
||||
font-family: var(--pico-font-family-monospace);
|
||||
}
|
||||
|
||||
nav li a:has(> div.logo) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1,680 +0,0 @@
|
||||
function setupForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.querySelectorAll('input').forEach(item => {
|
||||
item.addEventListener('change', (e) => {
|
||||
e.target.setAttribute('aria-invalid', !e.target.checkValidity());
|
||||
})
|
||||
});
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = (response) => {
|
||||
if (button) {
|
||||
button.textContent = 'Saved';
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (response) => {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let checkboxes = form.querySelectorAll('input[type="checkbox"]');
|
||||
for (let checkbox of checkboxes) {
|
||||
fd.append(checkbox.getAttribute('name'), checkbox.checked);
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: form2json(fd)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupNetworkScanForm(formSelector, tableSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
console.error("form not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.innerHTML;
|
||||
}
|
||||
|
||||
const onSubmitFn = async (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
let table = document.querySelector(tableSelector);
|
||||
if (!table) {
|
||||
console.error("table not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const onSuccess = async (response) => {
|
||||
let result = await response.json();
|
||||
console.log('networks: ', result);
|
||||
|
||||
let tbody = table.querySelector('tbody');
|
||||
if (!tbody) {
|
||||
tbody = table.createTBody();
|
||||
}
|
||||
|
||||
while (tbody.rows.length > 0) {
|
||||
tbody.rows[0].remove();
|
||||
}
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let row = tbody.insertRow(-1);
|
||||
row.classList.add("network");
|
||||
row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid);
|
||||
row.onclick = function () {
|
||||
const input = document.querySelector('input.sta-ssid');
|
||||
const ssid = this.getAttribute('data-ssid');
|
||||
if (!input || !ssid) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = ssid;
|
||||
input.focus();
|
||||
};
|
||||
|
||||
row.insertCell().textContent = "#" + (i + 1);
|
||||
row.insertCell().innerHTML = result[i].hidden ? '<i>Hidden</i>' : result[i].ssid;
|
||||
|
||||
const signalCell = row.insertCell();
|
||||
const signalElement = document.createElement("kbd");
|
||||
signalElement.textContent = result[i].signalQuality + "%";
|
||||
if (result[i].signalQuality > 60) {
|
||||
signalElement.classList.add('greatSignal');
|
||||
} else if (result[i].signalQuality > 40) {
|
||||
signalElement.classList.add('normalSignal');
|
||||
} else {
|
||||
signalElement.classList.add('badSignal');
|
||||
}
|
||||
signalCell.appendChild(signalElement);
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = async (response) => {
|
||||
table.classList.remove('hidden');
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
};
|
||||
|
||||
let attempts = 6;
|
||||
let attemptFn = async () => {
|
||||
attempts--;
|
||||
|
||||
try {
|
||||
let response = await fetch(url, { cache: 'no-cache' });
|
||||
|
||||
if (response.status == 200) {
|
||||
await onSuccess(response);
|
||||
|
||||
} else if (attempts <= 0) {
|
||||
await onFailed(response);
|
||||
|
||||
} else {
|
||||
setTimeout(attemptFn, 5000);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (attempts <= 0) {
|
||||
onFailed(err);
|
||||
|
||||
} else {
|
||||
setTimeout(attemptFn, 10000);
|
||||
}
|
||||
}
|
||||
};
|
||||
attemptFn();
|
||||
};
|
||||
|
||||
form.addEventListener('submit', onSubmitFn);
|
||||
onSubmitFn();
|
||||
}
|
||||
|
||||
function setupRestoreBackupForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = (response) => {
|
||||
if (button) {
|
||||
button.textContent = 'Restored';
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (response) => {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const files = form.querySelector('#restore-file').files;
|
||||
if (files.length <= 0) {
|
||||
onFailed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(files[0]);
|
||||
reader.onload = async function () {
|
||||
try {
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: reader.result
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
};
|
||||
reader.onerror = function () {
|
||||
console.log(reader.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setupUpgradeForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
}
|
||||
|
||||
const statusToText = (status) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "None";
|
||||
case 1:
|
||||
return "No file";
|
||||
case 2:
|
||||
return "Success";
|
||||
case 3:
|
||||
return "Prohibited";
|
||||
case 4:
|
||||
return "Aborted";
|
||||
case 5:
|
||||
return "Error on start";
|
||||
case 6:
|
||||
return "Error on write";
|
||||
case 7:
|
||||
return "Error on finish";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const onResult = async (response) => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let resItem = form.querySelector('.upgrade-firmware-result');
|
||||
if (resItem && result.firmware.status > 1) {
|
||||
resItem.textContent = statusToText(result.firmware.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.firmware.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.firmware.error != "") {
|
||||
resItem.textContent += ": " + result.firmware.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resItem = form.querySelector('.upgrade-filesystem-result');
|
||||
if (resItem && result.filesystem.status > 1) {
|
||||
resItem.textContent = statusToText(result.filesystem.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.filesystem.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.filesystem.error != "") {
|
||||
resItem.textContent += ": " + result.filesystem.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSuccess = (response) => {
|
||||
onResult(response);
|
||||
|
||||
if (button) {
|
||||
button.textContent = defaultText;
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (response) => {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
setTimeout(() => {
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Uploading...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
body: fd
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 500) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNetworkStatus() {
|
||||
let response = await fetch('/api/network/status', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setValue('.network-hostname', result.hostname);
|
||||
setValue('.network-mac', result.mac);
|
||||
setState('.network-connected', result.isConnected);
|
||||
setValue('.network-ssid', result.ssid);
|
||||
setValue('.network-signal', result.signalQuality);
|
||||
setValue('.network-ip', result.ip);
|
||||
setValue('.network-subnet', result.subnet);
|
||||
setValue('.network-gateway', result.gateway);
|
||||
setValue('.network-dns', result.dns);
|
||||
|
||||
setBusy('.main-busy', '.main-table', false);
|
||||
}
|
||||
|
||||
async function loadNetworkSettings() {
|
||||
let response = await fetch('/api/network/settings', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setInputValue('.network-hostname', result.hostname);
|
||||
setCheckboxValue('.network-use-dhcp', result.useDhcp);
|
||||
setInputValue('.network-static-ip', result.staticConfig.ip);
|
||||
setInputValue('.network-static-gateway', result.staticConfig.gateway);
|
||||
setInputValue('.network-static-subnet', result.staticConfig.subnet);
|
||||
setInputValue('.network-static-dns', result.staticConfig.dns);
|
||||
setBusy('#network-settings-busy', '#network-settings', false);
|
||||
|
||||
setInputValue('.sta-ssid', result.sta.ssid);
|
||||
setInputValue('.sta-password', result.sta.password);
|
||||
setInputValue('.sta-channel', result.sta.channel);
|
||||
setBusy('#sta-settings-busy', '#sta-settings', false);
|
||||
|
||||
setInputValue('.ap-ssid', result.ap.ssid);
|
||||
setInputValue('.ap-password', result.ap.password);
|
||||
setInputValue('.ap-channel', result.ap.channel);
|
||||
setBusy('#ap-settings-busy', '#ap-settings', false);
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
let response = await fetch('/api/settings', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setCheckboxValue('.system-debug', result.system.debug);
|
||||
setCheckboxValue('.system-use-serial', result.system.useSerial);
|
||||
setCheckboxValue('.system-use-telnet', result.system.useTelnet);
|
||||
setBusy('#system-settings-busy', '#system-settings', false);
|
||||
|
||||
setCheckboxValue('.portal-use-auth', result.portal.useAuth);
|
||||
setInputValue('.portal-login', result.portal.login);
|
||||
setInputValue('.portal-password', result.portal.password);
|
||||
setBusy('#portal-settings-busy', '#portal-settings', false);
|
||||
|
||||
setInputValue('.opentherm-in-pin', result.opentherm.inPin);
|
||||
setInputValue('.opentherm-out-pin', result.opentherm.outPin);
|
||||
setInputValue('.opentherm-member-id-code', result.opentherm.memberIdCode);
|
||||
setCheckboxValue('.opentherm-dhw-present', result.opentherm.dhwPresent);
|
||||
setCheckboxValue('.opentherm-sw-mode', result.opentherm.summerWinterMode);
|
||||
setCheckboxValue('.opentherm-heating-ch2-enabled', result.opentherm.heatingCh2Enabled);
|
||||
setCheckboxValue('.opentherm-heating-ch1-to-ch2', result.opentherm.heatingCh1ToCh2);
|
||||
setCheckboxValue('.opentherm-dhw-to-ch2', result.opentherm.dhwToCh2);
|
||||
setCheckboxValue('.opentherm-dhw-blocking', result.opentherm.dhwBlocking);
|
||||
setCheckboxValue('.opentherm-sync-modulation-with-heating', result.opentherm.modulationSyncWithHeating);
|
||||
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
|
||||
|
||||
setInputValue('.mqtt-server', result.mqtt.server);
|
||||
setInputValue('.mqtt-port', result.mqtt.port);
|
||||
setInputValue('.mqtt-user', result.mqtt.user);
|
||||
setInputValue('.mqtt-password', result.mqtt.password);
|
||||
setInputValue('.mqtt-prefix', result.mqtt.prefix);
|
||||
setInputValue('.mqtt-interval', result.mqtt.interval);
|
||||
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
|
||||
|
||||
setRadioValue('.outdoor-sensor-type', result.sensors.outdoor.type);
|
||||
setInputValue('.outdoor-sensor-pin', result.sensors.outdoor.pin);
|
||||
setInputValue('.outdoor-sensor-offset', result.sensors.outdoor.offset);
|
||||
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
|
||||
|
||||
setRadioValue('.indoor-sensor-type', result.sensors.indoor.type);
|
||||
setInputValue('.indoor-sensor-pin', result.sensors.indoor.pin);
|
||||
setInputValue('.indoor-sensor-offset', result.sensors.indoor.offset);
|
||||
setInputValue('.indoor-sensor-ble-addresss', result.sensors.indoor.bleAddresss);
|
||||
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
|
||||
|
||||
setCheckboxValue('.extpump-use', result.externalPump.use);
|
||||
setInputValue('.extpump-pin', result.externalPump.pin);
|
||||
setInputValue('.extpump-pc-time', result.externalPump.postCirculationTime);
|
||||
setInputValue('.extpump-as-interval', result.externalPump.antiStuckInterval);
|
||||
setInputValue('.extpump-as-time', result.externalPump.antiStuckTime);
|
||||
setBusy('#extpump-settings-busy', '#extpump-settings', false);
|
||||
}
|
||||
|
||||
async function loadVars() {
|
||||
let response = await fetch('/api/vars');
|
||||
let result = await response.json();
|
||||
|
||||
setState('.ot-connected', result.states.otStatus);
|
||||
setState('.ot-emergency', result.states.emergency);
|
||||
setState('.ot-heating', result.states.heating);
|
||||
setState('.ot-dhw', result.states.dhw);
|
||||
setState('.ot-flame', result.states.flame);
|
||||
setState('.ot-fault', result.states.fault);
|
||||
setState('.ot-diagnostic', result.states.diagnostic);
|
||||
setState('.ot-external-pump', result.states.externalPump);
|
||||
|
||||
setValue('.ot-modulation', result.sensors.modulation);
|
||||
setValue('.ot-pressure', result.sensors.pressure);
|
||||
setValue('.ot-dhw-flow-rate', result.sensors.dhwFlowRate);
|
||||
setValue('.ot-fault-code', result.sensors.faultCode ? ("E" + result.sensors.faultCode) : "-");
|
||||
|
||||
setValue('.indoor-temp', result.temperatures.indoor);
|
||||
setValue('.outdoor-temp', result.temperatures.outdoor);
|
||||
setValue('.heating-temp', result.temperatures.heating);
|
||||
setValue('.heating-setpoint-temp', result.parameters.heatingSetpoint);
|
||||
setValue('.dhw-temp', result.temperatures.dhw);
|
||||
|
||||
setBusy('.ot-busy', '.ot-table', false);
|
||||
|
||||
setValue('.version', result.system.version);
|
||||
setValue('.build-date', result.system.buildDate);
|
||||
setValue('.uptime', result.system.uptime);
|
||||
setValue('.uptime-days', Math.floor(result.system.uptime / 86400));
|
||||
setValue('.uptime-hours', Math.floor(result.system.uptime % 86400 / 3600));
|
||||
setValue('.uptime-min', Math.floor(result.system.uptime % 3600 / 60));
|
||||
setValue('.uptime-sec', Math.floor(result.system.uptime % 60));
|
||||
setValue('.total-heap', result.system.totalHeap);
|
||||
setValue('.free-heap', result.system.freeHeap);
|
||||
setValue('.min-free-heap', result.system.minFreeHeap);
|
||||
setValue('.max-free-block-heap', result.system.maxFreeBlockHeap);
|
||||
setValue('.min-max-free-block-heap', result.system.minMaxFreeBlockHeap);
|
||||
setValue('.reset-reason', result.system.resetReason);
|
||||
setState('.mqtt-connected', result.system.mqttConnected);
|
||||
|
||||
setBusy('.system-busy', '.system-table', false);
|
||||
}
|
||||
|
||||
function setBusy(busySelector, contentSelector, value) {
|
||||
let busy = document.querySelector(busySelector);
|
||||
let content = document.querySelector(contentSelector);
|
||||
if (!busy || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
busy.classList.add('hidden');
|
||||
content.classList.remove('hidden');
|
||||
|
||||
} else {
|
||||
busy.classList.remove('hidden');
|
||||
content.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function setState(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setAttribute('aria-invalid', !value);
|
||||
}
|
||||
|
||||
function setValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.innerHTML = value;
|
||||
}
|
||||
|
||||
function setCheckboxValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.checked = value;
|
||||
}
|
||||
|
||||
function setRadioValue(selector, value) {
|
||||
let items = document.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
item.checked = item.value == value;
|
||||
}
|
||||
}
|
||||
|
||||
function setInputValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.value = value;
|
||||
}
|
||||
|
||||
|
||||
function form2json(data) {
|
||||
let method = function (object, pair) {
|
||||
let keys = pair[0].replace(/\]/g, '').split('[');
|
||||
let key = keys[0];
|
||||
let value = pair[1];
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = value === 'true';
|
||||
} else if (typeof (value) === 'string' && value.trim() !== '' && !isNaN(value)) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
if (keys.length > 1) {
|
||||
let i, x, segment;
|
||||
let last = value;
|
||||
let type = isNaN(keys[1]) ? {} : [];
|
||||
value = segment = object[key] || type;
|
||||
|
||||
for (i = 1; i < keys.length; i++) {
|
||||
x = keys[i];
|
||||
if (i == keys.length - 1) {
|
||||
if (Array.isArray(segment)) {
|
||||
segment.push(last);
|
||||
} else {
|
||||
segment[x] = last;
|
||||
}
|
||||
} else if (segment[x] == undefined) {
|
||||
segment[x] = isNaN(keys[i + 1]) ? {} : [];
|
||||
}
|
||||
segment = segment[x];
|
||||
}
|
||||
}
|
||||
|
||||
object[key] = value;
|
||||
return object;
|
||||
}
|
||||
|
||||
let object = Array.from(data).reduce(method, {});
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
4
src_data/static/pico.min.css
vendored
331
src_data/styles/app.css
Normal file
@@ -0,0 +1,331 @@
|
||||
@media (min-width: 576px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: calc(var(--pico-spacing) * 0.75);
|
||||
--pico-block-spacing-horizontal: calc(var(--pico-spacing) * 0.75);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: var(--pico-spacing);
|
||||
--pico-block-spacing-horizontal: var(--pico-spacing);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.25);
|
||||
--pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.25);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.5);
|
||||
--pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.5);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.75);
|
||||
--pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.75);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
header,
|
||||
main,
|
||||
footer {
|
||||
padding-top: 1rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*nav li a:has(> div.logo) {
|
||||
margin-bottom: 0;
|
||||
}*/
|
||||
nav li :where(a, [role=link]) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
details>div {
|
||||
padding: 0 var(--pico-form-element-spacing-horizontal);
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
:nth-last-child(1 of table tr:not(.hidden)) th,
|
||||
:nth-last-child(1 of table tr:not(.hidden)) td {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button.success {
|
||||
background-color: var(--pico-form-element-valid-border-color);
|
||||
border-color: var(--pico-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
button.failed {
|
||||
background-color: var(--pico-form-element-invalid-border-color);
|
||||
border-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
tr.network:hover {
|
||||
--pico-background-color: var(--pico-primary-focus);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: 0.25rem solid var(--pico-form-element-invalid-border-color);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal);
|
||||
vertical-align: baseline;
|
||||
line-height: var(--pico-line-height);
|
||||
background-color: var(--pico-code-kbd-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
color: var(--pico-code-kbd-color);
|
||||
font-weight: bolder;
|
||||
/*font-size: 1.3rem;*/
|
||||
font-family: var(--pico-font-family-monospace);
|
||||
}
|
||||
|
||||
|
||||
.thermostat {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 2fr 0.5fr;
|
||||
grid-template-rows: 0.25fr 1fr 0.25fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
grid-template-areas:
|
||||
". thermostat-header ."
|
||||
"thermostat-minus thermostat-temp thermostat-plus"
|
||||
"thermostat-control thermostat-control thermostat-control";
|
||||
|
||||
border: .25rem solid var(--pico-blockquote-border-color);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.thermostat-header {
|
||||
justify-self: center;
|
||||
align-self: end;
|
||||
grid-area: thermostat-header;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
border-bottom: .25rem solid var(--pico-primary-hover-border);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.thermostat-temp {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0.5fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"thermostat-temp-target"
|
||||
"thermostat-temp-current";
|
||||
grid-area: thermostat-temp;
|
||||
}
|
||||
|
||||
.thermostat-temp-target {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: thermostat-temp-target;
|
||||
|
||||
font-weight: bold;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.thermostat-temp-current {
|
||||
justify-self: center;
|
||||
align-self: start;
|
||||
grid-area: thermostat-temp-current;
|
||||
|
||||
color: var(--pico-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.thermostat-minus {
|
||||
justify-self: end;
|
||||
align-self: center;
|
||||
grid-area: thermostat-minus;
|
||||
}
|
||||
|
||||
.thermostat-plus {
|
||||
justify-self: start;
|
||||
align-self: center;
|
||||
grid-area: thermostat-plus;
|
||||
}
|
||||
|
||||
.thermostat-control {
|
||||
justify-self: center;
|
||||
align-self: start;
|
||||
grid-area: thermostat-control;
|
||||
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.notify {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0.5fr;
|
||||
gap: 0.5rem 0.5rem;
|
||||
grid-template-areas:
|
||||
"notify-content"
|
||||
"notify-actions";
|
||||
}
|
||||
|
||||
.notify-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notify-content {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: notify-content;
|
||||
}
|
||||
|
||||
.notify-actions {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: notify-actions;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.notify {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 1fr 10rem;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0rem 0.5rem;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"notify-icon notify-content notify-actions";
|
||||
}
|
||||
|
||||
.notify-icon {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: notify-icon;
|
||||
}
|
||||
|
||||
.notify-content {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: notify-content;
|
||||
}
|
||||
|
||||
.notify-actions {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: notify-actions;
|
||||
}
|
||||
}
|
||||
|
||||
.notify {
|
||||
margin: 1rem;
|
||||
padding: 0.5rem;
|
||||
border: .25rem solid var(--pico-blockquote-border-color);
|
||||
}
|
||||
|
||||
.notify-error {
|
||||
border-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
.notify-error .notify-icon {
|
||||
color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
.notify-alarm {
|
||||
border-color: #c89048;
|
||||
}
|
||||
|
||||
.notify-alarm .notify-icon {
|
||||
color: #c89048;
|
||||
}
|
||||
|
||||
.notify-icon i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
|
||||
[class*=" icons-"],
|
||||
[class=icons],
|
||||
[class^=icons-] {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
*:has(> [class*=" icons-"], > [class=icons], > [class^=icons-]):has(+ * > [class*=" icons-"], + * > [class=icons], + * > [class^=icons-]) {
|
||||
margin: 0 0.5rem 0 0;
|
||||
}
|
||||
|
||||
[data-tooltip]:has(> [class*=" icons-"], > [class=icons], > [class^=icons-]) {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.icons-color-green {
|
||||
color: var(--pico-form-element-valid-active-border-color);
|
||||
}
|
||||
|
||||
.icons-color-yellow {
|
||||
color: #c89048;
|
||||
}
|
||||
|
||||
.icons-color-gray {
|
||||
color: var(--pico-form-element-placeholder-color);
|
||||
}
|
||||
|
||||
.icons-color-red {
|
||||
color: var(--pico-form-element-invalid-active-border-color);
|
||||
}
|
||||
80
src_data/styles/iconly.css
Normal file
@@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* Icons icon font. Generated by Iconly: https://iconly.io/
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-display: auto;
|
||||
font-family: "Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("/static/fonts/iconly.eot?1732913937966");
|
||||
src: url("/static/fonts/iconly.eot?#iefix") format("embedded-opentype"), url("/static/fonts/iconly.woff2?1732913937966") format("woff2"), url("/static/fonts/iconly.woff?1732913937966") format("woff"), url("/static/fonts/iconly.ttf?1732913937966") format("truetype"), url("/static/fonts/iconly.svg?1732913937966#Icons") format("svg");
|
||||
}
|
||||
|
||||
[class="icons"], [class^="icons-"], [class*=" icons-"] {
|
||||
display: inline-block;
|
||||
font-family: "Icons" !important;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.icons-plus:before {
|
||||
content: "\e000";
|
||||
}
|
||||
|
||||
.icons-minus:before {
|
||||
content: "\e001";
|
||||
}
|
||||
|
||||
.icons-unlocked:before {
|
||||
content: "\e002";
|
||||
}
|
||||
|
||||
.icons-locked:before {
|
||||
content: "\e003";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-1:before {
|
||||
content: "\e004";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-0:before {
|
||||
content: "\e005";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-2:before {
|
||||
content: "\e006";
|
||||
}
|
||||
|
||||
.icons-error:before {
|
||||
content: "\e007";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-3:before {
|
||||
content: "\e008";
|
||||
}
|
||||
|
||||
.icons-down:before {
|
||||
content: "\e009";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-4:before {
|
||||
content: "\e00a";
|
||||
}
|
||||
|
||||
.icons-success:before {
|
||||
content: "\e00b";
|
||||
}
|
||||
|
||||
.icons-up:before {
|
||||
content: "\e00c";
|
||||
}
|
||||
|
||||
.icons-alarm:before {
|
||||
content: "\e00d";
|
||||
}
|
||||
4
src_data/styles/pico.min.css
vendored
Normal file
458
src_data/timezones.json
Normal file
@@ -0,0 +1,458 @@
|
||||
{
|
||||
"Africa/Abidjan":"GMT0",
|
||||
"Africa/Accra":"GMT0",
|
||||
"Africa/Addis_Ababa":"EAT-3",
|
||||
"Africa/Algiers":"CET-1",
|
||||
"Africa/Asmara":"EAT-3",
|
||||
"Africa/Bamako":"GMT0",
|
||||
"Africa/Bangui":"WAT-1",
|
||||
"Africa/Banjul":"GMT0",
|
||||
"Africa/Bissau":"GMT0",
|
||||
"Africa/Blantyre":"CAT-2",
|
||||
"Africa/Brazzaville":"WAT-1",
|
||||
"Africa/Bujumbura":"CAT-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",
|
||||
"Africa/Dakar":"GMT0",
|
||||
"Africa/Dar_es_Salaam":"EAT-3",
|
||||
"Africa/Djibouti":"EAT-3",
|
||||
"Africa/Douala":"WAT-1",
|
||||
"Africa/El_Aaiun":"<+01>-1",
|
||||
"Africa/Freetown":"GMT0",
|
||||
"Africa/Gaborone":"CAT-2",
|
||||
"Africa/Harare":"CAT-2",
|
||||
"Africa/Johannesburg":"SAST-2",
|
||||
"Africa/Juba":"CAT-2",
|
||||
"Africa/Kampala":"EAT-3",
|
||||
"Africa/Khartoum":"CAT-2",
|
||||
"Africa/Kigali":"CAT-2",
|
||||
"Africa/Kinshasa":"WAT-1",
|
||||
"Africa/Lagos":"WAT-1",
|
||||
"Africa/Libreville":"WAT-1",
|
||||
"Africa/Lome":"GMT0",
|
||||
"Africa/Luanda":"WAT-1",
|
||||
"Africa/Lubumbashi":"CAT-2",
|
||||
"Africa/Lusaka":"CAT-2",
|
||||
"Africa/Malabo":"WAT-1",
|
||||
"Africa/Maputo":"CAT-2",
|
||||
"Africa/Maseru":"SAST-2",
|
||||
"Africa/Mbabane":"SAST-2",
|
||||
"Africa/Mogadishu":"EAT-3",
|
||||
"Africa/Monrovia":"GMT0",
|
||||
"Africa/Nairobi":"EAT-3",
|
||||
"Africa/Ndjamena":"WAT-1",
|
||||
"Africa/Niamey":"WAT-1",
|
||||
"Africa/Nouakchott":"GMT0",
|
||||
"Africa/Ouagadougou":"GMT0",
|
||||
"Africa/Porto-Novo":"WAT-1",
|
||||
"Africa/Sao_Tome":"GMT0",
|
||||
"Africa/Tripoli":"EET-2",
|
||||
"Africa/Tunis":"CET-1",
|
||||
"Africa/Windhoek":"CAT-2",
|
||||
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla":"AST4",
|
||||
"America/Antigua":"AST4",
|
||||
"America/Araguaina":"<-03>3",
|
||||
"America/Argentina/Buenos_Aires":"<-03>3",
|
||||
"America/Argentina/Catamarca":"<-03>3",
|
||||
"America/Argentina/Cordoba":"<-03>3",
|
||||
"America/Argentina/Jujuy":"<-03>3",
|
||||
"America/Argentina/La_Rioja":"<-03>3",
|
||||
"America/Argentina/Mendoza":"<-03>3",
|
||||
"America/Argentina/Rio_Gallegos":"<-03>3",
|
||||
"America/Argentina/Salta":"<-03>3",
|
||||
"America/Argentina/San_Juan":"<-03>3",
|
||||
"America/Argentina/San_Luis":"<-03>3",
|
||||
"America/Argentina/Tucuman":"<-03>3",
|
||||
"America/Argentina/Ushuaia":"<-03>3",
|
||||
"America/Aruba":"AST4",
|
||||
"America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan":"EST5",
|
||||
"America/Bahia":"<-03>3",
|
||||
"America/Bahia_Banderas":"CST6",
|
||||
"America/Barbados":"AST4",
|
||||
"America/Belem":"<-03>3",
|
||||
"America/Belize":"CST6",
|
||||
"America/Blanc-Sablon":"AST4",
|
||||
"America/Boa_Vista":"<-04>4",
|
||||
"America/Bogota":"<-05>5",
|
||||
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande":"<-04>4",
|
||||
"America/Cancun":"EST5",
|
||||
"America/Caracas":"<-04>4",
|
||||
"America/Cayenne":"<-03>3",
|
||||
"America/Cayman":"EST5",
|
||||
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua":"CST6",
|
||||
"America/Costa_Rica":"CST6",
|
||||
"America/Creston":"MST7",
|
||||
"America/Cuiaba":"<-04>4",
|
||||
"America/Curacao":"AST4",
|
||||
"America/Danmarkshavn":"GMT0",
|
||||
"America/Dawson":"MST7",
|
||||
"America/Dawson_Creek":"MST7",
|
||||
"America/Denver":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica":"AST4",
|
||||
"America/Edmonton":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe":"<-05>5",
|
||||
"America/El_Salvador":"CST6",
|
||||
"America/Fort_Nelson":"MST7",
|
||||
"America/Fortaleza":"<-03>3",
|
||||
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"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",
|
||||
"America/Guadeloupe":"AST4",
|
||||
"America/Guatemala":"CST6",
|
||||
"America/Guayaquil":"<-05>5",
|
||||
"America/Guyana":"<-04>4",
|
||||
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo":"MST7",
|
||||
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vevay":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vincennes":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Winamac":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Inuvik":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica":"EST5",
|
||||
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kralendijk":"AST4",
|
||||
"America/La_Paz":"<-04>4",
|
||||
"America/Lima":"<-05>5",
|
||||
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes":"AST4",
|
||||
"America/Maceio":"<-03>3",
|
||||
"America/Managua":"CST6",
|
||||
"America/Manaus":"<-04>4",
|
||||
"America/Marigot":"AST4",
|
||||
"America/Martinique":"AST4",
|
||||
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan":"MST7",
|
||||
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida":"CST6",
|
||||
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.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":"CST6",
|
||||
"America/Montevideo":"<-03>3",
|
||||
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat":"AST4",
|
||||
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha":"<-02>2",
|
||||
"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":"<-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",
|
||||
"America/Phoenix":"MST7",
|
||||
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain":"AST4",
|
||||
"America/Porto_Velho":"<-04>4",
|
||||
"America/Puerto_Rico":"AST4",
|
||||
"America/Punta_Arenas":"<-03>3",
|
||||
"America/Rainy_River":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife":"<-03>3",
|
||||
"America/Regina":"CST6",
|
||||
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco":"<-05>5",
|
||||
"America/Santarem":"<-03>3",
|
||||
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo":"AST4",
|
||||
"America/Sao_Paulo":"<-03>3",
|
||||
"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",
|
||||
"America/St_Kitts":"AST4",
|
||||
"America/St_Lucia":"AST4",
|
||||
"America/St_Thomas":"AST4",
|
||||
"America/St_Vincent":"AST4",
|
||||
"America/Swift_Current":"CST6",
|
||||
"America/Tegucigalpa":"CST6",
|
||||
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tijuana":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Toronto":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola":"AST4",
|
||||
"America/Vancouver":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse":"MST7",
|
||||
"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":"<+08>-8",
|
||||
"Antarctica/Davis":"<+07>-7",
|
||||
"Antarctica/DumontDUrville":"<+10>-10",
|
||||
"Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Antarctica/Mawson":"<+05>-5",
|
||||
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer":"<-03>3",
|
||||
"Antarctica/Rothera":"<-03>3",
|
||||
"Antarctica/Syowa":"<+03>-3",
|
||||
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok":"<+05>-5",
|
||||
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden":"<+03>-3",
|
||||
"Asia/Almaty":"<+05>-5",
|
||||
"Asia/Amman":"<+03>-3",
|
||||
"Asia/Anadyr":"<+12>-12",
|
||||
"Asia/Aqtau":"<+05>-5",
|
||||
"Asia/Aqtobe":"<+05>-5",
|
||||
"Asia/Ashgabat":"<+05>-5",
|
||||
"Asia/Atyrau":"<+05>-5",
|
||||
"Asia/Baghdad":"<+03>-3",
|
||||
"Asia/Bahrain":"<+03>-3",
|
||||
"Asia/Baku":"<+04>-4",
|
||||
"Asia/Bangkok":"<+07>-7",
|
||||
"Asia/Barnaul":"<+07>-7",
|
||||
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek":"<+06>-6",
|
||||
"Asia/Brunei":"<+08>-8",
|
||||
"Asia/Chita":"<+09>-9",
|
||||
"Asia/Choibalsan":"<+08>-8",
|
||||
"Asia/Colombo":"<+0530>-5:30",
|
||||
"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/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",
|
||||
"Asia/Irkutsk":"<+08>-8",
|
||||
"Asia/Jakarta":"WIB-7",
|
||||
"Asia/Jayapura":"WIT-9",
|
||||
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul":"<+0430>-4:30",
|
||||
"Asia/Kamchatka":"<+12>-12",
|
||||
"Asia/Karachi":"PKT-5",
|
||||
"Asia/Kathmandu":"<+0545>-5:45",
|
||||
"Asia/Khandyga":"<+09>-9",
|
||||
"Asia/Kolkata":"IST-5:30",
|
||||
"Asia/Krasnoyarsk":"<+07>-7",
|
||||
"Asia/Kuala_Lumpur":"<+08>-8",
|
||||
"Asia/Kuching":"<+08>-8",
|
||||
"Asia/Kuwait":"<+03>-3",
|
||||
"Asia/Macau":"CST-8",
|
||||
"Asia/Magadan":"<+11>-11",
|
||||
"Asia/Makassar":"WITA-8",
|
||||
"Asia/Manila":"PST-8",
|
||||
"Asia/Muscat":"<+04>-4",
|
||||
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk":"<+07>-7",
|
||||
"Asia/Novosibirsk":"<+07>-7",
|
||||
"Asia/Omsk":"<+06>-6",
|
||||
"Asia/Oral":"<+05>-5",
|
||||
"Asia/Phnom_Penh":"<+07>-7",
|
||||
"Asia/Pontianak":"WIB-7",
|
||||
"Asia/Pyongyang":"KST-9",
|
||||
"Asia/Qatar":"<+03>-3",
|
||||
"Asia/Qyzylorda":"<+05>-5",
|
||||
"Asia/Riyadh":"<+03>-3",
|
||||
"Asia/Sakhalin":"<+11>-11",
|
||||
"Asia/Samarkand":"<+05>-5",
|
||||
"Asia/Seoul":"KST-9",
|
||||
"Asia/Shanghai":"CST-8",
|
||||
"Asia/Singapore":"<+08>-8",
|
||||
"Asia/Srednekolymsk":"<+11>-11",
|
||||
"Asia/Taipei":"CST-8",
|
||||
"Asia/Tashkent":"<+05>-5",
|
||||
"Asia/Tbilisi":"<+04>-4",
|
||||
"Asia/Tehran":"<+0330>-3:30",
|
||||
"Asia/Thimphu":"<+06>-6",
|
||||
"Asia/Tokyo":"JST-9",
|
||||
"Asia/Tomsk":"<+07>-7",
|
||||
"Asia/Ulaanbaatar":"<+08>-8",
|
||||
"Asia/Urumqi":"<+06>-6",
|
||||
"Asia/Ust-Nera":"<+10>-10",
|
||||
"Asia/Vientiane":"<+07>-7",
|
||||
"Asia/Vladivostok":"<+10>-10",
|
||||
"Asia/Yakutsk":"<+09>-9",
|
||||
"Asia/Yangon":"<+0630>-6:30",
|
||||
"Asia/Yekaterinburg":"<+05>-5",
|
||||
"Asia/Yerevan":"<+04>-4",
|
||||
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde":"<-01>1",
|
||||
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik":"GMT0",
|
||||
"Atlantic/South_Georgia":"<-02>2",
|
||||
"Atlantic/St_Helena":"GMT0",
|
||||
"Atlantic/Stanley":"<-03>3",
|
||||
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane":"AEST-10",
|
||||
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Darwin":"ACST-9:30",
|
||||
"Australia/Eucla":"<+0845>-8:45",
|
||||
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman":"AEST-10",
|
||||
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
|
||||
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Perth":"AWST-8",
|
||||
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Astrakhan":"<+04>-4",
|
||||
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Busingen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Chisinau":"EET-2EEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Copenhagen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin":"IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Istanbul":"<+03>-3",
|
||||
"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":"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",
|
||||
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk":"<+03>-3",
|
||||
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow":"MSK-3",
|
||||
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara":"<+04>-4",
|
||||
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Saratov":"<+04>-4",
|
||||
"Europe/Simferopol":"MSK-3",
|
||||
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk":"<+04>-4",
|
||||
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"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":"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",
|
||||
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo":"EAT-3",
|
||||
"Indian/Chagos":"<+06>-6",
|
||||
"Indian/Christmas":"<+07>-7",
|
||||
"Indian/Cocos":"<+0630>-6:30",
|
||||
"Indian/Comoro":"EAT-3",
|
||||
"Indian/Kerguelen":"<+05>-5",
|
||||
"Indian/Mahe":"<+04>-4",
|
||||
"Indian/Maldives":"<+05>-5",
|
||||
"Indian/Mauritius":"<+04>-4",
|
||||
"Indian/Mayotte":"EAT-3",
|
||||
"Indian/Reunion":"<+04>-4",
|
||||
"Pacific/Apia":"<+13>-13",
|
||||
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Pacific/Bougainville":"<+11>-11",
|
||||
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk":"<+10>-10",
|
||||
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate":"<+11>-11",
|
||||
"Pacific/Enderbury":"<+13>-13",
|
||||
"Pacific/Fakaofo":"<+13>-13",
|
||||
"Pacific/Fiji":"<+12>-12",
|
||||
"Pacific/Funafuti":"<+12>-12",
|
||||
"Pacific/Galapagos":"<-06>6",
|
||||
"Pacific/Gambier":"<-09>9",
|
||||
"Pacific/Guadalcanal":"<+11>-11",
|
||||
"Pacific/Guam":"ChST-10",
|
||||
"Pacific/Honolulu":"HST10",
|
||||
"Pacific/Kiritimati":"<+14>-14",
|
||||
"Pacific/Kosrae":"<+11>-11",
|
||||
"Pacific/Kwajalein":"<+12>-12",
|
||||
"Pacific/Majuro":"<+12>-12",
|
||||
"Pacific/Marquesas":"<-0930>9:30",
|
||||
"Pacific/Midway":"SST11",
|
||||
"Pacific/Nauru":"<+12>-12",
|
||||
"Pacific/Niue":"<-11>11",
|
||||
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea":"<+11>-11",
|
||||
"Pacific/Pago_Pago":"SST11",
|
||||
"Pacific/Palau":"<+09>-9",
|
||||
"Pacific/Pitcairn":"<-08>8",
|
||||
"Pacific/Pohnpei":"<+11>-11",
|
||||
"Pacific/Port_Moresby":"<+10>-10",
|
||||
"Pacific/Rarotonga":"<-10>10",
|
||||
"Pacific/Saipan":"ChST-10",
|
||||
"Pacific/Tahiti":"<-10>10",
|
||||
"Pacific/Tarawa":"<+12>-12",
|
||||
"Pacific/Tongatapu":"<+13>-13",
|
||||
"Pacific/Wake":"<+12>-12",
|
||||
"Pacific/Wallis":"<+12>-12",
|
||||
|
||||
"Etc/UTC":"UTC0",
|
||||
"Etc/GMT":"GMT0",
|
||||
"Etc/GMT+1":"<-01>1",
|
||||
"Etc/GMT+2":"<-02>2",
|
||||
"Etc/GMT+3":"<-03>3",
|
||||
"Etc/GMT+4":"<-04>4",
|
||||
"Etc/GMT+5":"<-05>5",
|
||||
"Etc/GMT+6":"<-06>6",
|
||||
"Etc/GMT+7":"<-07>7",
|
||||
"Etc/GMT+8":"<-08>8",
|
||||
"Etc/GMT+9":"<-09>9",
|
||||
"Etc/GMT+10":"<-10>10",
|
||||
"Etc/GMT+11":"<-11>11",
|
||||
"Etc/GMT+12":"<-12>12",
|
||||
"Etc/GMT-1":"<+01>-1",
|
||||
"Etc/GMT-2":"<+02>-2",
|
||||
"Etc/GMT-3":"<+03>-3",
|
||||
"Etc/GMT-4":"<+04>-4",
|
||||
"Etc/GMT-5":"<+05>-5",
|
||||
"Etc/GMT-6":"<+06>-6",
|
||||
"Etc/GMT-7":"<+07>-7",
|
||||
"Etc/GMT-8":"<+08>-8",
|
||||
"Etc/GMT-9":"<+09>-9",
|
||||
"Etc/GMT-10":"<+10>-10",
|
||||
"Etc/GMT-11":"<+11>-11",
|
||||
"Etc/GMT-12":"<+12>-12",
|
||||
"Etc/GMT-13":"<+13>-13",
|
||||
"Etc/GMT-14":"<+14>-14",
|
||||
"Etc/Zulu":"UTC0"
|
||||
}
|
||||
@@ -1,17 +1,43 @@
|
||||
import shutil
|
||||
import gzip
|
||||
import os
|
||||
Import("env")
|
||||
|
||||
|
||||
def post_build(source, target, env):
|
||||
copy_to_build_dir({
|
||||
source[0].get_abspath(): "firmware_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.factory.bin"): "firmware_%s_%s.factory.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
source[0].get_abspath(): "firmware_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.factory.bin"): "firmware_%s_%s.factory.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.elf"): "firmware_%s_%s.elf" % (env["PIOENV"], env.GetProjectOption("version"))
|
||||
}, os.path.join(env["PROJECT_DIR"], "build"));
|
||||
|
||||
|
||||
env.Execute("pio run --target buildfs --environment %s" % env["PIOENV"]);
|
||||
|
||||
|
||||
def before_buildfs(source, target, env):
|
||||
env.Execute("npm install --silent")
|
||||
env.Execute("npx gulp build_all --no-deprecation")
|
||||
"""
|
||||
src = os.path.join(env["PROJECT_DIR"], "src_data")
|
||||
dst = os.path.join(env["PROJECT_DIR"], "data")
|
||||
|
||||
for root, dirs, files in os.walk(src, topdown=False):
|
||||
for name in files:
|
||||
src_path = os.path.join(root, name)
|
||||
|
||||
with open(src_path, 'rb') as f_in:
|
||||
dst_name = name + ".gz"
|
||||
dst_path = os.path.join(dst, os.path.relpath(root, src), dst_name)
|
||||
|
||||
if os.path.exists(os.path.join(dst, os.path.relpath(root, src))) == False:
|
||||
os.mkdir(os.path.join(dst, os.path.relpath(root, src)))
|
||||
|
||||
with gzip.open(dst_path, 'wb', 9) as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
print("Compressed '%s' to '%s'" % (src_path, dst_path))
|
||||
"""
|
||||
|
||||
def after_buildfs(source, target, env):
|
||||
copy_to_build_dir({
|
||||
source[0].get_abspath(): "filesystem_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
@@ -31,4 +57,6 @@ def copy_to_build_dir(files, build_dir):
|
||||
|
||||
|
||||
env.AddPostAction("buildprog", post_build)
|
||||
env.AddPreAction("$BUILD_DIR/spiffs.bin", before_buildfs)
|
||||
env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs)
|
||||
env.AddPostAction("buildfs", after_buildfs)
|
||||
@@ -23,10 +23,15 @@ Import("env")
|
||||
platform = env.PioPlatform()
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from os.path import join
|
||||
|
||||
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
|
||||
import esptool
|
||||
def normalize_paths(cmd):
|
||||
for i, arg in enumerate(cmd):
|
||||
if isinstance(arg, str) and '/' in arg:
|
||||
cmd[i] = os.path.normpath(arg)
|
||||
return cmd
|
||||
|
||||
def esp32_create_combined_bin(source, target, env):
|
||||
print("Generating combined binary for serial flashing")
|
||||
@@ -39,26 +44,21 @@ def esp32_create_combined_bin(source, target, env):
|
||||
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
|
||||
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
|
||||
chip = env.get("BOARD_MCU")
|
||||
flash_size = env.BoardConfig().get("upload.flash_size")
|
||||
flash_freq = env.BoardConfig().get("build.f_flash", '40m')
|
||||
flash_freq = flash_freq.replace('000000L', 'm')
|
||||
flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
|
||||
memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
|
||||
if flash_mode == "qio" or flash_mode == "qout":
|
||||
flash_mode = "dio"
|
||||
if memory_type == "opi_opi" or memory_type == "opi_qspi":
|
||||
flash_mode = "dout"
|
||||
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
|
||||
flash_mode = env["__get_board_flash_mode"](env)
|
||||
flash_freq = env["__get_board_f_flash"](env)
|
||||
|
||||
cmd = [
|
||||
"--chip",
|
||||
chip,
|
||||
"merge_bin",
|
||||
"merge-bin",
|
||||
"-o",
|
||||
new_file_name,
|
||||
"--flash_mode",
|
||||
"--flash-mode",
|
||||
flash_mode,
|
||||
"--flash_freq",
|
||||
"--flash-freq",
|
||||
flash_freq,
|
||||
"--flash_size",
|
||||
"--flash-size",
|
||||
flash_size,
|
||||
]
|
||||
|
||||
@@ -71,9 +71,12 @@ def esp32_create_combined_bin(source, target, env):
|
||||
print(f" - {hex(app_offset)} | {firmware_name}")
|
||||
cmd += [hex(app_offset), firmware_name]
|
||||
|
||||
print('Using esptool.py arguments: %s' % ' '.join(cmd))
|
||||
|
||||
esptool.main(cmd)
|
||||
# print('Using esptool.py arguments: %s' % ' '.join(cmd))
|
||||
cmdline = [env.subst("$OBJCOPY")] + normalize_paths(cmd)
|
||||
print('Command Line: %s' % cmdline)
|
||||
result = subprocess.run(cmdline, text=True, check=False, stdout=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
print(f"esptool create firmware failed with exit code: {result.returncode}")
|
||||
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"editor": "wokwi",
|
||||
"parts": [
|
||||
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} }
|
||||
],
|
||||
"connections": [
|
||||
[ "esp:TX0", "$serialMonitor:RX", "", [] ],
|
||||
[ "esp:RX0", "$serialMonitor:TX", "", [] ]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[wokwi]
|
||||
version = 1
|
||||
elf = "../../.pio/build/nodemcu_32s/firmware.elf"
|
||||
firmware = "../../.pio/build/nodemcu_32s/firmware.bin"
|
||||
|
||||
[[net.forward]]
|
||||
from = "localhost:9080"
|
||||
to = "target:80"
|
||||