Compare commits
269 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1a03117257 | ||
|
|
b421780f7b | ||
|
|
987c101394 | ||
|
|
4002f5b6c2 | ||
|
|
21edbb7432 | ||
|
|
88f217abcc | ||
|
|
89f3578f27 | ||
|
|
9c47bf1ddb | ||
|
|
4d199876fb | ||
|
|
2ffd19e850 | ||
|
|
d0aabbe82a | ||
|
|
f2e4f2f631 | ||
|
|
d374ddc02a | ||
|
|
cad9e50a78 | ||
|
|
f74d0713d7 | ||
|
|
114b7fb5a7 | ||
|
|
1b969dcb33 | ||
|
|
0c64c08ff8 | ||
|
|
e6b9a2901c | ||
|
|
ca0ef94478 | ||
|
|
335429a52e | ||
|
|
2561e92ab9 | ||
|
|
2a67716f65 | ||
|
|
2adbda6832 | ||
|
|
99088fb723 | ||
|
|
5e3751ca03 | ||
|
|
ef63f48f57 | ||
|
|
4de7119d6c | ||
|
|
280c7f2887 | ||
|
|
85ffd4188f | ||
|
|
133015d7b9 | ||
|
|
8731311c62 | ||
|
|
5856a45d37 | ||
|
|
827c18513b | ||
|
|
6f08685859 | ||
|
|
ccbec44775 | ||
|
|
5a70403444 | ||
|
|
a9c9457918 | ||
|
|
520baa4920 | ||
|
|
30ae602ab9 | ||
|
|
a6098555dc | ||
|
|
60c860bc26 | ||
|
|
7463687f1b | ||
|
|
5ee1c7029b | ||
|
|
70f2760413 | ||
|
|
07ce1db304 | ||
|
|
04a6b4e1b0 | ||
|
|
4c48dc048a | ||
|
|
feac3bbdf4 | ||
|
|
1ad1f26d4f | ||
|
|
f22c64e30c | ||
|
|
a9db175dba | ||
|
|
b7c090465b | ||
|
|
50a049915b | ||
|
|
e38bda6b4a | ||
|
|
ab1e9c761f | ||
|
|
b36e4dca42 | ||
|
|
fb01d9f566 | ||
|
|
46999fe61c | ||
|
|
5846812813 | ||
|
|
67ae236f25 | ||
|
|
347723cbba | ||
|
|
06659b749a | ||
|
|
83347765a8 | ||
|
|
68f412e670 | ||
|
|
cf4a60dd2d | ||
|
|
ff5da950c1 | ||
|
|
ab21913aa7 | ||
|
|
7b2014e7b4 | ||
|
|
025a185bbf | ||
|
|
e9bb3e46c8 | ||
|
|
f4fe8c7366 | ||
|
|
4e980b6e5b | ||
|
|
2df2205d60 | ||
|
|
4bf3b575db | ||
|
|
c87e08c6af | ||
|
|
e4e349ba15 | ||
|
|
2b5d66173e | ||
|
|
0236a0dd8a | ||
|
|
8875fd019a | ||
|
|
7149f52d62 | ||
|
|
214e840ec2 | ||
|
|
6c23d5032b | ||
|
|
47879d5486 | ||
|
|
315a975aa8 | ||
|
|
21ed8f2a14 | ||
|
|
8d92409d7b | ||
|
|
15645f4d30 | ||
|
|
a5581f3778 | ||
|
|
d5e55cf0ae | ||
|
|
adbf67ac13 | ||
|
|
e13984f869 | ||
|
|
38889bb59d | ||
|
|
6a9a069043 | ||
|
|
468a7dfc02 | ||
|
|
2a28f664cf | ||
|
|
8e80cecc22 | ||
|
|
a55c521e7b | ||
|
|
a47888c17a | ||
|
|
e9c91ed6b4 | ||
|
|
b6276ddb3f | ||
|
|
17b000bdfd | ||
|
|
c048f31672 | ||
|
|
be5f2b74bc | ||
|
|
ea86bbff30 | ||
|
|
b416110d4f | ||
|
|
84c3859c5d | ||
|
|
b56146f759 | ||
|
|
2db1c5194a | ||
|
|
29ff38c285 | ||
|
|
dce94b0f98 | ||
|
|
1f81ec1ba5 | ||
|
|
e8f26aff65 | ||
|
|
bc0ba5bdd8 | ||
|
|
bc23bbc9f3 | ||
|
|
d61b8a8ecb | ||
|
|
3fbb26fd91 | ||
|
|
3ed2b22d06 | ||
|
|
5ecbddc929 | ||
|
|
dbcca514b0 | ||
|
|
96d506ba57 | ||
|
|
03aeaa9441 | ||
|
|
5bdc28675f | ||
|
|
5633d0d2ee | ||
|
|
85cd37c4ae | ||
|
|
45630c3be9 | ||
|
|
dd293e9802 | ||
|
|
412740c5d2 | ||
|
|
c95a19eb42 | ||
|
|
9b32ccca16 | ||
|
|
60f66a4ead | ||
|
|
7740d9c4c7 | ||
|
|
c5434e0a45 | ||
|
|
cd0e8a6a11 | ||
|
|
88682eef13 | ||
|
|
c0a181632a | ||
|
|
a457ab31be | ||
|
|
3284e84b67 | ||
|
|
e379df388c | ||
|
|
ff91e328cb | ||
|
|
522f8ec699 | ||
|
|
ad1fe601b3 | ||
|
|
40d9606bea | ||
|
|
10df1c1d34 | ||
|
|
ead8c64e92 | ||
|
|
9bc4c06cad | ||
|
|
7763ee9fa9 | ||
|
|
2c26b1cb92 | ||
|
|
02d2f0f524 | ||
|
|
df99aae812 | ||
|
|
4d0c87f0b5 | ||
|
|
46de4e0cfc | ||
|
|
83296855ba | ||
|
|
0ded2c53d8 | ||
|
|
8ec3655340 | ||
|
|
eb2dcd7c09 | ||
|
|
8a4b598161 | ||
|
|
0dee4c20ce | ||
|
|
761788792f | ||
|
|
70e577e29f | ||
|
|
b268ff4007 | ||
|
|
92a2cb9d56 | ||
|
|
e82d47e1dc | ||
|
|
7bfad224fe | ||
|
|
d07f73edf4 | ||
|
|
d2271513f2 | ||
|
|
b5a0550c72 | ||
|
|
c556c38cbc | ||
|
|
44fdff61bd | ||
|
|
18acf059fc | ||
|
|
227060591f | ||
|
|
31cefce4bc | ||
|
|
5672ff0c3d | ||
|
|
8bccfcb95d | ||
|
|
5443ab82ce | ||
|
|
361628f4f5 | ||
|
|
75d31b73ff | ||
|
|
065155c930 | ||
|
|
ee5c6fc953 | ||
|
|
7c5810e6d1 | ||
|
|
9f24efb0ab |
20
.github/workflows/stale.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: "Close stale issues and PR"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: >
|
||||
This issue is stale because it has been open 15 days with no activity. Remove stale label or comment or this will be closed in 5 days.
|
||||
close-issue-message: >
|
||||
This issue was closed because it has been stalled for 5 days with no activity.
|
||||
days-before-stale: 15
|
||||
days-before-close: 5
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: "documentation,bug,enhancement"
|
||||
7
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
.pio
|
||||
.vscode
|
||||
build/*
|
||||
build/*.bin
|
||||
data/*
|
||||
secrets.ini
|
||||
node_modules
|
||||
package-lock.json
|
||||
!.gitkeep
|
||||
227
README.md
@@ -1,4 +1,13 @@
|
||||

|
||||
<div align="center">
|
||||
|
||||

|
||||
<br>
|
||||
[](https://github.com/Laxilef/OTGateway/releases)
|
||||
[](https://github.com/Laxilef/OTGateway/releases/latest)
|
||||
[](LICENSE.txt)
|
||||
[](https://t.me/otgateway)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
- Hot water temperature control
|
||||
@@ -7,7 +16,7 @@
|
||||
- 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))
|
||||
- Ability to connect an external sensors to monitor outdoor and indoor temperature ([compatible sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#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)
|
||||
- Diagnostics:
|
||||
@@ -22,203 +31,53 @@
|
||||
- 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)*
|
||||
- [Home Assistant](https://www.home-assistant.io/) integration via MQTT. The ability to create any automation for the boiler!
|
||||
|
||||

|
||||
|
||||
## Tested on
|
||||
| Boiler | Master Member ID | Notes |
|
||||
| --- | --- | --- |
|
||||
| BAXI ECO Nova | default | Pressure sensor not supported, modulation level not stable |
|
||||
| BAXI Ampera | 1028 | Pressure sensor not supported, only heating (DHW not tested) |
|
||||
| [Remeha Calenta Ace 40C](https://github.com/Laxilef/OTGateway/issues/1#issuecomment-1726081554) | default | - |
|
||||
| [Baxi Nuvola DUO-TEC HT 16](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1751061488) | default | - |
|
||||
| [AEG GBA124](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1765857609) | default | Pressure sensor not supported |
|
||||
| [Ferroli DOMIcompact C 24](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1765310058)<br><sub>Board: MF08FA</sub> | 211 | Pressure sensor not supported |
|
||||
| [Thermet Ecocondens Silver 35kW](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1767026384) | default | Pressure sensor not supported |
|
||||
| [BAXI LUNA-3](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1794187178) | default | - |
|
||||
## 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)
|
||||
* [External temperature sensors](https://github.com/Laxilef/OTGateway/wiki#external-temperature-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)
|
||||
* [Advanced Settings](https://github.com/Laxilef/OTGateway/wiki#advanced-settings)
|
||||
* [Equitherm mode](https://github.com/Laxilef/OTGateway/wiki#equitherm-mode)
|
||||
* [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)
|
||||
* [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)
|
||||
* [Temperature sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#temperature-sensors)
|
||||
* [FAQ & Troubleshooting](https://github.com/Laxilef/OTGateway/wiki/FAQ-&-Troubleshooting)
|
||||
* [OT adapters](https://github.com/Laxilef/OTGateway/wiki/OT-adapters)
|
||||
* [Adapters on sale](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#adapters-on-sale)
|
||||
* [DIY](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#diy)
|
||||
* [Files for production](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#files-for-production)
|
||||
* [Connection](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#connection)
|
||||
* [Leds on board](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#leds-on-board)
|
||||
|
||||
|
||||
## PCB
|
||||
<img src="/assets/pcb.svg" width="27%" /> <img src="/assets/pcb_3d.png" width="30%" /> <img src="/assets/after_assembly.png" width="40%" />
|
||||
|
||||
Housing for installation on DIN rail - D2MG. Occupies only 2 DIN modules.<br>
|
||||
The 220V > 5V power supply is already on the board, so additional power supplies are not needed.<br>
|
||||
To save money, 2 levels are ordered as one board. After manufacturing, the boards need to be divided into 2 parts - upper and lower. The boards are inexpensively (5pcs for $2) manufactured at JLCPCB (Remove Order Number = Specify a location).<br><br>
|
||||
Some components can be replaced with similar ones (for example use a fuse and led with legs). Some SMD components (for example optocouplers) can be replaced with similar SOT components.<br>Most of the components can be purchased inexpensively on Aliexpress, the rest in your local stores.
|
||||
|
||||
#### Connection
|
||||
The outdoor temperature sensor must be connected to the **TEMP1** connector, the indoor temperature sensor must be connected to the **TEMP2** connector. The power supply for the sensors must be connected to the **3.3V** connector **(NOT 5V!)**, GND to **GND**.<br>
|
||||
**The opentherm connection polarity does not matter.**
|
||||
<!-- **Important!** On this board opentherm IN pin = 5, OUT pin = 4 -->
|
||||
|
||||
#### Leds
|
||||
| LED | States |
|
||||
| --- | --- |
|
||||
| OT RX | Flashes when a response to the request is received from the boiler |
|
||||
| Status | Controller status.<br>On, not blinking - no errors;<br>2 flashes - no connection to Wifi;<br>3 flashes - no connection to boiler;<br>4 flashes - boiler is fault;<br>5 flashes - emergency mode (no connection to Wifi or to the MQTT server)<br>10 fast flashes - end of the list of errors |
|
||||
| Power | Always on when power is on |
|
||||
|
||||
#### Files for production
|
||||
- [Schematic](/assets/Schematic.pdf)
|
||||
- [BOM](/assets/BOM.xlsx)
|
||||
- [Gerber](/assets/gerber.zip)
|
||||
|
||||
## Another compatible OpenTherm Adapters
|
||||
- [Ihor Melnyk OpenTherm Adapter](http://ihormelnyk.com/opentherm_adapter)
|
||||
- [DIYLESS Master OpenTherm Shield](https://diyless.com/product/master-opentherm-shield)
|
||||
- [OpenTherm master shield for Wemos/Lolin](https://www.tindie.com/products/thehognl/opentherm-master-shield-for-wemoslolin/)
|
||||
- And others. It's just that the adapter must implement [the schema](http://ihormelnyk.com/Content/Pages/opentherm_adapter/opentherm_adapter_schematic_o.png)
|
||||
|
||||
## Compatible Temperature Sensors
|
||||
* DS18B20
|
||||
* DS1822
|
||||
* DS1820
|
||||
* MAX31820
|
||||
* MAX31850
|
||||
|
||||
[See more](https://github.com/milesburton/Arduino-Temperature-Control-Library#usage)
|
||||
|
||||
# Quick Start
|
||||
1. Download the latest firmware from the [releases page](https://github.com/Laxilef/OTGateway/releases) (or compile yourself) and flash your ESP8266 board using the [ESP Flash Download Tool](https://www.espressif.com/en/support/download/other-tools) or other software.
|
||||
2. Connect to *OpenTherm Gateway* hotspot, password: otgateway123456
|
||||
3. Open configuration page in browser: 192.168.4.1
|
||||
4. Set up a connection to your wifi network
|
||||
5. Set up a connection to your MQTT server: ip, port, user, password
|
||||
6. Set up a **Opentherm pin IN** & **Opentherm pin OUT**. No change for my board. Typically used **IN** = 4, **OUT** = 5
|
||||
7. Set up a **Outdoor sensor pin** & **Indoor sensor pin**. No change for my board.
|
||||
8. if necessary, set up a the master member ID ([see more](#tested-on))
|
||||
9. Restart module (required after changing OT pins and/or sensors pins!)
|
||||
|
||||
After connecting to your wifi network, you can go to the setup page at the address that ESP8266 received.
|
||||
The OTGateway device will be automatically added to homeassistant if MQTT server ip, login and password are correct.
|
||||
|
||||
## HomeAsssistant settings
|
||||
By default, the "Equitherm" and "PID" modes are disabled. In this case, the boiler will simply maintain the temperature you set.
|
||||
To use "Equitherm" or "PID" modes, the controller needs to know the temperature inside and outside the house.<br><br>
|
||||
The temperature inside the house can be set using simple automation:
|
||||
<details>
|
||||
|
||||
**sensor.livingroom_temperature** - temperature sensor inside the house.<br>
|
||||
**number.opentherm_indoor_temp** - an entity that stores the temperature value inside the house. The default does not need to be changed.
|
||||
|
||||
```yaml
|
||||
alias: Set boiler indoor temp
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- sensor.livingroom_temperature
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ has_value('number.opentherm_indoor_temp') and (states('sensor.livingroom_temperature')|float(0) - states('number.opentherm_indoor_temp')|float(0)) | abs | round(2) >= 0.01 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ states('sensor.livingroom_temperature')|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: number.opentherm_indoor_temp
|
||||
mode: single
|
||||
```
|
||||
</details>
|
||||
|
||||
If your boiler does not support the installation of an outdoor temperature sensor or does not provide this value via the opentherm protocol, then you can use an external sensor or use automation.
|
||||
<details>
|
||||
<summary>Simple automation</summary>
|
||||
|
||||
**weather.home** - [weather entity](https://www.home-assistant.io/integrations/weather/). It is important that the address of your home is entered correctly in the Home Assistant settings.<br>
|
||||
**number.opentherm_outdoor_temp** - an entity that stores the temperature value outside the house. The default does not need to be changed.
|
||||
|
||||
```yaml
|
||||
alias: Set boiler outdoor temp
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- weather.home
|
||||
attribute: temperature
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 1
|
||||
seconds: 0
|
||||
- platform: time_pattern
|
||||
seconds: /30
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ has_value('weather.home') and (state_attr('weather.home', 'temperature')|float(0) - states('number.opentherm_outdoor_temp')|float(0)) | abs | round(2) >= 0.1 }}"
|
||||
then:
|
||||
- service: number.set_value
|
||||
data:
|
||||
value: "{{ state_attr('weather.home', 'temperature')|float(0)|round(2) }}"
|
||||
target:
|
||||
entity_id: number.opentherm_outdoor_temp
|
||||
mode: single
|
||||
```
|
||||
</details>
|
||||
After these settings, you can enable the "Equitherm" and/or "PID" modes and configure them as described below.
|
||||
|
||||
|
||||
## About modes
|
||||
### Equitherm
|
||||
Weather-compensated temperature control maintains a comfortable set temperature in the house. The algorithm requires temperature sensors in the house and outside.<br> Instead of an outdoor sensor, you can use the weather forecast and automation for HA.
|
||||
|
||||
#### Ratios:
|
||||
***N*** - heating curve coefficient. The coefficient is selected individually, depending on the insulation of the room, the heated area, etc.<br>
|
||||
Range: 0.001...10, default: 0.7, step 0.001
|
||||
|
||||
|
||||
***K*** - сorrection for desired room temperature.<br>
|
||||
Range: 0...10, default: 3, step 0.01
|
||||
|
||||
|
||||
***T*** - thermostat correction.<br>
|
||||
Range: 0...10, default: 2, step 0.01
|
||||
|
||||
#### Instructions for fit coefficients:
|
||||
**Tip.** I created a [table in Excel](/assets/equitherm_calc.xlsx) in which you can enter temperature parameters inside and outside the house and select coefficients. On the graph you can see the temperature that the boiler will set.
|
||||
|
||||
1. Set the ***K*** and ***T*** coefficients to 0.
|
||||
2. The first thing you need to do is to fit the curve (***N*** coefficient). If your home has low heat loss, then start with 0.5. Otherwise start at 0.7. When the temperature inside the house stops changing, increase or decrease the coefficient value in increments of 0.1 to select the optimal curve.<br>
|
||||
Please note that passive heating (sun) will affect the house temperature during curve fitting. This process is not fast and will take you 1-2 days.
|
||||
Important. During curve fitting, the temperature must be kept stable as the outside temperature changes.<br>
|
||||
At this stage, it is important for you to stabilize the indoor temperature at exactly 20 (+- 0.5) degrees.<br>
|
||||
For example. You fit curve 0.67; set temperature 20; the temperature in the house is 20.1 degrees while the outside temperature is -10 degrees and -5 degrees. This is good.
|
||||
3. After fitting the curve, you must select the ***K*** coefficient. It influences the boiler temperature correction to maintain the set temperature.
|
||||
For example. Set temperature: 23 degrees; temperature in the house: 20 degrees. Try setting it to 2 and see how the temperature in the house changes after stabilization. Select the value so that the temperature in the house is close to the set.
|
||||
4. Now you can choose the ***T*** coefficient. Simply put, it affects the sharpness of the temperature change. If you want fast heating, then set a high value (6-10), but then the room may overheat. If you want smooth heating, set 1-5. Choose the optimal value for yourself.
|
||||
5. Check to see if it works correctly at different set temperatures over several days.
|
||||
|
||||
Read more about the algorithm [here](https://wdn.su/blog/1154).
|
||||
|
||||
### PID
|
||||
See [Wikipedia](https://en.wikipedia.org/wiki/PID_controller).
|
||||

|
||||
|
||||
In Google you can find instructions for tuning the PID controller.
|
||||
|
||||
<!--### Use Equitherm mode + PID mode
|
||||
@todo-->
|
||||
|
||||
## Dependencies
|
||||
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler) (for ESP8266)
|
||||
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
|
||||
- [NTPClient](https://github.com/arduino-libraries/NTPClient)
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
|
||||
- [PubSubClient](https://github.com/knolleary/pubsubclient)
|
||||
- [TelnetStream](https://github.com/jandrassy/TelnetStream)
|
||||
- [EEManager](https://github.com/GyverLibs/EEManager)
|
||||
- [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)
|
||||
- [WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
- [TinyLogger](https://github.com/laxilef/TinyLogger)
|
||||
|
||||
## Debug
|
||||
To display DEBUG messages you must enable debug in settings (switch is disabled by default).
|
||||
|
||||
BIN
assets/CPL.csv
Normal file
|
3322
assets/Schematic.pdf
BIN
assets/dhw_meter.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
16
assets/ha/dhw_meter.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
dhw_meter:
|
||||
sensor:
|
||||
- platform: integration
|
||||
unique_id: hot_water_meter
|
||||
name: hot_water_meter
|
||||
source: sensor.opentherm_dhw_flow_rate
|
||||
unit_time: min
|
||||
method: left
|
||||
round: 2
|
||||
|
||||
homeassistant:
|
||||
customize:
|
||||
sensor.hot_water_meter:
|
||||
friendly_name: "Hot water"
|
||||
device_class: "water"
|
||||
icon: "mdi:water-pump"
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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
|
||||
30
assets/ha/report_temp_to_controller.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
|
Before Width: | Height: | Size: 716 KiB After Width: | Height: | Size: 675 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 154 KiB |
0
build/.gitkeep
Normal file
0
data/.gitkeep
Normal file
0
data/static/.gitkeep
Normal file
7
esp32_partitions.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xE000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x1B0000,
|
||||
app1, app, ota_1, 0x1C0000, 0x1B0000,
|
||||
spiffs, data, spiffs, 0x370000, 0x80000,
|
||||
coredump, data, coredump, 0x3F0000, 0x10000,
|
||||
|
142
gulpfile.js
Normal file
@@ -0,0 +1,142 @@
|
||||
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');
|
||||
|
||||
// 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/'
|
||||
}
|
||||
],
|
||||
static: [
|
||||
{
|
||||
src: 'src_data/fonts/*.*',
|
||||
dest: 'data/static/fonts/'
|
||||
},
|
||||
{
|
||||
src: 'src_data/images/*.*',
|
||||
dest: 'data/static/images/'
|
||||
}
|
||||
],
|
||||
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(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(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(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(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);
|
||||
85
lib/BufferedWebServer/BufferedWebServer.h
Normal file
@@ -0,0 +1,85 @@
|
||||
class BufferedWebServer {
|
||||
public:
|
||||
BufferedWebServer(WebServer* webServer, size_t bufferSize = 64) {
|
||||
this->webServer = webServer;
|
||||
this->bufferSize = bufferSize;
|
||||
this->buffer = (uint8_t*)malloc(bufferSize * sizeof(*this->buffer));
|
||||
}
|
||||
|
||||
~BufferedWebServer() {
|
||||
free(this->buffer);
|
||||
}
|
||||
|
||||
void send(int code, const char* contentType, JsonDocument& 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));
|
||||
#else
|
||||
this->webServer->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
this->webServer->sendHeader(F("Content-Length"), String(measureJson(content)));
|
||||
this->webServer->send(code, contentType, emptyString);
|
||||
#endif
|
||||
|
||||
serializeJson(content, *this);
|
||||
this->flush();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
this->webServer->chunkedResponseFinalize();
|
||||
#else
|
||||
this->webServer->sendContent(emptyString);
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t write(uint8_t c) {
|
||||
this->buffer[this->bufferPos++] = c;
|
||||
|
||||
if (this->bufferPos >= this->bufferSize) {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t length) {
|
||||
size_t written = 0;
|
||||
while (written < length) {
|
||||
size_t copySize = this->bufferSize - this->bufferPos;
|
||||
if (written + copySize > length) {
|
||||
copySize = length - written;
|
||||
}
|
||||
|
||||
memcpy(this->buffer + this->bufferPos, buffer + written, copySize);
|
||||
this->bufferPos += copySize;
|
||||
|
||||
if (this->bufferPos >= this->bufferSize) {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
written += copySize;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
if (this->bufferPos == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->webServer->sendContent((const char*)this->buffer, this->bufferPos);
|
||||
this->bufferPos = 0;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected:
|
||||
WebServer* webServer = nullptr;
|
||||
uint8_t* buffer;
|
||||
size_t bufferSize = 64;
|
||||
size_t bufferPos = 0;
|
||||
};
|
||||
@@ -2,127 +2,208 @@
|
||||
#include <OpenTherm.h>
|
||||
|
||||
class CustomOpenTherm : public OpenTherm {
|
||||
private:
|
||||
unsigned long send_ts = millis();
|
||||
void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt);
|
||||
void(*yieldCallback)(void*);
|
||||
void* yieldArg;
|
||||
|
||||
public:
|
||||
typedef std::function<void()> YieldCallback;
|
||||
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) {}
|
||||
void setHandleSendRequestCallback(void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt)) {
|
||||
this->handleSendRequestCallback = handleSendRequestCallback;
|
||||
~CustomOpenTherm() {}
|
||||
|
||||
CustomOpenTherm* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
this->yieldCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
void setYieldCallback(void(*yieldCallback)(void*)) {
|
||||
this->yieldCallback = yieldCallback;
|
||||
this->yieldArg = nullptr;
|
||||
CustomOpenTherm* setBeforeSendRequestCallback(BeforeSendRequestCallback callback = nullptr) {
|
||||
this->beforeSendRequestCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
void setYieldCallback(void(*yieldCallback)(void*), void* arg) {
|
||||
this->yieldCallback = yieldCallback;
|
||||
this->yieldArg = arg;
|
||||
CustomOpenTherm* setAfterSendRequestCallback(AfterSendRequestCallback callback = nullptr) {
|
||||
this->afterSendRequestCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
||||
_attempt++;
|
||||
while (send_ts > 0 && millis() - send_ts < 200) {
|
||||
if (yieldCallback != NULL) {
|
||||
yieldCallback(yieldArg);
|
||||
|
||||
while (!this->isReady()) {
|
||||
if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
} else {
|
||||
::yield();
|
||||
}
|
||||
|
||||
this->process();
|
||||
}
|
||||
|
||||
if (this->beforeSendRequestCallback) {
|
||||
this->beforeSendRequestCallback(request, _attempt);
|
||||
}
|
||||
|
||||
unsigned long _response;
|
||||
if (!sendRequestAync(request)) {
|
||||
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
|
||||
if (!this->sendRequestAsync(request)) {
|
||||
_response = 0;
|
||||
} else {
|
||||
while (!isReady()) {
|
||||
if (yieldCallback != NULL) {
|
||||
yieldCallback(yieldArg);
|
||||
|
||||
} else {
|
||||
while (true) {
|
||||
this->process();
|
||||
|
||||
if (this->status == OpenThermStatus::READY || this->status == OpenThermStatus::DELAY) {
|
||||
break;
|
||||
} else if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
} else {
|
||||
::yield();
|
||||
}
|
||||
|
||||
process();
|
||||
}
|
||||
|
||||
_response = getLastResponse();
|
||||
_response = this->getLastResponse();
|
||||
_responseStatus = this->getLastResponseStatus();
|
||||
}
|
||||
|
||||
OpenThermResponseStatus _responseStatus = getLastResponseStatus();
|
||||
if (handleSendRequestCallback != NULL) {
|
||||
handleSendRequestCallback(request, _response, _responseStatus, _attempt);
|
||||
if (this->afterSendRequestCallback) {
|
||||
this->afterSendRequestCallback(request, _response, _responseStatus, _attempt);
|
||||
}
|
||||
|
||||
send_ts = millis();
|
||||
if (_responseStatus == OpenThermResponseStatus::SUCCESS || _responseStatus == OpenThermResponseStatus::INVALID || _attempt >= attempts) {
|
||||
return _response;
|
||||
|
||||
} else {
|
||||
return sendRequest(request, attempts, _attempt);
|
||||
return this->sendRequest(request, attempts, _attempt);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking) {
|
||||
return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, summerWinterMode, dhwBlocking));
|
||||
}
|
||||
|
||||
unsigned long buildSetBoilerStatusRequest(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);
|
||||
unsigned int data = enableCentralHeating
|
||||
| (enableHotWater << 1)
|
||||
| (enableCooling << 2)
|
||||
| (enableOutsideTemperatureCompensation << 3)
|
||||
| (enableCentralHeating2 << 4)
|
||||
| (summerWinterMode << 5)
|
||||
| (dhwBlocking << 6);
|
||||
data <<= 8;
|
||||
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data);
|
||||
|
||||
return this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::READ_DATA,
|
||||
OpenThermMessageID::Status,
|
||||
data
|
||||
));
|
||||
}
|
||||
|
||||
bool setBoilerTemperature(float temperature) {
|
||||
unsigned int data = temperatureToData(temperature);
|
||||
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data));
|
||||
bool setHeatingCh1Temp(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TSet,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setBoilerTemperature2(float temperature) {
|
||||
unsigned int data = temperatureToData(temperature);
|
||||
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TsetCH2, data));
|
||||
bool setHeatingCh2Temp(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TsetCH2,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setDhwTemp(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TdhwSet,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setRoomSetpoint(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TrSet,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setRoomSetpointCh2(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::TrSetCH2,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool setRoomTemp(float temperature) {
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::Tr,
|
||||
temperatureToData(temperature)
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool sendBoilerReset() {
|
||||
unsigned int data = 1;
|
||||
data <<= 8;
|
||||
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool sendServiceReset() {
|
||||
unsigned int data = 10;
|
||||
data <<= 8;
|
||||
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
bool sendWaterFilling() {
|
||||
unsigned int data = 2;
|
||||
data <<= 8;
|
||||
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
|
||||
unsigned long response = this->sendRequest(buildRequest(
|
||||
OpenThermMessageType::WRITE_DATA,
|
||||
OpenThermMessageID::RemoteRequest,
|
||||
data
|
||||
));
|
||||
|
||||
return isValidResponse(response);
|
||||
}
|
||||
|
||||
// converters
|
||||
float f88(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>
|
||||
static unsigned int toFloat(const T val) {
|
||||
return (unsigned int)(val * 256);
|
||||
}
|
||||
|
||||
int16_t s16(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;
|
||||
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) {
|
||||
|
||||
@@ -1,89 +1,145 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include "strings.h"
|
||||
|
||||
class HomeAssistantHelper {
|
||||
public:
|
||||
HomeAssistantHelper(PubSubClient& client) :
|
||||
client(&client)
|
||||
{
|
||||
typedef std::function<void(const char*, bool)> PublishEventCallback;
|
||||
|
||||
HomeAssistantHelper() = default;
|
||||
|
||||
void setWriter() {
|
||||
this->writer = nullptr;
|
||||
}
|
||||
|
||||
void setDevicePrefix(String value) {
|
||||
devicePrefix = value;
|
||||
void setWriter(MqttWriter* writer) {
|
||||
this->writer = writer;
|
||||
}
|
||||
|
||||
void setDeviceVersion(String value) {
|
||||
deviceVersion = value;
|
||||
void setPublishEventCallback(PublishEventCallback callback) {
|
||||
this->publishEventCallback = callback;
|
||||
}
|
||||
|
||||
void setDeviceManufacturer(String value) {
|
||||
deviceManufacturer = value;
|
||||
void setDevicePrefix(const char* value) {
|
||||
this->devicePrefix = value;
|
||||
}
|
||||
|
||||
void setDeviceModel(String value) {
|
||||
deviceModel = value;
|
||||
void setDeviceVersion(const char* value) {
|
||||
this->deviceVersion = value;
|
||||
}
|
||||
|
||||
void setDeviceName(String value) {
|
||||
deviceName = value;
|
||||
void setDeviceManufacturer(const char* value) {
|
||||
this->deviceManufacturer = value;
|
||||
}
|
||||
|
||||
void setDeviceConfigUrl(String value) {
|
||||
deviceConfigUrl = value;
|
||||
void setDeviceModel(const char* value) {
|
||||
this->deviceModel = value;
|
||||
}
|
||||
|
||||
void setDeviceName(const char* value) {
|
||||
this->deviceName = value;
|
||||
}
|
||||
|
||||
void setDeviceConfigUrl(const char* value) {
|
||||
this->deviceConfigUrl = value;
|
||||
}
|
||||
|
||||
bool publish(const char* topic, JsonDocument& doc) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_IDENTIFIERS)][0] = devicePrefix;
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_SW_VERSION)] = deviceVersion;
|
||||
|
||||
if (deviceManufacturer) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MANUFACTURER)] = deviceManufacturer;
|
||||
}
|
||||
|
||||
if (deviceModel) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MODEL)] = deviceModel;
|
||||
if (this->writer == nullptr) {
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deviceName) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_NAME)] = deviceName;
|
||||
}
|
||||
|
||||
if (deviceConfigUrl) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_CONF_URL)] = deviceConfigUrl;
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_IDENTIFIERS)][0] = this->devicePrefix;
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_SW_VERSION)] = this->deviceVersion;
|
||||
|
||||
if (this->deviceManufacturer != nullptr) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MANUFACTURER)] = this->deviceManufacturer;
|
||||
}
|
||||
|
||||
// Feeding the watchdog
|
||||
yield();
|
||||
if (this->deviceModel != nullptr) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MODEL)] = this->deviceModel;
|
||||
}
|
||||
|
||||
client->beginPublish(topic, measureJson(doc), true);
|
||||
serializeJson(doc, *client);
|
||||
return client->endPublish();
|
||||
if (this->deviceName != nullptr) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_NAME)] = this->deviceName;
|
||||
}
|
||||
|
||||
if (this->deviceConfigUrl != nullptr) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_CONF_URL)] = this->deviceConfigUrl;
|
||||
}
|
||||
|
||||
bool result = this->writer->publish(topic, doc, true);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool publish(const char* topic) {
|
||||
return client->publish(topic, NULL, true);
|
||||
if (this->writer == nullptr) {
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = writer->publish(topic, nullptr, 0, true);
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
String getTopic(const char* category, const char* name, const char* nameSeparator = "/") {
|
||||
template <class T>
|
||||
String getTopic(T category, T name, char nameSeparator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(prefix);
|
||||
topic.concat("/");
|
||||
topic.concat(this->prefix);
|
||||
topic.concat('/');
|
||||
topic.concat(category);
|
||||
topic.concat("/");
|
||||
topic.concat(devicePrefix);
|
||||
topic.concat('/');
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(nameSeparator);
|
||||
topic.concat(name);
|
||||
topic.concat("/config");
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
String getDeviceTopic(T value, char separator = '/') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(separator);
|
||||
topic.concat(value);
|
||||
return topic;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
String getObjectId(T value, char separator = '_') {
|
||||
String topic = "";
|
||||
topic.concat(this->devicePrefix);
|
||||
topic.concat(separator);
|
||||
topic.concat(value);
|
||||
return topic;
|
||||
}
|
||||
|
||||
protected:
|
||||
PubSubClient* client;
|
||||
String prefix = "homeassistant";
|
||||
String devicePrefix = "";
|
||||
String deviceVersion = "1.0";
|
||||
String deviceManufacturer = "Community";
|
||||
String deviceModel = "";
|
||||
String deviceName = "";
|
||||
String deviceConfigUrl = "";
|
||||
PublishEventCallback publishEventCallback;
|
||||
MqttWriter* writer = nullptr;
|
||||
const char* prefix = "homeassistant";
|
||||
const char* devicePrefix = "";
|
||||
const char* deviceVersion = "1.0";
|
||||
const char* deviceManufacturer = nullptr;
|
||||
const char* deviceModel = nullptr;
|
||||
const char* deviceName = nullptr;
|
||||
const char* deviceConfigUrl = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,37 +1,70 @@
|
||||
#pragma once
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
const char HA_DEVICE[] PROGMEM = "device";
|
||||
const char HA_IDENTIFIERS[] PROGMEM = "identifiers";
|
||||
const char HA_SW_VERSION[] PROGMEM = "sw_version";
|
||||
const char HA_MANUFACTURER[] PROGMEM = "manufacturer";
|
||||
const char HA_MODEL[] PROGMEM = "model";
|
||||
const char HA_NAME[] PROGMEM = "name";
|
||||
const char HA_CONF_URL[] PROGMEM = "configuration_url";
|
||||
const char HA_COMMAND_TOPIC[] PROGMEM = "command_topic";
|
||||
const char HA_COMMAND_TEMPLATE[] PROGMEM = "command_template";
|
||||
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_STATE_TOPIC[] PROGMEM = "state_topic";
|
||||
const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template";
|
||||
const char HA_OPTIONS[] PROGMEM = "options";
|
||||
const char HA_AVAILABILITY[] PROGMEM = "availability";
|
||||
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_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_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_EXPIRE_AFTER[] PROGMEM = "expire_after";
|
||||
const char HA_ENTITY_BINARY_SENSOR[] PROGMEM = "binary_sensor";
|
||||
const char HA_ENTITY_BUTTON[] PROGMEM = "button";
|
||||
const char HA_ENTITY_FAN[] PROGMEM = "fan";
|
||||
const char HA_ENTITY_CLIMATE[] PROGMEM = "climate";
|
||||
const char HA_ENTITY_NUMBER[] PROGMEM = "number";
|
||||
const char HA_ENTITY_SELECT[] PROGMEM = "select";
|
||||
const char HA_ENTITY_SENSOR[] PROGMEM = "sensor";
|
||||
const char HA_ENTITY_SWITCH[] PROGMEM = "switch";
|
||||
|
||||
const char HA_DEVICE[] PROGMEM = "device";
|
||||
const char HA_IDENTIFIERS[] PROGMEM = "identifiers";
|
||||
const char HA_SW_VERSION[] PROGMEM = "sw_version";
|
||||
const char HA_MANUFACTURER[] PROGMEM = "manufacturer";
|
||||
const char HA_MODEL[] PROGMEM = "model";
|
||||
const char HA_NAME[] PROGMEM = "name";
|
||||
const char HA_CONF_URL[] PROGMEM = "configuration_url";
|
||||
const char HA_COMMAND_TOPIC[] PROGMEM = "command_topic";
|
||||
const char HA_COMMAND_TEMPLATE[] PROGMEM = "command_template";
|
||||
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_STATE_TOPIC[] PROGMEM = "state_topic";
|
||||
const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template";
|
||||
const char HA_OPTIONS[] PROGMEM = "options";
|
||||
const char HA_AVAILABILITY[] PROGMEM = "availability";
|
||||
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_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_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_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";
|
||||
const char HA_TEMPERATURE_COMMAND_TOPIC[] PROGMEM = "temperature_command_topic";
|
||||
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";
|
||||
const char HA_MODE_STATE_TEMPLATE[] PROGMEM = "mode_state_template";
|
||||
const char HA_MODES[] PROGMEM = "modes";
|
||||
const char HA_ACTION_TOPIC[] PROGMEM = "action_topic";
|
||||
const char HA_ACTION_TEMPLATE[] PROGMEM = "action_template";
|
||||
const char HA_MIN_TEMP[] PROGMEM = "min_temp";
|
||||
const char HA_MAX_TEMP[] PROGMEM = "max_temp";
|
||||
const char HA_TEMP_STEP[] PROGMEM = "temp_step";
|
||||
const char HA_PRESET_MODE_COMMAND_TOPIC[] PROGMEM = "preset_mode_command_topic";
|
||||
const char HA_PRESET_MODE_COMMAND_TEMPLATE[] PROGMEM = "preset_mode_command_template";
|
||||
const char HA_PRESET_MODE_STATE_TOPIC[] PROGMEM = "preset_mode_state_topic";
|
||||
const char HA_PRESET_MODE_VALUE_TEMPLATE[] PROGMEM = "preset_mode_value_template";
|
||||
const char HA_PRESET_MODES[] PROGMEM = "preset_modes";
|
||||
|
||||
24
lib/MqttWriter/MqttWiFiClient.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <WiFiClient.h>
|
||||
|
||||
class MqttWiFiClient : public WiFiClient {
|
||||
public:
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
void flush() override {
|
||||
if (this->connected()) {
|
||||
WiFiClient::flush(0);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
this->abort();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
void setSync(bool) {}
|
||||
|
||||
bool getSync() {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
};
|
||||
220
lib/MqttWriter/MqttWriter.h
Normal file
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <MqttClient.h>
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <mutex>
|
||||
#endif
|
||||
|
||||
|
||||
class MqttWriter {
|
||||
public:
|
||||
typedef std::function<void()> YieldCallback;
|
||||
typedef std::function<void(const char*, size_t, size_t, bool)> PublishEventCallback;
|
||||
typedef std::function<void(size_t, size_t)> FlushEventCallback;
|
||||
|
||||
MqttWriter(MqttClient* client, size_t bufferSize = 64) {
|
||||
this->client = client;
|
||||
this->bufferSize = bufferSize;
|
||||
this->buffer = (uint8_t*) malloc(bufferSize * sizeof(*this->buffer));
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
this->mutex = new std::mutex();
|
||||
#endif
|
||||
}
|
||||
|
||||
~MqttWriter() {
|
||||
free(this->buffer);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
delete this->mutex;
|
||||
#endif
|
||||
}
|
||||
|
||||
MqttWriter* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
this->yieldCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
MqttWriter* setPublishEventCallback(PublishEventCallback callback) {
|
||||
this->publishEventCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
MqttWriter* setFlushEventCallback(FlushEventCallback callback) {
|
||||
this->flushEventCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
bool lock() {
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (!this->mutex->try_lock()) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (this->isLocked()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->locked = true;
|
||||
this->writeAfterLock = 0;
|
||||
this->lockedTime = millis();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isLocked() {
|
||||
return this->locked;
|
||||
}
|
||||
|
||||
void unlock() {
|
||||
this->locked = false;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
this->mutex->unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool publish(const char* topic, JsonDocument& doc, bool retained = false) {
|
||||
if (!this->client->connected()) {
|
||||
this->bufferPos = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!this->lock()) {
|
||||
if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
}
|
||||
}
|
||||
|
||||
this->bufferPos = 0;
|
||||
size_t docSize = measureJson(doc);
|
||||
size_t written = 0;
|
||||
if (this->client->beginMessage(topic, docSize, retained)) {
|
||||
serializeJson(doc, *this);
|
||||
this->flush();
|
||||
this->client->endMessage();
|
||||
|
||||
written = this->writeAfterLock;
|
||||
}
|
||||
this->unlock();
|
||||
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, written, docSize, written == docSize);
|
||||
}
|
||||
|
||||
return written == docSize;
|
||||
}
|
||||
|
||||
bool publish(const char* topic, const char* buffer, bool retained = false) {
|
||||
return this->publish(topic, (const uint8_t*) buffer, strlen(buffer), retained);
|
||||
}
|
||||
|
||||
bool publish(const char* topic, const uint8_t* buffer, size_t length, bool retained = false) {
|
||||
if (!this->client->connected()) {
|
||||
this->bufferPos = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!this->lock()) {
|
||||
if (this->yieldCallback) {
|
||||
this->yieldCallback();
|
||||
}
|
||||
}
|
||||
|
||||
this->bufferPos = 0;
|
||||
size_t written = 0;
|
||||
bool result = false;
|
||||
if (!length || buffer == nullptr) {
|
||||
result = this->client->beginMessage(topic, retained) && this->client->endMessage();
|
||||
|
||||
} else if (this->client->beginMessage(topic, length, retained)) {
|
||||
this->write(buffer, length);
|
||||
this->flush();
|
||||
this->client->endMessage();
|
||||
|
||||
written = this->writeAfterLock;
|
||||
result = written == length;
|
||||
}
|
||||
this->unlock();
|
||||
|
||||
if (this->publishEventCallback) {
|
||||
this->publishEventCallback(topic, written, length, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t write(uint8_t c) {
|
||||
this->buffer[this->bufferPos++] = c;
|
||||
|
||||
if (this->bufferPos >= this->bufferSize) {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t length) {
|
||||
size_t written = 0;
|
||||
while (written < length) {
|
||||
size_t copySize = this->bufferSize - this->bufferPos;
|
||||
if (written + copySize > length) {
|
||||
copySize = length - written;
|
||||
}
|
||||
|
||||
memcpy(this->buffer + this->bufferPos, buffer + written, copySize);
|
||||
this->bufferPos += copySize;
|
||||
|
||||
if (this->bufferPos >= this->bufferSize) {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
written += copySize;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
bool flush() {
|
||||
if (this->bufferPos == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->client->connected()) {
|
||||
this->bufferPos = 0;
|
||||
}
|
||||
|
||||
size_t length = this->bufferPos;
|
||||
size_t written = this->client->write(this->buffer, length);
|
||||
this->client->flush();
|
||||
this->bufferPos = 0;
|
||||
|
||||
if (this->isLocked()) {
|
||||
this->writeAfterLock += written;
|
||||
}
|
||||
|
||||
if (this->flushEventCallback) {
|
||||
this->flushEventCallback(written, length);
|
||||
}
|
||||
|
||||
return written == length;
|
||||
}
|
||||
|
||||
protected:
|
||||
YieldCallback yieldCallback;
|
||||
PublishEventCallback publishEventCallback;
|
||||
FlushEventCallback flushEventCallback;
|
||||
MqttClient* client;
|
||||
uint8_t* buffer;
|
||||
size_t bufferSize = 64;
|
||||
size_t bufferPos = 0;
|
||||
bool locked = false;
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
mutable std::mutex* mutex;
|
||||
#endif
|
||||
unsigned long lockedTime = 0;
|
||||
size_t writeAfterLock = 0;
|
||||
};
|
||||
158
lib/NetworkUtils/NetworkConnection.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "NetworkConnection.h"
|
||||
using namespace NetworkUtils;
|
||||
|
||||
void NetworkConnection::setup(bool useDhcp) {
|
||||
setUseDhcp(useDhcp);
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
wifi_set_event_handler_cb(NetworkConnection::onEvent);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
WiFi.onEvent(NetworkConnection::onEvent);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NetworkConnection::reset() {
|
||||
status = Status::NONE;
|
||||
rawDisconnectReason = 0;
|
||||
disconnectReason = DisconnectReason::NONE;
|
||||
}
|
||||
|
||||
void NetworkConnection::setUseDhcp(bool value) {
|
||||
useDhcp = value;
|
||||
}
|
||||
|
||||
NetworkConnection::Status NetworkConnection::getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
NetworkConnection::DisconnectReason NetworkConnection::getDisconnectReason() {
|
||||
return disconnectReason;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
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
|
||||
if ((wifi_station_get_connect_status() == STATION_GOT_IP) && !wifi_station_get_reconnect_policy()) {
|
||||
wifi_station_disconnect();
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_STAMODE_AUTHMODE_CHANGE:
|
||||
// https://github.com/esp8266/Arduino/blob/d5eb265f78bff9deb7063d10030a02d021c8c66c/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp#L241
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
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:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
NetworkConnection::DisconnectReason NetworkConnection::convertDisconnectReason(uint8_t reason) {
|
||||
switch (reason) {
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
case REASON_BEACON_TIMEOUT:
|
||||
return DisconnectReason::BEACON_TIMEOUT;
|
||||
|
||||
case REASON_NO_AP_FOUND:
|
||||
return DisconnectReason::NO_AP_FOUND;
|
||||
|
||||
case REASON_AUTH_FAIL:
|
||||
return DisconnectReason::AUTH_FAIL;
|
||||
|
||||
case REASON_ASSOC_FAIL:
|
||||
return DisconnectReason::ASSOC_FAIL;
|
||||
|
||||
case REASON_HANDSHAKE_TIMEOUT:
|
||||
return DisconnectReason::HANDSHAKE_TIMEOUT;
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
case WIFI_REASON_BEACON_TIMEOUT:
|
||||
return DisconnectReason::BEACON_TIMEOUT;
|
||||
|
||||
case WIFI_REASON_NO_AP_FOUND:
|
||||
return DisconnectReason::NO_AP_FOUND;
|
||||
|
||||
case WIFI_REASON_AUTH_FAIL:
|
||||
return DisconnectReason::AUTH_FAIL;
|
||||
|
||||
case WIFI_REASON_ASSOC_FAIL:
|
||||
return DisconnectReason::ASSOC_FAIL;
|
||||
|
||||
case WIFI_REASON_HANDSHAKE_TIMEOUT:
|
||||
return DisconnectReason::HANDSHAKE_TIMEOUT;
|
||||
#endif
|
||||
|
||||
default:
|
||||
return DisconnectReason::OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
bool NetworkConnection::useDhcp = false;
|
||||
NetworkConnection::Status NetworkConnection::status = Status::NONE;
|
||||
NetworkConnection::DisconnectReason NetworkConnection::disconnectReason = DisconnectReason::NONE;
|
||||
uint8_t NetworkConnection::rawDisconnectReason = 0;
|
||||
47
lib/NetworkUtils/NetworkConnection.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "lwip/etharp.h"
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
namespace NetworkUtils {
|
||||
struct NetworkConnection {
|
||||
enum class Status {
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
NONE
|
||||
};
|
||||
|
||||
enum class DisconnectReason {
|
||||
BEACON_TIMEOUT,
|
||||
NO_AP_FOUND,
|
||||
AUTH_FAIL,
|
||||
ASSOC_FAIL,
|
||||
HANDSHAKE_TIMEOUT,
|
||||
DHCP_TIMEOUT,
|
||||
OTHER,
|
||||
NONE
|
||||
};
|
||||
|
||||
static Status status;
|
||||
static DisconnectReason disconnectReason;
|
||||
static uint8_t rawDisconnectReason;
|
||||
|
||||
static void setup(bool useDhcp);
|
||||
static void setUseDhcp(bool value);
|
||||
static void reset();
|
||||
static Status getStatus();
|
||||
static DisconnectReason getDisconnectReason();
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
static void onEvent(System_Event_t *evt);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
static void onEvent(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static DisconnectReason convertDisconnectReason(uint8_t reason);
|
||||
static bool useDhcp;
|
||||
};
|
||||
}
|
||||
459
lib/NetworkUtils/NetworkMgr.h
Normal file
@@ -0,0 +1,459 @@
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "lwip/etharp.h"
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <esp_wifi.h>
|
||||
#endif
|
||||
#include <NetworkConnection.h>
|
||||
|
||||
namespace NetworkUtils {
|
||||
class NetworkMgr {
|
||||
public:
|
||||
typedef std::function<void()> YieldCallback;
|
||||
typedef std::function<void(unsigned int)> DelayCallback;
|
||||
|
||||
NetworkMgr() {
|
||||
NetworkConnection::setup(this->useDhcp);
|
||||
this->resetWifi();
|
||||
}
|
||||
|
||||
NetworkMgr* setYieldCallback(YieldCallback callback = nullptr) {
|
||||
this->yieldCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setDelayCallback(DelayCallback callback = nullptr) {
|
||||
this->delayCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setHostname(const char* value) {
|
||||
this->hostname = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
|
||||
this->apName = ssid;
|
||||
this->apPassword = password;
|
||||
this->apChannel = channel;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
|
||||
this->staSsid = ssid;
|
||||
this->staPassword = password;
|
||||
this->staChannel = channel;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setUseDhcp(bool value) {
|
||||
this->useDhcp = value;
|
||||
NetworkConnection::setup(this->useDhcp);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
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);
|
||||
this->staticDns.fromString(dns);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
NetworkMgr* setStaticConfig(IPAddress& ip, IPAddress& gateway, IPAddress& subnet, IPAddress& dns) {
|
||||
this->staticIp = ip;
|
||||
this->staticGateway = gateway;
|
||||
this->staticSubnet = subnet;
|
||||
this->staticDns = dns;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
bool hasStaCredentials() {
|
||||
return this->staSsid != nullptr;
|
||||
}
|
||||
|
||||
bool isConnected() {
|
||||
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTED;
|
||||
}
|
||||
|
||||
bool isConnecting() {
|
||||
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTING;
|
||||
}
|
||||
|
||||
bool isStaEnabled() {
|
||||
return (WiFi.getMode() & WIFI_STA) != 0;
|
||||
}
|
||||
|
||||
bool isApEnabled() {
|
||||
return (WiFi.getMode() & WIFI_AP) != 0;
|
||||
}
|
||||
|
||||
bool hasApClients() {
|
||||
if (!this->isApEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return WiFi.softAPgetStationNum() > 0;
|
||||
}
|
||||
|
||||
short int getRssi() {
|
||||
return WiFi.RSSI();
|
||||
}
|
||||
|
||||
IPAddress getApIp() {
|
||||
return WiFi.softAPIP();
|
||||
}
|
||||
|
||||
IPAddress getStaIp() {
|
||||
return WiFi.localIP();
|
||||
}
|
||||
|
||||
IPAddress getStaSubnet() {
|
||||
return WiFi.subnetMask();
|
||||
}
|
||||
|
||||
IPAddress getStaGateway() {
|
||||
return WiFi.gatewayIP();
|
||||
}
|
||||
|
||||
IPAddress getStaDns() {
|
||||
return WiFi.dnsIP();
|
||||
}
|
||||
|
||||
String getStaMac() {
|
||||
return WiFi.macAddress();
|
||||
}
|
||||
|
||||
const char* getStaSsid() {
|
||||
return this->staSsid;
|
||||
}
|
||||
|
||||
const char* getStaPassword() {
|
||||
return this->staPassword;
|
||||
}
|
||||
|
||||
byte getStaChannel() {
|
||||
return this->staChannel;
|
||||
}
|
||||
|
||||
bool resetWifi() {
|
||||
// set policy manual for work 13 ch
|
||||
{
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
wifi_country_t country = {"CN", 1, 13, WIFI_COUNTRY_POLICY_AUTO};
|
||||
wifi_set_country(&country);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
const wifi_country_t country = {"CN", 1, 13, CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER, WIFI_COUNTRY_POLICY_AUTO};
|
||||
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
|
||||
WiFi.setSleepMode(WIFI_NONE_SLEEP);
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
WiFi.setSleep(USE_BLE ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE);
|
||||
#endif
|
||||
|
||||
WiFi.softAPdisconnect();
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
/*if (wifi_softap_dhcps_status() == DHCP_STARTED) {
|
||||
wifi_softap_dhcps_stop();
|
||||
}*/
|
||||
#endif
|
||||
|
||||
WiFi.disconnect(false, true);
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
/*if (wifi_station_dhcpc_status() == DHCP_STARTED) {
|
||||
wifi_station_dhcpc_stop();
|
||||
}*/
|
||||
|
||||
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() {
|
||||
this->reconnectFlag = true;
|
||||
}
|
||||
|
||||
bool connect(bool force = false, unsigned int timeout = 1000u) {
|
||||
if (this->isConnected() && !force) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (force && !this->isApEnabled()) {
|
||||
this->resetWifi();
|
||||
NetworkConnection::reset();
|
||||
|
||||
} else {
|
||||
/*#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (wifi_station_dhcpc_status() == DHCP_STARTED) {
|
||||
wifi_station_dhcpc_stop();
|
||||
}
|
||||
#endif*/
|
||||
|
||||
this->disconnect();
|
||||
}
|
||||
|
||||
if (!this->hasStaCredentials()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->delayCallback(250);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (this->setWifiHostname(this->hostname)) {
|
||||
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
|
||||
|
||||
} else {
|
||||
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!WiFi.mode((WiFiMode_t)(WiFi.getMode() | WIFI_STA))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->delayCallback(250);
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (this->setWifiHostname(this->hostname)) {
|
||||
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
|
||||
|
||||
} else {
|
||||
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
|
||||
}
|
||||
|
||||
this->delayCallback(250);
|
||||
#endif
|
||||
|
||||
if (!this->useDhcp) {
|
||||
WiFi.config(this->staticIp, this->staticGateway, this->staticSubnet, this->staticDns);
|
||||
}
|
||||
|
||||
WiFi.begin(this->staSsid, this->staPassword, this->staChannel);
|
||||
|
||||
unsigned long beginConnectionTime = millis();
|
||||
while (millis() - beginConnectionTime < timeout) {
|
||||
this->delayCallback(100);
|
||||
|
||||
NetworkConnection::Status status = NetworkConnection::getStatus();
|
||||
if (status != NetworkConnection::Status::CONNECTING && status != NetworkConnection::Status::NONE) {
|
||||
return status == NetworkConnection::Status::CONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
WiFi.disconnect(false, true);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
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();
|
||||
NetworkConnection::reset();
|
||||
this->delayCallback(1000);
|
||||
|
||||
} else if (this->isConnected()) {
|
||||
if (!this->connected) {
|
||||
this->connectedTime = millis();
|
||||
this->connected = true;
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_NETWORK),
|
||||
F("Connected, downtime: %lu s., IP: %s, RSSI: %hhd"),
|
||||
(millis() - this->disconnectedTime) / 1000,
|
||||
WiFi.localIP().toString().c_str(),
|
||||
WiFi.RSSI()
|
||||
);
|
||||
}
|
||||
|
||||
if (this->isApEnabled() && millis() - this->connectedTime > this->reconnectInterval && !this->hasApClients()) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because STA connected"));
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (millis() - this->prevArpGratuitous > 60000) {
|
||||
this->stationKeepAliveNow();
|
||||
this->prevArpGratuitous = millis();
|
||||
}
|
||||
#endif
|
||||
|
||||
} else {
|
||||
if (this->connected) {
|
||||
this->disconnectedTime = millis();
|
||||
this->connected = false;
|
||||
|
||||
Log.sinfoln(
|
||||
FPSTR(L_NETWORK),
|
||||
F("Disconnected, reason: %d, uptime: %lu s."),
|
||||
NetworkConnection::getDisconnectReason(),
|
||||
(millis() - this->connectedTime) / 1000
|
||||
);
|
||||
}
|
||||
|
||||
if (!this->hasStaCredentials() && !this->isApEnabled()) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("No STA credentials, start AP"));
|
||||
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
this->delayCallback(250);
|
||||
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
|
||||
|
||||
} else if (!this->isApEnabled() && millis() - this->disconnectedTime > this->failedConnectTimeout) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK), F("Disconnected for a long time, start AP"));
|
||||
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
this->delayCallback(250);
|
||||
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
|
||||
|
||||
} else if (this->isConnecting() && millis() - this->prevReconnectingTime > this->resetConnectionTimeout) {
|
||||
Log.swarningln(FPSTR(L_NETWORK), F("Connection timeout, reset wifi..."));
|
||||
this->resetWifi();
|
||||
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..."));
|
||||
|
||||
NetworkConnection::reset();
|
||||
if (!this->connect(true, this->connectionTimeout)) {
|
||||
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d, raw reason: %d"), NetworkConnection::getStatus(), NetworkConnection::getDisconnectReason(), NetworkConnection::rawDisconnectReason);
|
||||
}
|
||||
|
||||
this->prevReconnectingTime = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static byte rssiToSignalQuality(short int rssi) {
|
||||
return constrain(map(rssi, -100, -50, 0, 100), 0, 100);
|
||||
}
|
||||
|
||||
protected:
|
||||
const unsigned int reconnectInterval = 5000;
|
||||
const unsigned int failedConnectTimeout = 120000;
|
||||
const unsigned int connectionTimeout = 15000;
|
||||
const unsigned int resetConnectionTimeout = 30000;
|
||||
|
||||
YieldCallback yieldCallback = []() {
|
||||
::yield();
|
||||
};
|
||||
DelayCallback delayCallback = [](unsigned int time) {
|
||||
::delay(time);
|
||||
};
|
||||
|
||||
const char* hostname = "esp";
|
||||
const char* apName = "ESP";
|
||||
const char* apPassword = nullptr;
|
||||
byte apChannel = 1;
|
||||
|
||||
const char* staSsid = nullptr;
|
||||
const char* staPassword = nullptr;
|
||||
byte staChannel = 0;
|
||||
|
||||
bool useDhcp = true;
|
||||
IPAddress staticIp;
|
||||
IPAddress staticGateway;
|
||||
IPAddress staticSubnet;
|
||||
IPAddress staticDns;
|
||||
|
||||
bool connected = false;
|
||||
bool reconnectFlag = false;
|
||||
unsigned long prevArpGratuitous = 0;
|
||||
unsigned long prevReconnectingTime = 0;
|
||||
unsigned long connectedTime = 0;
|
||||
unsigned long disconnectedTime = 0;
|
||||
|
||||
|
||||
bool setWifiHostname(const char* hostname) {
|
||||
if (!this->isHostnameValid(hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strcmp(WiFi.getHostname(), hostname) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return WiFi.setHostname(hostname);
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
/**
|
||||
* @brief
|
||||
* https://github.com/arendst/Tasmota/blob/e6515883f0ee5451931b6280ff847b117de5a231/tasmota/tasmota_support/support_wifi.ino#L1196
|
||||
*/
|
||||
static void stationKeepAliveNow(void) {
|
||||
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
|
||||
if (
|
||||
(interface->flags & NETIF_FLAG_LINK_UP)
|
||||
&& (interface->flags & NETIF_FLAG_UP)
|
||||
&& interface->num == STATION_IF
|
||||
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
|
||||
) {
|
||||
etharp_gratuitous(interface);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief check RFC compliance
|
||||
*
|
||||
* @param value
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
static bool isHostnameValid(const char* value) {
|
||||
size_t len = strlen(value);
|
||||
if (len > 24) {
|
||||
return false;
|
||||
} else if (value[len - 1] == '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (!isalnum(value[i]) && value[i] != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
227
lib/WebServerHandlers/DynamicPage.h
Normal file
@@ -0,0 +1,227 @@
|
||||
#include <FS.h>
|
||||
|
||||
|
||||
class DynamicPage : public RequestHandler {
|
||||
public:
|
||||
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
|
||||
typedef std::function<bool()> BeforeSendCallback;
|
||||
typedef std::function<String(const char*)> TemplateCallback;
|
||||
|
||||
DynamicPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) {
|
||||
this->uri = uri;
|
||||
this->fs = fs;
|
||||
this->path = path;
|
||||
this->cacheHeader = cacheHeader;
|
||||
}
|
||||
|
||||
DynamicPage* setCanHandleCallback(CanHandleCallback callback = nullptr) {
|
||||
this->canHandleCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
DynamicPage* setBeforeSendCallback(BeforeSendCallback callback = nullptr) {
|
||||
this->beforeSendCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
DynamicPage* setTemplateCallback(TemplateCallback callback = nullptr) {
|
||||
this->templateCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
|
||||
if (this->beforeSendCallback && !this->beforeSendCallback()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
File file = this->fs->open(this->path, "r");
|
||||
if (!file) {
|
||||
return false;
|
||||
|
||||
} else if (file.isDirectory()) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->cacheHeader != nullptr) {
|
||||
server.sendHeader("Cache-Control", this->cacheHeader);
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
if (!server.chunkedResponseModeStart(200, F("text/html"))) {
|
||||
server.send(505, F("text/html"), F("HTTP1.1 required"));
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.send(200, "text/html", emptyString);
|
||||
#endif
|
||||
|
||||
uint8_t* argStartPos = nullptr;
|
||||
uint8_t* argEndPos = nullptr;
|
||||
uint8_t argName[16];
|
||||
size_t sizeArgName = 0;
|
||||
bool argNameProcess = false;
|
||||
while (file.available()) {
|
||||
uint8_t buf[64];
|
||||
size_t length = file.read(buf, sizeof(buf));
|
||||
size_t offset = 0;
|
||||
|
||||
if (argNameProcess) {
|
||||
argEndPos = (uint8_t*) memchr(buf, '}', length);
|
||||
|
||||
if (argEndPos != nullptr) {
|
||||
size_t fullSizeArgName = sizeArgName + (argEndPos - buf);
|
||||
if (fullSizeArgName < sizeof(argName)) {
|
||||
// copy full arg name
|
||||
if (argEndPos - buf > 0) {
|
||||
memcpy(argName + sizeArgName, buf, argEndPos - buf);
|
||||
}
|
||||
argName[fullSizeArgName] = '\0';
|
||||
|
||||
// send arg value
|
||||
String argValue = this->templateCallback((const char*) argName);
|
||||
if (argValue.length()) {
|
||||
server.sendContent(argValue.c_str());
|
||||
|
||||
} else if (fullSizeArgName > 0) {
|
||||
server.sendContent("{");
|
||||
server.sendContent((const char*) argName);
|
||||
server.sendContent("}");
|
||||
}
|
||||
|
||||
offset = size_t(argEndPos - buf + 1);
|
||||
sizeArgName = 0;
|
||||
argNameProcess = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (argNameProcess) {
|
||||
server.sendContent("{");
|
||||
|
||||
if (sizeArgName > 0) {
|
||||
argName[sizeArgName] = '\0';
|
||||
server.sendContent((const char*) argName);
|
||||
}
|
||||
|
||||
argNameProcess = false;
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
uint8_t* currentBuf = buf + offset;
|
||||
size_t currentLength = length - offset;
|
||||
|
||||
argStartPos = (uint8_t*) memchr(currentBuf, '{', currentLength);
|
||||
|
||||
// send all content
|
||||
if (argStartPos == nullptr) {
|
||||
if (currentLength > 0) {
|
||||
server.sendContent((const char*) currentBuf, currentLength);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
argEndPos = (uint8_t*) memchr(argStartPos, '}', length - (argStartPos - buf));
|
||||
if (argEndPos != nullptr) {
|
||||
sizeArgName = argEndPos - argStartPos - 1;
|
||||
|
||||
// send all content if arg len > space
|
||||
if (sizeArgName >= sizeof(argName)) {
|
||||
if (currentLength > 0) {
|
||||
server.sendContent((const char*) currentBuf, currentLength);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// arg name
|
||||
memcpy(argName, argStartPos + 1, sizeArgName);
|
||||
argName[sizeArgName] = '\0';
|
||||
|
||||
// send arg value
|
||||
String argValue = this->templateCallback((const char*) argName);
|
||||
if (argValue.length()) {
|
||||
// send content before var
|
||||
if (argStartPos - buf > 0) {
|
||||
server.sendContent((const char*) currentBuf, argStartPos - buf);
|
||||
}
|
||||
|
||||
server.sendContent(argValue.c_str());
|
||||
|
||||
} else {
|
||||
server.sendContent((const char*) currentBuf, argEndPos - currentBuf + 1);
|
||||
}
|
||||
|
||||
offset = size_t(argEndPos - currentBuf + 1);
|
||||
|
||||
} else {
|
||||
sizeArgName = length - size_t(argStartPos - currentBuf) - 1;
|
||||
|
||||
// send all content if arg len > space
|
||||
if (sizeArgName >= sizeof(argName)) {
|
||||
if (currentLength) {
|
||||
server.sendContent((const char*) currentBuf, currentLength);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// send content before var
|
||||
if (argStartPos - buf > 0) {
|
||||
server.sendContent((const char*) currentBuf, argStartPos - buf);
|
||||
}
|
||||
|
||||
// copy arg name chunk
|
||||
if (sizeArgName > 0) {
|
||||
memcpy(argName, argStartPos + 1, sizeArgName);
|
||||
}
|
||||
|
||||
argNameProcess = true;
|
||||
|
||||
break;
|
||||
}
|
||||
} while(true);
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
server.chunkedResponseFinalize();
|
||||
#else
|
||||
server.sendContent(emptyString);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
FS* fs = nullptr;
|
||||
CanHandleCallback canHandleCallback;
|
||||
BeforeSendCallback beforeSendCallback;
|
||||
TemplateCallback templateCallback;
|
||||
String eTag;
|
||||
const char* uri = nullptr;
|
||||
const char* path = nullptr;
|
||||
const char* cacheHeader = nullptr;
|
||||
};
|
||||
109
lib/WebServerHandlers/StaticPage.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#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) {
|
||||
this->uri = uri;
|
||||
this->fs = fs;
|
||||
this->path = path;
|
||||
this->cacheHeader = cacheHeader;
|
||||
}
|
||||
|
||||
StaticPage* setCanHandleCallback(CanHandleCallback callback = nullptr) {
|
||||
this->canHandleCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
StaticPage* setBeforeSendCallback(BeforeSendCallback callback = nullptr) {
|
||||
this->beforeSendCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
|
||||
if (this->beforeSendCallback && !this->beforeSendCallback()) {
|
||||
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 (server.header("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) {
|
||||
return false;
|
||||
|
||||
} else if (file.isDirectory()) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->cacheHeader != nullptr) {
|
||||
server.sendHeader("Cache-Control", this->cacheHeader);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
if (server._eTagEnabled && this->eTag.length() > 0) {
|
||||
server.sendHeader("ETag", this->eTag);
|
||||
}
|
||||
|
||||
server.streamFile(file, F("text/html"), method);
|
||||
#else
|
||||
server.streamFile(file, F("text/html"), 200);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
FS* fs = nullptr;
|
||||
CanHandleCallback canHandleCallback;
|
||||
BeforeSendCallback beforeSendCallback;
|
||||
String eTag;
|
||||
const char* uri = nullptr;
|
||||
String path;
|
||||
const char* cacheHeader = nullptr;
|
||||
};
|
||||
218
lib/WebServerHandlers/UpgradeHandler.h
Normal file
@@ -0,0 +1,218 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
class UpgradeHandler : public RequestHandler {
|
||||
public:
|
||||
enum class UpgradeType {
|
||||
FIRMWARE = 0,
|
||||
FILESYSTEM = 1
|
||||
};
|
||||
|
||||
enum class UpgradeStatus {
|
||||
NONE,
|
||||
NO_FILE,
|
||||
SUCCESS,
|
||||
PROHIBITED,
|
||||
ABORTED,
|
||||
ERROR_ON_START,
|
||||
ERROR_ON_WRITE,
|
||||
ERROR_ON_FINISH
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
UpgradeType type;
|
||||
UpgradeStatus status;
|
||||
String error;
|
||||
} UpgradeResult;
|
||||
|
||||
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
|
||||
typedef std::function<bool(const String&)> CanUploadCallback;
|
||||
typedef std::function<bool(UpgradeType)> BeforeUpgradeCallback;
|
||||
typedef std::function<void(const UpgradeResult&, const UpgradeResult&)> AfterUpgradeCallback;
|
||||
|
||||
UpgradeHandler(const char* uri) {
|
||||
this->uri = uri;
|
||||
}
|
||||
|
||||
UpgradeHandler* setCanHandleCallback(CanHandleCallback callback = nullptr) {
|
||||
this->canHandleCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
UpgradeHandler* setCanUploadCallback(CanUploadCallback callback = nullptr) {
|
||||
this->canUploadCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
UpgradeHandler* setBeforeUpgradeCallback(BeforeUpgradeCallback callback = nullptr) {
|
||||
this->beforeUpgradeCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
UpgradeHandler* setAfterUpgradeCallback(AfterUpgradeCallback callback = nullptr) {
|
||||
this->afterUpgradeCallback = callback;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
bool canHandle(HTTPMethod method, const String uri) override {
|
||||
#else
|
||||
bool canHandle(HTTPMethod method, const String& uri) override {
|
||||
#endif
|
||||
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 {
|
||||
#endif
|
||||
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);
|
||||
}
|
||||
|
||||
this->firmwareResult.status = UpgradeStatus::NONE;
|
||||
this->firmwareResult.error.clear();
|
||||
|
||||
this->filesystemResult.status = UpgradeStatus::NONE;
|
||||
this->filesystemResult.error.clear();
|
||||
|
||||
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")) {
|
||||
result = &this->firmwareResult;
|
||||
} else if (upload.name.equals("filesystem")) {
|
||||
result = &this->filesystemResult;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result->status != UpgradeStatus::NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->beforeUpgradeCallback && !this->beforeUpgradeCallback(result->type)) {
|
||||
result->status = UpgradeStatus::PROHIBITED;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!upload.filename.length()) {
|
||||
result->status = UpgradeStatus::NO_FILE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
// reset
|
||||
if (Update.isRunning()) {
|
||||
Update.end(false);
|
||||
Update.clearError();
|
||||
}
|
||||
|
||||
bool begin = false;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
Update.runAsync(true);
|
||||
|
||||
if (result->type == UpgradeType::FIRMWARE) {
|
||||
begin = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000, U_FLASH);
|
||||
|
||||
} else if (result->type == UpgradeType::FILESYSTEM) {
|
||||
close_all_fs();
|
||||
begin = Update.begin((size_t)FS_end - (size_t)FS_start, U_FS);
|
||||
}
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
if (result->type == UpgradeType::FIRMWARE) {
|
||||
begin = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
|
||||
|
||||
} else if (result->type == UpgradeType::FILESYSTEM) {
|
||||
begin = Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!begin || Update.hasError()) {
|
||||
result->status = UpgradeStatus::ERROR_ON_START;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
result->error = Update.getErrorString();
|
||||
#else
|
||||
result->error = Update.errorString();
|
||||
#endif
|
||||
|
||||
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s', on start: %s"), upload.filename.c_str(), result->error.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s', started"), upload.filename.c_str());
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
|
||||
Update.end(false);
|
||||
|
||||
result->status = UpgradeStatus::ERROR_ON_WRITE;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
result->error = Update.getErrorString();
|
||||
#else
|
||||
result->error = Update.errorString();
|
||||
#endif
|
||||
|
||||
Log.serrorln(
|
||||
FPSTR(L_PORTAL_OTA),
|
||||
F("File '%s', on writing %d bytes: %s"),
|
||||
upload.filename.c_str(), upload.totalSize, result->error.c_str()
|
||||
);
|
||||
|
||||
} else {
|
||||
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s', writed %d bytes"), upload.filename.c_str(), upload.totalSize);
|
||||
}
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_END) {
|
||||
if (Update.end(true)) {
|
||||
result->status = UpgradeStatus::SUCCESS;
|
||||
|
||||
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s': finish"), upload.filename.c_str());
|
||||
|
||||
} else {
|
||||
result->status = UpgradeStatus::ERROR_ON_FINISH;
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
result->error = Update.getErrorString();
|
||||
#else
|
||||
result->error = Update.errorString();
|
||||
#endif
|
||||
|
||||
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s', on finish: %s"), upload.filename.c_str(), result->error);
|
||||
}
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
||||
Update.end(false);
|
||||
result->status = UpgradeStatus::ABORTED;
|
||||
|
||||
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s': aborted"), upload.filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
CanHandleCallback canHandleCallback;
|
||||
CanUploadCallback canUploadCallback;
|
||||
BeforeUpgradeCallback beforeUpgradeCallback;
|
||||
AfterUpgradeCallback afterUpgradeCallback;
|
||||
const char* uri = nullptr;
|
||||
|
||||
UpgradeResult firmwareResult{UpgradeType::FIRMWARE, UpgradeStatus::NONE};
|
||||
UpgradeResult filesystemResult{UpgradeType::FILESYSTEM, UpgradeStatus::NONE};
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
class IntParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
IntParameter(const char* id, const char* label, int value, const uint8_t length = 10) : WiFiManagerParameter("") {
|
||||
init(id, label, String(value).c_str(), length, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
int getValue() {
|
||||
return atoi(WiFiManagerParameter::getValue());
|
||||
}
|
||||
};
|
||||
|
||||
class UnsignedIntParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
UnsignedIntParameter(const char* id, const char* label, unsigned int value, const uint8_t length = 10) : WiFiManagerParameter("") {
|
||||
init(id, label, String(value).c_str(), length, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
unsigned int getValue() {
|
||||
return (unsigned int) atoi(WiFiManagerParameter::getValue());
|
||||
}
|
||||
};
|
||||
|
||||
class CheckboxParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
const char* checked = "type=\"checkbox\" checked";
|
||||
const char* noChecked = "type=\"checkbox\"";
|
||||
const char* trueVal = "T";
|
||||
|
||||
CheckboxParameter(const char* id, const char* label, bool value) : WiFiManagerParameter("") {
|
||||
init(id, label, value ? trueVal : "0", 1, "", WFM_LABEL_AFTER);
|
||||
}
|
||||
|
||||
const char* getValue() const override {
|
||||
return trueVal;
|
||||
}
|
||||
|
||||
const char* getCustomHTML() const override {
|
||||
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? checked : noChecked;
|
||||
}
|
||||
|
||||
bool getCheckboxValue() {
|
||||
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? true : false;
|
||||
}
|
||||
};
|
||||
|
||||
class SeparatorParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
SeparatorParameter() : WiFiManagerParameter("<hr>") {}
|
||||
};
|
||||
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
232
platformio.ini
@@ -8,46 +8,85 @@
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
;extra_configs = secrets.ini
|
||||
extra_configs = secrets.default.ini
|
||||
|
||||
[env]
|
||||
version = 1.4.3
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
arduino-libraries/NTPClient@^3.2.1
|
||||
bblanchon/ArduinoJson@^6.20.0
|
||||
ihormelnyk/OpenTherm Library@^1.1.4
|
||||
knolleary/PubSubClient@^2.8
|
||||
jandrassy/TelnetStream@^1.2.4
|
||||
gyverlibs/EEManager@^2.0
|
||||
gyverlibs/GyverPID@^3.3
|
||||
bblanchon/ArduinoJson@^7.0.4
|
||||
;ihormelnyk/OpenTherm Library@^1.1.5
|
||||
https://github.com/ihormelnyk/opentherm_library#master
|
||||
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
|
||||
https://github.com/PaulStoffregen/OneWire#master
|
||||
milesburton/DallasTemperature@^3.11.0
|
||||
https://github.com/Laxilef/WiFiManager/archive/refs/heads/patch-1.zip
|
||||
;https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
|
||||
build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH -mtext-section-literals
|
||||
laxilef/TinyLogger@^1.1.0
|
||||
build_flags =
|
||||
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
|
||||
;-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH
|
||||
-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_HTTP_SERVER -D DEBUG_ESP_PORT=Serial
|
||||
-D BUILD_VERSION='"${this.version}"'
|
||||
-D BUILD_ENV='"$PIOENV"'
|
||||
-D USE_SERIAL=${secrets.use_serial}
|
||||
-D USE_TELNET=${secrets.use_telnet}
|
||||
-D DEBUG_BY_DEFAULT=${secrets.debug}
|
||||
-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_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
|
||||
version = 1.3.3
|
||||
monitor_filters = direct
|
||||
board_build.flash_mode = dio
|
||||
board_build.filesystem = littlefs
|
||||
|
||||
; Defaults
|
||||
[esp8266_defaults]
|
||||
platform = espressif8266
|
||||
platform = espressif8266@^4.2.1
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
nrwiersma/ESP8266Scheduler@^1.0
|
||||
nrwiersma/ESP8266Scheduler@^1.2
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/build.py
|
||||
build_flags = ${env.build_flags}
|
||||
board_build.ldscript = eagle.flash.4m1m.ld
|
||||
|
||||
[esp32_defaults]
|
||||
platform = espressif32
|
||||
;platform = espressif32@^6.7
|
||||
platform = https://github.com/platformio/platform-espressif32.git
|
||||
platform_packages =
|
||||
;platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/archive/refs/tags/2.0.17.zip
|
||||
framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.1
|
||||
framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.1/esp32-arduino-libs-3.0.1.zip
|
||||
board_build.partitions = esp32_partitions.csv
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
laxilef/ESP32Scheduler@^1.0.0
|
||||
laxilef/ESP32Scheduler@^1.0.1
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/esp32.py
|
||||
post:tools/build.py
|
||||
build_flags = ${env.build_flags}
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D CORE_DEBUG_LEVEL=0
|
||||
|
||||
|
||||
; Boards
|
||||
@@ -57,14 +96,15 @@ 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_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
|
||||
|
||||
[env:d1_mini_lite]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
@@ -72,14 +112,15 @@ 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_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
|
||||
|
||||
[env:d1_mini_pro]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
@@ -87,71 +128,136 @@ 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_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
|
||||
|
||||
[env:nodemcu_8266]
|
||||
platform = ${esp8266_defaults.platform}
|
||||
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_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
|
||||
|
||||
[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_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
|
||||
|
||||
[env:s3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_s3_mini
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-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 ARDUINO_USB_MODE=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D USE_BLE=1
|
||||
-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
|
||||
|
||||
[env:c3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = lolin_c3_mini
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-mtext-section-literals
|
||||
build_flags =
|
||||
-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
|
||||
-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
|
||||
${esp32_defaults.build_flags}
|
||||
-D USE_BLE=1
|
||||
-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
|
||||
|
||||
[env:nodemcu_32s]
|
||||
[env:nodemcu_32]
|
||||
platform = ${esp32_defaults.platform}
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = nodemcu-32s
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
h2zero/NimBLE-Arduino@^1.4.1
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-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 USE_BLE=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
|
||||
;-D WOKWI=1
|
||||
|
||||
[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
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
-D USE_BLE=1
|
||||
-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
|
||||
|
||||
20
secrets.default.ini
Normal file
@@ -0,0 +1,20 @@
|
||||
[secrets]
|
||||
use_serial = true
|
||||
use_telnet = true
|
||||
debug = true
|
||||
hostname = opentherm
|
||||
|
||||
ap_ssid = OpenTherm Gateway
|
||||
ap_password = otgateway123456
|
||||
|
||||
sta_ssid =
|
||||
sta_password =
|
||||
|
||||
portal_login = admin
|
||||
portal_password = admin
|
||||
|
||||
mqtt_server =
|
||||
mqtt_port = 1883
|
||||
mqtt_user =
|
||||
mqtt_password =
|
||||
mqtt_prefix = opentherm
|
||||
1580
src/HaHelper.h
352
src/MainTask.h
@@ -1,134 +1,247 @@
|
||||
#include <Blinker.h>
|
||||
|
||||
using namespace NetworkUtils;
|
||||
|
||||
extern NetworkMgr* network;
|
||||
extern MqttTask* tMqtt;
|
||||
extern SensorsTask* tSensors;
|
||||
extern OpenThermTask* tOt;
|
||||
extern FileData fsSettings, fsNetworkSettings;
|
||||
extern ESPTelnetStream* telnetStream;
|
||||
|
||||
|
||||
class MainTask : public Task {
|
||||
public:
|
||||
MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
|
||||
MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
|
||||
this->blinker = new Blinker();
|
||||
|
||||
network->setDelayCallback([this](unsigned int time) {
|
||||
this->delay(time);
|
||||
})->setYieldCallback([this]() {
|
||||
this->yield();
|
||||
});
|
||||
}
|
||||
|
||||
~MainTask() {
|
||||
delete this->blinker;
|
||||
}
|
||||
|
||||
protected:
|
||||
Blinker* blinker = nullptr;
|
||||
unsigned long lastHeapInfo = 0;
|
||||
unsigned long firstFailConnect = 0;
|
||||
unsigned int heapSize = 0;
|
||||
unsigned int minFreeHeapSize = 0;
|
||||
enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
|
||||
|
||||
const char* getTaskName() {
|
||||
Blinker* blinker = nullptr;
|
||||
unsigned long firstFailConnect = 0;
|
||||
unsigned long lastHeapInfo = 0;
|
||||
unsigned int minFreeHeap = 0;
|
||||
unsigned int minMaxFreeBlockHeap = 0;
|
||||
unsigned long restartSignalTime = 0;
|
||||
bool heatingEnabled = false;
|
||||
unsigned long heatingDisabledTime = 0;
|
||||
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
|
||||
unsigned long externalPumpStartTime = 0;
|
||||
bool telnetStarted = false;
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Main";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}
|
||||
}*/
|
||||
|
||||
void setup() {
|
||||
#ifdef LED_STATUS_PIN
|
||||
pinMode(LED_STATUS_PIN, OUTPUT);
|
||||
digitalWrite(LED_STATUS_PIN, false);
|
||||
#endif
|
||||
|
||||
#if defined(ESP32)
|
||||
heapSize = ESP.getHeapSize();
|
||||
#elif defined(ESP8266)
|
||||
heapSize = 81920;
|
||||
#elif
|
||||
heapSize = 99999;
|
||||
#endif
|
||||
minFreeHeapSize = heapSize;
|
||||
int getTaskPriority() override {
|
||||
return 3;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() {}
|
||||
|
||||
void loop() {
|
||||
if (eeSettings.tick()) {
|
||||
INFO("Settings updated (EEPROM)");
|
||||
network->loop();
|
||||
|
||||
if (fsSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (vars.parameters.restartAfterTime > 0 && millis() - vars.parameters.restartSignalTime > vars.parameters.restartAfterTime) {
|
||||
vars.parameters.restartAfterTime = 0;
|
||||
|
||||
INFO("Received restart message...");
|
||||
eeSettings.updateNow();
|
||||
INFO("Restart...");
|
||||
delay(1000);
|
||||
|
||||
ESP.restart();
|
||||
if (fsNetworkSettings.tick() == FD_WRITE) {
|
||||
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
|
||||
if (vars.actions.restart) {
|
||||
vars.actions.restart = false;
|
||||
this->restartSignalTime = millis();
|
||||
|
||||
// save settings
|
||||
fsSettings.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."));
|
||||
}
|
||||
|
||||
vars.states.mqtt = tMqtt->isConnected();
|
||||
vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0;
|
||||
|
||||
if (vars.states.emergency && !settings.emergency.enable) {
|
||||
vars.states.emergency = false;
|
||||
}
|
||||
|
||||
if (network->isConnected()) {
|
||||
if (!this->telnetStarted && telnetStream != nullptr) {
|
||||
telnetStream->begin(23, false);
|
||||
this->telnetStarted = true;
|
||||
}
|
||||
|
||||
if (settings.mqtt.enable && !tMqtt->isEnabled()) {
|
||||
tMqtt->enable();
|
||||
|
||||
} else if (!settings.mqtt.enable && tMqtt->isEnabled()) {
|
||||
tMqtt->disable();
|
||||
}
|
||||
|
||||
if (firstFailConnect != 0) {
|
||||
firstFailConnect = 0;
|
||||
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onMqttFault && !tMqtt->isEnabled()) {
|
||||
vars.states.emergency = true;
|
||||
|
||||
} else if (vars.states.emergency && !settings.emergency.onMqttFault) {
|
||||
vars.states.emergency = false;
|
||||
}
|
||||
|
||||
vars.states.rssi = WiFi.RSSI();
|
||||
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);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this->telnetStarted) {
|
||||
telnetStream->stop();
|
||||
this->telnetStarted = false;
|
||||
}
|
||||
|
||||
if (tMqtt->isEnabled()) {
|
||||
tMqtt->disable();
|
||||
}
|
||||
|
||||
if (settings.emergency.enable && !vars.states.emergency) {
|
||||
if (firstFailConnect == 0) {
|
||||
firstFailConnect = millis();
|
||||
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onNetworkFault) {
|
||||
if (this->firstFailConnect == 0) {
|
||||
this->firstFailConnect = millis();
|
||||
}
|
||||
|
||||
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
|
||||
if (millis() - this->firstFailConnect > (settings.emergency.tresholdTime * 1000)) {
|
||||
vars.states.emergency = true;
|
||||
INFO("Emergency mode enabled");
|
||||
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
this->yield();
|
||||
|
||||
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
|
||||
tOt->enable();
|
||||
|
||||
this->ledStatus();
|
||||
this->externalPump();
|
||||
this->yield();
|
||||
|
||||
|
||||
// telnet
|
||||
if (this->telnetStarted) {
|
||||
telnetStream->loop();
|
||||
this->yield();
|
||||
}
|
||||
|
||||
#ifdef LED_STATUS_PIN
|
||||
ledStatus(LED_STATUS_PIN);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TELNET
|
||||
yield();
|
||||
|
||||
// anti memory leak
|
||||
TelnetStream.flush();
|
||||
while (TelnetStream.available() > 0) {
|
||||
TelnetStream.read();
|
||||
for (Stream* stream : Log.getStreams()) {
|
||||
while (stream->available() > 0) {
|
||||
stream->read();
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (settings.debug) {
|
||||
unsigned int freeHeapSize = ESP.getFreeHeap();
|
||||
unsigned int minFreeHeapSizeDiff = 0;
|
||||
// heap info
|
||||
this->heap();
|
||||
|
||||
if (freeHeapSize < minFreeHeapSize) {
|
||||
minFreeHeapSizeDiff = minFreeHeapSize - freeHeapSize;
|
||||
minFreeHeapSize = freeHeapSize;
|
||||
}
|
||||
|
||||
if (millis() - lastHeapInfo > 10000 || minFreeHeapSizeDiff > 0) {
|
||||
DEBUG_F("Free heap size: %u of %u bytes, min: %u bytes (diff: %u bytes)\n", freeHeapSize, heapSize, minFreeHeapSize, minFreeHeapSizeDiff);
|
||||
lastHeapInfo = millis();
|
||||
}
|
||||
// restart
|
||||
if (this->restartSignalTime > 0 && millis() - this->restartSignalTime > 10000) {
|
||||
this->restartSignalTime = 0;
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
void ledStatus(uint8_t ledPin) {
|
||||
void heap() {
|
||||
unsigned int freeHeap = getFreeHeap();
|
||||
unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap();
|
||||
|
||||
if (!this->restartSignalTime && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
|
||||
this->restartSignalTime = millis();
|
||||
}
|
||||
|
||||
if (!settings.system.debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t minFreeHeap = getFreeHeap(true);
|
||||
size_t minFreeHeapDiff = 0;
|
||||
if (minFreeHeap < this->minFreeHeap || this->minFreeHeap == 0) {
|
||||
minFreeHeapDiff = this->minFreeHeap - minFreeHeap;
|
||||
this->minFreeHeap = minFreeHeap;
|
||||
}
|
||||
|
||||
size_t minMaxFreeBlockHeap = getMaxFreeBlockHeap(true);
|
||||
size_t minMaxFreeBlockHeapDiff = 0;
|
||||
if (minMaxFreeBlockHeap < this->minMaxFreeBlockHeap || this->minMaxFreeBlockHeap == 0) {
|
||||
minMaxFreeBlockHeapDiff = this->minMaxFreeBlockHeap - minMaxFreeBlockHeap;
|
||||
this->minMaxFreeBlockHeap = minMaxFreeBlockHeap;
|
||||
}
|
||||
|
||||
if (millis() - this->lastHeapInfo > 20000 || minFreeHeapDiff > 0 || minMaxFreeBlockHeapDiff > 0) {
|
||||
Log.sverboseln(
|
||||
FPSTR(L_MAIN),
|
||||
F("Free heap size: %u of %u bytes (min: %u, diff: %u), max free block: %u (min: %u, diff: %u, frag: %hhu%%)"),
|
||||
freeHeap, getTotalHeap(), this->minFreeHeap, minFreeHeapDiff, maxFreeBlockHeap, this->minMaxFreeBlockHeap, minMaxFreeBlockHeapDiff, getHeapFrag()
|
||||
);
|
||||
this->lastHeapInfo = millis();
|
||||
}
|
||||
}
|
||||
|
||||
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->blinker == nullptr) {
|
||||
this->blinker = new Blinker(ledPin);
|
||||
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 (WiFi.status() != WL_CONNECTED) {
|
||||
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!network->isConnected()) {
|
||||
errors[errCount++] = 2;
|
||||
}
|
||||
|
||||
@@ -152,14 +265,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;
|
||||
@@ -178,4 +291,95 @@ protected:
|
||||
|
||||
this->blinker->tick();
|
||||
}
|
||||
|
||||
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.states.externalPump) {
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vars.states.heating && this->heatingEnabled) {
|
||||
this->heatingEnabled = false;
|
||||
this->heatingDisabledTime = millis();
|
||||
|
||||
} else if (vars.states.heating && !this->heatingEnabled) {
|
||||
this->heatingEnabled = true;
|
||||
}
|
||||
|
||||
if (!settings.externalPump.use) {
|
||||
if (vars.states.externalPump) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (vars.states.externalPump && !this->heatingEnabled) {
|
||||
if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
|
||||
digitalWrite(configuredGpio, LOW);
|
||||
|
||||
vars.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
|
||||
Log.sinfoln("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.states.externalPump = false;
|
||||
vars.parameters.extPumpLastEnableTime = millis();
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time"));
|
||||
}
|
||||
|
||||
} else if (vars.states.externalPump && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) {
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
|
||||
|
||||
} else if (!vars.states.externalPump && this->heatingEnabled) {
|
||||
vars.states.externalPump = true;
|
||||
this->externalPumpStartTime = millis();
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
|
||||
|
||||
digitalWrite(configuredGpio, HIGH);
|
||||
|
||||
Log.sinfoln("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;
|
||||
this->externalPumpStartTime = millis();
|
||||
this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK;
|
||||
|
||||
digitalWrite(configuredGpio, HIGH);
|
||||
|
||||
Log.sinfoln("EXTPUMP", F("Enabled: anti stuck"));
|
||||
}
|
||||
}
|
||||
};
|
||||
952
src/MqttTask.h
1051
src/OpenThermTask.h
720
src/PortalTask.h
Normal file
@@ -0,0 +1,720 @@
|
||||
#define PORTAL_CACHE_TIME "max-age=86400"
|
||||
#define PORTAL_CACHE settings.system.debug ? nullptr : PORTAL_CACHE_TIME
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <Updater.h>
|
||||
using WebServer = ESP8266WebServer;
|
||||
#else
|
||||
#include <WebServer.h>
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#include <BufferedWebServer.h>
|
||||
#include <StaticPage.h>
|
||||
#include <DynamicPage.h>
|
||||
#include <UpgradeHandler.h>
|
||||
#include <DNSServer.h>
|
||||
|
||||
using namespace NetworkUtils;
|
||||
|
||||
extern NetworkMgr* network;
|
||||
extern FileData fsSettings, fsNetworkSettings;
|
||||
extern MqttTask* tMqtt;
|
||||
|
||||
|
||||
class PortalTask : public LeanTask {
|
||||
public:
|
||||
PortalTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
|
||||
this->webServer = new WebServer(80);
|
||||
this->bufferedWebServer = new BufferedWebServer(this->webServer, 32u);
|
||||
this->dnsServer = new DNSServer();
|
||||
}
|
||||
|
||||
~PortalTask() {
|
||||
delete this->bufferedWebServer;
|
||||
|
||||
if (this->webServer != nullptr) {
|
||||
this->stopWebServer();
|
||||
delete this->webServer;
|
||||
}
|
||||
|
||||
if (this->dnsServer != nullptr) {
|
||||
this->stopDnsServer();
|
||||
delete this->dnsServer;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
const unsigned int changeStateInterval = 5000;
|
||||
|
||||
WebServer* webServer = nullptr;
|
||||
BufferedWebServer* bufferedWebServer = nullptr;
|
||||
DNSServer* dnsServer = nullptr;
|
||||
|
||||
bool webServerEnabled = false;
|
||||
bool dnsServerEnabled = false;
|
||||
unsigned long webServerChangeState = 0;
|
||||
unsigned long dnsServerChangeState = 0;
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Portal";
|
||||
}
|
||||
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}*/
|
||||
|
||||
int getTaskPriority() override {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
this->dnsServer->setTTL(0);
|
||||
this->dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
this->webServer->enableETag(true);
|
||||
#endif
|
||||
|
||||
// index page
|
||||
/*auto indexPage = (new DynamicPage("/", &LittleFS, "/pages/index.html"))
|
||||
->setTemplateCallback([](const char* var) -> String {
|
||||
String result;
|
||||
|
||||
if (strcmp(var, "ver") == 0) {
|
||||
result = BUILD_VERSION;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
this->webServer->addHandler(indexPage);*/
|
||||
this->webServer->addHandler(new StaticPage("/", &LittleFS, "/pages/index.html", PORTAL_CACHE));
|
||||
|
||||
// dashboard page
|
||||
auto dashboardPage = (new StaticPage("/dashboard.html", &LittleFS, "/pages/dashboard.html", PORTAL_CACHE))
|
||||
->setBeforeSendCallback([this]() {
|
||||
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->requestAuthentication(DIGEST_AUTH);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this->webServer->addHandler(dashboardPage);
|
||||
|
||||
// restart
|
||||
this->webServer->on("/restart.html", HTTP_GET, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->send(401);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vars.actions.restart = true;
|
||||
this->webServer->sendHeader("Location", "/");
|
||||
this->webServer->send(302);
|
||||
});
|
||||
|
||||
// network settings page
|
||||
auto networkPage = (new StaticPage("/network.html", &LittleFS, "/pages/network.html", PORTAL_CACHE))
|
||||
->setBeforeSendCallback([this]() {
|
||||
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->requestAuthentication(DIGEST_AUTH);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this->webServer->addHandler(networkPage);
|
||||
|
||||
// settings page
|
||||
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/pages/settings.html", PORTAL_CACHE))
|
||||
->setBeforeSendCallback([this]() {
|
||||
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->requestAuthentication(DIGEST_AUTH);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this->webServer->addHandler(settingsPage);
|
||||
|
||||
// upgrade page
|
||||
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/pages/upgrade.html", PORTAL_CACHE))
|
||||
->setBeforeSendCallback([this]() {
|
||||
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->requestAuthentication(DIGEST_AUTH);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
this->webServer->addHandler(upgradePage);
|
||||
|
||||
// OTA
|
||||
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) {
|
||||
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->sendHeader("Connection", "close");
|
||||
this->webServer->send(401);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->setBeforeUpgradeCallback([](UpgradeHandler::UpgradeType type) -> bool {
|
||||
return true;
|
||||
})->setAfterUpgradeCallback([this](const UpgradeHandler::UpgradeResult& fwResult, const UpgradeHandler::UpgradeResult& fsResult) {
|
||||
unsigned short status = 200;
|
||||
if (fwResult.status == UpgradeHandler::UpgradeStatus::SUCCESS || fsResult.status == UpgradeHandler::UpgradeStatus::SUCCESS) {
|
||||
vars.actions.restart = true;
|
||||
|
||||
} else {
|
||||
status = 400;
|
||||
}
|
||||
|
||||
String response = "{\"firmware\": {\"status\": ";
|
||||
response.concat((short int) fwResult.status);
|
||||
response.concat(", \"error\": \"");
|
||||
response.concat(fwResult.error);
|
||||
response.concat("\"}, \"filesystem\": {\"status\": ");
|
||||
response.concat((short int) fsResult.status);
|
||||
response.concat(", \"error\": \"");
|
||||
response.concat(fsResult.error);
|
||||
response.concat("\"}}");
|
||||
this->webServer->send(status, "application/json", response);
|
||||
});
|
||||
this->webServer->addHandler(upgradeHandler);
|
||||
|
||||
|
||||
// backup
|
||||
this->webServer->on("/api/backup/save", HTTP_GET, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument networkSettingsDoc;
|
||||
networkSettingsToJson(networkSettings, networkSettingsDoc);
|
||||
networkSettingsDoc.shrinkToFit();
|
||||
|
||||
JsonDocument settingsDoc;
|
||||
settingsToJson(settings, settingsDoc);
|
||||
settingsDoc.shrinkToFit();
|
||||
|
||||
JsonDocument doc;
|
||||
doc["network"] = networkSettingsDoc;
|
||||
doc["settings"] = settingsDoc;
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"backup.json\""));
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
});
|
||||
|
||||
this->webServer->on("/api/backup/restore", HTTP_POST, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
String plain = this->webServer->arg(0);
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/backup/restore %d bytes: %s"), plain.length(), plain.c_str());
|
||||
|
||||
if (plain.length() < 5) {
|
||||
this->webServer->send(406);
|
||||
return;
|
||||
|
||||
} else if (plain.length() > 2048) {
|
||||
this->webServer->send(413);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError dErr = deserializeJson(doc, plain);
|
||||
plain.clear();
|
||||
|
||||
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
|
||||
this->webServer->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (doc["settings"] && jsonToSettings(doc["settings"], settings)) {
|
||||
vars.actions.restart = true;
|
||||
fsSettings.update();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (doc["network"] && jsonToNetworkSettings(doc["network"], networkSettings)) {
|
||||
fsNetworkSettings.update();
|
||||
network->setHostname(networkSettings.hostname)
|
||||
->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel)
|
||||
->setApCredentials(networkSettings.ap.ssid, networkSettings.ap.password, networkSettings.ap.channel)
|
||||
->setUseDhcp(networkSettings.useDhcp)
|
||||
->setStaticConfig(
|
||||
networkSettings.staticConfig.ip,
|
||||
networkSettings.staticConfig.gateway,
|
||||
networkSettings.staticConfig.subnet,
|
||||
networkSettings.staticConfig.dns
|
||||
)
|
||||
->reconnect();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->webServer->send(changed ? 201 : 200);
|
||||
});
|
||||
|
||||
// network
|
||||
this->webServer->on("/api/network/settings", HTTP_GET, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
networkSettingsToJson(networkSettings, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
});
|
||||
|
||||
this->webServer->on("/api/network/settings", HTTP_POST, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
String plain = this->webServer->arg(0);
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/network/settings %d bytes: %s"), plain.length(), plain.c_str());
|
||||
|
||||
if (plain.length() < 5) {
|
||||
this->webServer->send(406);
|
||||
return;
|
||||
|
||||
} else if (plain.length() > 512) {
|
||||
this->webServer->send(413);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError dErr = deserializeJson(doc, plain);
|
||||
plain.clear();
|
||||
|
||||
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
|
||||
this->webServer->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = jsonToNetworkSettings(doc, networkSettings);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
networkSettingsToJson(networkSettings, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
|
||||
|
||||
if (changed) {
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
fsNetworkSettings.update();
|
||||
network->setHostname(networkSettings.hostname)
|
||||
->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel)
|
||||
->setApCredentials(networkSettings.ap.ssid, networkSettings.ap.password, networkSettings.ap.channel)
|
||||
->setUseDhcp(networkSettings.useDhcp)
|
||||
->setStaticConfig(
|
||||
networkSettings.staticConfig.ip,
|
||||
networkSettings.staticConfig.gateway,
|
||||
networkSettings.staticConfig.subnet,
|
||||
networkSettings.staticConfig.dns
|
||||
)
|
||||
->reconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this->webServer->on("/api/network/scan", HTTP_GET, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
this->webServer->send(401);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto apCount = WiFi.scanComplete();
|
||||
if (apCount <= 0) {
|
||||
if (apCount != WIFI_SCAN_RUNNING) {
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
WiFi.scanNetworks(true, true);
|
||||
#else
|
||||
WiFi.scanNetworks(true, true, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
this->webServer->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
for (short int i = 0; i < apCount; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
doc[i]["ssid"] = ssid;
|
||||
doc[i]["bssid"] = WiFi.BSSIDstr(i);
|
||||
doc[i]["signalQuality"] = NetworkMgr::rssiToSignalQuality(WiFi.RSSI(i));
|
||||
doc[i]["channel"] = WiFi.channel(i);
|
||||
doc[i]["hidden"] = !ssid.length();
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
const bss_info* info = WiFi.getScanInfoByIndex(i);
|
||||
doc[i]["auth"] = info->authmode;
|
||||
#else
|
||||
doc[i]["auth"] = WiFi.encryptionType(i);
|
||||
#endif
|
||||
}
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
|
||||
WiFi.scanDelete();
|
||||
});
|
||||
|
||||
|
||||
// settings
|
||||
this->webServer->on("/api/settings", HTTP_GET, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
settingsToJson(settings, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
});
|
||||
|
||||
this->webServer->on("/api/settings", HTTP_POST, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
String plain = this->webServer->arg(0);
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/settings %d bytes: %s"), plain.length(), plain.c_str());
|
||||
|
||||
if (plain.length() < 5) {
|
||||
this->webServer->send(406);
|
||||
return;
|
||||
|
||||
} else if (plain.length() > 2048) {
|
||||
this->webServer->send(413);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError dErr = deserializeJson(doc, plain);
|
||||
plain.clear();
|
||||
|
||||
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
|
||||
this->webServer->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = jsonToSettings(doc, settings);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
settingsToJson(settings, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
|
||||
|
||||
if (changed) {
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
fsSettings.update();
|
||||
tMqtt->resetPublishedSettingsTime();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// vars
|
||||
this->webServer->on("/api/vars", HTTP_GET, [this]() {
|
||||
JsonDocument doc;
|
||||
varsToJson(vars, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
});
|
||||
|
||||
this->webServer->on("/api/vars", HTTP_POST, [this]() {
|
||||
if (this->isAuthRequired()) {
|
||||
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
|
||||
return this->webServer->send(401);
|
||||
}
|
||||
}
|
||||
|
||||
String plain = this->webServer->arg(0);
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/vars %d bytes: %s"), plain.length(), plain.c_str());
|
||||
|
||||
if (plain.length() < 5) {
|
||||
this->webServer->send(406);
|
||||
return;
|
||||
|
||||
} else if (plain.length() > 1024) {
|
||||
this->webServer->send(413);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError dErr = deserializeJson(doc, plain);
|
||||
plain.clear();
|
||||
|
||||
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
|
||||
this->webServer->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = jsonToVars(doc, vars);
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
varsToJson(vars, doc);
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
|
||||
|
||||
if (changed) {
|
||||
doc.clear();
|
||||
doc.shrinkToFit();
|
||||
|
||||
tMqtt->resetPublishedVarsTime();
|
||||
}
|
||||
});
|
||||
|
||||
this->webServer->on("/api/info", HTTP_GET, [this]() {
|
||||
bool isConnected = network->isConnected();
|
||||
|
||||
JsonDocument doc;
|
||||
doc["network"]["hostname"] = networkSettings.hostname;
|
||||
doc["network"]["mac"] = network->getStaMac();
|
||||
doc["network"]["connected"] = isConnected;
|
||||
doc["network"]["ssid"] = network->getStaSsid();
|
||||
doc["network"]["signalQuality"] = isConnected ? NetworkMgr::rssiToSignalQuality(network->getRssi()) : 0;
|
||||
doc["network"]["channel"] = isConnected ? network->getStaChannel() : 0;
|
||||
doc["network"]["ip"] = isConnected ? network->getStaIp().toString() : "";
|
||||
doc["network"]["subnet"] = isConnected ? network->getStaSubnet().toString() : "";
|
||||
doc["network"]["gateway"] = isConnected ? network->getStaGateway().toString() : "";
|
||||
doc["network"]["dns"] = isConnected ? network->getStaDns().toString() : "";
|
||||
|
||||
doc["system"]["buildVersion"] = BUILD_VERSION;
|
||||
doc["system"]["buildDate"] = __DATE__ " " __TIME__;
|
||||
doc["system"]["buildEnv"] = BUILD_ENV;
|
||||
doc["system"]["uptime"] = millis() / 1000ul;
|
||||
doc["system"]["totalHeap"] = getTotalHeap();
|
||||
doc["system"]["freeHeap"] = getFreeHeap();
|
||||
doc["system"]["minFreeHeap"] = getFreeHeap(true);
|
||||
doc["system"]["maxFreeBlockHeap"] = getMaxFreeBlockHeap();
|
||||
doc["system"]["minMaxFreeBlockHeap"] = getMaxFreeBlockHeap(true);
|
||||
doc["system"]["resetReason"] = getResetReason();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
doc["system"]["chipModel"] = esp_is_8285() ? "ESP8285" : "ESP8266";
|
||||
doc["system"]["chipRevision"] = 0;
|
||||
doc["system"]["chipCores"] = 1;
|
||||
doc["system"]["cpuFreq"] = ESP.getCpuFreqMHz();
|
||||
doc["system"]["coreVersion"] = ESP.getCoreVersion();
|
||||
doc["system"]["flashSize"] = ESP.getFlashChipSize();
|
||||
doc["system"]["flashRealSize"] = ESP.getFlashChipRealSize();
|
||||
#elif ARDUINO_ARCH_ESP32
|
||||
doc["system"]["chipModel"] = ESP.getChipModel();
|
||||
doc["system"]["chipRevision"] = ESP.getChipRevision();
|
||||
doc["system"]["chipCores"] = ESP.getChipCores();
|
||||
doc["system"]["cpuFreq"] = ESP.getCpuFreqMHz();
|
||||
doc["system"]["coreVersion"] = ESP.getSdkVersion();
|
||||
doc["system"]["flashSize"] = ESP.getFlashChipSize();
|
||||
doc["system"]["flashRealSize"] = doc["system"]["flashSize"];
|
||||
#else
|
||||
doc["system"]["chipModel"] = 0;
|
||||
doc["system"]["chipRevision"] = 0;
|
||||
doc["system"]["chipCores"] = 0;
|
||||
doc["system"]["cpuFreq"] = 0;
|
||||
doc["system"]["coreVersion"] = 0;
|
||||
doc["system"]["flashSize"] = 0;
|
||||
doc["system"]["flashRealSize"] = 0;
|
||||
#endif
|
||||
|
||||
|
||||
doc.shrinkToFit();
|
||||
|
||||
this->bufferedWebServer->send(200, "application/json", doc);
|
||||
});
|
||||
|
||||
|
||||
// not found
|
||||
this->webServer->onNotFound([this]() {
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Page not found, uri: %s"), this->webServer->uri().c_str());
|
||||
|
||||
const String uri = this->webServer->uri();
|
||||
if (uri.equals("/")) {
|
||||
this->webServer->send(200, "text/plain", F("The file system is not flashed!"));
|
||||
|
||||
} else if (network->isApEnabled()) {
|
||||
this->onCaptivePortal();
|
||||
|
||||
} else {
|
||||
this->webServer->send(404, "text/plain", F("Page not found"));
|
||||
}
|
||||
});
|
||||
|
||||
this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/images/favicon.ico", PORTAL_CACHE);
|
||||
this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// web server
|
||||
if (!this->stateWebServer() && (network->isApEnabled() || network->isConnected()) && millis() - this->webServerChangeState >= this->changeStateInterval) {
|
||||
this->startWebServer();
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected"));
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
|
||||
} else if (this->stateWebServer() && !network->isApEnabled() && !network->isStaEnabled()) {
|
||||
this->stopWebServer();
|
||||
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down"));
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// dns server
|
||||
if (!this->stateDnsServer() && this->stateWebServer() && network->isApEnabled() && network->hasApClients() && millis() - this->dnsServerChangeState >= this->changeStateInterval) {
|
||||
this->startDnsServer();
|
||||
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up"));
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
|
||||
} else if (this->stateDnsServer() && (!network->isApEnabled() || !this->stateWebServer())) {
|
||||
this->stopDnsServer();
|
||||
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down"));
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (this->stateDnsServer()) {
|
||||
this->dnsServer->processNextRequest();
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
::delay(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (this->stateWebServer()) {
|
||||
this->webServer->handleClient();
|
||||
}
|
||||
|
||||
if (!this->stateDnsServer() && !this->stateWebServer()) {
|
||||
this->delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
bool isAuthRequired() {
|
||||
return !network->isApEnabled() && settings.portal.auth && strlen(settings.portal.password);
|
||||
}
|
||||
|
||||
void onCaptivePortal() {
|
||||
const String uri = this->webServer->uri();
|
||||
|
||||
if (uri.equals("/connecttest.txt")) {
|
||||
this->webServer->sendHeader(F("Location"), F("http://logout.net"));
|
||||
this->webServer->send(302);
|
||||
|
||||
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to http://logout.net with 302 code"));
|
||||
|
||||
} else if (uri.equals("/wpad.dat")) {
|
||||
this->webServer->send(404);
|
||||
|
||||
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 404 code"));
|
||||
|
||||
} else if (uri.equals("/success.txt")) {
|
||||
this->webServer->send(200);
|
||||
|
||||
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 200 code"));
|
||||
|
||||
} else {
|
||||
String portalUrl = "http://" + network->getApIp().toString() + '/';
|
||||
|
||||
this->webServer->sendHeader("Location", portalUrl.c_str());
|
||||
this->webServer->send(302);
|
||||
|
||||
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to portal page with 302 code"));
|
||||
}
|
||||
}
|
||||
|
||||
bool stateWebServer() {
|
||||
return this->webServerEnabled;
|
||||
}
|
||||
|
||||
void startWebServer() {
|
||||
if (this->stateWebServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->webServer->begin();
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
this->webServer->getServer().setNoDelay(true);
|
||||
#endif
|
||||
this->webServerEnabled = true;
|
||||
this->webServerChangeState = millis();
|
||||
}
|
||||
|
||||
void stopWebServer() {
|
||||
if (!this->stateWebServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->webServer->handleClient();
|
||||
this->webServer->stop();
|
||||
this->webServerEnabled = false;
|
||||
this->webServerChangeState = millis();
|
||||
}
|
||||
|
||||
bool stateDnsServer() {
|
||||
return this->dnsServerEnabled;
|
||||
}
|
||||
|
||||
void startDnsServer() {
|
||||
if (this->stateDnsServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->dnsServer->start(53, "*", network->getApIp());
|
||||
this->dnsServerEnabled = true;
|
||||
this->dnsServerChangeState = millis();
|
||||
}
|
||||
|
||||
void stopDnsServer() {
|
||||
if (!this->stateDnsServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->dnsServer->processNextRequest();
|
||||
this->dnsServer->stop();
|
||||
this->dnsServerEnabled = false;
|
||||
this->dnsServerChangeState = millis();
|
||||
}
|
||||
};
|
||||
@@ -1,127 +1,136 @@
|
||||
#include <Equitherm.h>
|
||||
#include <GyverPID.h>
|
||||
#include <PIDtuner.h>
|
||||
|
||||
Equitherm etRegulator;
|
||||
GyverPID pidRegulator(0, 0, 0);
|
||||
PIDtuner pidTuner;
|
||||
|
||||
|
||||
class RegulatorTask : public LeanTask {
|
||||
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;
|
||||
|
||||
const char* getTaskName() {
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
const char* getTaskName() override {
|
||||
return "Regulator";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
/*BaseType_t getTaskCore() override {
|
||||
return 1;
|
||||
}*/
|
||||
|
||||
int getTaskPriority() override {
|
||||
return 4;
|
||||
}
|
||||
#endif
|
||||
|
||||
void loop() {
|
||||
byte newTemp = vars.parameters.heatingSetpoint;
|
||||
float newTemp = vars.parameters.heatingSetpoint;
|
||||
|
||||
if (vars.states.emergency) {
|
||||
if (settings.heating.turbo) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
INFO("[REGULATOR] Turbo mode auto disabled");
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
|
||||
}
|
||||
|
||||
newTemp = getEmergencyModeTemp();
|
||||
newTemp = this->getEmergencyModeTemp();
|
||||
|
||||
} else {
|
||||
if (vars.tuning.enable || tunerInit) {
|
||||
if (settings.heating.turbo) {
|
||||
settings.heating.turbo = false;
|
||||
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;
|
||||
|
||||
INFO("[REGULATOR] Turbo mode auto disabled");
|
||||
}
|
||||
|
||||
newTemp = getTuningModeTemp();
|
||||
|
||||
if (newTemp == 0) {
|
||||
vars.tuning.enable = false;
|
||||
}
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
|
||||
}
|
||||
|
||||
if (!vars.tuning.enable) {
|
||||
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || (settings.equitherm.enable && settings.pid.enable))) {
|
||||
settings.heating.turbo = false;
|
||||
|
||||
INFO("[REGULATOR] Turbo mode auto disabled");
|
||||
}
|
||||
|
||||
newTemp = getNormalModeTemp();
|
||||
}
|
||||
newTemp = this->getNormalModeTemp();
|
||||
}
|
||||
|
||||
// Ограничиваем, если до этого не ограничило
|
||||
if (newTemp < vars.parameters.heatingMinTemp || newTemp > vars.parameters.heatingMaxTemp) {
|
||||
newTemp = constrain(newTemp, vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||
}
|
||||
// Limits
|
||||
newTemp = constrain(
|
||||
newTemp,
|
||||
!settings.opentherm.nativeHeatingControl ? settings.heating.minTemp : THERMOSTAT_INDOOR_MIN_TEMP,
|
||||
!settings.opentherm.nativeHeatingControl ? settings.heating.maxTemp : THERMOSTAT_INDOOR_MAX_TEMP
|
||||
);
|
||||
|
||||
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
|
||||
if (fabs(vars.parameters.heatingSetpoint - newTemp) > 0.4999f) {
|
||||
vars.parameters.heatingSetpoint = newTemp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
byte getEmergencyModeTemp() {
|
||||
float getEmergencyModeTemp() {
|
||||
float newTemp = 0;
|
||||
|
||||
// if use equitherm
|
||||
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != 1) {
|
||||
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||
if (settings.emergency.useEquitherm) {
|
||||
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
|
||||
|
||||
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
|
||||
if (fabs(prevEtResult - etResult) > 0.4999f) {
|
||||
prevEtResult = etResult;
|
||||
newTemp += etResult;
|
||||
|
||||
INFO_F("[REGULATOR][EQUITHERM] New emergency result: %u (%f) \n", (int)round(etResult), etResult);
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New emergency result: %.2f"), etResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevEtResult;
|
||||
}
|
||||
|
||||
} else if(settings.emergency.usePid) {
|
||||
if (vars.parameters.heatingEnabled) {
|
||||
float pidResult = getPidTemp(
|
||||
settings.heating.minTemp,
|
||||
settings.heating.maxTemp
|
||||
);
|
||||
|
||||
if (fabs(prevPidResult - pidResult) > 0.4999f) {
|
||||
prevPidResult = pidResult;
|
||||
newTemp += pidResult;
|
||||
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New emergency result: %.2f"), pidResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else if (!vars.parameters.heatingEnabled && prevPidResult != 0) {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else {
|
||||
// default temp, manual mode
|
||||
newTemp = settings.emergency.target;
|
||||
}
|
||||
|
||||
return round(newTemp);
|
||||
return newTemp;
|
||||
}
|
||||
|
||||
byte getNormalModeTemp() {
|
||||
float getNormalModeTemp() {
|
||||
float newTemp = 0;
|
||||
|
||||
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
|
||||
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001f) {
|
||||
prevHeatingTarget = settings.heating.target;
|
||||
INFO_F("[REGULATOR] New target: %f \n", settings.heating.target);
|
||||
Log.sinfoln(FPSTR(L_REGULATOR), F("New target: %.2f"), settings.heating.target);
|
||||
|
||||
if (settings.equitherm.enable && settings.pid.enable) {
|
||||
if (/*settings.equitherm.enable && */settings.pid.enable) {
|
||||
pidRegulator.integral = 0;
|
||||
INFO_F("[REGULATOR][PID] Integral sum has been reset");
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
}
|
||||
|
||||
// if use equitherm
|
||||
if (settings.equitherm.enable) {
|
||||
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
|
||||
|
||||
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
|
||||
if (fabs(prevEtResult - etResult) > 0.4999f) {
|
||||
prevEtResult = etResult;
|
||||
newTemp += etResult;
|
||||
|
||||
INFO_F("[REGULATOR][EQUITHERM] New result: %u (%f) \n", (int)round(etResult), etResult);
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %.2f"), etResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevEtResult;
|
||||
@@ -129,24 +138,30 @@ protected:
|
||||
}
|
||||
|
||||
// if use pid
|
||||
if (settings.pid.enable && 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.enable) {
|
||||
if (vars.parameters.heatingEnabled) {
|
||||
float pidResult = getPidTemp(
|
||||
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
|
||||
settings.pid.maxTemp
|
||||
);
|
||||
|
||||
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
|
||||
prevPidResult = pidResult;
|
||||
newTemp += pidResult;
|
||||
if (fabs(prevPidResult - pidResult) > 0.4999f) {
|
||||
prevPidResult = pidResult;
|
||||
newTemp += pidResult;
|
||||
|
||||
INFO_F("[REGULATOR][PID] New result: %d (%f) \n", (int)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;
|
||||
}
|
||||
|
||||
} else if (settings.pid.enable && !vars.parameters.heatingEnabled && prevPidResult != 0) {
|
||||
newTemp += prevPidResult;
|
||||
} else if (fabs(pidRegulator.integral) > 0.0001f) {
|
||||
pidRegulator.integral = 0;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
|
||||
// default temp, manual mode
|
||||
@@ -154,103 +169,46 @@ protected:
|
||||
newTemp = settings.heating.target;
|
||||
}
|
||||
|
||||
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;
|
||||
INFO(F("[REGULATOR][TUNING] Stopped"));
|
||||
}
|
||||
|
||||
if (!vars.tuning.enable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (vars.tuning.regulator == 0) {
|
||||
// @TODO дописать
|
||||
INFO(F("[REGULATOR][TUNING][EQUITHERM] Not implemented"));
|
||||
return 0;
|
||||
|
||||
} else if (vars.tuning.regulator == 1) {
|
||||
// PID tuner
|
||||
float defaultTemp = settings.equitherm.enable
|
||||
? getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp)
|
||||
: settings.heating.target;
|
||||
|
||||
if (tunerInit && pidTuner.getState() == 3) {
|
||||
INFO(F("[REGULATOR][TUNING][PID] Finished"));
|
||||
pidTuner.debugText(&INFO_STREAM);
|
||||
|
||||
pidTuner.reset();
|
||||
tunerInit = false;
|
||||
tunerRegulator = 0;
|
||||
tunerState = 0;
|
||||
|
||||
if (pidTuner.getAccuracy() < 90) {
|
||||
WARN(F("[REGULATOR][TUNING][PID] 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) {
|
||||
INFO(F("[REGULATOR][TUNING][PID] 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;
|
||||
INFO_F("[REGULATOR][TUNING][PID] Started. Start value: %f, step: %f \n", 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) {
|
||||
INFO(F("[REGULATOR][TUNING][PID] Log:"));
|
||||
pidTuner.debugText(&INFO_STREAM);
|
||||
tunerState = pidTuner.getState();
|
||||
}
|
||||
|
||||
return round(defaultTemp + pidTuner.getOutput());
|
||||
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Equitherm Temp
|
||||
* Calculations in degrees C, conversion occurs when using F
|
||||
*
|
||||
* @param minTemp
|
||||
* @param maxTemp
|
||||
* @return float
|
||||
*/
|
||||
float getEquithermTemp(int minTemp, int maxTemp) {
|
||||
float targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target;
|
||||
float indoorTemp = vars.temperatures.indoor;
|
||||
float outdoorTemp = vars.temperatures.outdoor;
|
||||
|
||||
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
minTemp = f2c(minTemp);
|
||||
maxTemp = f2c(maxTemp);
|
||||
targetTemp = f2c(targetTemp);
|
||||
indoorTemp = f2c(indoorTemp);
|
||||
outdoorTemp = f2c(outdoorTemp);
|
||||
}
|
||||
|
||||
if (vars.states.emergency) {
|
||||
etRegulator.Kt = 0;
|
||||
etRegulator.indoorTemp = 0;
|
||||
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||
if (settings.sensors.indoor.type == SensorType::MANUAL) {
|
||||
etRegulator.Kt = 0;
|
||||
etRegulator.indoorTemp = 0;
|
||||
|
||||
} else {
|
||||
etRegulator.Kt = settings.equitherm.t_factor;
|
||||
etRegulator.indoorTemp = indoorTemp;
|
||||
}
|
||||
|
||||
etRegulator.outdoorTemp = outdoorTemp;
|
||||
|
||||
} else if (settings.pid.enable) {
|
||||
etRegulator.Kt = 0;
|
||||
etRegulator.indoorTemp = round(vars.temperatures.indoor);
|
||||
etRegulator.outdoorTemp = round(vars.temperatures.outdoor);
|
||||
etRegulator.indoorTemp = round(indoorTemp);
|
||||
etRegulator.outdoorTemp = round(outdoorTemp);
|
||||
|
||||
} else {
|
||||
if (settings.heating.turbo) {
|
||||
@@ -258,56 +216,47 @@ protected:
|
||||
} else {
|
||||
etRegulator.Kt = settings.equitherm.t_factor;
|
||||
}
|
||||
etRegulator.indoorTemp = vars.temperatures.indoor;
|
||||
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||
etRegulator.indoorTemp = indoorTemp;
|
||||
etRegulator.outdoorTemp = outdoorTemp;
|
||||
}
|
||||
|
||||
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;
|
||||
etRegulator.targetTemp = targetTemp;
|
||||
float result = etRegulator.getResult();
|
||||
|
||||
return etRegulator.getResult();
|
||||
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
result = c2f(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float getPidTemp(int minTemp, int maxTemp) {
|
||||
pidRegulator.Kp = settings.pid.p_factor;
|
||||
pidRegulator.Ki = settings.pid.i_factor;
|
||||
pidRegulator.Kd = settings.pid.d_factor;
|
||||
if (fabs(pidRegulator.Kp - settings.pid.p_factor) >= 0.0001f) {
|
||||
pidRegulator.Kp = settings.pid.p_factor;
|
||||
pidRegulator.integral = 0;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
|
||||
if (fabs(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) {
|
||||
pidRegulator.Ki = settings.pid.i_factor;
|
||||
pidRegulator.integral = 0;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
|
||||
if (fabs(pidRegulator.Kd - settings.pid.d_factor) >= 0.0001f) {
|
||||
pidRegulator.Kd = settings.pid.d_factor;
|
||||
pidRegulator.integral = 0;
|
||||
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
|
||||
}
|
||||
|
||||
pidRegulator.setLimits(minTemp, maxTemp);
|
||||
pidRegulator.setDt(settings.pid.dt * 1000u);
|
||||
pidRegulator.input = vars.temperatures.indoor;
|
||||
pidRegulator.setpoint = settings.heating.target;
|
||||
|
||||
return pidRegulator.getResultNow();
|
||||
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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,152 +1,382 @@
|
||||
#include <OneWire.h>
|
||||
#include <DallasTemperature.h>
|
||||
|
||||
#if USE_BLE
|
||||
#include <NimBLEDevice.h>
|
||||
#endif
|
||||
|
||||
class SensorsTask : public LeanTask {
|
||||
public:
|
||||
SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {}
|
||||
SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
|
||||
this->oneWireOutdoorSensor = new OneWire();
|
||||
this->outdoorSensor = new DallasTemperature(this->oneWireOutdoorSensor);
|
||||
this->outdoorSensor->setWaitForConversion(false);
|
||||
|
||||
this->oneWireIndoorSensor = new OneWire();
|
||||
this->indoorSensor = new DallasTemperature(this->oneWireIndoorSensor);
|
||||
this->indoorSensor->setWaitForConversion(false);
|
||||
}
|
||||
|
||||
~SensorsTask() {
|
||||
delete this->outdoorSensor;
|
||||
delete this->oneWireOutdoorSensor;
|
||||
delete this->indoorSensor;
|
||||
delete this->oneWireIndoorSensor;
|
||||
}
|
||||
|
||||
protected:
|
||||
OneWire* oneWireOutdoorSensor;
|
||||
OneWire* oneWireIndoorSensor;
|
||||
OneWire* oneWireOutdoorSensor = nullptr;
|
||||
OneWire* oneWireIndoorSensor = nullptr;
|
||||
|
||||
DallasTemperature* outdoorSensor;
|
||||
DallasTemperature* indoorSensor;
|
||||
DallasTemperature* outdoorSensor = nullptr;
|
||||
DallasTemperature* indoorSensor = nullptr;
|
||||
|
||||
bool initOutdoorSensor = false;
|
||||
unsigned long startConversionTime = 0;
|
||||
unsigned long initOutdoorSensorTime = 0;
|
||||
unsigned long startOutdoorConversionTime = 0;
|
||||
float filteredOutdoorTemp = 0;
|
||||
bool emptyOutdoorTemp = true;
|
||||
|
||||
|
||||
bool initIndoorSensor = false;
|
||||
unsigned long initIndoorSensorTime = 0;
|
||||
unsigned long startIndoorConversionTime = 0;
|
||||
float filteredIndoorTemp = 0;
|
||||
bool emptyIndoorTemp = true;
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#if USE_BLE
|
||||
BLEClient* pBleClient = nullptr;
|
||||
bool initBleSensor = false;
|
||||
bool initBleNotify = false;
|
||||
#endif
|
||||
|
||||
const char* getTaskName() {
|
||||
const char* getTaskName() override {
|
||||
return "Sensors";
|
||||
}
|
||||
|
||||
BaseType_t getTaskCore() override {
|
||||
// https://github.com/h2zero/NimBLE-Arduino/issues/676
|
||||
#if USE_BLE && defined(CONFIG_BT_NIMBLE_PINNED_TO_CORE)
|
||||
return CONFIG_BT_NIMBLE_PINNED_TO_CORE;
|
||||
#else
|
||||
return tskNO_AFFINITY;
|
||||
#endif
|
||||
}
|
||||
|
||||
int getTaskPriority() override {
|
||||
return 4;
|
||||
}
|
||||
#endif
|
||||
|
||||
void loop() {
|
||||
if (settings.sensors.outdoor.type == 2) {
|
||||
bool indoorTempUpdated = false;
|
||||
bool outdoorTempUpdated = false;
|
||||
|
||||
if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.outdoor.gpio)) {
|
||||
outdoorTemperatureSensor();
|
||||
outdoorTempUpdated = true;
|
||||
}
|
||||
|
||||
if (settings.sensors.indoor.type == 2) {
|
||||
if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
|
||||
indoorTemperatureSensor();
|
||||
indoorTempUpdated = true;
|
||||
}
|
||||
#if USE_BLE
|
||||
else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) {
|
||||
indoorTemperatureBluetoothSensor();
|
||||
indoorTempUpdated = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (outdoorTempUpdated) {
|
||||
float newTemp = settings.sensors.outdoor.offset;
|
||||
if (settings.system.unitSystem == UnitSystem::METRIC) {
|
||||
newTemp += this->filteredOutdoorTemp;
|
||||
|
||||
} else if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
newTemp += c2f(this->filteredOutdoorTemp);
|
||||
}
|
||||
|
||||
if (fabs(vars.temperatures.outdoor - newTemp) > 0.099f) {
|
||||
vars.temperatures.outdoor = newTemp;
|
||||
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor);
|
||||
}
|
||||
}
|
||||
|
||||
if (indoorTempUpdated) {
|
||||
float newTemp = settings.sensors.indoor.offset;
|
||||
if (settings.system.unitSystem == UnitSystem::METRIC) {
|
||||
newTemp += this->filteredIndoorTemp;
|
||||
|
||||
} else if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
|
||||
newTemp += c2f(this->filteredIndoorTemp);
|
||||
}
|
||||
|
||||
if (fabs(vars.temperatures.indoor - newTemp) > 0.099f) {
|
||||
vars.temperatures.indoor = newTemp;
|
||||
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void outdoorTemperatureSensor() {
|
||||
if (!initOutdoorSensor) {
|
||||
oneWireOutdoorSensor = new OneWire(settings.sensors.outdoor.pin);
|
||||
outdoorSensor = new DallasTemperature(oneWireOutdoorSensor);
|
||||
outdoorSensor->begin();
|
||||
outdoorSensor->setResolution(12);
|
||||
outdoorSensor->setWaitForConversion(false);
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
initOutdoorSensor = true;
|
||||
#if USE_BLE
|
||||
void indoorTemperatureBluetoothSensor() {
|
||||
static bool initBleNotify = false;
|
||||
if (!initBleSensor && millis() > 5000) {
|
||||
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE"));
|
||||
BLEDevice::init("");
|
||||
|
||||
pBleClient = BLEDevice::createClient();
|
||||
pBleClient->setConnectTimeout(5);
|
||||
|
||||
initBleSensor = true;
|
||||
}
|
||||
|
||||
unsigned long estimateConversionTime = millis() - startConversionTime;
|
||||
if (estimateConversionTime < outdoorSensor->millisToWaitForConversion()) {
|
||||
if (!initBleSensor || pBleClient->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset init notify flag
|
||||
this->initBleNotify = false;
|
||||
|
||||
// Connect to the remote BLE Server.
|
||||
BLEAddress bleServerAddress(settings.sensors.indoor.bleAddress);
|
||||
if (!pBleClient->connect(bleServerAddress)) {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), "Failed connecting to device at %s", bleServerAddress.toString().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = outdoorSensor->isConversionComplete();
|
||||
if (!completed && estimateConversionTime >= 1000) {
|
||||
// fail, retry
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
Log.sinfoln(FPSTR(L_SENSORS_BLE), "Connected to device at %s", bleServerAddress.toString().c_str());
|
||||
|
||||
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (no response)");
|
||||
NimBLEUUID serviceUUID((uint16_t) 0x181AU);
|
||||
BLERemoteService* pRemoteService = pBleClient->getService(serviceUUID);
|
||||
if (!pRemoteService) {
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Failed to find service UUID: %s"), serviceUUID.toString().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found service UUID: %s"), serviceUUID.toString().c_str());
|
||||
|
||||
// 0x2A6E - Notify temperature x0.01C (pvvx)
|
||||
if (!this->initBleNotify) {
|
||||
NimBLEUUID charUUID((uint16_t) 0x2A6E);
|
||||
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
|
||||
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
|
||||
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) {
|
||||
if (length != 2) {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data"));
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f);
|
||||
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
|
||||
|
||||
if (this->emptyIndoorTemp) {
|
||||
this->filteredIndoorTemp = rawTemp;
|
||||
this->emptyIndoorTemp = false;
|
||||
|
||||
} else {
|
||||
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
|
||||
});
|
||||
|
||||
if (this->initBleNotify) {
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
|
||||
} else {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 0x2A1F - Notify temperature x0.1C (atc1441/pvvx)
|
||||
if (!this->initBleNotify) {
|
||||
NimBLEUUID charUUID((uint16_t) 0x2A1F);
|
||||
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
|
||||
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
|
||||
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) {
|
||||
if (length != 2) {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data"));
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1);
|
||||
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
|
||||
|
||||
if (this->emptyIndoorTemp) {
|
||||
this->filteredIndoorTemp = rawTemp;
|
||||
this->emptyIndoorTemp = false;
|
||||
|
||||
} else {
|
||||
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
|
||||
});
|
||||
|
||||
if (this->initBleNotify) {
|
||||
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
|
||||
} else {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->initBleNotify) {
|
||||
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics"));
|
||||
pBleClient->disconnect();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void outdoorTemperatureSensor() {
|
||||
if (!this->initOutdoorSensor) {
|
||||
if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Starting on GPIO %hhu..."), settings.sensors.outdoor.gpio);
|
||||
|
||||
this->oneWireOutdoorSensor->begin(settings.sensors.outdoor.gpio);
|
||||
this->oneWireOutdoorSensor->reset();
|
||||
this->outdoorSensor->begin();
|
||||
this->initOutdoorSensorTime = millis();
|
||||
|
||||
Log.straceln(
|
||||
FPSTR(L_SENSORS_OUTDOOR),
|
||||
F("Devices on bus: %hhu, DS18* devices: %hhu"),
|
||||
this->outdoorSensor->getDeviceCount(),
|
||||
this->outdoorSensor->getDS18Count()
|
||||
);
|
||||
|
||||
if (this->outdoorSensor->getDeviceCount() > 0) {
|
||||
this->initOutdoorSensor = true;
|
||||
this->outdoorSensor->setResolution(12);
|
||||
this->outdoorSensor->requestTemperatures();
|
||||
this->startOutdoorConversionTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started"));
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long estimateConversionTime = millis() - this->startOutdoorConversionTime;
|
||||
if (estimateConversionTime < this->outdoorSensor->millisToWaitForConversion()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = this->outdoorSensor->isConversionComplete();
|
||||
if (!completed && estimateConversionTime >= 1000) {
|
||||
this->initOutdoorSensor = false;
|
||||
|
||||
Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (no response)"));
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = outdoorSensor->getTempCByIndex(0);
|
||||
float rawTemp = this->outdoorSensor->getTempCByIndex(0);
|
||||
if (rawTemp == DEVICE_DISCONNECTED_C) {
|
||||
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (not connected)");
|
||||
this->initOutdoorSensor = false;
|
||||
|
||||
Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (not connected)"));
|
||||
|
||||
} else {
|
||||
DEBUG_F("[SENSORS][OUTDOOR] Raw temp: %f \n", rawTemp);
|
||||
Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp);
|
||||
|
||||
if (emptyOutdoorTemp) {
|
||||
filteredOutdoorTemp = rawTemp;
|
||||
emptyOutdoorTemp = false;
|
||||
if (this->emptyOutdoorTemp) {
|
||||
this->filteredOutdoorTemp = rawTemp;
|
||||
this->emptyOutdoorTemp = false;
|
||||
|
||||
} else {
|
||||
filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100;
|
||||
|
||||
if (fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099) {
|
||||
vars.temperatures.outdoor = filteredOutdoorTemp + settings.sensors.outdoor.offset;
|
||||
INFO_F("[SENSORS][OUTDOOR] New temp: %f \n", filteredOutdoorTemp);
|
||||
}
|
||||
this->filteredOutdoorTemp = floor(this->filteredOutdoorTemp * 100) / 100;
|
||||
this->outdoorSensor->requestTemperatures();
|
||||
this->startOutdoorConversionTime = millis();
|
||||
}
|
||||
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
}
|
||||
|
||||
void indoorTemperatureSensor() {
|
||||
if (!initIndoorSensor) {
|
||||
oneWireIndoorSensor = new OneWire(settings.sensors.indoor.pin);
|
||||
indoorSensor = new DallasTemperature(oneWireIndoorSensor);
|
||||
indoorSensor->begin();
|
||||
indoorSensor->setResolution(12);
|
||||
indoorSensor->setWaitForConversion(false);
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
initIndoorSensor = true;
|
||||
if (!this->initIndoorSensor) {
|
||||
if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Starting on GPIO %hhu..."), settings.sensors.indoor.gpio);
|
||||
|
||||
this->oneWireIndoorSensor->begin(settings.sensors.indoor.gpio);
|
||||
this->oneWireIndoorSensor->reset();
|
||||
this->indoorSensor->begin();
|
||||
this->initIndoorSensorTime = millis();
|
||||
|
||||
Log.straceln(
|
||||
FPSTR(L_SENSORS_INDOOR),
|
||||
F("Devices on bus: %hhu, DS18* devices: %hhu"),
|
||||
this->indoorSensor->getDeviceCount(),
|
||||
this->indoorSensor->getDS18Count()
|
||||
);
|
||||
|
||||
if (this->indoorSensor->getDeviceCount() > 0) {
|
||||
this->initIndoorSensor = true;
|
||||
this->indoorSensor->setResolution(12);
|
||||
this->indoorSensor->requestTemperatures();
|
||||
this->startIndoorConversionTime = millis();
|
||||
|
||||
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started"));
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long estimateConversionTime = millis() - startConversionTime;
|
||||
if (estimateConversionTime < indoorSensor->millisToWaitForConversion()) {
|
||||
unsigned long estimateConversionTime = millis() - this->startIndoorConversionTime;
|
||||
if (estimateConversionTime < this->indoorSensor->millisToWaitForConversion()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = indoorSensor->isConversionComplete();
|
||||
bool completed = this->indoorSensor->isConversionComplete();
|
||||
if (!completed && estimateConversionTime >= 1000) {
|
||||
// fail, retry
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
this->initIndoorSensor = false;
|
||||
|
||||
ERROR("[SENSORS][INDOOR] Could not read temperature data (no response)");
|
||||
Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (no response)"));
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = indoorSensor->getTempCByIndex(0);
|
||||
float rawTemp = this->indoorSensor->getTempCByIndex(0);
|
||||
if (rawTemp == DEVICE_DISCONNECTED_C) {
|
||||
ERROR("[SENSORS][INDOOR] Could not read temperature data (not connected)");
|
||||
this->initIndoorSensor = false;
|
||||
|
||||
Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (not connected)"));
|
||||
|
||||
} else {
|
||||
DEBUG_F("[SENSORS][INDOOR] Raw temp: %f \n", rawTemp);
|
||||
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
|
||||
|
||||
if (emptyIndoorTemp) {
|
||||
filteredIndoorTemp = rawTemp;
|
||||
emptyIndoorTemp = false;
|
||||
if (this->emptyIndoorTemp) {
|
||||
this->filteredIndoorTemp = rawTemp;
|
||||
this->emptyIndoorTemp = false;
|
||||
|
||||
} else {
|
||||
filteredIndoorTemp += (rawTemp - filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
filteredIndoorTemp = floor(filteredIndoorTemp * 100) / 100;
|
||||
|
||||
if (fabs(vars.temperatures.indoor - filteredIndoorTemp) > 0.099) {
|
||||
vars.temperatures.indoor = filteredIndoorTemp + settings.sensors.indoor.offset;
|
||||
INFO_F("[SENSORS][INDOOR] New temp: %f \n", filteredIndoorTemp);
|
||||
}
|
||||
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
|
||||
this->indoorSensor->requestTemperatures();
|
||||
this->startIndoorConversionTime = millis();
|
||||
}
|
||||
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
}
|
||||
};
|
||||
176
src/Settings.h
@@ -1,52 +1,116 @@
|
||||
struct Settings {
|
||||
bool debug = false;
|
||||
char hostname[80] = "opentherm";
|
||||
struct NetworkSettings {
|
||||
char hostname[25] = DEFAULT_HOSTNAME;
|
||||
bool useDhcp = true;
|
||||
|
||||
struct {
|
||||
byte inPin = OT_IN_PIN_DEFAULT;
|
||||
byte outPin = OT_OUT_PIN_DEFAULT;
|
||||
char ip[16] = "192.168.0.100";
|
||||
char gateway[16] = "192.168.0.1";
|
||||
char subnet[16] = "255.255.255.0";
|
||||
char dns[16] = "192.168.0.1";
|
||||
} staticConfig;
|
||||
|
||||
struct {
|
||||
char ssid[33] = DEFAULT_AP_SSID;
|
||||
char password[65] = DEFAULT_AP_PASSWORD;
|
||||
byte channel = 6;
|
||||
} ap;
|
||||
|
||||
struct {
|
||||
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;
|
||||
|
||||
struct {
|
||||
bool enable = USE_SERIAL;
|
||||
unsigned int baudrate = 115200;
|
||||
} serial;
|
||||
|
||||
struct {
|
||||
bool enable = USE_TELNET;
|
||||
unsigned short port = 23;
|
||||
} telnet;
|
||||
|
||||
UnitSystem unitSystem = UnitSystem::METRIC;
|
||||
byte statusLedGpio = DEFAULT_STATUS_LED_GPIO;
|
||||
} system;
|
||||
|
||||
struct {
|
||||
bool auth = false;
|
||||
char login[13] = DEFAULT_PORTAL_LOGIN;
|
||||
char password[33] = DEFAULT_PORTAL_PASSWORD;
|
||||
} portal;
|
||||
|
||||
struct {
|
||||
UnitSystem unitSystem = UnitSystem::METRIC;
|
||||
byte inGpio = DEFAULT_OT_IN_GPIO;
|
||||
byte outGpio = DEFAULT_OT_OUT_GPIO;
|
||||
byte rxLedGpio = DEFAULT_OT_RX_LED_GPIO;
|
||||
byte faultStateGpio = DEFAULT_OT_FAULT_STATE_GPIO;
|
||||
byte invertFaultState = false;
|
||||
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;
|
||||
bool getMinMaxTemp = true;
|
||||
bool nativeHeatingControl = false;
|
||||
} opentherm;
|
||||
|
||||
struct {
|
||||
char server[80];
|
||||
unsigned int port = 1883;
|
||||
char user[32];
|
||||
char password[32];
|
||||
char prefix[80] = "opentherm";
|
||||
unsigned int interval = 5000;
|
||||
bool enable = false;
|
||||
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;
|
||||
float target = DEFAULT_HEATING_TARGET_TEMP;
|
||||
unsigned short tresholdTime = 120;
|
||||
bool useEquitherm = false;
|
||||
bool usePid = false;
|
||||
bool onNetworkFault = true;
|
||||
bool onMqttFault = true;
|
||||
} emergency;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
bool turbo = false;
|
||||
float target = 40.0f;
|
||||
float target = DEFAULT_HEATING_TARGET_TEMP;
|
||||
float hysteresis = 0.5f;
|
||||
byte minTemp = 20.0f;
|
||||
byte maxTemp = 90.0f;
|
||||
byte minTemp = DEFAULT_HEATING_MIN_TEMP;
|
||||
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
byte maxModulation = 100;
|
||||
} heating;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
byte target = 40;
|
||||
byte minTemp = 30.0f;
|
||||
byte maxTemp = 60.0f;
|
||||
float target = DEFAULT_DHW_TARGET_TEMP;
|
||||
byte minTemp = DEFAULT_DHW_MIN_TEMP;
|
||||
byte maxTemp = DEFAULT_DHW_MAX_TEMP;
|
||||
} dhw;
|
||||
|
||||
struct {
|
||||
bool enable = false;
|
||||
float p_factor = 3;
|
||||
float i_factor = 0.2f;
|
||||
float p_factor = 2;
|
||||
float i_factor = 0.0055f;
|
||||
float d_factor = 0;
|
||||
byte minTemp = 0.0f;
|
||||
byte maxTemp = 90.0f;
|
||||
unsigned short dt = 180;
|
||||
byte minTemp = 0;
|
||||
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
} pid;
|
||||
|
||||
struct {
|
||||
@@ -58,29 +122,31 @@ struct Settings {
|
||||
|
||||
struct {
|
||||
struct {
|
||||
// 0 - boiler, 1 - manual, 2 - ds18b20
|
||||
byte type = 0;
|
||||
byte pin = SENSOR_OUTDOOR_PIN_DEFAULT;
|
||||
SensorType type = SensorType::BOILER;
|
||||
byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO;
|
||||
float offset = 0.0f;
|
||||
} outdoor;
|
||||
|
||||
struct {
|
||||
// 1 - manual, 2 - ds18b20
|
||||
byte type = 1;
|
||||
byte pin = SENSOR_INDOOR_PIN_DEFAULT;
|
||||
SensorType type = SensorType::MANUAL;
|
||||
byte gpio = DEFAULT_SENSOR_INDOOR_GPIO;
|
||||
uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
float offset = 0.0f;
|
||||
} indoor;
|
||||
} sensors;
|
||||
|
||||
struct {
|
||||
bool use = false;
|
||||
byte gpio = DEFAULT_EXT_PUMP_GPIO;
|
||||
unsigned short postCirculationTime = 600;
|
||||
unsigned int antiStuckInterval = 2592000;
|
||||
unsigned short antiStuckTime = 300;
|
||||
} externalPump;
|
||||
|
||||
char validationValue[8] = SETTINGS_VALID_VALUE;
|
||||
} settings;
|
||||
|
||||
struct Variables {
|
||||
struct {
|
||||
bool enable = false;
|
||||
byte regulator = 0;
|
||||
} tuning;
|
||||
|
||||
struct {
|
||||
bool otStatus = false;
|
||||
bool emergency = false;
|
||||
@@ -89,35 +155,51 @@ struct Variables {
|
||||
bool flame = false;
|
||||
bool fault = false;
|
||||
bool diagnostic = false;
|
||||
byte faultCode = 0;
|
||||
int8_t rssi = 0;
|
||||
bool externalPump = false;
|
||||
bool mqtt = false;
|
||||
} states;
|
||||
|
||||
struct {
|
||||
float modulation = 0.0f;
|
||||
float pressure = 0.0f;
|
||||
float dhwFlowRate = 0.0f;
|
||||
byte faultCode = 0;
|
||||
int8_t rssi = 0;
|
||||
} sensors;
|
||||
|
||||
struct {
|
||||
float indoor = 0.0f;
|
||||
float outdoor = 0.0f;
|
||||
float heating = 0.0f;
|
||||
float heatingReturn = 0.0f;
|
||||
float dhw = 0.0f;
|
||||
float exhaust = 0.0f;
|
||||
} temperatures;
|
||||
|
||||
struct {
|
||||
unsigned long restartSignalTime = 0;
|
||||
unsigned int restartAfterTime = 0;
|
||||
bool heatingEnabled = false;
|
||||
byte heatingMinTemp = 20;
|
||||
byte heatingMaxTemp = 90;
|
||||
byte heatingSetpoint = 0.0f;
|
||||
byte dhwMinTemp = 30;
|
||||
byte dhwMaxTemp = 60;
|
||||
uint8_t slaveMemberIdCode;
|
||||
uint8_t slaveType;
|
||||
uint8_t slaveVersion;
|
||||
uint8_t masterType;
|
||||
uint8_t masterVersion;
|
||||
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
|
||||
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
|
||||
float heatingSetpoint = 0;
|
||||
unsigned long extPumpLastEnableTime = 0;
|
||||
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
|
||||
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
|
||||
byte maxModulation = 0;
|
||||
uint8_t slaveMemberId = 0;
|
||||
uint8_t slaveFlags = 0;
|
||||
uint8_t slaveType = 0;
|
||||
uint8_t slaveVersion = 0;
|
||||
float slaveOtVersion = 0.0f;
|
||||
uint8_t masterMemberId = 0;
|
||||
uint8_t masterFlags = 0;
|
||||
uint8_t masterType = 0;
|
||||
uint8_t masterVersion = 0;
|
||||
float masterOtVersion = 0;
|
||||
} parameters;
|
||||
|
||||
struct {
|
||||
bool restart = false;
|
||||
bool resetFault = false;
|
||||
bool resetDiagnostic = false;
|
||||
} actions;
|
||||
} vars;
|
||||
@@ -1,296 +0,0 @@
|
||||
#define WM_MDNS
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameters.h>
|
||||
#include <netif/etharp.h>
|
||||
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerParameter* wmHostname;
|
||||
WiFiManagerParameter* wmMqttServer;
|
||||
UnsignedIntParameter* wmMqttPort;
|
||||
WiFiManagerParameter* wmMqttUser;
|
||||
WiFiManagerParameter* wmMqttPassword;
|
||||
WiFiManagerParameter* wmMqttPrefix;
|
||||
UnsignedIntParameter* wmMqttPublishInterval;
|
||||
UnsignedIntParameter* wmOtInPin;
|
||||
UnsignedIntParameter* wmOtOutPin;
|
||||
UnsignedIntParameter* wmOtMemberIdCode;
|
||||
CheckboxParameter* wmOtDHWPresent;
|
||||
UnsignedIntParameter* wmOutdoorSensorPin;
|
||||
UnsignedIntParameter* wmIndoorSensorPin;
|
||||
|
||||
SeparatorParameter* wmSep1;
|
||||
SeparatorParameter* wmSep2;
|
||||
|
||||
|
||||
class WifiManagerTask : public Task {
|
||||
public:
|
||||
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
|
||||
|
||||
protected:
|
||||
bool connected = false;
|
||||
unsigned long lastArpGratuitous = 0;
|
||||
|
||||
const char* getTaskName() {
|
||||
return "WifiManager";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
wm.setDebugOutput(settings.debug);
|
||||
//wm.setDebugOutput(settings.debug, WM_DEBUG_VERBOSE);
|
||||
|
||||
wm.setTitle("OpenTherm Gateway");
|
||||
wm.setCustomMenuHTML(PSTR(
|
||||
"<style>.wrap h1 {display: none;} .wrap h3 {display: none;} .nh {margin: 0 0 1em 0;} .nh .logo {font-size: 1.8em; margin: 0.5em; text-align: center;} .nh .links {text-align: center;}</style>"
|
||||
"<div class=\"nh\">"
|
||||
"<div class=\"logo\">OpenTherm Gateway</div>"
|
||||
"<div class=\"links\"><a href=\"" OT_GATEWAY_REPO "\" target=\"_blank\">Repo</a> | <a href=\"" OT_GATEWAY_REPO "/issues\" target=\"_blank\">Issues</a> | <a href=\"" OT_GATEWAY_REPO "/releases\" target=\"_blank\">Releases</a> | <small>v" OT_GATEWAY_VERSION " (" __DATE__ ")</small></div>"
|
||||
"</div>"
|
||||
));
|
||||
|
||||
std::vector<const char *> menu = {"custom", "wifi", "param", "sep", "info", "update", "restart"};
|
||||
wm.setMenu(menu);
|
||||
|
||||
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
|
||||
wm.addParameter(wmHostname);
|
||||
|
||||
wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80);
|
||||
wm.addParameter(wmMqttServer);
|
||||
|
||||
wmMqttPort = new UnsignedIntParameter("mqtt_port", "MQTT port", settings.mqtt.port, 6);
|
||||
wm.addParameter(wmMqttPort);
|
||||
|
||||
wmMqttUser = new WiFiManagerParameter("mqtt_user", "MQTT username", settings.mqtt.user, 32);
|
||||
wm.addParameter(wmMqttUser);
|
||||
|
||||
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32, "type=\"password\"");
|
||||
wm.addParameter(wmMqttPassword);
|
||||
|
||||
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32);
|
||||
wm.addParameter(wmMqttPrefix);
|
||||
|
||||
wmMqttPublishInterval = new UnsignedIntParameter("mqtt_publish_interval", "MQTT publish interval", settings.mqtt.interval, 5);
|
||||
wm.addParameter(wmMqttPublishInterval);
|
||||
|
||||
wmSep1 = new SeparatorParameter();
|
||||
wm.addParameter(wmSep1);
|
||||
|
||||
wmOtInPin = new UnsignedIntParameter("ot_in_pin", "Opentherm pin IN", settings.opentherm.inPin, 2);
|
||||
wm.addParameter(wmOtInPin);
|
||||
|
||||
wmOtOutPin = new UnsignedIntParameter("ot_out_pin", "Opentherm pin OUT", settings.opentherm.outPin, 2);
|
||||
wm.addParameter(wmOtOutPin);
|
||||
|
||||
wmOtMemberIdCode = new UnsignedIntParameter("ot_member_id_code", "Opentherm member id", settings.opentherm.memberIdCode, 5);
|
||||
wm.addParameter(wmOtMemberIdCode);
|
||||
|
||||
wmOtDHWPresent = new CheckboxParameter("ot_dhw_present", "Opentherm DHW present", settings.opentherm.dhwPresent);
|
||||
wm.addParameter(wmOtDHWPresent);
|
||||
|
||||
wmSep2 = new SeparatorParameter();
|
||||
wm.addParameter(wmSep2);
|
||||
|
||||
wmOutdoorSensorPin = new UnsignedIntParameter("outdoor_sensor_pin", "Outdoor sensor pin", settings.sensors.outdoor.pin, 2);
|
||||
wm.addParameter(wmOutdoorSensorPin);
|
||||
|
||||
wmIndoorSensorPin = new UnsignedIntParameter("indoor_sensor_pin", "Indoor sensor pin", settings.sensors.indoor.pin, 2);
|
||||
wm.addParameter(wmIndoorSensorPin);
|
||||
|
||||
//wm.setCleanConnect(true);
|
||||
wm.setRestorePersistent(false);
|
||||
|
||||
wm.setHostname(settings.hostname);
|
||||
wm.setWiFiAutoReconnect(true);
|
||||
wm.setAPClientCheck(true);
|
||||
wm.setConfigPortalBlocking(false);
|
||||
wm.setSaveParamsCallback(saveParamsCallback);
|
||||
wm.setConfigPortalTimeout(wm.getWiFiIsSaved() ? 180 : 0);
|
||||
wm.setDisableConfigPortal(false);
|
||||
|
||||
wm.autoConnect(AP_SSID, AP_PASSWORD);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (connected && WiFi.status() != WL_CONNECTED) {
|
||||
connected = false;
|
||||
|
||||
if (wm.getWebPortalActive()) {
|
||||
wm.stopWebPortal();
|
||||
}
|
||||
|
||||
#ifdef USE_TELNET
|
||||
TelnetStream.stop();
|
||||
#endif
|
||||
|
||||
INFO("[wifi] Disconnected");
|
||||
|
||||
} else if (!connected && WiFi.status() == WL_CONNECTED) {
|
||||
connected = true;
|
||||
|
||||
wm.setConfigPortalTimeout(180);
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
if (!wm.getWebPortalActive()) {
|
||||
wm.startWebPortal();
|
||||
}
|
||||
|
||||
#ifdef USE_TELNET
|
||||
TelnetStream.begin();
|
||||
#endif
|
||||
|
||||
INFO_F("[wifi] Connected. IP address: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
if (connected && millis() - lastArpGratuitous > 60000) {
|
||||
arpGratuitous();
|
||||
lastArpGratuitous = millis();
|
||||
}
|
||||
#endif
|
||||
|
||||
wm.process();
|
||||
}
|
||||
|
||||
static void saveParamsCallback() {
|
||||
bool changed = false;
|
||||
bool needRestart = false;
|
||||
|
||||
if (strcmp(wmHostname->getValue(), settings.hostname) != 0) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
strcpy(settings.hostname, wmHostname->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttServer->getValue(), settings.mqtt.server) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.server, wmMqttServer->getValue());
|
||||
}
|
||||
|
||||
if (wmMqttPort->getValue() != settings.mqtt.port) {
|
||||
changed = true;
|
||||
|
||||
settings.mqtt.port = wmMqttPort->getValue();
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttUser->getValue(), settings.mqtt.user) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.user, wmMqttUser->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttPassword->getValue(), settings.mqtt.password) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttPrefix->getValue(), settings.mqtt.prefix) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
|
||||
}
|
||||
|
||||
if (wmMqttPublishInterval->getValue() != settings.mqtt.interval) {
|
||||
changed = true;
|
||||
|
||||
settings.mqtt.interval = wmMqttPublishInterval->getValue();
|
||||
}
|
||||
|
||||
if (wmOtInPin->getValue() != settings.opentherm.inPin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.opentherm.inPin = wmOtInPin->getValue();
|
||||
}
|
||||
|
||||
if (wmOtOutPin->getValue() != settings.opentherm.outPin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.opentherm.outPin = wmOtOutPin->getValue();
|
||||
}
|
||||
|
||||
if (wmOtMemberIdCode->getValue() != settings.opentherm.memberIdCode) {
|
||||
changed = true;
|
||||
|
||||
settings.opentherm.memberIdCode = wmOtMemberIdCode->getValue();
|
||||
}
|
||||
|
||||
if (wmOtDHWPresent->getCheckboxValue() != settings.opentherm.dhwPresent) {
|
||||
changed = true;
|
||||
|
||||
settings.opentherm.dhwPresent = wmOtDHWPresent->getCheckboxValue();
|
||||
}
|
||||
|
||||
if (wmOutdoorSensorPin->getValue() != settings.sensors.outdoor.pin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.sensors.outdoor.pin = wmOutdoorSensorPin->getValue();
|
||||
}
|
||||
|
||||
if (wmIndoorSensorPin->getValue() != settings.sensors.indoor.pin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.sensors.indoor.pin = wmIndoorSensorPin->getValue();
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (needRestart) {
|
||||
vars.parameters.restartAfterTime = 5000;
|
||||
vars.parameters.restartSignalTime = millis();
|
||||
}
|
||||
|
||||
INFO_F(
|
||||
"New settings:\r\n"
|
||||
" Hostname: %s\r\n"
|
||||
" Mqtt server: %s:%d\r\n"
|
||||
" Mqtt user: %s\r\n"
|
||||
" Mqtt pass: %s\r\n"
|
||||
" Mqtt prefix: %s\r\n"
|
||||
" Mqtt publish interval: %d\r\n"
|
||||
" OT in pin: %d\r\n"
|
||||
" OT out pin: %d\r\n"
|
||||
" OT member id code: %d\r\n"
|
||||
" OT DHW present: %d\r\n"
|
||||
" Outdoor sensor pin: %d\r\n"
|
||||
" Indoor sensor pin: %d\r\n",
|
||||
settings.hostname,
|
||||
settings.mqtt.server,
|
||||
settings.mqtt.port,
|
||||
settings.mqtt.user,
|
||||
settings.mqtt.password,
|
||||
settings.mqtt.prefix,
|
||||
settings.mqtt.interval,
|
||||
settings.opentherm.inPin,
|
||||
settings.opentherm.outPin,
|
||||
settings.opentherm.memberIdCode,
|
||||
settings.opentherm.dhwPresent,
|
||||
settings.sensors.outdoor.pin,
|
||||
settings.sensors.indoor.pin
|
||||
);
|
||||
|
||||
eeSettings.updateNow();
|
||||
INFO(F("Settings saved"));
|
||||
}
|
||||
|
||||
static void arpGratuitous() {
|
||||
struct netif* netif = netif_list;
|
||||
while (netif) {
|
||||
etharp_gratuitous(netif);
|
||||
netif = netif->next;
|
||||
}
|
||||
}
|
||||
};
|
||||
174
src/defines.h
@@ -1,59 +1,153 @@
|
||||
#define OT_GATEWAY_VERSION "1.3.3"
|
||||
#define OT_GATEWAY_REPO "https://github.com/Laxilef/OTGateway"
|
||||
#define AP_SSID "OpenTherm Gateway"
|
||||
#define AP_PASSWORD "otgateway123456"
|
||||
#define USE_TELNET
|
||||
#define PROJECT_NAME "OpenTherm Gateway"
|
||||
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
|
||||
|
||||
#define EMERGENCY_TIME_TRESHOLD 120000
|
||||
#define MQTT_RECONNECT_INTERVAL 5000
|
||||
#define MQTT_KEEPALIVE 30
|
||||
#define MQTT_RECONNECT_INTERVAL 15000
|
||||
|
||||
#define OPENTHERM_OFFLINE_TRESHOLD 10
|
||||
#define EXT_SENSORS_INTERVAL 5000
|
||||
#define EXT_SENSORS_FILTER_K 0.15
|
||||
|
||||
#define EXT_SENSORS_INTERVAL 5000
|
||||
#define EXT_SENSORS_FILTER_K 0.15
|
||||
#define CONFIG_URL "http://%s/"
|
||||
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
|
||||
#define GPIO_IS_NOT_CONFIGURED 0xff
|
||||
|
||||
#define CONFIG_URL "http://%s/"
|
||||
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
|
||||
#define DEFAULT_HEATING_TARGET_TEMP 40
|
||||
#define DEFAULT_HEATING_MIN_TEMP 20
|
||||
#define DEFAULT_HEATING_MAX_TEMP 90
|
||||
|
||||
#define DEFAULT_DHW_TARGET_TEMP 40
|
||||
#define DEFAULT_DHW_MIN_TEMP 30
|
||||
#define DEFAULT_DHW_MAX_TEMP 60
|
||||
|
||||
#define THERMOSTAT_INDOOR_DEFAULT_TEMP 20
|
||||
#define THERMOSTAT_INDOOR_MIN_TEMP 5
|
||||
#define THERMOSTAT_INDOOR_MAX_TEMP 30
|
||||
|
||||
#ifndef OT_IN_PIN_DEFAULT
|
||||
#define OT_IN_PIN_DEFAULT 0
|
||||
#ifndef BUILD_VERSION
|
||||
#define BUILD_VERSION "0.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef OT_OUT_PIN_DEFAULT
|
||||
#define OT_OUT_PIN_DEFAULT 0
|
||||
#ifndef BUILD_ENV
|
||||
#define BUILD_ENV "undefined"
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_OUTDOOR_PIN_DEFAULT
|
||||
#define SENSOR_OUTDOOR_PIN_DEFAULT 0
|
||||
#ifndef USE_SERIAL
|
||||
#define USE_SERIAL true
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_INDOOR_PIN_DEFAULT
|
||||
#define SENSOR_INDOOR_PIN_DEFAULT 0
|
||||
#ifndef USE_TELNET
|
||||
#define USE_TELNET true
|
||||
#endif
|
||||
|
||||
#ifdef USE_TELNET
|
||||
#define INFO_STREAM TelnetStream
|
||||
#define WARN_STREAM TelnetStream
|
||||
#define ERROR_STREAM TelnetStream
|
||||
#define DEBUG_STREAM if (settings.debug) TelnetStream
|
||||
#define WM_DEBUG_PORT TelnetStream
|
||||
#else
|
||||
#define INFO_STREAM Serial
|
||||
#define WARN_STREAM Serial
|
||||
#define ERROR_STREAM Serial
|
||||
#define DEBUG_STREAM if (settings.debug) Serial
|
||||
#define WM_DEBUG_PORT Serial
|
||||
#ifndef USE_BLE
|
||||
#define USE_BLE false
|
||||
#endif
|
||||
|
||||
#define INFO(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.println(__VA_ARGS__);
|
||||
#define INFO_F(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.printf(__VA_ARGS__);
|
||||
#define WARN(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.println(__VA_ARGS__);
|
||||
#define WARN_F(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.printf(__VA_ARGS__);
|
||||
#define ERROR(...) ERROR_STREAM.print("\r[ERROR] "); ERROR_STREAM.println(__VA_ARGS__);
|
||||
#define DEBUG(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.println(__VA_ARGS__);
|
||||
#define DEBUG_F(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.printf(__VA_ARGS__);
|
||||
#ifndef DEFAULT_HOSTNAME
|
||||
#define DEFAULT_HOSTNAME "opentherm"
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_AP_SSID
|
||||
#define DEFAULT_AP_SSID "OpenTherm Gateway"
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_AP_PASSWORD
|
||||
#define DEFAULT_AP_PASSWORD "otgateway123456"
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_STA_SSID
|
||||
#define DEFAULT_STA_SSID ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_STA_PASSWORD
|
||||
#define DEFAULT_STA_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef DEBUG_BY_DEFAULT
|
||||
#define DEBUG_BY_DEFAULT false
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_STATUS_LED_GPIO
|
||||
#define DEFAULT_STATUS_LED_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_PORTAL_LOGIN
|
||||
#define DEFAULT_PORTAL_LOGIN ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_PORTAL_PASSWORD
|
||||
#define DEFAULT_PORTAL_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_MQTT_SERVER
|
||||
#define DEFAULT_MQTT_SERVER ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_MQTT_PORT
|
||||
#define DEFAULT_MQTT_PORT 1883
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_MQTT_USER
|
||||
#define DEFAULT_MQTT_USER ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_MQTT_PASSWORD
|
||||
#define DEFAULT_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_MQTT_PREFIX
|
||||
#define DEFAULT_MQTT_PREFIX "opentherm"
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_OT_IN_GPIO
|
||||
#define DEFAULT_OT_IN_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#ifndef DEFAULT_OT_OUT_GPIO
|
||||
#define DEFAULT_OT_OUT_GPIO GPIO_IS_NOT_CONFIGURED
|
||||
#endif
|
||||
|
||||
#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 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 <= 16)
|
||||
#endif
|
||||
|
||||
#define GPIO_IS_VALID(gpioNum) (gpioNum != GPIO_IS_NOT_CONFIGURED && GPIO_IS_VALID_GPIO(gpioNum))
|
||||
|
||||
enum class SensorType : byte {
|
||||
BOILER,
|
||||
MANUAL,
|
||||
DS18B20,
|
||||
BLUETOOTH
|
||||
};
|
||||
|
||||
enum class UnitSystem : byte {
|
||||
METRIC,
|
||||
IMPERIAL
|
||||
};
|
||||
|
||||
char buffer[255];
|
||||
174
src/main.cpp
@@ -1,72 +1,165 @@
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "strings.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <TelnetStream.h>
|
||||
#include <EEManager.h>
|
||||
#include <FileData.h>
|
||||
#include <LittleFS.h>
|
||||
#include <ESPTelnetStream.h>
|
||||
#include <TinyLogger.h>
|
||||
#include <NetworkMgr.h>
|
||||
#include "Settings.h"
|
||||
#include "utils.h"
|
||||
|
||||
EEManager eeSettings(settings, 30000);
|
||||
|
||||
#if defined(ESP32)
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <ESP32Scheduler.h>
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#elif defined(ESP8266)
|
||||
#elif defined(ARDUINO_ARCH_ESP8266)
|
||||
#include <Scheduler.h>
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#elif
|
||||
#else
|
||||
#error Wrong board. Supported boards: esp8266, esp32
|
||||
#endif
|
||||
|
||||
#include "WifiManagerTask.h"
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#include "MqttTask.h"
|
||||
#include "OpenThermTask.h"
|
||||
#include "SensorsTask.h"
|
||||
#include "RegulatorTask.h"
|
||||
#include "PortalTask.h"
|
||||
#include "MainTask.h"
|
||||
|
||||
using namespace NetworkUtils;
|
||||
|
||||
// Vars
|
||||
FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000);
|
||||
FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000);
|
||||
ESPTelnetStream* telnetStream = nullptr;
|
||||
NetworkMgr* network = nullptr;
|
||||
|
||||
// Tasks
|
||||
WifiManagerTask* tWm;
|
||||
MqttTask* tMqtt;
|
||||
OpenThermTask* tOt;
|
||||
SensorsTask* tSensors;
|
||||
RegulatorTask* tRegulator;
|
||||
PortalTask* tPortal;
|
||||
MainTask* tMain;
|
||||
|
||||
|
||||
void setup() {
|
||||
#ifndef USE_TELNET
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n\n");
|
||||
#endif
|
||||
LittleFS.begin();
|
||||
|
||||
EEPROM.begin(eeSettings.blockSize());
|
||||
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
|
||||
if (eeSettingsResult == 0) {
|
||||
INFO("Settings loaded");
|
||||
Log.setLevel(TinyLogger::Level::VERBOSE);
|
||||
Log.setServiceTemplate("\033[1m[%s]\033[22m");
|
||||
Log.setLevelTemplate("\033[1m[%s]\033[22m");
|
||||
Log.setMsgPrefix("\033[m ");
|
||||
Log.setDateTemplate("\033[1m[%H:%M:%S]\033[22m");
|
||||
Log.setDateCallback([] {
|
||||
unsigned int time = millis() / 1000;
|
||||
int sec = time % 60;
|
||||
int min = time % 3600 / 60;
|
||||
int hour = time / 3600;
|
||||
|
||||
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
|
||||
INFO("Settings not valid, reset and restart...");
|
||||
eeSettings.reset();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
return tm{sec, min, hour};
|
||||
});
|
||||
|
||||
Serial.begin(115200);
|
||||
Log.addStream(&Serial);
|
||||
Log.print("\n\n\r");
|
||||
|
||||
} else if (eeSettingsResult == 1) {
|
||||
INFO("Settings NOT loaded, first start");
|
||||
|
||||
} else if (eeSettingsResult == 2) {
|
||||
INFO("Settings NOT loaded (error)");
|
||||
// network settings
|
||||
switch (fsNetworkSettings.read()) {
|
||||
case FD_FS_ERR:
|
||||
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default"));
|
||||
break;
|
||||
case FD_FILE_ERR:
|
||||
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Bad data, load default"));
|
||||
break;
|
||||
case FD_WRITE:
|
||||
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Not found, load default"));
|
||||
break;
|
||||
case FD_ADD:
|
||||
case FD_READ:
|
||||
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Loaded"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
tWm = new WifiManagerTask(true);
|
||||
Scheduler.start(tWm);
|
||||
// settings
|
||||
switch (fsSettings.read()) {
|
||||
case FD_FS_ERR:
|
||||
Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default"));
|
||||
break;
|
||||
case FD_FILE_ERR:
|
||||
Log.swarningln(FPSTR(L_SETTINGS), F("Bad data, load default"));
|
||||
break;
|
||||
case FD_WRITE:
|
||||
Log.sinfoln(FPSTR(L_SETTINGS), F("Not found, load default"));
|
||||
break;
|
||||
case FD_ADD:
|
||||
case FD_READ:
|
||||
Log.sinfoln(FPSTR(L_SETTINGS), F("Loaded"));
|
||||
|
||||
tMqtt = new MqttTask(false);
|
||||
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
|
||||
Log.swarningln(FPSTR(L_SETTINGS), F("Not valid, set default and restart..."));
|
||||
fsSettings.reset();
|
||||
::delay(5000);
|
||||
ESP.restart();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// logs
|
||||
if (!settings.system.serial.enable) {
|
||||
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.enable) {
|
||||
telnetStream = new ESPTelnetStream;
|
||||
telnetStream->setKeepAliveInterval(500);
|
||||
Log.addStream(telnetStream);
|
||||
}
|
||||
|
||||
Log.setLevel(settings.system.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
|
||||
|
||||
// network
|
||||
network = (new NetworkMgr)
|
||||
->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
|
||||
)
|
||||
->setUseDhcp(networkSettings.useDhcp)
|
||||
->setStaticConfig(
|
||||
networkSettings.staticConfig.ip,
|
||||
networkSettings.staticConfig.gateway,
|
||||
networkSettings.staticConfig.subnet,
|
||||
networkSettings.staticConfig.dns
|
||||
);
|
||||
|
||||
// tasks
|
||||
tMqtt = new MqttTask(false, 500);
|
||||
Scheduler.start(tMqtt);
|
||||
|
||||
tOt = new OpenThermTask(false);
|
||||
tOt = new OpenThermTask(true, 750);
|
||||
Scheduler.start(tOt);
|
||||
|
||||
tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL);
|
||||
@@ -75,14 +168,17 @@ void setup() {
|
||||
tRegulator = new RegulatorTask(true, 10000);
|
||||
Scheduler.start(tRegulator);
|
||||
|
||||
tMain = new MainTask(true, 50);
|
||||
tPortal = new PortalTask(true, 0);
|
||||
Scheduler.start(tPortal);
|
||||
|
||||
tMain = new MainTask(true, 100);
|
||||
Scheduler.start(tMain);
|
||||
|
||||
Scheduler.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
#if defined(ESP32)
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
vTaskDelete(NULL);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
24
src/strings.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#ifndef PROGMEM
|
||||
#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";
|
||||
1504
src/utils.h
Normal file
BIN
src_data/fonts/iconly.eot
Normal file
78
src_data/fonts/iconly.svg
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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="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="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" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 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
BIN
src_data/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
331
src_data/locales/en.json
Normal file
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "License",
|
||||
"source": "Source code",
|
||||
"help": "Help",
|
||||
"issues": "Issues & questions",
|
||||
"releases": "Releases"
|
||||
},
|
||||
|
||||
"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",
|
||||
"sdk": "Core/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 and sensors",
|
||||
"otDiag": "OpenTherm diagnostic"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "Heating",
|
||||
"dhw": "DHW",
|
||||
"temp.current": "Current",
|
||||
"enable": "Enable",
|
||||
"turbo": "Turbo mode"
|
||||
},
|
||||
|
||||
"state": {
|
||||
"ot": "OpenTherm connected",
|
||||
"mqtt": "MQTT connected",
|
||||
"emergency": "Emergency",
|
||||
"heating": "Heating",
|
||||
"dhw": "DHW",
|
||||
"flame": "Flame",
|
||||
"fault": "Fault",
|
||||
"diag": "Diagnostic",
|
||||
"extpump": "External pump",
|
||||
"modulation": "Modulation",
|
||||
"pressure": "Pressure",
|
||||
"dhwFlowRate": "DHW flow rate",
|
||||
"faultCode": "Fault code",
|
||||
"indoorTemp": "Indoor temp",
|
||||
"outdoorTemp": "Outdoor temp",
|
||||
"heatingTemp": "Heating temp",
|
||||
"heatingSetpointTemp": "Heating setpoint temp",
|
||||
"heatingReturnTemp": "Heating return temp",
|
||||
"dhwTemp": "DHW temp",
|
||||
"exhaustTemp": "Exhaust temp"
|
||||
}
|
||||
},
|
||||
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
"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",
|
||||
"emergency.events": "Events",
|
||||
"emergency.regulators": "Using regulators",
|
||||
"equitherm": "Equitherm settings",
|
||||
"pid": "PID settings",
|
||||
"ot": "OpenTherm settings",
|
||||
"ot.options": "Options",
|
||||
"mqtt": "MQTT settings",
|
||||
"outdorSensor": "Outdoor sensor settings",
|
||||
"indoorSensor": "Indoor sensor settings",
|
||||
"extPump": "External pump settings"
|
||||
},
|
||||
|
||||
"enable": "Enable",
|
||||
"note": {
|
||||
"restart": "After changing these settings, the device must be restarted for the changes to take effect.",
|
||||
"blankNotUse": "blank - not use"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "Minimum temperature",
|
||||
"max": "Maximum temperature"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "Login",
|
||||
"password": "Password",
|
||||
"auth": "Require authentication"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "Unit system",
|
||||
"metric": "Metric <small>(celsius, liters, bar)</small>",
|
||||
"imperial": "Imperial <small>(fahrenheit, gallons, psi)</small>",
|
||||
"statusLedGpio": "Status LED GPIO",
|
||||
"debug": "Debug mode",
|
||||
"serial": {
|
||||
"enable": "Enable Serial port",
|
||||
"baud": {
|
||||
"title": "Serial port baud rate",
|
||||
"note": "Available: 9600, 19200, 38400, 57600, 74880, 115200"
|
||||
}
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "Enable Telnet",
|
||||
"port": {
|
||||
"title": "Telnet port",
|
||||
"note": "Default: 23"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "Hysteresis",
|
||||
"maxMod": "Max modulation level"
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "<b>!</b> Emergency mode can be useful <u>only</u> when using Equitherm and/or PID (when normal work) and when reporting indoor/outdoor temperature via MQTT or API. In this mode, sensor values that are reported via MQTT/API are not used.",
|
||||
|
||||
"target": {
|
||||
"title": "Target temperature",
|
||||
"note": "<u>Indoor temperature</u> if Equitherm or PID is <b>enabled</b><br /><u>Heat carrier temperature</u> if Equitherm and PID <b>is disabled</b>"
|
||||
},
|
||||
"treshold": "Treshold time <small>(sec)</small>",
|
||||
|
||||
"events": {
|
||||
"network": "On network fault",
|
||||
"mqtt": "On MQTT fault"
|
||||
},
|
||||
|
||||
"regulators": {
|
||||
"equitherm": "Equitherm <small>(requires at least an external/boiler <u>outdoor</u> sensor)</small>",
|
||||
"pid": "PID <small>(requires at least an external/BLE <u>indoor</u> sensor)</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>"
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"inGpio": "In GPIO",
|
||||
"outGpio": "Out GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberIdCode": "Master MemberID code",
|
||||
|
||||
"options": {
|
||||
"dhwPresent": "DHW present",
|
||||
"summerWinterMode": "Summer/winter mode",
|
||||
"heatingCh2Enabled": "Heating CH2 always enabled",
|
||||
"heatingCh1ToCh2": "Duplicate heating CH1 to CH2",
|
||||
"dhwToCh2": "Duplicate DHW to CH2",
|
||||
"dhwBlocking": "DHW blocking",
|
||||
"modulationSyncWithHeating": "Sync modulation with heating",
|
||||
"getMinMaxTemp": "Get min/max temp from boiler"
|
||||
},
|
||||
|
||||
"faultState": {
|
||||
"gpio": "Fault state GPIO",
|
||||
"note": "Can be useful to switch on another boiler <u>via relay</u>. Blank - not use.",
|
||||
"invert": "Invert fault state"
|
||||
},
|
||||
|
||||
"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 and hysteresis in firmware."
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant Discovery",
|
||||
"server": "Server",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"password": "Password",
|
||||
"prefix": "Prefix",
|
||||
"interval": "Publish interval <small>(sec)</small>"
|
||||
},
|
||||
|
||||
"tempSensor": {
|
||||
"source": {
|
||||
"type": "Source type",
|
||||
"boiler": "From boiler via OpenTherm",
|
||||
"manual": "Manual via MQTT/API",
|
||||
"ext": "External (DS18B20)",
|
||||
"ble": "BLE device <i>(ONLY for some ESP32 which support BLE)</i>"
|
||||
},
|
||||
"gpio": "GPIO",
|
||||
"offset": "Temp offset <small>(calibration)</small>",
|
||||
"bleAddress": {
|
||||
"title": "BLE address",
|
||||
"note": "ONLY for some ESP32 which support BLE"
|
||||
}
|
||||
},
|
||||
|
||||
"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>"
|
||||
}
|
||||
},
|
||||
|
||||
"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 10 seconds."
|
||||
},
|
||||
|
||||
"settingsFile": "Settings file",
|
||||
"fw": "Firmware",
|
||||
"fs": "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src_data/locales/ru.json
Normal file
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"values": {
|
||||
"logo": "OpenTherm Gateway",
|
||||
"nav": {
|
||||
"license": "Лицензия",
|
||||
"source": "Исходный код",
|
||||
"help": "Помощь",
|
||||
"issues": "Проблемы и вопросы",
|
||||
"releases": "Релизы"
|
||||
},
|
||||
|
||||
"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": "Дата",
|
||||
"sdk": "Ядро/SDK"
|
||||
},
|
||||
"uptime": "Аптайм",
|
||||
"memory": {
|
||||
"title": "ОЗУ",
|
||||
"maxFreeBlock": "макс. блок",
|
||||
"min": "мин."
|
||||
},
|
||||
"board": "Плата",
|
||||
"chip": {
|
||||
"model": "Чип",
|
||||
"cores": "Кол-во ядер",
|
||||
"freq": "частота"
|
||||
},
|
||||
"flash": {
|
||||
"size": "Размер ПЗУ",
|
||||
"realSize": "реальный размер"
|
||||
},
|
||||
"lastResetReason": "Причина перезагрузки"
|
||||
}
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"name": "Дашборд",
|
||||
"title": "Дашборд - OpenTherm Gateway",
|
||||
|
||||
"section": {
|
||||
"control": "Управление",
|
||||
"states": "Состояние и сенсоры",
|
||||
"otDiag": "Диагностика OpenTherm"
|
||||
},
|
||||
|
||||
"thermostat": {
|
||||
"heating": "Отопление",
|
||||
"dhw": "ГВС",
|
||||
"temp.current": "Текущая",
|
||||
"enable": "Вкл",
|
||||
"turbo": "Турбо"
|
||||
},
|
||||
|
||||
"state": {
|
||||
"ot": "OpenTherm подключение",
|
||||
"mqtt": "MQTT подключение",
|
||||
"emergency": "Аварийный режим",
|
||||
"heating": "Отопление",
|
||||
"dhw": "ГВС",
|
||||
"flame": "Пламя",
|
||||
"fault": "Ошибка",
|
||||
"diag": "Диагностика",
|
||||
"extpump": "Внешний насос",
|
||||
"modulation": "Уровень модуляции",
|
||||
"pressure": "Давление",
|
||||
"dhwFlowRate": "Расход ГВС",
|
||||
"faultCode": "Код ошибки",
|
||||
"indoorTemp": "Внутренняя темп.",
|
||||
"outdoorTemp": "Наружная темп.",
|
||||
"heatingTemp": "Темп. отопления",
|
||||
"heatingSetpointTemp": "Уставка темп. отопления",
|
||||
"heatingReturnTemp": "Темп. обратки отопления",
|
||||
"dhwTemp": "Темп. ГВС",
|
||||
"exhaustTemp": "Темп. выхлопных газов"
|
||||
}
|
||||
},
|
||||
|
||||
"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 для автоматического выбора"
|
||||
}
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Настройки - OpenTherm Gateway",
|
||||
"name": "Настройки",
|
||||
|
||||
"section": {
|
||||
"portal": "Настройки портала",
|
||||
"system": "Системные настройки",
|
||||
"diag": "Диагностика",
|
||||
"heating": "Настройки отопления",
|
||||
"dhw": "Настройки ГВС",
|
||||
"emergency": "Настройки аварийного режима",
|
||||
"emergency.events": "События",
|
||||
"emergency.regulators": "Используемые регуляторы",
|
||||
"equitherm": "Настройки ПЗА",
|
||||
"pid": "Настройки ПИД",
|
||||
"ot": "Настройки OpenTherm",
|
||||
"ot.options": "Опции",
|
||||
"mqtt": "Настройки MQTT",
|
||||
"outdorSensor": "Настройки наружного датчика температуры",
|
||||
"indoorSensor": "Настройки внутреннего датчика температуры",
|
||||
"extPump": "Настройки дополнительного насоса"
|
||||
},
|
||||
|
||||
"enable": "Вкл",
|
||||
"note": {
|
||||
"restart": "После изменения этих настроек устройство необходимо перезагрузить, чтобы изменения вступили в силу.",
|
||||
"blankNotUse": "пусто - не использовать"
|
||||
},
|
||||
|
||||
"temp": {
|
||||
"min": "Мин. температура",
|
||||
"max": "Макс. температура"
|
||||
},
|
||||
|
||||
"portal": {
|
||||
"login": "Логин",
|
||||
"password": "Пароль",
|
||||
"auth": "Требовать аутентификацию"
|
||||
},
|
||||
|
||||
"system": {
|
||||
"unit": "Система единиц",
|
||||
"metric": "Метрическая <small>(цильсии, литры, бары)</small>",
|
||||
"imperial": "Imperial <small>(фаренгейты, галлоны, psi)</small>",
|
||||
"statusLedGpio": "Статус LED GPIO",
|
||||
"debug": "Отладка",
|
||||
"serial": {
|
||||
"enable": "Вкл. Serial порт",
|
||||
"baud": {
|
||||
"title": "Скорость Serial порта",
|
||||
"note": "Доступно: 9600, 19200, 38400, 57600, 74880, 115200"
|
||||
}
|
||||
},
|
||||
"telnet": {
|
||||
"enable": "Вкл. Telnet",
|
||||
"port": {
|
||||
"title": "Telnet порт",
|
||||
"note": "По умолчанию: 23"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"heating": {
|
||||
"hyst": "Гистерезис",
|
||||
"maxMod": "Макс. уровень модуляции"
|
||||
},
|
||||
|
||||
"emergency": {
|
||||
"desc": "<b>!</b> Аварийный режим может быть полезен <u>только</u> при использовании ПЗА и/или ПИД и при передачи наружной/внутренней температуры через MQTT или API. В этом режиме значения датчиков, передаваемые через MQTT/API, не используются.",
|
||||
|
||||
"target": {
|
||||
"title": "Целевая температура",
|
||||
"note": "Целевая <u>внутренняя температура</u> если ПЗА и/или ПИД <b>включены</b><br />Целевая <u>температура теплоносителя</u> если ПЗА и ПИД <b>выключены</b>"
|
||||
},
|
||||
"treshold": "Пороговое время включения <small>(сек)</small>",
|
||||
|
||||
"events": {
|
||||
"network": "При отключении сети",
|
||||
"mqtt": "При отключении MQTT"
|
||||
},
|
||||
|
||||
"regulators": {
|
||||
"equitherm": "ПЗА <small>(требуется внешний или подключенный к котлу датчик <u>наружной</u> температуры)</small>",
|
||||
"pid": "ПИД <small>(требуется внешний/BLE датчик <u>внутренней</u> температуры)</small>"
|
||||
}
|
||||
},
|
||||
|
||||
"equitherm": {
|
||||
"n": "Коэффициент N",
|
||||
"k": "Коэффициент K",
|
||||
"t": {
|
||||
"title": "Коэффициент T",
|
||||
"note": "Не используется, если ПИД включен"
|
||||
}
|
||||
},
|
||||
|
||||
"pid": {
|
||||
"p": "Коэффициент P",
|
||||
"i": "Коэффициент I",
|
||||
"d": "Коэффициент D",
|
||||
"dt": "DT <small>в секундах</small>"
|
||||
},
|
||||
|
||||
"ot": {
|
||||
"inGpio": "Вход GPIO",
|
||||
"outGpio": "Выход GPIO",
|
||||
"ledGpio": "RX LED GPIO",
|
||||
"memberIdCode": "Master MemberID код",
|
||||
|
||||
"options": {
|
||||
"dhwPresent": "Контур ГВС",
|
||||
"summerWinterMode": "Летний/зимний режим",
|
||||
"heatingCh2Enabled": "Канал 2 отопления всегда вкл.",
|
||||
"heatingCh1ToCh2": "Дублировать параметры отопления канала 1 в канал 2",
|
||||
"dhwToCh2": "Дублировать параметры ГВС в канал 2",
|
||||
"dhwBlocking": "DHW blocking",
|
||||
"modulationSyncWithHeating": "Синхронизировать модуляцию с отоплением",
|
||||
"getMinMaxTemp": "Получать мин. и макс. температуру от котла"
|
||||
},
|
||||
|
||||
"faultState": {
|
||||
"gpio": "Fault state GPIO",
|
||||
"note": "Can be useful to switch on another boiler <u>via relay</u>. Blank - not use.",
|
||||
"invert": "Invert fault state"
|
||||
},
|
||||
|
||||
"nativeHeating": {
|
||||
"title": "Передать управление отоплением котлу",
|
||||
"note": "Работает <u>ТОЛЬКО</u> если котел требует и принимает целевую температуру в помещении и сам регулирует температуру теплоносителя на основе встроенного режима кривых. Несовместимо с ПИД и ПЗА, а также с гистерезисом встроенного ПО."
|
||||
}
|
||||
},
|
||||
|
||||
"mqtt": {
|
||||
"homeAssistantDiscovery": "Home Assistant Discovery",
|
||||
"server": "Адрес сервера",
|
||||
"port": "Порт",
|
||||
"user": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"prefix": "Префикс",
|
||||
"interval": "Интервал публикации (в секундах)"
|
||||
},
|
||||
|
||||
"tempSensor": {
|
||||
"source": {
|
||||
"type": "Источник данных",
|
||||
"boiler": "От котла через OpenTherm",
|
||||
"manual": "Вручную через MQTT/API",
|
||||
"ext": "Внешний датчик (DS18B20)",
|
||||
"ble": "BLE устройство <i>(ТОЛЬКО для некоторых плат ESP32 с поддержкой BLE)</i>"
|
||||
},
|
||||
"gpio": "GPIO",
|
||||
"offset": "Смещение температуры <small>(калибровка)</small>",
|
||||
"bleAddress": {
|
||||
"title": "BLE адрес",
|
||||
"note": "ТОЛЬКО для некоторых плат ESP32 с поддержкой BLE"
|
||||
}
|
||||
},
|
||||
|
||||
"extPump": {
|
||||
"use": "Использовать доп. насос",
|
||||
"gpio": "GPIO реле",
|
||||
"postCirculationTime": "Время постциркуляции <small>(в минутах)</small>",
|
||||
"antiStuckInterval": "Интервал защиты от блокировки <small>(в днях)</small>",
|
||||
"antiStuckTime": "Время работы насоса <small>(в минутах)</small>"
|
||||
}
|
||||
},
|
||||
|
||||
"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": "После успешного обновления устройство автоматически перезагрузится через 10 секунд."
|
||||
},
|
||||
|
||||
"settingsFile": "Файл настроек",
|
||||
"fw": "Прошивка",
|
||||
"fs": "Файловая система"
|
||||
}
|
||||
}
|
||||
}
|
||||
425
src_data/pages/dashboard.html
Normal file
@@ -0,0 +1,425 @@
|
||||
<!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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</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" id="thermostat-heating">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span id="thermostat-heating-target"></span> <span class="temp-unit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="thermostat-heating-current"></span> <span class="temp-unit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus"><button id="thermostat-heating-minus" class="outline"><i class="icons-down"></i></button></div>
|
||||
<div class="thermostat-plus"><button id="thermostat-heating-plus" class="outline"><i class="icons-up"></i></button></div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="thermostat-heating-enabled" value="true">
|
||||
<label htmlFor="thermostat-heating-enabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
|
||||
<input type="checkbox" role="switch" id="thermostat-heating-turbo" value="true">
|
||||
<label htmlFor="thermostat-heating-turbo" data-i18n>dashboard.thermostat.turbo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="thermostat" id="thermostat-dhw">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span id="thermostat-dhw-target"></span> <span class="temp-unit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="thermostat-dhw-current"></span> <span class="temp-unit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus"><button class="outline" id="thermostat-dhw-minus"><i class="icons-down"></i></button></div>
|
||||
<div class="thermostat-plus"><button class="outline" id="thermostat-dhw-plus"><i class="icons-up"></i></button></div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="thermostat-dhw-enabled" value="true">
|
||||
<label htmlFor="thermostat-dhw-enabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>dashboard.section.states</b></summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.ot</th>
|
||||
<td><input type="radio" id="ot-connected" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.mqtt</th>
|
||||
<td><input type="radio" id="mqtt-connected" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.emergency</th>
|
||||
<td><input type="radio" id="ot-emergency" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.heating</th>
|
||||
<td><input type="radio" id="ot-heating" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.dhw</th>
|
||||
<td><input type="radio" id="ot-dhw" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.flame</th>
|
||||
<td><input type="radio" id="ot-flame" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.fault</th>
|
||||
<td><input type="radio" id="ot-fault" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.diag</th>
|
||||
<td><input type="radio" id="ot-diagnostic" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.extpump</th>
|
||||
<td><input type="radio" id="ot-external-pump" aria-invalid="false" checked disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.modulation</th>
|
||||
<td><b id="ot-modulation"></b> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.pressure</th>
|
||||
<td><b id="ot-pressure"></b> <span class="pressure-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.dhwFlowRate</th>
|
||||
<td><b id="ot-dhw-flow-rate"></b> <span class="volume-unit"></span>/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.faultCode</th>
|
||||
<td><b id="ot-fault-code"></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.indoorTemp</th>
|
||||
<td><b id="indoor-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.outdoorTemp</th>
|
||||
<td><b id="outdoor-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.heatingTemp</th>
|
||||
<td><b id="heating-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.heatingSetpointTemp</th>
|
||||
<td><b id="heating-setpoint-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.heatingReturnTemp</th>
|
||||
<td><b id="heating-return-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.dhwTemp</th>
|
||||
<td><b id="dhw-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>dashboard.state.exhaustTemp</th>
|
||||
<td><b id="exhaust-temp"></b> <span class="temp-unit"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>dashboard.section.otDiag</b></summary>
|
||||
<pre><b>Vendor:</b> <span id="slave-vendor"></span>
|
||||
<b>Member ID:</b> <span id="slave-member-id"></span>
|
||||
<b>Flags:</b> <span id="slave-flags"></span>
|
||||
<b>Type:</b> <span id="slave-type"></span>
|
||||
<b>Version:</b> <span id="slave-version"></span>
|
||||
<b>OT version:</b> <span id="slave-ot-version"></span>
|
||||
<b>Heating limits:</b> <span id="heating-min-temp"></span>...<span id="heating-max-temp"></span> <span class="temp-unit"></span>
|
||||
<b>DHW limits:</b> <span id="dhw-min-temp"></span>...<span id="dhw-max-temp"></span> <span class="temp-unit"></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"></script>
|
||||
<script>
|
||||
let modifiedTime = null;
|
||||
let noRegulators;
|
||||
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();
|
||||
|
||||
document.querySelector('#thermostat-heating-minus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.heating.target -= 0.5;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
let minTemp;
|
||||
if (noRegulators) {
|
||||
minTemp = prevSettings.heating.minTemp;
|
||||
} else {
|
||||
minTemp = prevSettings.system.unitSystem == 0 ? 5 : 41;
|
||||
}
|
||||
|
||||
if (prevSettings && newSettings.heating.target < minTemp) {
|
||||
newSettings.heating.target = minTemp;
|
||||
}
|
||||
|
||||
setValue('#thermostat-heating-target', newSettings.heating.target);
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-heating-plus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.heating.target += 0.5;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
let maxTemp;
|
||||
if (noRegulators) {
|
||||
maxTemp = prevSettings.heating.maxTemp;
|
||||
} else {
|
||||
maxTemp = prevSettings.system.unitSystem == 0 ? 30 : 86;
|
||||
}
|
||||
|
||||
if (prevSettings && newSettings.heating.target > maxTemp) {
|
||||
newSettings.heating.target = maxTemp;
|
||||
}
|
||||
|
||||
setValue('#thermostat-heating-target', newSettings.heating.target);
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-dhw-minus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.dhw.target -= 1.0;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
if (newSettings.dhw.target < prevSettings.dhw.minTemp) {
|
||||
newSettings.dhw.target = prevSettings.dhw.minTemp;
|
||||
}
|
||||
|
||||
setValue('#thermostat-dhw-target', newSettings.dhw.target);
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-dhw-plus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.dhw.target += 1.0;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
if (newSettings.dhw.target > prevSettings.dhw.maxTemp) {
|
||||
newSettings.dhw.target = prevSettings.dhw.maxTemp;
|
||||
}
|
||||
|
||||
setValue('#thermostat-dhw-target', newSettings.dhw.target);
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-heating-enabled').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.heating.enable = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-heating-turbo').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.heating.turbo = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
document.querySelector('#thermostat-dhw-enabled').addEventListener('change', (event) => {
|
||||
modifiedTime = Date.now();
|
||||
newSettings.dhw.enable = event.currentTarget.checked;
|
||||
});
|
||||
|
||||
setTimeout(async function onLoadPage() {
|
||||
if (modifiedTime) {
|
||||
if ((Date.now() - modifiedTime) < 5000) {
|
||||
setTimeout(onLoadPage, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
modifiedTime = null;
|
||||
}
|
||||
|
||||
// settings
|
||||
try {
|
||||
let modified = prevSettings && (
|
||||
(prevSettings.heating.enable != newSettings.heating.enable)
|
||||
|| (prevSettings.heating.turbo != newSettings.heating.turbo)
|
||||
|| (prevSettings.heating.target != newSettings.heating.target)
|
||||
|| (prevSettings.opentherm.dhwPresent && prevSettings.dhw.enable != newSettings.dhw.enable)
|
||||
|| (prevSettings.opentherm.dhwPresent && prevSettings.dhw.target != newSettings.dhw.target)
|
||||
);
|
||||
|
||||
if (modified) {
|
||||
console.log(newSettings);
|
||||
}
|
||||
|
||||
let parameters = { cache: 'no-cache' };
|
||||
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();
|
||||
noRegulators = !result.opentherm.nativeHeatingControl && !result.equitherm.enable && !result.pid.enable;
|
||||
prevSettings = result;
|
||||
newSettings.heating.enable = result.heating.enable;
|
||||
newSettings.heating.turbo = result.heating.turbo;
|
||||
newSettings.heating.target = result.heating.target;
|
||||
newSettings.dhw.enable = result.dhw.enable;
|
||||
newSettings.dhw.target = result.dhw.target;
|
||||
|
||||
if (result.opentherm.dhwPresent) {
|
||||
show('#thermostat-dhw');
|
||||
} else {
|
||||
hide('#thermostat-dhw');
|
||||
}
|
||||
|
||||
setCheckboxValue('#thermostat-heating-enabled', result.heating.enable);
|
||||
setCheckboxValue('#thermostat-heating-turbo', result.heating.turbo);
|
||||
setValue('#thermostat-heating-target', result.heating.target);
|
||||
|
||||
setCheckboxValue('#thermostat-dhw-enabled', result.dhw.enable);
|
||||
setValue('#thermostat-dhw-target', result.dhw.target);
|
||||
|
||||
setValue('.temp-unit', temperatureUnit(result.system.unitSystem));
|
||||
setValue('.pressure-unit', pressureUnit(result.system.unitSystem));
|
||||
setValue('.volume-unit', volumeUnit(result.system.unitSystem));
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// vars
|
||||
try {
|
||||
const response = await fetch('/api/vars', { cache: 'no-cache' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setValue('#thermostat-heating-current', noRegulators ? result.temperatures.heating : result.temperatures.indoor);
|
||||
setValue('#thermostat-dhw-current', result.temperatures.dhw);
|
||||
|
||||
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);
|
||||
setState('#mqtt-connected', result.states.mqtt);
|
||||
|
||||
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-return-temp', result.temperatures.heatingReturn);
|
||||
setValue('#dhw-temp', result.temperatures.dhw);
|
||||
setValue('#exhaust-temp', result.temperatures.exhaust);
|
||||
|
||||
setValue('#heating-min-temp', result.parameters.heatingMinTemp);
|
||||
setValue('#heating-max-temp', result.parameters.heatingMaxTemp);
|
||||
setValue('#heating-setpoint-temp', result.parameters.heatingSetpoint);
|
||||
setValue('#dhw-min-temp', result.parameters.dhwMinTemp);
|
||||
setValue('#dhw-max-temp', result.parameters.dhwMaxTemp);
|
||||
|
||||
setValue('#slave-member-id', result.parameters.slaveMemberId);
|
||||
setValue('#slave-vendor', memberIdToVendor(result.parameters.slaveMemberId));
|
||||
|
||||
setValue('#slave-flags', result.parameters.slaveFlags);
|
||||
setValue('#slave-type', result.parameters.slaveType);
|
||||
setValue('#slave-version', result.parameters.slaveVersion);
|
||||
setValue('#slave-ot-version', result.parameters.slaveOtVersion);
|
||||
setBusy('#dashboard-busy', '#dashboard-container', false);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setTimeout(onLoadPage, 10000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
218
src_data/pages/index.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</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><input type="radio" id="network-connected" aria-invalid="false" checked disabled /></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.sdk</span>: <b id="core-version"></b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.uptime</th>
|
||||
<td>
|
||||
<b id="uptime-days"></b> days,
|
||||
<b id="uptime-hours"></b> hours,
|
||||
<b id="uptime-min"></b> min.,
|
||||
<b id="uptime-sec"></b> sec.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" data-i18n>index.system.memory.title</th>
|
||||
<td>
|
||||
<b id="free-heap"></b> of <b id="total-heap"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="min-free-heap"></b> bytes)<br />
|
||||
<span data-i18n>index.system.memory.maxFreeBlock</span>: <b id="max-free-block-heap"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="min-max-free-block-heap"></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-revision"></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="cpu-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></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="grid">
|
||||
<a href="/dashboard.html" role="button" data-i18n>dashboard.name</a>
|
||||
<a href="/settings.html" role="button" data-i18n>settings.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"></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' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setValue('#network-hostname', result.network.hostname);
|
||||
setValue('#network-mac', result.network.mac);
|
||||
setState('#network-connected', result.network.connected);
|
||||
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.system.buildVersion);
|
||||
setValue('#build-date', result.system.buildDate);
|
||||
setValue('#build-env', result.system.buildEnv);
|
||||
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);
|
||||
|
||||
setValue('#chip-model', result.system.chipModel);
|
||||
setValue('#chip-revision', result.system.chipRevision);
|
||||
setValue('#chip-cores', result.system.chipCores);
|
||||
setValue('#cpu-freq', result.system.cpuFreq);
|
||||
setValue('#core-version', result.system.coreVersion);
|
||||
setValue('#flash-size', result.system.flashSize / 1024 / 1024);
|
||||
setValue('#flash-real-size', result.system.flashRealSize / 1024 / 1024);
|
||||
|
||||
setBusy('#system-busy', '#system-table', false);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setTimeout(onLoadPage, 10000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
220
src_data/pages/network.html
Normal file
@@ -0,0 +1,220 @@
|
||||
<!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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</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 for="network-hostname">
|
||||
<span data-i18n>network.params.hostname</span>
|
||||
<input type="text" id="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" id="network-use-dhcp" name="useDhcp" value="true">
|
||||
<span data-i18n>network.params.dhcp</span>
|
||||
</label>
|
||||
<br />
|
||||
<hr />
|
||||
<h4 data-i18n>network.section.static</h4>
|
||||
|
||||
<label for="network-static-ip">
|
||||
<span data-i18n>network.params.ip</span>
|
||||
<input type="text" id="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">
|
||||
<span data-i18n>network.params.gateway</span>
|
||||
<input type="text" id="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">
|
||||
<span data-i18n>network.params.subnet</span>
|
||||
<input type="text" id="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">
|
||||
<span data-i18n>network.params.dns</span>
|
||||
<input type="text" id="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" 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 for="sta-ssid">
|
||||
<span data-i18n>network.wifi.ssid</span>
|
||||
<input type="text" id="sta-ssid" name="sta[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label for="sta-password">
|
||||
<span data-i18n>network.wifi.password</span>
|
||||
<input type="password" id="sta-password" name="sta[password]" maxlength="64" required>
|
||||
</label>
|
||||
|
||||
<label for="sta-channel">
|
||||
<span data-i18n>network.wifi.channel</span>
|
||||
<input type="number" inputmode="numeric" id="sta-channel" 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 for="ap-ssid">
|
||||
<span data-i18n>network.wifi.ssid</span>
|
||||
<input type="text" id="ap-ssid" name="ap[ssid]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label for="ap-password">
|
||||
<span data-i18n>network.wifi.password</span>
|
||||
<input type="text" id="ap-password" name="ap[password]" maxlength="64" required>
|
||||
</label>
|
||||
|
||||
<label for="ap-channel">
|
||||
<span data-i18n>network.wifi.channel</span>
|
||||
<input type="number" inputmode="numeric" id="ap-channel" 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"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
const fillData = (data) => {
|
||||
setInputValue('#network-hostname', data.hostname);
|
||||
setCheckboxValue('#network-use-dhcp', data.useDhcp);
|
||||
setInputValue('#network-static-ip', data.staticConfig.ip);
|
||||
setInputValue('#network-static-gateway', data.staticConfig.gateway);
|
||||
setInputValue('#network-static-subnet', data.staticConfig.subnet);
|
||||
setInputValue('#network-static-dns', data.staticConfig.dns);
|
||||
setBusy('#network-settings-busy', '#network-settings', false);
|
||||
|
||||
setInputValue('#sta-ssid', data.sta.ssid);
|
||||
setInputValue('#sta-password', data.sta.password);
|
||||
setInputValue('#sta-channel', data.sta.channel);
|
||||
setBusy('#sta-settings-busy', '#sta-settings', false);
|
||||
|
||||
setInputValue('#ap-ssid', data.ap.ssid);
|
||||
setInputValue('#ap-password', data.ap.password);
|
||||
setInputValue('#ap-channel', data.ap.channel);
|
||||
setBusy('#ap-settings-busy', '#ap-settings', false);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/network/settings', { cache: 'no-cache' });
|
||||
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>
|
||||
833
src_data/pages/settings.html
Normal file
@@ -0,0 +1,833 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n>settings.title</title>
|
||||
<link rel="stylesheet" href="/static/app.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<article>
|
||||
<hgroup>
|
||||
<h2 data-i18n>settings.name</h2>
|
||||
<p></p>
|
||||
</hgroup>
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.portal</b></summary>
|
||||
<div>
|
||||
<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">
|
||||
<span data-i18n>settings.portal.login</span>
|
||||
<input type="text" id="portal-login" name="portal[login]" maxlength="12" required>
|
||||
</label>
|
||||
|
||||
<label for="portal-password">
|
||||
<span data-i18n>settings.portal.password</span>
|
||||
<input type="password" id="portal-password" name="portal[password]" maxlength="32" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label for="portal-auth">
|
||||
<input type="checkbox" id="portal-auth" name="portal[auth]" value="true">
|
||||
<span data-i18n>settings.portal.auth</span>
|
||||
</label>
|
||||
<br />
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.system</b></summary>
|
||||
<div>
|
||||
<div id="system-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="system-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.system.unit</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="system-unit-system" name="system[unitSystem]" value="0" />
|
||||
<span data-i18n>settings.system.metric</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="system-unit-system" name="system[unitSystem]" value="1" />
|
||||
<span data-i18n>settings.system.imperial</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="system-status-led-gpio">
|
||||
<span data-i18n>settings.system.statusLedGpio</span>
|
||||
<input type="number" inputmode="numeric" id="system-status-led-gpio" name="system[statusLedGpio]" min="0" max="254" step="1">
|
||||
<small data-i18n>settings.note.blankNotUse</small>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.section.diag</legend>
|
||||
|
||||
<label for="system-debug">
|
||||
<input type="checkbox" id="system-debug" name="system[debug]" value="true">
|
||||
<span data-i18n>settings.system.debug</span>
|
||||
</label>
|
||||
|
||||
<label for="system-serial-enable">
|
||||
<input type="checkbox" id="system-serial-enable" name="system[serial][enable]" value="true">
|
||||
<span data-i18n>settings.system.serial.enable</span>
|
||||
</label>
|
||||
|
||||
<label for="system-telnet-enable">
|
||||
<input type="checkbox" id="system-telnet-enable" name="system[telnet][enable]" value="true">
|
||||
<span data-i18n>settings.system.telnet.enable</span>
|
||||
</label>
|
||||
|
||||
<div class="grid">
|
||||
<label for="system-serial-baudrate">
|
||||
<span data-i18n>settings.system.serial.baud.title</span>
|
||||
<input type="number" inputmode="numeric" id="system-serial-baudrate" name="system[serial][baudrate]" min="9600" max="115200" step="1" required>
|
||||
<small data-i18n>settings.system.serial.baud.note</small>
|
||||
</label>
|
||||
|
||||
<label for="system-telnet-port">
|
||||
<span data-i18n>settings.system.telnet.port.title</span>
|
||||
<input type="number" inputmode="numeric" id="system-telnet-port" name="system[telnet][port]" min="1" max="65535" step="1" required>
|
||||
<small data-i18n>settings.system.telnet.port.note</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<mark data-i18n>settings.note.restart</mark>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.heating</b></summary>
|
||||
<div>
|
||||
<div id="heating-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="heating-settings" class="hidden">
|
||||
<div class="grid">
|
||||
<label for="heating-min-temp">
|
||||
<span data-i18n>settings.temp.min</span>
|
||||
<input type="number" inputmode="numeric" id="heating-min-temp" name="heating[minTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
|
||||
<label for="heating-max-temp">
|
||||
<span data-i18n>settings.temp.max</span>
|
||||
<input type="number" inputmode="numeric" id="heating-max-temp" name="heating[maxTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="heating-hysteresis">
|
||||
<span data-i18n>settings.heating.hyst</span>
|
||||
<input type="number" inputmode="numeric" id="heating-hysteresis" name="heating[hysteresis]" min="0" max="5" step="0.05" required>
|
||||
</label>
|
||||
|
||||
<label for="heating-max-modulation">
|
||||
<span data-i18n>settings.heating.maxMod</span>
|
||||
<input type="number" inputmode="numeric" id="heating-max-modulation" name="heating[maxModulation]" min="1" max="100" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.dhw</b></summary>
|
||||
<div>
|
||||
<div id="dhw-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="dhw-settings" class="hidden">
|
||||
<div class="grid">
|
||||
<label for="dhw-min-temp">
|
||||
<span data-i18n>settings.temp.min</span>
|
||||
<input type="number" inputmode="numeric" id="dhw-min-temp" name="dhw[minTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
|
||||
<label for="dhw-max-temp">
|
||||
<span data-i18n>settings.temp.max</span>
|
||||
<input type="number" inputmode="numeric" id="dhw-max-temp" name="dhw[maxTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.emergency</b></summary>
|
||||
<div>
|
||||
<div id="emergency-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="emergency-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="emergency-enable">
|
||||
<input type="checkbox" id="emergency-enable" name="emergency[enable]" value="true">
|
||||
<span data-i18n>settings.enable</span>
|
||||
</label>
|
||||
|
||||
<small data-i18n>settings.emergency.desc</small>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="emergency-target">
|
||||
<span data-i18n>settings.emergency.target.title</span>
|
||||
<input type="number" inputmode="numeric" id="emergency-target" name="emergency[target]" min="0" max="0" step="1" required>
|
||||
<small data-i18n>settings.emergency.target.note</small>
|
||||
</label>
|
||||
|
||||
<label for="emergency-treshold-time">
|
||||
<span data-i18n>settings.emergency.treshold</span>
|
||||
<input type="number" inputmode="numeric" id="emergency-treshold-time" name="emergency[tresholdTime]" min="60" max="1800" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.section.emergency.events</legend>
|
||||
|
||||
<label for="emergency-on-network-fault">
|
||||
<input type="checkbox" id="emergency-on-network-fault" name="emergency[onNetworkFault]" value="true">
|
||||
<span data-i18n>settings.emergency.events.network</span>
|
||||
</label>
|
||||
|
||||
<label for="emergency-on-mqtt-fault">
|
||||
<input type="checkbox" id="emergency-on-mqtt-fault" name="emergency[onMqttFault]" value="true">
|
||||
<span data-i18n>settings.emergency.events.mqtt</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.section.emergency.regulators</legend>
|
||||
|
||||
<label for="emergency-use-equitherm">
|
||||
<input type="checkbox" id="emergency-use-equitherm" name="emergency[useEquitherm]" value="true">
|
||||
<span data-i18n>settings.emergency.regulators.equitherm</span>
|
||||
</label>
|
||||
<label for="emergency-use-pid">
|
||||
<input type="checkbox" id="emergency-use-pid" name="emergency[usePid]" value="true">
|
||||
<span data-i18n>settings.emergency.regulators.pid</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.equitherm</b></summary>
|
||||
<div>
|
||||
<div id="equitherm-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="equitherm-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="equitherm-enable">
|
||||
<input type="checkbox" id="equitherm-enable" name="equitherm[enable]" value="true">
|
||||
<span data-i18n>settings.enable</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="equitherm-n-factor">
|
||||
<span data-i18n>settings.equitherm.n</span>
|
||||
<input type="number" inputmode="numeric" id="equitherm-n-factor" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required>
|
||||
</label>
|
||||
|
||||
<label for="equitherm-k-factor">
|
||||
<span data-i18n>settings.equitherm.k</span>
|
||||
<input type="number" inputmode="numeric" id="equitherm-k-factor" name="equitherm[k_factor]" min="0" max="10" step="0.01" required>
|
||||
</label>
|
||||
|
||||
<label for="equitherm-t-factor">
|
||||
<span data-i18n>settings.equitherm.t.title</span>
|
||||
<input type="number" inputmode="numeric" id="equitherm-t-factor" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
|
||||
<small data-i18n>settings.equitherm.t.note</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.pid</b></summary>
|
||||
<div>
|
||||
<div id="pid-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="pid-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="pid-enable">
|
||||
<input type="checkbox" id="pid-enable" name="pid[enable]" value="true">
|
||||
<span data-i18n>settings.enable</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="pid-p-factor">
|
||||
<span data-i18n>settings.pid.p</span>
|
||||
<input type="number" inputmode="numeric" id="pid-p-factor" name="pid[p_factor]" min="0.1" max="1000" step="0.1" required>
|
||||
</label>
|
||||
|
||||
<label for="pid-i-factor">
|
||||
<span data-i18n>settings.pid.i</span>
|
||||
<input type="number" inputmode="numeric" id="pid-i-factor" name="pid[i_factor]" min="0" max="100" step="0.0001" required>
|
||||
</label>
|
||||
|
||||
<label for="pid-d-factor">
|
||||
<span data-i18n>settings.pid.d</span>
|
||||
<input type="number" inputmode="numeric" id="pid-d-factor" name="pid[d_factor]" min="0" max="100000" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label for="pid-dt">
|
||||
<span data-i18n>settings.pid.dt</span>
|
||||
<input type="number" inputmode="numeric" id="pid-dt" name="pid[dt]" min="30" max="600" step="1" required>
|
||||
</label>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="grid">
|
||||
<label for="pid-min-temp">
|
||||
<span data-i18n>settings.temp.min</span>
|
||||
<input type="number" inputmode="numeric" id="pid-min-temp" name="pid[minTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
|
||||
<label for="pid-max-temp">
|
||||
<span data-i18n>settings.temp.max</span>
|
||||
<input type="number" inputmode="numeric" id="pid-max-temp" name="pid[maxTemp]" min="0" max="0" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.ot</b></summary>
|
||||
<div>
|
||||
<div id="opentherm-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="opentherm-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.system.unit</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="opentherm-unit-system" name="opentherm[unitSystem]" value="0" />
|
||||
<span data-i18n>settings.system.metric</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="opentherm-unit-system" name="opentherm[unitSystem]" value="1" />
|
||||
<span data-i18n>settings.system.imperial</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="opentherm-in-gpio">
|
||||
<span data-i18n>settings.ot.inGpio</span>
|
||||
<input type="number" inputmode="numeric" id="opentherm-in-gpio" name="opentherm[inGpio]" min="0" max="254" step="1">
|
||||
</label>
|
||||
|
||||
<label for="opentherm-in-gpio">
|
||||
<span data-i18n>settings.ot.outGpio</span>
|
||||
<input type="number" inputmode="numeric" id="opentherm-out-gpio" name="opentherm[outGpio]" min="0" max="254" step="1">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="opentherm-rx-led-gpio">
|
||||
<span data-i18n>settings.ot.ledGpio</span>
|
||||
<input type="number" inputmode="numeric" id="opentherm-rx-led-gpio" name="opentherm[rxLedGpio]" min="0" max="254" step="1">
|
||||
<small data-i18n>settings.note.blankNotUse</small>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-member-id-code">
|
||||
<span data-i18n>settings.ot.memberIdCode</span>
|
||||
<input type="number" inputmode="numeric" id="opentherm-member-id-code" name="opentherm[memberIdCode]" min="0" max="65535" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.section.ot.options</legend>
|
||||
<label for="opentherm-dhw-present">
|
||||
<input type="checkbox" id="opentherm-dhw-present" name="opentherm[dhwPresent]" value="true">
|
||||
<span data-i18n>settings.ot.options.dhwPresent</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-sw-mode">
|
||||
<input type="checkbox" id="opentherm-sw-mode" name="opentherm[summerWinterMode]" value="true">
|
||||
<span data-i18n>settings.ot.options.summerWinterMode</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-heating-ch2-enabled">
|
||||
<input type="checkbox" id="opentherm-heating-ch2-enabled" name="opentherm[heatingCh2Enabled]" value="true">
|
||||
<span data-i18n>settings.ot.options.heatingCh2Enabled</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-heating-ch1-to-ch2">
|
||||
<input type="checkbox" id="opentherm-heating-ch1-to-ch2" name="opentherm[heatingCh1ToCh2]" value="true">
|
||||
<span data-i18n>settings.ot.options.heatingCh1ToCh2</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-dhw-to-ch2">
|
||||
<input type="checkbox" id="opentherm-dhw-to-ch2" name="opentherm[dhwToCh2]" value="true">
|
||||
<span data-i18n>settings.ot.options.dhwToCh2</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-dhw-blocking">
|
||||
<input type="checkbox" id="opentherm-dhw-blocking" name="opentherm[dhwBlocking]" value="true">
|
||||
<span data-i18n>settings.ot.options.dhwBlocking</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-sync-modulation-with-heating">
|
||||
<input type="checkbox" id="opentherm-sync-modulation-with-heating" name="opentherm[modulationSyncWithHeating]" value="true">
|
||||
<span data-i18n>settings.ot.options.modulationSyncWithHeating</span>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-get-min-max-temp">
|
||||
<input type="checkbox" id="opentherm-get-min-max-temp" name="opentherm[getMinMaxTemp]" value="true">
|
||||
<span data-i18n>settings.ot.options.getMinMaxTemp</span>
|
||||
</label>
|
||||
|
||||
<hr />
|
||||
<fieldset>
|
||||
<label for="opentherm-fault-state-gpio">
|
||||
<span data-i18n>settings.ot.faultState.gpio</span>
|
||||
<input type="number" inputmode="numeric" id="opentherm-fault-state-gpio" name="opentherm[faultStateGpio]" min="0" max="254" step="1">
|
||||
<small data-i18n>settings.ot.faultState.note</small>
|
||||
</label>
|
||||
|
||||
<label for="opentherm-invert-fault-state">
|
||||
<input type="checkbox" id="opentherm-invert-fault-state" name="opentherm[invertFaultState]" value="true">
|
||||
<span data-i18n>settings.ot.faultState.invert</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<hr />
|
||||
<label for="opentherm-native-heating-control">
|
||||
<input type="checkbox" id="opentherm-native-heating-control" name="opentherm[nativeHeatingControl]" value="true">
|
||||
<span data-i18n>settings.ot.nativeHeating.title</span><br />
|
||||
<small data-i18n>settings.ot.nativeHeating.note</small>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.mqtt</b></summary>
|
||||
<div>
|
||||
<div id="mqtt-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="mqtt-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="mqtt-enable">
|
||||
<input type="checkbox" id="mqtt-enable" name="mqtt[enable]" value="true">
|
||||
<span data-i18n>settings.enable</span>
|
||||
</label>
|
||||
|
||||
<label for="mqtt-ha-discovery">
|
||||
<input type="checkbox" id="mqtt-ha-discovery" name="mqtt[homeAssistantDiscovery]" value="true">
|
||||
<span data-i18n>settings.mqtt.homeAssistantDiscovery</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="mqtt-server">
|
||||
<span data-i18n>settings.mqtt.server</span>
|
||||
<input type="text" id="mqtt-server" name="mqtt[server]" maxlength="80" required>
|
||||
</label>
|
||||
|
||||
<label for="mqtt-port">
|
||||
<span data-i18n>settings.mqtt.port</span>
|
||||
<input type="number" inputmode="numeric" id="mqtt-port" name="mqtt[port]" min="1" max="65535" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="mqtt-user">
|
||||
<span data-i18n>settings.mqtt.user</span>
|
||||
<input type="text" id="mqtt-user" name="mqtt[user]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label for="mqtt-password">
|
||||
<span data-i18n>settings.mqtt.password</span>
|
||||
<input type="password" id="mqtt-password" name="mqtt[password]" maxlength="32">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="mqtt-prefix">
|
||||
<span data-i18n>settings.mqtt.prefix</span>
|
||||
<input type="text" id="mqtt-prefix" name="mqtt[prefix]" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label for="mqtt-interval">
|
||||
<span data-i18n>settings.mqtt.interval</span>
|
||||
<input type="number" inputmode="numeric" id="mqtt-interval" name="mqtt[interval]" min="3" max="60" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.outdorSensor</b></summary>
|
||||
<div>
|
||||
<div id="outdoor-sensor-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="outdoor-sensor-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.tempSensor.source.type</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="0" />
|
||||
<span data-i18n>settings.tempSensor.source.boiler</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="1" />
|
||||
<span data-i18n>settings.tempSensor.source.manual</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" />
|
||||
<span data-i18n>settings.tempSensor.source.ext</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="outdoor-sensor-gpio">
|
||||
<span data-i18n>settings.tempSensor.gpio</span>
|
||||
<input type="number" inputmode="numeric" id="outdoor-sensor-gpio" name="sensors[outdoor][gpio]" min="0" max="254" step="1">
|
||||
</label>
|
||||
|
||||
<label for="outdoor-sensor-offset">
|
||||
<span data-i18n>settings.tempSensor.offset</span>
|
||||
<input type="number" inputmode="numeric" id="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-10" max="10" step="0.01" required>
|
||||
</label>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.indoorSensor</b></summary>
|
||||
<div>
|
||||
<div id="indoor-sensor-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="indoor-sensor-settings" class="hidden">
|
||||
<fieldset>
|
||||
<legend data-i18n>settings.tempSensor.source.type</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="1" />
|
||||
<span data-i18n>settings.tempSensor.source.manual</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="2" />
|
||||
<span data-i18n>settings.tempSensor.source.ext</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="3" />
|
||||
<span data-i18n>settings.tempSensor.source.ble</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="indoor-sensor-gpio">
|
||||
<span data-i18n>settings.tempSensor.gpio</span>
|
||||
<input type="number" inputmode="numeric" id="indoor-sensor-gpio" name="sensors[indoor][gpio]" min="0" max="254" step="1">
|
||||
</label>
|
||||
|
||||
<div class="grid">
|
||||
<label for="indoor-sensor-offset">
|
||||
<span data-i18n>settings.tempSensor.offset</span>
|
||||
<input type="number" inputmode="numeric" id="indoor-sensor-offset" name="sensors[indoor][offset]" min="-10" max="10" step="0.01" required>
|
||||
</label>
|
||||
|
||||
<label for="indoor-sensor-ble-addresss">
|
||||
<span data-i18n>settings.tempSensor.bleAddress.title</span>
|
||||
<input type="text" id="indoor-sensor-ble-addresss" name="sensors[indoor][bleAddress]" pattern="([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}">
|
||||
<small data-i18n>settings.tempSensor.bleAddress.note</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n>button.save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.extPump</b></summary>
|
||||
<div>
|
||||
<div id="extpump-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="extpump-settings" class="hidden">
|
||||
<fieldset>
|
||||
<label for="extpump-use">
|
||||
<input type="checkbox" id="extpump-use" name="externalPump[use]" value="true">
|
||||
<span data-i18n>settings.extPump.use</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid">
|
||||
<label for="extpump-gpio">
|
||||
<span data-i18n>settings.extPump.gpio</span>
|
||||
<input type="number" inputmode="numeric" id="extpump-gpio" name="externalPump[gpio]" min="0" max="254" step="1">
|
||||
</label>
|
||||
|
||||
<label for="extpump-pc-time">
|
||||
<span data-i18n>settings.extPump.postCirculationTime</span>
|
||||
<input type="number" inputmode="numeric" id="extpump-pc-time" name="externalPump[postCirculationTime]" min="1" max="120" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<label for="extpump-as-interval">
|
||||
<span data-i18n>settings.extPump.antiStuckInterval</span>
|
||||
<input type="number" inputmode="numeric" id="extpump-as-interval" name="externalPump[antiStuckInterval]" min="1" max="366" step="1" required>
|
||||
</label>
|
||||
|
||||
<label for="extpump-as-time">
|
||||
<span data-i18n>settings.extPump.antiStuckTime</span>
|
||||
<input type="number" inputmode="numeric" id="extpump-as-time" name="externalPump[antiStuckTime]" min="1" max="20" step="1" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
|
||||
const fillData = (data) => {
|
||||
// System
|
||||
setCheckboxValue('#system-debug', data.system.debug);
|
||||
setCheckboxValue('#system-serial-enable', data.system.serial.enable);
|
||||
setInputValue('#system-serial-baudrate', data.system.serial.baudrate);
|
||||
setCheckboxValue('#system-telnet-enable', data.system.telnet.enable);
|
||||
setInputValue('#system-telnet-port', data.system.telnet.port);
|
||||
setRadioValue('.system-unit-system', data.system.unitSystem);
|
||||
setInputValue('#system-status-led-gpio', data.system.statusLedGpio < 255 ? data.system.statusLedGpio : '');
|
||||
setBusy('#system-settings-busy', '#system-settings', false);
|
||||
|
||||
// Portal
|
||||
setCheckboxValue('#portal-auth', data.portal.auth);
|
||||
setInputValue('#portal-login', data.portal.login);
|
||||
setInputValue('#portal-password', data.portal.password);
|
||||
setBusy('#portal-settings-busy', '#portal-settings', false);
|
||||
|
||||
// Opentherm
|
||||
setRadioValue('.opentherm-unit-system', data.opentherm.unitSystem);
|
||||
setInputValue('#opentherm-in-gpio', data.opentherm.inGpio < 255 ? data.opentherm.inGpio : '');
|
||||
setInputValue('#opentherm-out-gpio', data.opentherm.outGpio < 255 ? data.opentherm.outGpio : '');
|
||||
setInputValue('#opentherm-rx-led-gpio', data.opentherm.rxLedGpio < 255 ? data.opentherm.rxLedGpio : '');
|
||||
setInputValue('#opentherm-fault-state-gpio', data.opentherm.faultStateGpio < 255 ? data.opentherm.faultStateGpio : '');
|
||||
setCheckboxValue('#opentherm-invert-fault-state', data.opentherm.invertFaultState);
|
||||
setInputValue('#opentherm-member-id-code', data.opentherm.memberIdCode);
|
||||
setCheckboxValue('#opentherm-dhw-present', data.opentherm.dhwPresent);
|
||||
setCheckboxValue('#opentherm-sw-mode', data.opentherm.summerWinterMode);
|
||||
setCheckboxValue('#opentherm-heating-ch2-enabled', data.opentherm.heatingCh2Enabled);
|
||||
setCheckboxValue('#opentherm-heating-ch1-to-ch2', data.opentherm.heatingCh1ToCh2);
|
||||
setCheckboxValue('#opentherm-dhw-to-ch2', data.opentherm.dhwToCh2);
|
||||
setCheckboxValue('#opentherm-dhw-blocking', data.opentherm.dhwBlocking);
|
||||
setCheckboxValue('#opentherm-sync-modulation-with-heating', data.opentherm.modulationSyncWithHeating);
|
||||
setCheckboxValue('#opentherm-get-min-max-temp', data.opentherm.getMinMaxTemp);
|
||||
setCheckboxValue('#opentherm-native-heating-control', data.opentherm.nativeHeatingControl);
|
||||
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
|
||||
|
||||
// MQTT
|
||||
setCheckboxValue('#mqtt-enable', data.mqtt.enable);
|
||||
setCheckboxValue('#mqtt-ha-discovery', data.mqtt.homeAssistantDiscovery);
|
||||
setInputValue('#mqtt-server', data.mqtt.server);
|
||||
setInputValue('#mqtt-port', data.mqtt.port);
|
||||
setInputValue('#mqtt-user', data.mqtt.user);
|
||||
setInputValue('#mqtt-password', data.mqtt.password);
|
||||
setInputValue('#mqtt-prefix', data.mqtt.prefix);
|
||||
setInputValue('#mqtt-interval', data.mqtt.interval);
|
||||
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
|
||||
|
||||
// Outdoor sensor
|
||||
setRadioValue('.outdoor-sensor-type', data.sensors.outdoor.type);
|
||||
setInputValue('#outdoor-sensor-gpio', data.sensors.outdoor.gpio < 255 ? data.sensors.outdoor.gpio : '');
|
||||
setInputValue('#outdoor-sensor-offset', data.sensors.outdoor.offset);
|
||||
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
|
||||
|
||||
// Indoor sensor
|
||||
setRadioValue('.indoor-sensor-type', data.sensors.indoor.type);
|
||||
setInputValue('#indoor-sensor-gpio', data.sensors.indoor.gpio < 255 ? data.sensors.indoor.gpio : '');
|
||||
setInputValue('#indoor-sensor-offset', data.sensors.indoor.offset);
|
||||
setInputValue('#indoor-sensor-ble-addresss', data.sensors.indoor.bleAddress);
|
||||
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
|
||||
|
||||
// Extpump
|
||||
setCheckboxValue('#extpump-use', data.externalPump.use);
|
||||
setInputValue('#extpump-gpio', data.externalPump.gpio < 255 ? data.externalPump.gpio : '');
|
||||
setInputValue('#extpump-pc-time', data.externalPump.postCirculationTime);
|
||||
setInputValue('#extpump-as-interval', data.externalPump.antiStuckInterval);
|
||||
setInputValue('#extpump-as-time', data.externalPump.antiStuckTime);
|
||||
setBusy('#extpump-settings-busy', '#extpump-settings', false);
|
||||
|
||||
// Heating
|
||||
setInputValue('#heating-min-temp', data.heating.minTemp, {
|
||||
"min": data.system.unitSystem == 0 ? 0 : 32,
|
||||
"max": data.system.unitSystem == 0 ? 99 : 211
|
||||
});
|
||||
setInputValue('#heating-max-temp', data.heating.maxTemp, {
|
||||
"min": data.system.unitSystem == 0 ? 1 : 33,
|
||||
"max": data.system.unitSystem == 0 ? 100 : 212
|
||||
});
|
||||
setInputValue('#heating-hysteresis', data.heating.hysteresis);
|
||||
setInputValue('#heating-max-modulation', data.heating.maxModulation);
|
||||
setBusy('#heating-settings-busy', '#heating-settings', false);
|
||||
|
||||
// DHW
|
||||
setInputValue('#dhw-min-temp', data.dhw.minTemp, {
|
||||
"min": data.system.unitSystem == 0 ? 0 : 32,
|
||||
"max": data.system.unitSystem == 0 ? 99 : 211
|
||||
});
|
||||
setInputValue('#dhw-max-temp', data.dhw.maxTemp, {
|
||||
"min": data.system.unitSystem == 0 ? 1 : 33,
|
||||
"max": data.system.unitSystem == 0 ? 100 : 212
|
||||
});
|
||||
setBusy('#dhw-settings-busy', '#dhw-settings', false);
|
||||
|
||||
// Emergency mode
|
||||
setCheckboxValue('#emergency-enable', data.emergency.enable);
|
||||
setInputValue('#emergency-treshold-time', data.emergency.tresholdTime);
|
||||
setCheckboxValue('#emergency-use-equitherm', data.emergency.useEquitherm);
|
||||
setCheckboxValue('#emergency-use-pid', data.emergency.usePid);
|
||||
setCheckboxValue('#emergency-on-network-fault', data.emergency.onNetworkFault);
|
||||
setCheckboxValue('#emergency-on-mqtt-fault', data.emergency.onMqttFault);
|
||||
setInputValue('#emergency-target', data.emergency.target, {
|
||||
"min": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.minTemp : 10,
|
||||
"max": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.maxTemp : 30,
|
||||
});
|
||||
setBusy('#emergency-settings-busy', '#emergency-settings', false);
|
||||
|
||||
// Equitherm
|
||||
setCheckboxValue('#equitherm-enable', data.equitherm.enable);
|
||||
setInputValue('#equitherm-n-factor', data.equitherm.n_factor);
|
||||
setInputValue('#equitherm-k-factor', data.equitherm.k_factor);
|
||||
setInputValue('#equitherm-t-factor', data.equitherm.t_factor);
|
||||
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
|
||||
|
||||
// PID
|
||||
setCheckboxValue('#pid-enable', data.pid.enable);
|
||||
setInputValue('#pid-p-factor', data.pid.p_factor);
|
||||
setInputValue('#pid-i-factor', data.pid.i_factor);
|
||||
setInputValue('#pid-d-factor', data.pid.d_factor);
|
||||
setInputValue('#pid-dt', data.pid.dt);
|
||||
setInputValue('#pid-min-temp', data.pid.minTemp, {
|
||||
"min": 0,
|
||||
"max": data.system.unitSystem == 0 ? 99 : 211
|
||||
});
|
||||
setInputValue('#pid-max-temp', data.pid.maxTemp, {
|
||||
"min": 1,
|
||||
"max": data.system.unitSystem == 0 ? 100 : 212
|
||||
});
|
||||
setBusy('#pid-settings-busy', '#pid-settings', false);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings', { cache: 'no-cache' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
fillData(result);
|
||||
|
||||
setupForm('#portal-settings', fillData, ['portal.login', 'portal.password']);
|
||||
setupForm('#system-settings', fillData);
|
||||
setupForm('#heating-settings', fillData);
|
||||
setupForm('#dhw-settings', fillData);
|
||||
setupForm('#emergency-settings', fillData);
|
||||
setupForm('#equitherm-settings', fillData);
|
||||
setupForm('#pid-settings', fillData);
|
||||
setupForm('#opentherm-settings', fillData);
|
||||
setupForm('#mqtt-settings', fillData, ['mqtt.user', 'mqtt.password', 'mqtt.prefix']);
|
||||
setupForm('#outdoor-sensor-settings', fillData);
|
||||
setupForm('#indoor-sensor-settings', fillData, ['sensors.indoor.bleAddress']);
|
||||
setupForm('#extpump-settings', fillData);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
111
src_data/pages/upgrade.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!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">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">
|
||||
<div class="logo" data-i18n>logo</div>
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
|
||||
<li>
|
||||
<select id="lang" aria-label="Lang">
|
||||
<option value="en" selected>EN</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 class="grid">
|
||||
<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"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
setupRestoreBackupForm('#restore');
|
||||
setupUpgradeForm('#upgrade');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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 ?? 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`);
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
680
src_data/scripts/utils.js
Normal file
@@ -0,0 +1,680 @@
|
||||
function 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());
|
||||
})
|
||||
});
|
||||
|
||||
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 = 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',
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 = 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.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>" + result[i].bssid + "</i>") : result[i].ssid;
|
||||
|
||||
// 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.setAttribute('data-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.setAttribute('data-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' });
|
||||
|
||||
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 = i18n('button.wait');
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = (response) => {
|
||||
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 = (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);
|
||||
}
|
||||
};
|
||||
|
||||
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 = 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) {
|
||||
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',
|
||||
body: fd
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 500) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setBusy(busySelector, contentSelector, value) {
|
||||
if (!value) {
|
||||
hide(busySelector);
|
||||
show(contentSelector);
|
||||
|
||||
} else {
|
||||
show(busySelector);
|
||||
hide(contentSelector);
|
||||
}
|
||||
}
|
||||
|
||||
function setState(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setAttribute('aria-invalid', !value);
|
||||
}
|
||||
|
||||
function setValue(selector, value) {
|
||||
let items = document.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
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, attrs = {}) {
|
||||
let items = document.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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show(selector) {
|
||||
let items = document.querySelectorAll(selector);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
if (item.classList.contains('hidden')) {
|
||||
item.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hide(selector) {
|
||||
let items = document.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;
|
||||
}
|
||||
|
||||
function temperatureUnit(unitSystem) {
|
||||
return unit2str(unitSystem, {
|
||||
0: "°C",
|
||||
1: "°F"
|
||||
});
|
||||
}
|
||||
|
||||
function pressureUnit(unitSystem) {
|
||||
return unit2str(unitSystem, {
|
||||
0: "bar",
|
||||
1: "psi"
|
||||
});
|
||||
}
|
||||
|
||||
function volumeUnit(unitSystem) {
|
||||
return unit2str(unitSystem, {
|
||||
0: "L",
|
||||
1: "gal"
|
||||
});
|
||||
}
|
||||
|
||||
function 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 Fourtech/Luna 3",
|
||||
2: "AWB/Brink",
|
||||
4: "ATAG/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",
|
||||
56: "Baxi Luna Duo-Tec",
|
||||
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);
|
||||
}
|
||||
203
src_data/styles/app.css
Normal file
@@ -0,0 +1,203 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
article {
|
||||
--pico-block-spacing-vertical: var(--pico-spacing);
|
||||
--pico-block-spacing-horizontal: var(--pico-spacing);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
68
src_data/styles/iconly.css
Normal file
@@ -0,0 +1,68 @@
|
||||
/*!
|
||||
* 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?1718563596894");
|
||||
src: url("/static/fonts/iconly.eot?#iefix") format("embedded-opentype"), url("/static/fonts/iconly.woff2?1718563596894") format("woff2"), url("/static/fonts/iconly.woff?1718563596894") format("woff"), url("/static/fonts/iconly.ttf?1718563596894") format("truetype"), url("/static/fonts/iconly.svg?1718563596894#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-wifi-strength-3:before {
|
||||
content: "\e008";
|
||||
}
|
||||
|
||||
.icons-down:before {
|
||||
content: "\e009";
|
||||
}
|
||||
|
||||
.icons-wifi-strength-4:before {
|
||||
content: "\e00a";
|
||||
}
|
||||
|
||||
.icons-up:before {
|
||||
content: "\e00c";
|
||||
}
|
||||
4
src_data/styles/pico.min.css
vendored
Normal file
@@ -1,21 +1,61 @@
|
||||
import shutil
|
||||
import gzip
|
||||
import os
|
||||
Import("env")
|
||||
|
||||
|
||||
def post_build(source, target, env):
|
||||
if os.path.exists(os.path.join(env["PROJECT_DIR"], "build")) == False:
|
||||
return
|
||||
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")),
|
||||
}, os.path.join(env["PROJECT_DIR"], "build"));
|
||||
|
||||
env.Execute("pio run --target buildfs --environment %s" % env["PIOENV"]);
|
||||
|
||||
files = {
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.bin"): "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")),
|
||||
}
|
||||
|
||||
for src in files:
|
||||
if os.path.exists(src):
|
||||
dest = os.path.join(env["PROJECT_DIR"], "build", files[src])
|
||||
|
||||
print("Copying '%s' to '%s'" % (src, dest))
|
||||
shutil.copy(src, dest)
|
||||
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")
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", post_build)
|
||||
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")),
|
||||
}, os.path.join(env["PROJECT_DIR"], "build"));
|
||||
|
||||
|
||||
def copy_to_build_dir(files, build_dir):
|
||||
if os.path.exists(build_dir) == False:
|
||||
return
|
||||
|
||||
for src in files:
|
||||
if os.path.exists(src):
|
||||
dst = os.path.join(build_dir, files[src])
|
||||
|
||||
print("Copying '%s' to '%s'" % (src, dst))
|
||||
shutil.copy(src, dst)
|
||||
|
||||
|
||||
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)
|
||||
12
wokwi/nodemcu_32s/diagram.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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", "", [] ]
|
||||
]
|
||||
}
|
||||
|
||||
8
wokwi/nodemcu_32s/wokwi.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[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"
|
||||