161 Commits

Author SHA1 Message Date
Yurii
f4af237472 chore: bump version to 1.4.6 2024-11-16 14:19:40 +03:00
Yurii
c3d0d94806 fix; fix typo 2024-11-01 12:49:35 +03:00
Yurii
e4211c872c fix: hysteresis with native heating control has been fixed 2024-11-01 12:47:32 +03:00
Yurii
467cfea449 feat: ability to use return heat carrier temp as indoor temp 2024-11-01 04:16:50 +03:00
Yurii
0e3473e065 fix: fix typo 2024-11-01 04:10:56 +03:00
Yurii
8780e5245a fix: for some HA entities added missing parameter enabled_by_default 2024-11-01 03:33:35 +03:00
Yurii
94e8288d76 feat: added entities to HA: connected, rssi, battery, humidity for indoor and outdoor sensors; some entities are disabled by default 2024-11-01 02:36:45 +03:00
Yurii
261a53207c refactor: improved turbo mode 2024-10-31 22:35:23 +03:00
Yurii
1dbc895cdb refactor: increased delay before sending data to MQTT after connection 2024-10-31 22:33:11 +03:00
Yurii
747d8841bc refactor: removed resetting modulation and power if response not valid 2024-10-31 22:30:37 +03:00
Yurii
7ed47a4eca chore: updated equitherm calculator 2024-10-31 22:29:04 +03:00
Yurii
0ccea290cb fix: try to auto determine the min boiler power 2024-10-31 05:29:24 +03:00
Yurii
c03df67900 fix: fixed crash if BLE service not found #88 2024-10-31 05:25:34 +03:00
Yurii
f86857c279 feat: improved turbo mode
- added turbo factor parameter
- implemented turbo mode for PID
2024-10-31 05:22:41 +03:00
Yurii
acd8348a5b feat: try to auto determine the min boiler power 2024-10-31 03:49:00 +03:00
Yurii
cdde3c30af chore: fix typos 2024-10-31 02:42:46 +03:00
Yurii
392242ef3e refactor: vendor list updated 2024-10-31 01:36:50 +03:00
Yurii
a6e8953807 refactor: reworked emergency mode; reworked hysteresis algorithm; improved detection of connection state for MANUAL & BOILER type sensors 2024-10-31 01:36:21 +03:00
Yurii
11b1277d79 refactor: added ID validation for opentherm response 2024-10-27 04:33:46 +03:00
Yurii
f62e687d3f fix: fixed pressure and flow validation when using correction 2024-10-27 04:31:35 +03:00
Yurii
42fa95969f fix: compilation on windows fixed for C3, C6 2024-10-27 03:36:18 +03:00
Yurii
56a0d1322f fix: display of fault code and diag code in logs fixed 2024-10-26 21:37:23 +03:00
Yurii
45762967ee refactor: fix wifi scan on esp32, connection timeouts changed 2024-10-26 20:18:23 +03:00
Yurii
10f9cde17a chore: bump pioarduino/platform-espressif32 from 3.0.6 to 3.1.0 rc2 2024-10-26 20:16:54 +03:00
Yurii
351a884685 refactor: increased max request size for /api/settings 2024-10-24 05:13:29 +03:00
Yurii
98db62cc9e fix: fix typo 2024-10-24 05:00:52 +03:00
Yurii
355d983437 chore: copy elf to build dir 2024-10-24 04:26:46 +03:00
Yurii
3d11d13631 feat: added crash recorder and ability to save dump 2024-10-24 04:01:14 +03:00
Yurii
6c4f8a78a0 refactor: added defaults for serial & telnet 2024-10-24 04:00:06 +03:00
Yurii
3fb5eb32c3 refactor: increased max request size for /api/vars and /api/backup/restore 2024-10-24 03:54:26 +03:00
Yurii
c1447098da chore: bump framework-arduinoespressif32 from 3.0.5 to 3.0.6 2024-10-24 03:52:01 +03:00
Yurii
87b222e7bc refactor: increased the max value of dt for pid to 1800 sec 2024-10-21 21:34:53 +03:00
Yurii
0eea1b8121 fix: change of log level when wifi is not connected 2024-10-18 06:45:16 +03:00
Yurii
7f701a74e7 feat: fault state gpio setting replaced with cascade control 2024-10-18 06:14:09 +03:00
Yurii
57cf98ca19 refactor: cosmetic changes; move maxModulation setting to opentherm section 2024-10-15 05:09:20 +03:00
Yurii
c32c643442 refactor: more logs 2024-10-15 04:21:20 +03:00
Yurii
5553a13cc0 feat: added log level setting 2024-10-15 04:07:00 +03:00
Yurii
a9e97c15ad refactor: more logs; improved sensor of current boiler power: added settings min & max boiler power 2024-10-15 02:10:46 +03:00
Yurii
dc62f99b7d feat: added polling of min modulation and max boiler power; added sensor for current boiler power 2024-10-14 19:54:26 +03:00
Yurii
05ff426b28 chore: bump version to 1.4.5 2024-10-13 22:39:20 +03:00
Yurii
45af7a30d8 refactor: cosmetic changes; added coeff. setting for filtering numeric values 2024-10-13 21:42:33 +03:00
Yurii
8aab541afa fix: added min DHW flow rate of 0.1 l/min 2024-10-11 01:38:05 +03:00
Yurii
7672c4b927 fix: fix types 2024-10-11 01:33:49 +03:00
Yurii
b0a9460257 feat: added correction coeff. settings for pressure and dhw flow rate 2024-10-11 01:29:50 +03:00
Yurii
3c69f1295e feat: added opentherm option for filtering numeric values 2024-10-10 21:08:13 +03:00
Yurii
282a02ecdb fix: added request for set opentherm version 2024-10-09 12:33:58 +03:00
Yurii
8503ef966f fix: small fix 2024-10-05 10:27:16 +03:00
Yurii
a4ee4c5224 feat: added diagnostic code polling via opentherm, added hex value for fault code and diag code 2024-10-05 10:03:14 +03:00
Yurii
4478e8f204 chore: updated platformio.ini for ESP32 C6 2024-10-01 01:45:57 +03:00
Yurii
a50c13fd8a Merge branch 'master' of https://github.com/Laxilef/OTGateway 2024-10-01 01:39:27 +03:00
Yurii
ee1e7f92b2 fix: added delay before start web server 2024-10-01 01:39:23 +03:00
Yurii
5704075682 chore: switch from PaulStoffregen/OneWire to pstolarz/OneWireNg 2024-10-01 01:38:57 +03:00
Yurii
52e4933923 fix: compatibility with framework-arduinoespressif32 version 3.0.5 2024-10-01 01:36:44 +03:00
Yurii
00a82ca3e5 chore: switch from espressif/arduino-esp32 to pioarduino/platform-espressif32 for new core 2024-10-01 01:35:36 +03:00
Yurii
7658aeaa8c feat: added opentherm option 'immergasFix' 2024-10-01 01:32:35 +03:00
Yurii
790ff5a011 feat: added Radiant to vendor list 2024-09-30 23:56:52 +03:00
dependabot[bot]
935cf27139 chore: bump peterus/platformio_dependabot from 1.1.1 to 1.2.0 (#80)
Bumps [peterus/platformio_dependabot](https://github.com/peterus/platformio_dependabot) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/peterus/platformio_dependabot/releases)
- [Commits](https://github.com/peterus/platformio_dependabot/compare/v1.1.1...v1.2.0)

---
updated-dependencies:
- dependency-name: peterus/platformio_dependabot
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 00:36:24 +03:00
Yurii
503068f6e7 chore: update readme 2024-09-06 20:29:50 +03:00
Yurii
5c868d589d chore: update files for production 2024-09-06 20:25:29 +03:00
Yurii
1cca8ffd5d chore: update assets 2024-09-06 19:40:30 +03:00
Yurii
f291eb33ac chore: added assets 2024-09-03 18:00:12 +03:00
Yurii
f0c505c332 chore: added blueprints 2024-09-03 17:38:59 +03:00
Yurii
7eafe4a90b fix: compatibility with framework-arduinoespressif32 version 3.0.4 2024-08-22 04:57:13 +03:00
Yurii
d23527a48b chore: bump framework-arduinoespressif32 from 3.0.1 to 3.0.4 2024-08-22 03:34:17 +03:00
Yurii
c341f86e5c refactor: cosmetic changes 2024-08-22 03:33:12 +03:00
Yurii
939ed6cdab chore: bump bblanchon/ArduinoJson from 7.0.4 to 7.1.0 2024-08-22 00:46:13 +03:00
Yurii
2da41707a9 chore: bump version to 1.4.4 2024-08-20 23:39:50 +03:00
Yurii
460cb01146 chore: removed wowki support 2024-08-20 23:28:09 +03:00
Yurii
1b2bc8e200 feat: added feat use of BLE external sensor; added events onIndoorSensorDisconnect and onOutdoorSensorDisconnect for emergency mode; added polling of rssi, humidity, battery for BLE sensors 2024-08-20 19:06:18 +03:00
Yurii
d5acb44648 fix: text of action buttons fixed 2024-08-20 19:01:08 +03:00
Yurii
c64cf41757 Merge branch 'master' of https://github.com/Laxilef/OTGateway 2024-08-20 02:13:42 +03:00
Yurii
9250bb26f2 fix: locale detection error fixed 2024-08-19 14:45:06 +03:00
dependabot[bot]
b2c6eca2d5 chore: bump peterus/platformio_dependabot from 1.1.0 to 1.1.1 (#77)
Bumps [peterus/platformio_dependabot](https://github.com/peterus/platformio_dependabot) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/peterus/platformio_dependabot/releases)
- [Commits](https://github.com/peterus/platformio_dependabot/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: peterus/platformio_dependabot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 17:27:41 +03:00
Yurii
5cec043015 chore: bump platformio_dependabot to 1.1.0 2024-06-22 20:28:44 +03:00
github-actions[bot]
7c9a483677 chore: Bump NimBLE-Arduino to 1.4.2 (#75) 2024-06-21 04:18:17 +03:00
github-actions[bot]
1bfe7a688a chore: Bump TinyLogger to 1.1.1 (#74)
Co-authored-by: root <root@5e361a757fb5>
2024-06-21 04:17:22 +03:00
Yurii
05104aa8eb chore: create dependabot.yml, pio-dependabot.yaml 2024-06-21 03:44:25 +03:00
Yurii
ca3c318d8f chore: bump version 2024-06-21 02:05:34 +03:00
Yurii
2648918dda feat: added multilanguage for portal 2024-06-21 02:01:38 +03:00
Yurii
8b50fdec21 fix: fix typo 2024-06-20 06:41:32 +03:00
Yurii
5b6e23251a fix: use static IP at startup fixed #70 2024-06-18 10:25:58 +03:00
Yurii
769daa0857 chore: bump version to 1.4.2 2024-06-15 03:03:50 +03:00
Yurii
76979531b8 feat: added more info about the build to the portal 2024-06-15 03:03:31 +03:00
Yurii
7779076498 refactor: optimizing work with network 2024-06-14 18:18:43 +03:00
Yurii
45e2e0334e chore: added nodemcu v3 board; changed default gpio for nodemcu 32s 2024-06-14 18:17:12 +03:00
Yurii
b631b496cb chore: bump version to 1.4.1 2024-06-13 17:15:37 +03:00
Yurii
36328a1db5 refactor: NetworkMgr code optimization 2024-06-13 17:09:41 +03:00
Yurii
a28c7f67b5 chore: added min version of espressif8266 for build #68 2024-06-13 17:07:53 +03:00
Yurii
bb9b6a5f4c fix: casting types in setupForm fixed #61 2024-06-11 20:19:54 +03:00
Yurii
ce7bd7e23b feat: migrate to arduino-esp32 core 3.0.1 2024-06-10 16:20:03 +03:00
Yurii
249d32ce35 chore: more info when scan wifi 2024-06-10 14:55:14 +03:00
Yurii
baf8adfb02 fix: validation GPIO and reset wifi for arduino-esp32 core 3.x.x fixed 2024-06-06 16:37:57 +03:00
Yurii
018a1c5188 fix: set ssid on portal fixed 2024-06-06 02:56:19 +03:00
Yurii
b600c130f0 fix: conflicts with sdk 3.x.x for esp32 fixed 2024-06-05 23:11:27 +03:00
Yurii
59eb05726a chore: bump platformio/framework-arduinoespressif32 to 2.0.17 2024-05-26 23:22:19 +03:00
Yurii
f245f37dfd chore: bump version 2024-05-26 17:41:12 +03:00
Yurii
a825412f37 feat: added fault state GPIO setting 2024-05-25 02:51:10 +03:00
Yurii
935f8bd0a8 fix: outdoor sensor GPIO validation fixed 2024-05-25 00:17:55 +03:00
Yurii
f78d2f38b8 fix: equitherm with BLE indoor sensor in emergency mode fixed 2024-04-25 13:38:03 +03:00
Yurii
ef083991e3 feat: added board info on portal 2024-04-23 10:30:42 +03:00
Yurii
3c0f846335 fix: added set target indoor temp to CH2 for native heating control #58 2024-04-23 08:13:03 +03:00
Yurii
85011ce4ea chore: bump version 2024-04-22 08:19:41 +03:00
Yurii
8687e122ca feat: added native heating control by boiler; refactoring; emergency settings removed from HA 2024-04-22 08:18:59 +03:00
Yurii
d35ea81080 fix: PID optimization, correction of default PID settings 2024-04-18 00:04:23 +03:00
Yurii
cca8ec58b4 fix: fix radio on settings page (portal) 2024-04-16 18:58:28 +03:00
Yurii
301b14bbd4 chore: bump version 2024-04-15 15:07:28 +03:00
Yurii
41ce9b268e fix: fix set heating temp on ITALTHERM TIME MAX 30F 2024-04-15 15:00:41 +03:00
Yurii
646939179e fix: serial on s2, s3 fixed 2024-04-15 05:49:46 +03:00
Yurii
73dddd18f0 fix: scan networks on s3 fixed 2024-04-15 02:47:42 +03:00
Yurii
f069de0415 chore: fix typo 2024-04-12 21:47:09 +03:00
Yurii
f4a4afeb29 chore: bump version 2024-04-12 20:47:02 +03:00
Yurii
f9824337dc chore: delete .gz files 2024-04-12 20:39:56 +03:00
Yurii
1cd8c6a336 chore: html files moved to src_data dir; compression files 2024-04-12 04:13:44 +03:00
Yurii
63228baebd chore: auto file compression when building a FS 2024-04-12 04:10:55 +03:00
Yurii
6bb261dfd7 feat: ability to use compressed files for StaticPage 2024-04-12 04:08:28 +03:00
Yurii
a026a962f0 fix: fixed thermostat temperature limits for different unit systems on dashboard 2024-04-12 02:03:43 +03:00
Yurii
db2faad741 feat: added Italtherm to vendor list 2024-04-12 01:58:33 +03:00
Yurii
fbc43dc535 feat: added settings for status led gpio, opentherm rx led gpio, emergency treshold time 2024-04-11 23:53:15 +03:00
Yurii
31dfc21d69 refactor: added info for emergency mode in settings 2024-04-11 04:38:12 +03:00
Yurii
96289cb0f7 fix: reset onewire before begin (fix DS18B20) 2024-04-11 04:09:11 +03:00
Yurii
73da3ee07a fix: fixing button groups on the mobile version 2024-04-11 03:13:39 +03:00
Yurii
a14281924f chore: bump version 2024-04-11 03:08:35 +03:00
Yurii
3dec390cce feat: many features
* added dashboard on portal
* added settings for serial port and telnet
* added on/off settings for mqtt
* added event selection for emergency mode
* refactor html & css
2024-04-11 03:06:56 +03:00
Yurii
9a29819d4f refactor: reworked layout and styles of the portal 2024-04-08 05:37:36 +03:00
Yurii
2af159d566 chore: updated css framework 2024-04-07 23:00:22 +03:00
Yurii
92ca257d32 feat: added slave parameters to index page on portal; added poll ID125 (opentherm protocol version) 2024-04-07 22:59:43 +03:00
Yurii
44b6620431 fix: rounding the DHW flow rate value 2024-04-07 19:22:15 +03:00
Yurii
b89f61ed58 chore: bump version 2024-04-06 20:28:41 +03:00
Yurii
ab1566bd45 fix: memory leak on esp32 fixed 2024-04-06 20:28:05 +03:00
Yurii
86734ab622 refactor: update portal (unit system) 2024-04-06 18:25:30 +03:00
Yurii
0a8dd2a076 feat: added support unit systems for pressure and flow rate 2024-04-06 18:19:06 +03:00
Yurii
a7a561622e Merge branch 'unit-system' 2024-04-06 17:38:24 +03:00
Yurii
b0e0f6fd7d feat: added setting to enable/disable polling of min and max temperatures via opentherm 2024-04-06 15:51:49 +03:00
Yurii
53eaa1d7f1 chore: bump version 2024-04-04 21:26:48 +03:00
Yurii
a7d796e0cc refactor: removed unused methods, replaced some methods to native 2024-03-31 22:29:53 +03:00
Yurii
4490b38130 fix: fixed reading exhaust gas temperature 2024-03-31 19:41:33 +03:00
Yurii
0ede2240a2 fix: temperature_unit for climate fixed; temp in vars.parameters.* by default fixed 2024-03-31 07:29:32 +03:00
Yurii
0cff35ee12 feat: update portal for unit systems 2024-03-31 06:32:23 +03:00
Yurii
14aef20234 fix: typo for HA_TEMPERATURE_UNIT 2024-03-31 02:49:36 +03:00
Yurii
560f8fbd51 feat: optimizing with different unit systems 2024-03-31 02:47:20 +03:00
Yurii
946414ad31 Merge branch 'master' into unit-system 2024-03-31 01:02:59 +03:00
Yurii
39a29042e1 fix: set max temp (ID57) as setpoint heating temp 2024-03-31 00:37:18 +03:00
Yurii
f544f01caa feat: polling of exhaust gas temperature (#42) and heating return temperature; added new sensors to HA 2024-03-30 00:04:51 +03:00
Yurii
41cca76bfa chore: update README 2024-03-24 20:38:02 +03:00
Yurii
942bc53043 chore: bump version 2024-03-24 19:28:26 +03:00
Yurii
1bad689b6b fix: revert board_build.ldscript for esp8266, update OpenTherm Library 2024-03-24 19:27:16 +03:00
Yurii
2f4dbcc205 feat: added unit system selection 2024-03-20 02:37:20 +03:00
Yurii
9e3ef7a465 chore: bump version 2024-03-14 13:08:11 +03:00
Yurii
a5f6749101 refactor: added SensorType enum 2024-03-14 13:07:42 +03:00
Yurii
b07dd46f55 refactor: optimization
* names changed: pin => gpio
* ability to change OpenTherm GPIO without rebooting
2024-03-10 04:10:18 +03:00
Yurii
07ab121788 chore: bump OpenTherm Library to master 2024-03-09 00:03:34 +03:00
Yurii
7cbc52a8b0 chore: bump version 2024-03-01 00:26:41 +03:00
Laxilef
e090be380c Merge pull request #49 from blitzu/patch-3
fix: fix typo in settings.html
2024-03-01 00:22:32 +03:00
Laxilef
0bf49d2249 Merge pull request #50 from blitzu/patch-2
fix: fix typo in network.html
2024-03-01 00:22:13 +03:00
Laxilef
a83d94d361 Merge pull request #51 from blitzu/patch-1
fix: fix typo in index.html
2024-03-01 00:21:49 +03:00
Laxilef
358980da4c Merge pull request #48 from blitzu/patch-4
fix: fix typo in upgrade.html
2024-03-01 00:21:13 +03:00
blitzu
f91e39d067 Update upgrade.html 2024-02-29 18:52:31 +02:00
blitzu
1d53f21d46 Update settings.html 2024-02-29 18:52:08 +02:00
blitzu
c225e7c2a8 Update network.html 2024-02-29 18:51:25 +02:00
blitzu
6831c4331f Update index.html 2024-02-29 18:50:36 +02:00
Yurii
8fb62ce8ae fix: set temperature for sensors in manual mode fixed 2024-02-23 03:50:30 +03:00
81 changed files with 18412 additions and 8907 deletions

11
.github/dependabot.yaml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

22
.github/workflows/pio-dependabot.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: PlatformIO Dependabot
on:
workflow_dispatch: # option to manually trigger the workflow
schedule:
# Runs every day at 00:00
- cron: "0 0 * * *"
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
name: run PlatformIO Dependabot
steps:
- name: Checkout
uses: actions/checkout@v4
- name: run PlatformIO Dependabot
uses: peterus/platformio_dependabot@v1.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
.pio
.vscode
build/*.bin
data/*
secrets.ini
node_modules
package-lock.json
!.gitkeep

View File

@@ -2,7 +2,7 @@
![logo](/assets/logo.svg)
<br>
[![GitHub version](https://img.shields.io/github/release/Laxilef/OTGateway.svg)](https://github.com/Laxilef/OTGateway/releases)
[![GitHub version](https://img.shields.io/github/release/Laxilef/OTGateway.svg?include_prereleases)](https://github.com/Laxilef/OTGateway/releases)
[![GitHub download](https://img.shields.io/github/downloads/Laxilef/OTGateway/total.svg)](https://github.com/Laxilef/OTGateway/releases/latest)
[![License](https://img.shields.io/github/license/Laxilef/OTGateway.svg)](LICENSE.txt)
[![Telegram](https://img.shields.io/badge/Telegram-Channel-33A8E3)](https://t.me/otgateway)
@@ -16,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:
@@ -31,7 +31,6 @@
- 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!
![logo](/assets/ha.png)
@@ -43,7 +42,7 @@ All available information and instructions can be found in the 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)
* [Settings](https://github.com/Laxilef/OTGateway/wiki#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)
@@ -72,7 +71,7 @@ All available information and instructions can be found in the wiki:
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
- [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient) /
- [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)

BIN
assets/2D_PCB_bottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

BIN
assets/2D_PCB_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
assets/3D_PCB.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" height="40" aria-label="Import blueprint to My Home Assistant" style="border-radius:24px;width:auto" viewBox="0 0 592 96">
<rect width="592" height="96" fill="#18BCF2" rx="48"/>
<path fill="#fff" d="M42.55 60.2h4.59V36.75h-4.59Zm10.35 0h4.59V43.99l7.23 10.58 7.24-10.62V60.2h4.56V36.75h-4.56v.03l-.4-.03-6.84 10.12-6.83-10.12-.4.03v-.03H52.9Zm29.38 0h4.59v-7.77h4.49c4.72 0 8.07-3.29 8.07-7.87 0-4.59-3.48-7.88-8.44-7.84l-4.66.03h-4.05Zm8.61-19.26c2.27-.04 3.88 1.47 3.88 3.62 0 2.14-1.47 3.65-3.51 3.65h-4.39v-7.27Zm22.98 19.66c6.97 0 11.92-5.02 11.92-12.09 0-7.1-4.95-12.13-12.02-12.13-7.04 0-12 4.99-12 12.13 0 7.07 5 12.09 12.1 12.09m0-4.19c-4.36 0-7.41-3.28-7.41-7.9 0-4.66 3.02-7.94 7.31-7.94 4.32 0 7.33 3.28 7.33 7.94 0 4.62-2.98 7.9-7.23 7.9m16.31 3.79h4.59v-8.31h3.52l4.79 8.31h5.19l-5.42-9.11c2.71-1.21 4.48-3.69 4.48-6.77 0-4.45-3.48-7.64-8.44-7.6l-4.65.03h-4.06Zm8.51-19.26c2.28 0 3.89 1.37 3.89 3.38 0 1.98-1.61 3.38-3.65 3.38h-4.16v-6.76Zm18.33 19.26h4.59V40.94h7v-4.19h-18.63v4.19h7.04Zm23.91 0h9.02c4.69 0 7.77-2.41 7.77-6.7 0-2.65-1.24-4.42-3.42-5.53 1.64-1.17 2.61-2.78 2.61-4.55 0-4.43-3.21-6.7-8.04-6.7l-3.45.03h-4.49Zm7.68-19.8c2.21-.03 3.61 1.07 3.61 2.95s-1.27 2.95-3.25 2.95h-3.55v-5.9Zm.53 9.65c2.41-.03 3.89 1.17 3.89 3.08 0 1.81-1.34 3.02-3.52 3.02h-4.09v-6.1h.24Zm12.97 10.15h14.9v-4.19H206.7V36.75h-4.59Zm18.32-8.74c0 5.62 3.69 9.21 9.51 9.21 5.9 0 9.65-3.59 9.65-9.21V36.75H235v14.71c0 3.01-1.97 4.95-4.99 4.95-3.01 0-4.99-1.94-4.99-4.95V36.75h-4.59Zm24.59 8.74h14.97v-4.19h-10.38v-5.66h8.84v-4.09h-8.84v-5.32h10.25v-4.19h-14.84Zm20.4 0h4.59v-7.77h4.49c4.72 0 8.07-3.29 8.07-7.87 0-4.59-3.48-7.88-8.44-7.84l-4.66.03h-4.05Zm8.61-19.26c2.28-.04 3.89 1.47 3.89 3.62 0 2.14-1.48 3.65-3.52 3.65h-4.39v-7.27Zm12.26 19.26h4.59v-8.31h3.52l4.79 8.31h5.19l-5.43-9.11c2.72-1.21 4.49-3.69 4.49-6.77 0-4.45-3.48-7.64-8.44-7.6l-4.65.03h-4.06Zm8.51-19.26c2.28 0 3.89 1.37 3.89 3.38 0 1.98-1.61 3.38-3.65 3.38h-4.16v-6.76Zm13.77 19.26h4.59V36.75h-4.59Zm10.35 0 4.59.03V44.12l11.66 16.08h4.55V36.75h-4.55v15.81l-11.46-15.81h-4.79Zm31.52 0h4.59V40.94h7.01v-4.19h-18.63v4.19h7.03Zm28.88 0h4.59V40.94h7v-4.19h-18.62v4.19h7.03Zm26 .4c6.97 0 11.92-5.02 11.92-12.09 0-7.1-4.95-12.13-12.02-12.13-7.04 0-12 4.99-12 12.13 0 7.07 5 12.09 12.1 12.09m0-4.19c-4.36 0-7.41-3.28-7.41-7.9 0-4.66 3.02-7.94 7.31-7.94 4.32 0 7.33 3.28 7.33 7.94 0 4.62-2.98 7.9-7.23 7.9"/>
<g style="transform:translate(95px,0)">
<rect width="137" height="64" x="344" y="16" fill="#F2F4F9" rx="32"/>
<path fill="#18BCF2" d="M394.419 37.047V60.5h-4.297V46.797L384.716 60.5h-4.157l-5.343-13.594V60.5h-4.188V37.047h4.188l7.422 18.36 7.484-18.36zm9.365 0 5.344 9.89 5.344-9.89h4.766l-7.969 14.14V60.5h-4.391v-9.312l-8.031-14.141zM457 60c0 1.65-1.35 3-3 3h-24c-1.65 0-3-1.35-3-3v-9c0-1.65.95-3.95 2.12-5.12l10.76-10.76a3 3 0 0 1 4.24 0l10.76 10.76c1.17 1.17 2.12 3.47 2.12 5.12z"/>
<path fill="#F2F4F9" stroke="#F2F4F9" d="M442 45.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
<path fill="#F2F4F9" stroke="#F2F4F9" stroke-miterlimit="10" d="M449.5 53.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM434.5 57.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
<path fill="none" stroke="#F2F4F9" stroke-miterlimit="10" stroke-width="2.25" d="M442 43.48V63l-7.5-7.5M449.5 51.46l-7.41 7.41"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,6 @@
# Package for Home Assistant Packages
# More info: https://www.home-assistant.io/docs/configuration/packages/
dhw_meter:
sensor:
- platform: integration

View File

@@ -1,29 +0,0 @@
# Script for reporting outdoor temperature to the controller from home assistant weather integration
# Updated: 07.12.2023
alias: Report outdoor temp to controller from weather
description: ""
variables:
# The source weather from which we take the temperature
source_entity: "weather.home"
# Target entity number where we set the temperature
# If the prefix has not changed, then you do not need to change it
target_entity: "number.opentherm_outdoor_temp"
trigger:
- platform: time_pattern
seconds: /30
condition:
- condition: template
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
then:
- service: number.set_value
data:
value: "{{ state_attr(source_entity, 'temperature')|float(0)|round(2) }}"
target:
entity_id: "{{ target_entity }}"
mode: single

View File

@@ -1,30 +0,0 @@
# Script for reporting indoor/outdoor temperature to the controller from any home assistant sensor
# Updated: 07.12.2023
alias: Report temp to controller
description: ""
variables:
# The source sensor from which we take the temperature
source_entity: "sensor.livingroom_temperature"
# Target entity number where we set the temperature
# To report indoor temperature: number.opentherm_indoor_temp
# To report outdoor temperature: number.opentherm_outdoor_temp
target_entity: "number.opentherm_indoor_temp"
trigger:
- platform: time_pattern
seconds: /30
condition:
- condition: template
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
then:
- service: number.set_value
data:
value: "{{ states(source_entity)|float(0)|round(2) }}"
target:
entity_id: "{{ target_entity }}"
mode: single

View File

@@ -0,0 +1,48 @@
# Blueprint for reporting indoor/outdoor temperature to OpenTherm Gateway from any home assistant sensor
# Updated: 03.09.2024
blueprint:
name: Report temp to OpenTherm Gateway
domain: automation
author: "Laxilef"
input:
source_entity:
name: Source entity
description: "Temperature data source"
selector:
entity:
multiple: false
filter:
- domain: sensor
device_class: temperature
target_entity:
name: Target entity
description: "Usually ``number.opentherm_indoor_temp`` or ``number.opentherm_outdoor_temp``"
default: "number.opentherm_indoor_temp"
selector:
entity:
multiple: false
filter:
- domain: number
mode: single
variables:
source_entity: !input source_entity
target_entity: !input target_entity
trigger:
- platform: time_pattern
seconds: /30
condition:
- condition: template
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
then:
- service: number.set_value
data:
value: "{{ states(source_entity)|float(0)|round(2) }}"
target:
entity_id: "{{ target_entity }}"

View File

@@ -0,0 +1,47 @@
# Blueprint for reporting temperature to OpenTherm Gateway from home assistant weather integration
# Updated: 03.09.2024
blueprint:
name: Report temp to OpenTherm Gateway from Weather
domain: automation
author: "Laxilef"
input:
source_entity:
name: Source entity
description: "Temperature data source"
selector:
entity:
multiple: false
filter:
- domain: weather
target_entity:
name: Target entity
description: "Usually ``number.opentherm_outdoor_temp``"
default: "number.opentherm_outdoor_temp"
selector:
entity:
multiple: false
filter:
- domain: number
mode: single
variables:
source_entity: !input source_entity
target_entity: !input target_entity
trigger:
- platform: time_pattern
seconds: /30
condition:
- condition: template
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
then:
- service: number.set_value
data:
value: "{{ state_attr(source_entity, 'temperature')|float(0)|round(2) }}"
target:
entity_id: "{{ target_entity }}"

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

0
data/.gitkeep Normal file
View File

View File

@@ -1,230 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css"/>
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2>Network</h2>
<p></p>
</hgroup>
<div class="main-busy" aria-busy="true"></div>
<table class="main-table hidden">
<tbody>
<tr>
<th scope="row">Hostname:</th>
<td><b class="network-hostname"></b></td>
</tr>
<tr>
<th scope="row">MAC:</th>
<td><b class="network-mac"></b></td>
</tr>
<tr>
<th scope="row">Connected:</th>
<td><input type="radio" class="network-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">SSID:</th>
<td><b class="network-ssid"></b></td>
</tr>
<tr>
<th scope="row">Signal:</th>
<td><b class="network-signal"></b> %</td>
</tr>
<tr>
<th scope="row">IP:</th>
<td><b class="network-ip"></b></td>
</tr>
<tr>
<th scope="row">Subnet:</th>
<td><b class="network-subnet"></b></td>
</tr>
<tr>
<th scope="row">Gateway:</th>
<td><b class="network-gateway"></b></td>
</tr>
<tr>
<th scope="row">DNS:</th>
<td><b class="network-dns"></b></td>
</tr>
</tbody>
</table>
<div class="grid">
<a href="/network.html" role="button">Network settings</a>
</div>
</div>
</article>
<article>
<div>
<hgroup>
<h2>System</h2>
<p></p>
</hgroup>
<div class="system-busy" aria-busy="true"></div>
<table class="system-table hidden">
<tbody>
<tr>
<th scope="row">Version:</th>
<td><b class="version"></b></td>
</tr>
<tr>
<th scope="row">Build date:</th>
<td><b class="build-date"></b></td>
</tr>
<tr>
<th scope="row">Uptime:</th>
<td><b class="uptime-days"></b> days, <b class="uptime-hours"></b> hours, <b class="uptime-min"></b> min., <b class="uptime-sec"></b> sec.</td>
</tr>
<tr>
<th scope="row">Free memory:</th>
<td><b class="free-heap"></b> of <b class="total-heap"></b> bytes (min: <b class="min-free-heap"></b> bytes)<br>max free block: <b class="max-free-block-heap"></b> bytes (min: <b class="min-max-free-block-heap"></b> bytes)</td>
</tr>
<tr>
<th scope="row">Last reset reason:</th>
<td><b class="reset-reason"></b></td>
</tr>
</tbody>
</table>
<div class="grid">
<a href="/settings.html" role="button">Settings</a>
<a href="/upgrade.html" role="button">Upgrade</a>
<a href="/restart.html" role="button" class="secondary restart">Restart</a>
</div>
</div>
</article>
<article>
<div>
<hgroup>
<h2>States and sensors</h2>
<p>More information and settings can be found in your home assistant after setting up network and MQTT</p>
</hgroup>
<div class="ot-busy" aria-busy="true"></div>
<table class="ot-table hidden">
<tbody>
<tr>
<th scope="row">OpenTherm connected:</th>
<td><input type="radio" class="ot-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">MQTT connected:</th>
<td><input type="radio" class="mqtt-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Emergency:</th>
<td><input type="radio" class="ot-emergency" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Heating:</th>
<td><input type="radio" class="ot-heating" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">DHW:</th>
<td><input type="radio" class="ot-dhw" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Flame:</th>
<td><input type="radio" class="ot-flame" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Fault:</th>
<td><input type="radio" class="ot-fault" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Diagnostic:</th>
<td><input type="radio" class="ot-diagnostic" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">External pump:</th>
<td><input type="radio" class="ot-external-pump" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Modulation:</th>
<td><b class="ot-modulation"></b> %</td>
</tr>
<tr>
<th scope="row">Pressure:</th>
<td><b class="ot-pressure"></b> bar</td>
</tr>
<tr>
<th scope="row">DHW flow rate:</th>
<td><b class="ot-dhw-flow-rate"></b> l/min</td>
</tr>
<tr>
<th scope="row">Fault code:</th>
<td><b class="ot-fault-code"></b></td>
</tr>
<tr>
<th scope="row">Indoor temp:</th>
<td><b class="indoor-temp"></b> C</td>
</tr>
<tr>
<th scope="row">Outdoor temp:</th>
<td><b class="outdoor-temp"></b> C</td>
</tr>
<tr>
<th scope="row">Heating temp:</th>
<td><b class="heating-temp"></b> C</td>
</tr>
<tr>
<th scope="row">Heating setpoint temp:</th>
<td><b class="heating-setpoint-temp"></b> C</td>
</tr>
<tr>
<th scope="row">DHW temp:</th>
<td><b class="dhw-temp"></b> C</td>
</tr>
</tbody>
</table>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
<a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
</small>
</footer>
<script src="/static/app.js"></script>
<script>
window.onload = async function () {
setTimeout(async function onLoadPage() {
await loadNetworkStatus();
await loadVars();
setTimeout(onLoadPage, 10000);
}, 1000);
};
</script>
</body>
</html>

View File

@@ -1,166 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Network - OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css" />
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2>Network settings</h2>
<p></p>
</hgroup>
<div id="network-settings-busy" aria-busy="true"></div>
<form action="/api/network/settings" id="network-settings" class="hidden">
<label for="hostname">
Hostname
<input type="text" class="network-hostname" name="hostname" maxlength="24" pattern="[A-Za-z0-9]+[A-Za-z0-9\-]+[A-Za-z0-9]+" required>
</label>
<label for="network-use-dhcp">
<input type="checkbox" class="network-use-dhcp" name="useDhcp" value="true">
Use DHCP
</label>
<br>
<hr>
<label for="network-static-ip">
Static IP:
<input type="text" class="network-static-ip" name="staticConfig[ip]" value="true" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
</label>
<label for="network-static-gateway">
Static gateway:
<input type="text" class="network-static-gateway" name="staticConfig[gateway]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
</label>
<label for="network-static-subnet">
Static subnet:
<input type="text" class="network-static-subnet" name="staticConfig[subnet]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
</label>
<label for="network-static-dns">
Static DNS:
<input type="text" class="network-static-dns" name="staticConfig[dns]" maxlength="16" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" required>
</label>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h3>Available networks</h3>
<p></p>
</hgroup>
<form action="/api/network/scan" id="network-scan">
<figure style="max-height: 25em;">
<table id="networks" role="grid">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">SSID</th>
<th scope="col">Signal</th>
</tr>
</thead>
<tbody></tbody>
</table>
</figure>
<button type="submit">Refresh</button>
</form>
<hr>
<div>
<hgroup>
<h2>WiFi settings</h2>
<p></p>
</hgroup>
<div id="sta-settings-busy" aria-busy="true"></div>
<form action="/api/network/settings" id="sta-settings" class="hidden">
<label for="sta-ssid">
SSID:
<input type="text" class="sta-ssid" name="sta[ssid]" maxlength="32" required>
</label>
<label for="sta-password">
Password:
<input type="password" class="sta-password" name="sta[password]" maxlength="64" required>
</label>
<label for="sta-channel">
Channel:
<input type="number" inputmode="numeric" class="sta-channel" name="sta[channel]" min="0" max="12" step="1" required>
<small>set 0 for auto select</small>
</label>
<button type="submit">Save</button>
</form>
</div>
</div>
</article>
<article>
<div>
<hgroup>
<h2>AP settings</h2>
<p></p>
</hgroup>
<div id="ap-settings-busy" aria-busy="true"></div>
<form action="/api/network/settings" id="ap-settings" class="hidden">
<label for="ap-ssid">
SSID:
<input type="text" class="ap-ssid" name="ap[ssid]" maxlength="32" required>
</label>
<label for="ap-password">
Password:
<input type="text" class="ap-password" name="ap[password]" maxlength="64" required>
</label>
<label for="ap-channel">
Channel:
<input type="number" inputmode="numeric" class="ap-channel" name="ap[channel]" min="1" max="12" step="1" required>
</label>
<button type="submit">Save</button>
</form>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
<a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
</small>
</footer>
<script src="/static/app.js"></script>
<script>
window.onload = async function () {
await loadNetworkSettings();
setupForm('#network-settings');
setupNetworkScanForm('#network-scan', '#networks');
setupForm('#sta-settings');
setupForm('#ap-settings');
};
</script>
</body>
</html>

View File

@@ -1,357 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Settings - OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css" />
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2>Portal settings</h2>
<p></p>
</hgroup>
<div id="portal-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="portal-settings" class="hidden">
<div class="grid">
<label for="portal-login">
Login
<input type="text" class="portal-login" name="portal[login]" maxlength="12" required>
</label>
<label for="portal-password">
Password
<input type="password" class="portal-password" name="portal[password]" maxlength="32" required>
</label>
</div>
<label for="portal-use-auth">
<input type="checkbox" class="portal-use-auth" name="portal[useAuth]" value="true">
Use auth
</label>
<br>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>OpenTherm settings</h2>
<p></p>
</hgroup>
<div id="opentherm-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="opentherm-settings" class="hidden">
<div class="grid">
<label for="opentherm-in-pin">
In GPIO
<input type="number" inputmode="numeric" class="opentherm-in-pin" name="opentherm[inPin]" min="0" max="99" step="1" required>
</label>
<label for="opentherm-in-pin">
Out GPIO
<input type="number" inputmode="numeric" class="opentherm-out-pin" name="opentherm[outPin]" min="0" max="99" step="1" required>
</label>
<label for="opentherm-member-id-code">
Master MemberID code
<input type="number" inputmode="numeric" class="opentherm-member-id-code" name="opentherm[memberIdCode]" min="0" max="65535" step="1" required>
</label>
</div>
<fieldset>
<mark>After changing GPIO, the ESP must be restarted for the changes to take effect.</mark>
</fieldset>
<fieldset>
<legend>Options</legend>
<label for="opentherm-dhw-present">
<input type="checkbox" class="opentherm-dhw-present" name="opentherm[dhwPresent]" value="true">
DHW present
</label>
<label for="opentherm-sw-mode">
<input type="checkbox" class="opentherm-sw-mode" name="opentherm[summerWinterMode]" value="true">
Summer/winter mode
</label>
<label for="opentherm-heating-ch2-enabled">
<input type="checkbox" class="opentherm-heating-ch2-enabled" name="opentherm[heatingCh2Enabled]" value="true">
Heating CH2 always enabled
</label>
<label for="opentherm-heating-ch1-to-ch2">
<input type="checkbox" class="opentherm-heating-ch1-to-ch2" name="opentherm[heatingCh1ToCh2]" value="true">
Duplicate heating CH1 to CH2
</label>
<label for="opentherm-dhw-to-ch2">
<input type="checkbox" class="opentherm-dhw-to-ch2" name="opentherm[dhwToCh2]" value="true">
Duplicate DHW to CH2
</label>
<label for="opentherm-dhw-blocking">
<input type="checkbox" class="opentherm-dhw-blocking" name="opentherm[dhwBlocking]" value="true">
DHW blocking
</label>
<label for="opentherm-sync-modulation-with-heating">
<input type="checkbox" class="opentherm-sync-modulation-with-heating" name="opentherm[modulationSyncWithHeating]" value="true">
Sync modulation with heating
</label>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>MQTT settings</h2>
<p></p>
</hgroup>
<div id="mqtt-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="mqtt-settings" class="hidden">
<div class="grid">
<label for="mqtt-server">
Server
<input type="text" class="mqtt-server" name="mqtt[server]" maxlength="80" required>
</label>
<label for="mqtt-port">
Port
<input type="number" inputmode="numeric" class="mqtt-port" name="mqtt[port]" min="1" max="65535" step="1" required>
</label>
</div>
<div class="grid">
<label for="mqtt-user">
User
<input type="text" class="mqtt-user" name="mqtt[user]" maxlength="32" required>
</label>
<label for="mqtt-password">
Password
<input type="password" class="mqtt-password" name="mqtt[password]" maxlength="32">
</label>
</div>
<div class="grid">
<label for="mqtt-prefix">
Prefix
<input type="text" class="mqtt-prefix" name="mqtt[prefix]" maxlength="32" required>
</label>
<label for="mqtt-interval">
Publish interval <small>(sec)</small>
<input type="number" inputmode="numeric" class="mqtt-interval" name="mqtt[interval]" min="3" max="60" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>Outdoor sensor settings</h2>
<p></p>
</hgroup>
<div id="outdoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="outdoor-sensor-settings" class="hidden">
<fieldset>
<legend>Source type</legend>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="0" />
From boiler via OpenTherm
</label>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="1" />
Manual via MQTT/API
</label>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" />
External (DS18B20)
</label>
</fieldset>
<label for="outdoor-sensor-pin">
GPIO
<input type="number" inputmode="numeric" class="outdoor-sensor-pin" name="sensors[outdoor][pin]" min="0" max="99" step="1" required>
</label>
<label for="outdoor-sensor-offset">
Temp offset (calibration)
<input type="number" inputmode="numeric" class="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-20" max="20" step="0.01" required>
</label>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>Indoor sensor settings</h2>
<p></p>
</hgroup>
<div id="indoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="indoor-sensor-settings" class="hidden">
<fieldset>
<legend>Source type</legend>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="1" />
Manual via MQTT/API
</label>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="2" />
External (DS18B20)
</label>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="3" />
BLE device <i>(ONLY for some ESP32 which support BLE)</i>
</label>
</fieldset>
<label for="indoor-sensor-pin">
GPIO
<input type="number" inputmode="numeric" class="indoor-sensor-pin" name="sensors[indoor][pin]" min="0" max="99" step="1" required>
</label>
<div class="grid">
<label for="indoor-sensor-offset">
Temp offset (calibration)
<input type="number" inputmode="numeric" class="indoor-sensor-offset" name="sensors[indoor][offset]" min="-20" max="20" step="0.01" required>
</label>
<label for="indoor-sensor-ble-addresss">
BLE addresss
<input type="text" class="indoor-sensor-ble-addresss" name="sensors[indoor][bleAddresss]" pattern="([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}">
<small>ONLY for some ESP32 which support BLE</small>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>External pump settings</h2>
<p></p>
</hgroup>
<div id="extpump-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="extpump-settings" class="hidden">
<label for="extpump-use">
<input type="checkbox" class="extpump-use" name="externalPump[use]" value="true">
Use external pump
</label>
<br>
<div class="grid">
<label for="extpump-pin">
Relay GPIO
<input type="number" inputmode="numeric" class="extpump-pin" name="externalPump[pin]" min="0" max="99" step="1" required>
</label>
<label for="extpump-pc-time">
Post circulation time <small>(min)</small>
<input type="number" inputmode="numeric" class="extpump-pc-time" name="externalPump[postCirculationTime]" min="1" max="120" step="1" required>
</label>
</div>
<div class="grid">
<label for="extpump-as-interval">
Anti stuck interval <small>(days)</small>
<input type="number" inputmode="numeric" class="extpump-as-interval" name="externalPump[antiStuckInterval]" min="1" max="366" step="1" required>
</label>
<label for="extpump-as-time">
Anti stuck time <small>(min)</small>
<input type="number" inputmode="numeric" class="extpump-as-time" name="externalPump[antiStuckTime]" min="1" max="20" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>System settings</h2>
<p></p>
</hgroup>
<div id="system-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="system-settings" class="hidden">
<fieldset>
<label for="system-debug">
<input type="checkbox" class="system-debug" name="system[debug]" value="true">
Debug mode
</label>
<label for="system-use-serial">
<input type="checkbox" class="system-use-serial" name="system[useSerial]" value="true">
Enable serial port
</label>
<label for="system-use-telnet">
<input type="checkbox" class="system-use-telnet" name="system[useTelnet]" value="true">
Enable telnet
</label>
</fieldset>
<fieldset>
<mark>After changing this settings, the ESP must be restarted for the changes to take effect.</mark>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
<a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
</small>
</footer>
<script src="/static/app.js"></script>
<script>
window.onload = async function () {
await loadSettings();
setupForm('#portal-settings');
setupForm('#opentherm-settings');
setupForm('#mqtt-settings');
setupForm('#outdoor-sensor-settings');
setupForm('#indoor-sensor-settings');
setupForm('#extpump-settings');
setupForm('#system-settings');
};
</script>
</body>
</html>

0
data/static/.gitkeep Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,108 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Upgrade - OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2>Backup & restore</h2>
<p>
In this section you can save and restore a backup of ALL settings.
</p>
</hgroup>
<form action="/api/backup/restore" id="restore">
<label for="restore-file">
Settings file:
<input type="file" name="settings" id="restore-file" accept=".json">
</label>
<div class="grid">
<button type="submit">Restore</button>
<button type="button" class="secondary" onclick="window.location='/api/backup/save';">Backup</button>
</div>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>Upgrade</h2>
<p>
In this section you can upgrade the firmware and filesystem of your device.<br>
Latest releases can be downloaded from the <a href="https://github.com/Laxilef/OTGateway/releases" target="_blank">Releases page</a> of the project repository.
</p>
</hgroup>
<form action="/api/upgrade" id="upgrade">
<fieldset class="primary">
<label for="firmware-file">
Firmware:
<div class="grid">
<input type="file" name="firmware" id="firmware-file" accept=".bin">
<button type="button" class="upgrade-firmware-result hidden" disabled></button>
</div>
</label>
<label for="filesystem-file">
Filesystem:
<div class="grid">
<input type="file" name="filesystem" id="filesystem-file" accept=".bin">
<button type="button" class="upgrade-filesystem-result hidden" disabled></button>
</div>
</label>
</fieldset>
<ul>
<li><mark>After a successful upgrade the filesystem, ALL settings will be reset to default values! Save backup before upgrading.</mark></li>
<li><mark>After a successful upgrade, the device will automatically reboot after 10 seconds.</mark></li>
</ul>
<button type="submit">Upgrade</button>
</form>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
<a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
</small>
</footer>
<script src="/static/app.js"></script>
<script>
window.onload = async function () {
setupRestoreBackupForm('#restore');
setupUpgradeForm('#upgrade');
};
</script>
</body>
</html>

142
gulpfile.js Normal file
View 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);

View File

@@ -10,7 +10,7 @@ public:
free(this->buffer);
}
void send(int code, const char* contentType, JsonDocument& content) {
void send(int code, const char* contentType, JsonDocument& content, bool pretty = false) {
#ifdef ARDUINO_ARCH_ESP8266
if (!this->webServer->chunkedResponseModeStart(code, contentType)) {
this->webServer->send(505, F("text/html"), F("HTTP1.1 required"));
@@ -24,7 +24,13 @@ public:
this->webServer->send(code, contentType, emptyString);
#endif
if (pretty) {
serializeJsonPretty(content, *this);
} else {
serializeJson(content, *this);
}
this->flush();
#ifdef ARDUINO_ARCH_ESP8266

View File

@@ -8,6 +8,7 @@ public:
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
~CustomOpenTherm() {}
CustomOpenTherm* setYieldCallback(YieldCallback callback = nullptr) {
this->yieldCallback = callback;
@@ -46,7 +47,7 @@ public:
unsigned long _response;
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
if (!this->sendRequestAync(request)) {
if (!this->sendRequestAsync(request)) {
_response = 0;
} else {
@@ -78,7 +79,7 @@ public:
}
}
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking) {
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking, uint8_t lb = 0) {
unsigned int data = enableCentralHeating
| (enableHotWater << 1)
| (enableCooling << 2)
@@ -86,9 +87,11 @@ public:
| (enableCentralHeating2 << 4)
| (summerWinterMode << 5)
| (dhwBlocking << 6);
data <<= 8;
return this->sendRequest(this->buildRequest(
data <<= 8;
data |= lb;
return this->sendRequest(buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Status,
data
@@ -96,90 +99,115 @@ public:
}
bool setHeatingCh1Temp(float temperature) {
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TSet,
this->temperatureToData(temperature)
temperatureToData(temperature)
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TSet);
}
bool setHeatingCh2Temp(float temperature) {
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TsetCH2,
this->temperatureToData(temperature)
temperatureToData(temperature)
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TsetCH2);
}
bool setDhwTemp(float temperature) {
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TdhwSet,
this->temperatureToData(temperature)
temperatureToData(temperature)
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TdhwSet);
}
bool setRoomSetpoint(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TrSet,
temperatureToData(temperature)
));
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TrSet);
}
bool setRoomSetpointCh2(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TrSetCH2,
temperatureToData(temperature)
));
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TrSetCH2);
}
bool setRoomTemp(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::Tr,
temperatureToData(temperature)
));
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::Tr);
}
bool sendBoilerReset() {
unsigned int data = 1;
data <<= 8;
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::Command,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
}
bool sendServiceReset() {
unsigned int data = 10;
data <<= 8;
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::Command,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
}
bool sendWaterFilling() {
unsigned int data = 2;
data <<= 8;
unsigned long response = this->sendRequest(this->buildRequest(
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::Command,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest);
}
static bool isValidResponseId(unsigned long response, OpenThermMessageID id) {
byte responseId = (response >> 16) & 0xFF;
return (byte)id == responseId;
}
// converters
float fromF88(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
float value = (int8_t)valueHB;
return value + (float)valueLB / 256.0;
}
template <class T> unsigned int toF88(T val) {
template <class T>
static unsigned int toFloat(const T val) {
return (unsigned int)(val * 256);
}
int16_t fromS16(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
int16_t value = valueHB;
return ((value << 8) + valueLB);
static short getInt(const unsigned long response) {
return response & 0xffff;
}
protected:

View File

@@ -27,7 +27,7 @@ public:
}
// лимит выходной величины
void setLimits(int min_output, int max_output) {
void setLimits(unsigned short min_output, unsigned short max_output) {
_minOut = min_output;
_maxOut = max_output;
}
@@ -40,7 +40,7 @@ public:
}
private:
int _minOut = 20, _maxOut = 90;
unsigned short _minOut = 20, _maxOut = 90;
// температура контура отопления в зависимости от наружной температуры
datatype getResultN() {
@@ -58,6 +58,6 @@ private:
// Расчет поправки (ошибки) термостата
datatype getResultT() {
return constrain((targetTemp - indoorTemp), -2, 2) * Kt;
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
}
};

View File

@@ -33,6 +33,8 @@ 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";
@@ -50,6 +52,7 @@ const char HA_TEMPERATURE_COMMAND_TOPIC[] PROGMEM = "temperature_command_t
const char HA_TEMPERATURE_COMMAND_TEMPLATE[] PROGMEM = "temperature_command_template";
const char HA_TEMPERATURE_STATE_TOPIC[] PROGMEM = "temperature_state_topic";
const char HA_TEMPERATURE_STATE_TEMPLATE[] PROGMEM = "temperature_state_template";
const char HA_TEMPERATURE_UNIT[] PROGMEM = "temperature_unit";
const char HA_MODE_COMMAND_TOPIC[] PROGMEM = "mode_command_topic";
const char HA_MODE_COMMAND_TEMPLATE[] PROGMEM = "mode_command_template";
const char HA_MODE_STATE_TOPIC[] PROGMEM = "mode_state_topic";

View File

@@ -1,54 +1,58 @@
#include "NetworkConnection.h"
using namespace Network;
using namespace NetworkUtils;
void Connection::setup(bool useDhcp) {
void NetworkConnection::setup(bool useDhcp) {
setUseDhcp(useDhcp);
#if defined(ARDUINO_ARCH_ESP8266)
wifi_set_event_handler_cb(Connection::onEvent);
wifi_set_event_handler_cb(NetworkConnection::onEvent);
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.onEvent(Connection::onEvent);
WiFi.onEvent(NetworkConnection::onEvent);
#endif
}
void Connection::reset() {
void NetworkConnection::reset() {
status = Status::NONE;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::NONE;
}
void Connection::setUseDhcp(bool value) {
void NetworkConnection::setUseDhcp(bool value) {
useDhcp = value;
}
Connection::Status Connection::getStatus() {
NetworkConnection::Status NetworkConnection::getStatus() {
return status;
}
Connection::DisconnectReason Connection::getDisconnectReason() {
NetworkConnection::DisconnectReason NetworkConnection::getDisconnectReason() {
return disconnectReason;
}
#if defined(ARDUINO_ARCH_ESP8266)
void Connection::onEvent(System_Event_t *event) {
void NetworkConnection::onEvent(System_Event_t *event) {
switch (event->event) {
case EVENT_STAMODE_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_GOT_IP:
status = Status::CONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_DHCP_TIMEOUT:
status = Status::DISCONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case EVENT_STAMODE_DISCONNECTED:
status = Status::DISCONNECTED;
rawDisconnectReason = event->event_info.disconnected.reason;
disconnectReason = convertDisconnectReason(event->event_info.disconnected.reason);
// https://github.com/esp8266/Arduino/blob/d5eb265f78bff9deb7063d10030a02d021c8c66c/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp#L231
@@ -63,6 +67,7 @@ void Connection::onEvent(System_Event_t *event) {
auto& src = event->event_info.auth_change;
if ((src.old_mode != AUTH_OPEN) && (src.new_mode == AUTH_OPEN)) {
status = Status::DISCONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::OTHER;
wifi_station_disconnect();
@@ -75,29 +80,31 @@ void Connection::onEvent(System_Event_t *event) {
}
}
#elif defined(ARDUINO_ARCH_ESP32)
void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
void NetworkConnection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
status = Status::CONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
status = Status::DISCONNECTED;
rawDisconnectReason = 0;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
status = Status::DISCONNECTED;
rawDisconnectReason = info.wifi_sta_disconnected.reason;
disconnectReason = convertDisconnectReason(info.wifi_sta_disconnected.reason);
break;
default:
@@ -106,7 +113,7 @@ void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
}
#endif
Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason) {
NetworkConnection::DisconnectReason NetworkConnection::convertDisconnectReason(uint8_t reason) {
switch (reason) {
#if defined(ARDUINO_ARCH_ESP8266)
case REASON_BEACON_TIMEOUT:
@@ -145,6 +152,7 @@ Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason)
}
}
bool Connection::useDhcp = false;
Connection::Status Connection::status = Status::NONE;
Connection::DisconnectReason Connection::disconnectReason = DisconnectReason::NONE;
bool NetworkConnection::useDhcp = false;
NetworkConnection::Status NetworkConnection::status = Status::NONE;
NetworkConnection::DisconnectReason NetworkConnection::disconnectReason = DisconnectReason::NONE;
uint8_t NetworkConnection::rawDisconnectReason = 0;

View File

@@ -5,8 +5,8 @@
#include <WiFi.h>
#endif
namespace Network {
struct Connection {
namespace NetworkUtils {
struct NetworkConnection {
enum class Status {
CONNECTED,
CONNECTING,
@@ -27,6 +27,7 @@ namespace Network {
static Status status;
static DisconnectReason disconnectReason;
static uint8_t rawDisconnectReason;
static void setup(bool useDhcp);
static void setUseDhcp(bool value);

View File

@@ -7,35 +7,35 @@
#endif
#include <NetworkConnection.h>
namespace Network {
class Manager {
namespace NetworkUtils {
class NetworkMgr {
public:
typedef std::function<void()> YieldCallback;
typedef std::function<void(unsigned int)> DelayCallback;
Manager() {
Connection::setup(this->useDhcp);
NetworkMgr() {
NetworkConnection::setup(this->useDhcp);
this->resetWifi();
}
Manager* setYieldCallback(YieldCallback callback = nullptr) {
NetworkMgr* setYieldCallback(YieldCallback callback = nullptr) {
this->yieldCallback = callback;
return this;
}
Manager* setDelayCallback(DelayCallback callback = nullptr) {
NetworkMgr* setDelayCallback(DelayCallback callback = nullptr) {
this->delayCallback = callback;
return this;
}
Manager* setHostname(const char* value) {
NetworkMgr* setHostname(const char* value) {
this->hostname = value;
return this;
}
Manager* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
NetworkMgr* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
this->apName = ssid;
this->apPassword = password;
this->apChannel = channel;
@@ -43,7 +43,7 @@ namespace Network {
return this;
}
Manager* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
NetworkMgr* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
this->staSsid = ssid;
this->staPassword = password;
this->staChannel = channel;
@@ -51,14 +51,14 @@ namespace Network {
return this;
}
Manager* setUseDhcp(bool value) {
NetworkMgr* setUseDhcp(bool value) {
this->useDhcp = value;
Connection::setup(this->useDhcp);
NetworkConnection::setup(this->useDhcp);
return this;
}
Manager* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
NetworkMgr* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
this->staticIp.fromString(ip);
this->staticGateway.fromString(gateway);
this->staticSubnet.fromString(subnet);
@@ -67,7 +67,7 @@ namespace Network {
return this;
}
Manager* setStaticConfig(IPAddress &ip, IPAddress &gateway, IPAddress &subnet, IPAddress &dns) {
NetworkMgr* setStaticConfig(IPAddress& ip, IPAddress& gateway, IPAddress& subnet, IPAddress& dns) {
this->staticIp = ip;
this->staticGateway = gateway;
this->staticSubnet = subnet;
@@ -81,11 +81,11 @@ namespace Network {
}
bool isConnected() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTED;
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTED;
}
bool isConnecting() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTING;
return this->isStaEnabled() && NetworkConnection::getStatus() == NetworkConnection::Status::CONNECTING;
}
bool isStaEnabled() {
@@ -147,16 +147,19 @@ namespace Network {
bool resetWifi() {
// set policy manual for work 13 ch
{
wifi_country_t country = {"CN", 1, 13, WIFI_COUNTRY_POLICY_MANUAL};
#ifdef ARDUINO_ARCH_ESP8266
wifi_country_t country = {"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
@@ -181,7 +184,12 @@ namespace Network {
wifi_station_dhcpc_set_maxtry(5);
#endif
#if defined(ARDUINO_ARCH_ESP32) && ESP_ARDUINO_VERSION_MAJOR < 3
// Nothing. Because memory leaks when turn off WiFi on ESP32 SDK < 3.0.0
return true;
#else
return WiFi.mode(WIFI_OFF);
#endif
}
void reconnect() {
@@ -195,6 +203,7 @@ namespace Network {
if (force && !this->isApEnabled()) {
this->resetWifi();
NetworkConnection::reset();
} else {
/*#ifdef ARDUINO_ARCH_ESP8266
@@ -203,14 +212,14 @@ namespace Network {
}
#endif*/
WiFi.disconnect(false, true);
this->disconnect();
}
if (!this->hasStaCredentials()) {
return false;
}
this->delayCallback(200);
this->delayCallback(250);
#ifdef ARDUINO_ARCH_ESP32
if (this->setWifiHostname(this->hostname)) {
@@ -225,7 +234,7 @@ namespace Network {
return false;
}
this->delayCallback(200);
this->delayCallback(250);
#ifdef ARDUINO_ARCH_ESP8266
if (this->setWifiHostname(this->hostname)) {
@@ -235,36 +244,66 @@ namespace Network {
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
}
this->delayCallback(200);
this->delayCallback(250);
#endif
#ifdef ARDUINO_ARCH_ESP32
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
#endif
if (!this->useDhcp) {
WiFi.begin(this->staSsid, this->staPassword, this->staChannel, nullptr, false);
WiFi.config(this->staticIp, this->staticGateway, this->staticSubnet, this->staticDns);
}
WiFi.reconnect();
WiFi.begin(this->staSsid, this->staPassword, this->staChannel);
} else {
WiFi.begin(this->staSsid, this->staPassword, this->staChannel, nullptr, true);
}
unsigned long beginConnectionTime = millis();
while (millis() - beginConnectionTime < timeout) {
this->delayCallback(100);
Connection::Status status = Connection::getStatus();
if (status != Connection::Status::CONNECTING && status != Connection::Status::NONE) {
return status == Connection::Status::CONNECTED;
NetworkConnection::Status status = NetworkConnection::getStatus();
if (status != NetworkConnection::Status::CONNECTING && status != NetworkConnection::Status::NONE) {
return status == NetworkConnection::Status::CONNECTED;
}
}
return false;
}
void disconnect() {
#ifdef ARDUINO_ARCH_ESP32
WiFi.disconnectAsync(false, true);
const unsigned long start = millis();
while (WiFi.isConnected() && (millis() - start) < 5000) {
this->delayCallback(100);
}
#else
WiFi.disconnect(false, true);
#endif
}
void loop() {
if (this->isConnected() && !this->hasStaCredentials()) {
if (this->reconnectFlag) {
this->delayCallback(5000);
Log.sinfoln(FPSTR(L_NETWORK), F("Reconnecting..."));
this->reconnectFlag = false;
this->disconnect();
NetworkConnection::reset();
this->delayCallback(1000);
} else if (this->isConnected() && !this->hasStaCredentials()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Reset"));
this->resetWifi();
Connection::reset();
this->delayCallback(200);
NetworkConnection::reset();
this->delayCallback(1000);
} else if (this->isConnected() && !this->reconnectFlag) {
} else if (this->isConnected()) {
if (!this->connected) {
this->connectedTime = millis();
this->connected = true;
@@ -279,7 +318,7 @@ namespace Network {
}
if (this->isApEnabled() && millis() - this->connectedTime > this->reconnectInterval && !this->hasApClients()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because connected, start only STA"));
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because STA connected"));
WiFi.mode(WIFI_STA);
return;
@@ -300,7 +339,7 @@ namespace Network {
Log.sinfoln(
FPSTR(L_NETWORK),
F("Disconnected, reason: %d, uptime: %lu s."),
Connection::getDisconnectReason(),
NetworkConnection::getDisconnectReason(),
(millis() - this->connectedTime) / 1000
);
}
@@ -322,16 +361,15 @@ namespace Network {
} else if (this->isConnecting() && millis() - this->prevReconnectingTime > this->resetConnectionTimeout) {
Log.swarningln(FPSTR(L_NETWORK), F("Connection timeout, reset wifi..."));
this->resetWifi();
Connection::reset();
this->delayCallback(200);
NetworkConnection::reset();
this->delayCallback(250);
} else if (!this->isConnecting() && this->hasStaCredentials() && (!this->prevReconnectingTime || millis() - this->prevReconnectingTime > this->reconnectInterval)) {
Log.sinfoln(FPSTR(L_NETWORK), F("Try connect..."));
this->reconnectFlag = false;
Connection::reset();
NetworkConnection::reset();
if (!this->connect(true, this->connectionTimeout)) {
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d"), Connection::getStatus(), Connection::getDisconnectReason());
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d, raw reason: %d"), NetworkConnection::getStatus(), NetworkConnection::getDisconnectReason(), NetworkConnection::rawDisconnectReason);
}
this->prevReconnectingTime = millis();
@@ -344,10 +382,10 @@ namespace Network {
}
protected:
const unsigned int reconnectInterval = 5000;
const unsigned int failedConnectTimeout = 120000;
const unsigned int connectionTimeout = 15000;
const unsigned int resetConnectionTimeout = 30000;
const unsigned int reconnectInterval = 15000;
const unsigned int failedConnectTimeout = 185000;
const unsigned int connectionTimeout = 5000;
const unsigned int resetConnectionTimeout = 90000;
YieldCallback yieldCallback = []() {
::yield();

View File

@@ -33,18 +33,16 @@ public:
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
return this->canHandle(method, uri);
}
#endif
bool canHandle(HTTPMethod method, const String& uri) override {
return uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (!this->canHandle(method, uri)) {
return false;
}

View File

@@ -1,4 +1,7 @@
#include <FS.h>
#include <detail/mimetable.h>
using namespace mime;
class StaticPage : public RequestHandler {
public:
@@ -25,18 +28,16 @@ public:
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
return this->canHandle(method, uri);
}
#endif
bool canHandle(HTTPMethod method, const String& uri) override {
return method == HTTP_GET && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (!this->canHandle(method, uri)) {
return false;
}
@@ -61,6 +62,14 @@ public:
}
#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;
@@ -93,6 +102,6 @@ protected:
BeforeSendCallback beforeSendCallback;
String eTag;
const char* uri = nullptr;
const char* path = nullptr;
String path;
const char* cacheHeader = nullptr;
};

View File

@@ -58,26 +58,26 @@ public:
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
bool canHandle(WebServer &server, HTTPMethod method, const String &uri) override {
return this->canHandle(method, uri);
}
#endif
bool canHandle(HTTPMethod method, const String& uri) override {
return method == HTTP_POST && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool canUpload(const String uri) override {
#else
bool canUpload(const String& uri) override {
bool canUpload(WebServer &server, const String &uri) override {
return this->canUpload(uri);
}
#endif
bool canUpload(const String& uri) override {
return uri.equals(this->uri) && (!this->canUploadCallback || this->canUploadCallback(uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (this->afterUpgradeCallback) {
this->afterUpgradeCallback(this->firmwareResult, this->filesystemResult);
}
@@ -91,11 +91,7 @@ public:
return true;
}
#if defined(ARDUINO_ARCH_ESP32)
void upload(WebServer& server, const String uri, HTTPUpload& upload) override {
#else
void upload(WebServer& server, const String& uri, HTTPUpload& upload) override {
#endif
UpgradeResult* result;
if (upload.name.equals("firmware")) {
result = &this->firmwareResult;
@@ -174,7 +170,7 @@ public:
Log.serrorln(
FPSTR(L_PORTAL_OTA),
F("File '%s', on writing %d bytes: %s"),
upload.filename.c_str(), upload.totalSize, result->error
upload.filename.c_str(), upload.totalSize, result->error.c_str()
);
} else {

17
package.json Normal file
View 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"
}
}

View File

@@ -11,74 +11,91 @@
[platformio]
;extra_configs = secrets.ini
extra_configs = secrets.default.ini
core_dir = .pio
[env]
version = 1.4.6
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^7.0.3
;ihormelnyk/OpenTherm Library@^1.1.4
https://github.com/Laxilef/opentherm_library/archive/refs/heads/fix_start_bit.zip
arduino-libraries/ArduinoMqttClient@^0.1.8
bblanchon/ArduinoJson@^7.1.0
;ihormelnyk/OpenTherm Library@^1.1.5
https://github.com/ihormelnyk/opentherm_library#master
;arduino-libraries/ArduinoMqttClient@^0.1.8
https://github.com/Laxilef/ArduinoMqttClient.git#esp32_core_310
lennarthennigs/ESP Telnet@^2.2
gyverlibs/FileData@^1.0.2
gyverlibs/GyverPID@^3.3.2
gyverlibs/GyverBlinker@^1.0
milesburton/DallasTemperature@^3.11.0
laxilef/TinyLogger@^1.1.0
gyverlibs/GyverBlinker@^1.1.1
https://github.com/pstolarz/Arduino-Temperature-Control-Library.git#OneWireNg
laxilef/TinyLogger@^1.1.1
build_type = ${secrets.build_type}
build_flags =
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
;-D PIO_FRAMEWORK_ARDUINO_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_PORT=Serial
-D USE_SERIAL=${secrets.use_serial}
-D USE_TELNET=${secrets.use_telnet}
-D DEBUG_BY_DEFAULT=${secrets.debug}
-D HOSTNAME_DEFAULT='"${secrets.hostname}"'
-D AP_SSID_DEFAULT='"${secrets.ap_ssid}"'
-D AP_PASSWORD_DEFAULT='"${secrets.ap_password}"'
-D STA_SSID_DEFAULT='"${secrets.sta_ssid}"'
-D STA_PASSWORD_DEFAULT='"${secrets.sta_password}"'
-D PORTAL_LOGIN_DEFAULT='"${secrets.portal_login}"'
-D PORTAL_PASSWORD_DEFAULT='"${secrets.portal_password}"'
-D MQTT_SERVER_DEFAULT='"${secrets.mqtt_server}"'
-D MQTT_PORT_DEFAULT=${secrets.mqtt_port}
-D MQTT_USER_DEFAULT='"${secrets.mqtt_user}"'
-D MQTT_PASSWORD_DEFAULT='"${secrets.mqtt_password}"'
-D MQTT_PREFIX_DEFAULT='"${secrets.mqtt_prefix}"'
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_HTTP_SERVER -D DEBUG_ESP_PORT=Serial
-D BUILD_VERSION='"${this.version}"'
-D BUILD_ENV='"$PIOENV"'
-D DEFAULT_SERIAL_ENABLE=${secrets.serial_enable}
-D DEFAULT_SERIAL_BAUD=${secrets.serial_baud}
-D DEFAULT_TELNET_ENABLE=${secrets.telnet_enable}
-D DEFAULT_TELNET_PORT=${secrets.telnet_port}
-D DEFAULT_LOG_LEVEL=${secrets.log_level}
-D DEFAULT_HOSTNAME='"${secrets.hostname}"'
-D DEFAULT_AP_SSID='"${secrets.ap_ssid}"'
-D DEFAULT_AP_PASSWORD='"${secrets.ap_password}"'
-D DEFAULT_STA_SSID='"${secrets.sta_ssid}"'
-D DEFAULT_STA_PASSWORD='"${secrets.sta_password}"'
-D DEFAULT_PORTAL_LOGIN='"${secrets.portal_login}"'
-D DEFAULT_PORTAL_PASSWORD='"${secrets.portal_password}"'
-D DEFAULT_MQTT_SERVER='"${secrets.mqtt_server}"'
-D DEFAULT_MQTT_PORT=${secrets.mqtt_port}
-D DEFAULT_MQTT_USER='"${secrets.mqtt_user}"'
-D DEFAULT_MQTT_PASSWORD='"${secrets.mqtt_password}"'
-D DEFAULT_MQTT_PREFIX='"${secrets.mqtt_prefix}"'
upload_speed = 921600
monitor_speed = 115200
monitor_filters = direct
board_build.flash_mode = dio
board_build.filesystem = littlefs
version = 1.4.0-rc.15
; Defaults
[esp8266_defaults]
platform = espressif8266
platform = espressif8266@^4.2.1
lib_deps =
${env.lib_deps}
nrwiersma/ESP8266Scheduler@^1.1
nrwiersma/ESP8266Scheduler@^1.2
lib_ignore =
extra_scripts =
post:tools/build.py
build_type = ${env.build_type}
build_flags = ${env.build_flags}
board_build.ldscript = eagle.flash.1m256.ld
;board_build.ldscript = eagle.flash.4m1m.ld
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 =
; framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.5
; framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.1/esp32-arduino-libs-idf-release_v5.1-33fbade6.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc2/platform-espressif32.zip
platform_packages =
board_build.partitions = esp32_partitions.csv
lib_deps =
${env.lib_deps}
laxilef/ESP32Scheduler@^1.0.1
nimble_lib = h2zero/NimBLE-Arduino@^1.4.2
lib_ignore =
extra_scripts =
post:tools/esp32.py
post:tools/build.py
build_type = ${env.build_type}
build_flags =
${env.build_flags}
-D CORE_DEBUG_LEVEL=0
-Wl,--wrap=esp_panic_handler
; Boards
@@ -89,14 +106,15 @@ lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_type = ${esp8266_defaults.build_type}
build_flags =
${esp8266_defaults.build_flags}
-D OT_IN_PIN_DEFAULT=4
-D OT_OUT_PIN_DEFAULT=5
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
-D SENSOR_INDOOR_PIN_DEFAULT=14
-D LED_STATUS_PIN=13
-D LED_OT_RX_PIN=15
-D DEFAULT_OT_IN_GPIO=4
-D DEFAULT_OT_OUT_GPIO=5
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=14
-D DEFAULT_STATUS_LED_GPIO=13
-D DEFAULT_OT_RX_LED_GPIO=15
[env:d1_mini_lite]
platform = ${esp8266_defaults.platform}
@@ -105,14 +123,15 @@ lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_type = ${esp8266_defaults.build_type}
build_flags =
${esp8266_defaults.build_flags}
-D OT_IN_PIN_DEFAULT=4
-D OT_OUT_PIN_DEFAULT=5
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
-D SENSOR_INDOOR_PIN_DEFAULT=14
-D LED_STATUS_PIN=13
-D LED_OT_RX_PIN=15
-D DEFAULT_OT_IN_GPIO=4
-D DEFAULT_OT_OUT_GPIO=5
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=14
-D DEFAULT_STATUS_LED_GPIO=13
-D DEFAULT_OT_RX_LED_GPIO=15
[env:d1_mini_pro]
platform = ${esp8266_defaults.platform}
@@ -121,106 +140,159 @@ lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_type = ${esp8266_defaults.build_type}
build_flags =
${esp8266_defaults.build_flags}
-D OT_IN_PIN_DEFAULT=4
-D OT_OUT_PIN_DEFAULT=5
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
-D SENSOR_INDOOR_PIN_DEFAULT=14
-D LED_STATUS_PIN=13
-D LED_OT_RX_PIN=15
-D DEFAULT_OT_IN_GPIO=4
-D DEFAULT_OT_OUT_GPIO=5
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=14
-D DEFAULT_STATUS_LED_GPIO=13
-D DEFAULT_OT_RX_LED_GPIO=15
[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_type = ${esp8266_defaults.build_type}
build_flags =
${esp8266_defaults.build_flags}
-D DEFAULT_OT_IN_GPIO=13
-D DEFAULT_OT_OUT_GPIO=15
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=4
-D DEFAULT_STATUS_LED_GPIO=2
-D DEFAULT_OT_RX_LED_GPIO=16
[env:s2_mini]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = lolin_s2_mini
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps = ${esp32_defaults.lib_deps}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-DARDUINO_USB_MODE=1
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
-D OT_IN_PIN_DEFAULT=33
-D OT_OUT_PIN_DEFAULT=35
-D SENSOR_OUTDOOR_PIN_DEFAULT=9
-D SENSOR_INDOOR_PIN_DEFAULT=7
-D LED_STATUS_PIN=11
-D LED_OT_RX_PIN=12
-D ARDUINO_USB_MODE=0
-D ARDUINO_USB_CDC_ON_BOOT=1
-D DEFAULT_OT_IN_GPIO=33
-D DEFAULT_OT_OUT_GPIO=35
-D DEFAULT_SENSOR_OUTDOOR_GPIO=9
-D DEFAULT_SENSOR_INDOOR_GPIO=7
-D DEFAULT_STATUS_LED_GPIO=11
-D DEFAULT_OT_RX_LED_GPIO=12
[env:s3_mini]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = lolin_s3_mini
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
${esp32_defaults.nimble_lib}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-DARDUINO_USB_MODE=1
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
-D ARDUINO_USB_MODE=0
-D ARDUINO_USB_CDC_ON_BOOT=1
-D USE_BLE=1
-D OT_IN_PIN_DEFAULT=35
-D OT_OUT_PIN_DEFAULT=36
-D SENSOR_OUTDOOR_PIN_DEFAULT=13
-D SENSOR_INDOOR_PIN_DEFAULT=12
-D LED_STATUS_PIN=11
-D LED_OT_RX_PIN=10
-D DEFAULT_OT_IN_GPIO=35
-D DEFAULT_OT_OUT_GPIO=36
-D DEFAULT_SENSOR_OUTDOOR_GPIO=13
-D DEFAULT_SENSOR_INDOOR_GPIO=12
-D DEFAULT_STATUS_LED_GPIO=11
-D DEFAULT_OT_RX_LED_GPIO=10
[env:c3_mini]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = lolin_c3_mini
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
${esp32_defaults.nimble_lib}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-mtext-section-literals
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D OT_IN_PIN_DEFAULT=8
-D OT_OUT_PIN_DEFAULT=10
-D SENSOR_OUTDOOR_PIN_DEFAULT=0
-D SENSOR_INDOOR_PIN_DEFAULT=1
-D LED_STATUS_PIN=4
-D LED_OT_RX_PIN=5
-D DEFAULT_OT_IN_GPIO=8
-D DEFAULT_OT_OUT_GPIO=10
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
-D DEFAULT_SENSOR_INDOOR_GPIO=1
-D DEFAULT_STATUS_LED_GPIO=4
-D DEFAULT_OT_RX_LED_GPIO=5
[env:nodemcu_32s]
[env:nodemcu_32]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = nodemcu-32s
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
${esp32_defaults.nimble_lib}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D OT_IN_PIN_DEFAULT=21
-D OT_OUT_PIN_DEFAULT=22
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
-D SENSOR_INDOOR_PIN_DEFAULT=13
-D LED_STATUS_PIN=2 ; 18
-D LED_OT_RX_PIN=19
;-D WOKWI=1
-D DEFAULT_OT_IN_GPIO=16
-D DEFAULT_OT_OUT_GPIO=4
-D DEFAULT_SENSOR_OUTDOOR_GPIO=15
-D DEFAULT_SENSOR_INDOOR_GPIO=26
-D DEFAULT_STATUS_LED_GPIO=2
-D DEFAULT_OT_RX_LED_GPIO=19
[env:d1_mini32]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = wemos_d1_mini32
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
${esp32_defaults.nimble_lib}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D OT_IN_PIN_DEFAULT=21
-D OT_OUT_PIN_DEFAULT=22
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
-D SENSOR_INDOOR_PIN_DEFAULT=18
-D LED_STATUS_PIN=2
-D LED_OT_RX_PIN=19
-D DEFAULT_OT_IN_GPIO=21
-D DEFAULT_OT_OUT_GPIO=22
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=18
-D DEFAULT_STATUS_LED_GPIO=2
-D DEFAULT_OT_RX_LED_GPIO=19
[env:esp32_c6]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = esp32-c6-devkitm-1
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
;${esp32_defaults.nimble_lib}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-mtext-section-literals
build_type = ${esp32_defaults.build_type}
build_flags =
${esp32_defaults.build_flags}
; Currently the NimBLE library is incompatible with ESP32 C6
;-D USE_BLE=1

View File

@@ -1,7 +1,11 @@
[secrets]
use_serial = true
use_telnet = true
debug = true
build_type = release
serial_enable = true
serial_baud = 115200
telnet_enable = true
telnet_port = 23
log_level = 5
hostname = opentherm
ap_ssid = OpenTherm Gateway

132
src/CrashRecorder.h Normal file
View File

@@ -0,0 +1,132 @@
#pragma once
#include <Arduino.h>
#ifdef ARDUINO_ARCH_ESP32
#include "esp_err.h"
#endif
#ifdef ARDUINO_ARCH_ESP8266
extern "C" {
#include <user_interface.h>
}
// https://github.com/espressif/ESP8266_RTOS_SDK/blob/master/components/esp8266/include/esp_attr.h
#define _COUNTER_STRINGIFY(COUNTER) #COUNTER
#define _SECTION_ATTR_IMPL(SECTION, COUNTER) __attribute__((section(SECTION "." _COUNTER_STRINGIFY(COUNTER))))
#define __NOINIT_ATTR _SECTION_ATTR_IMPL(".noinit", __COUNTER__)
#endif
namespace CrashRecorder {
typedef struct {
unsigned int data[32];
uint8_t length;
bool continues;
} backtrace_t;
typedef struct {
unsigned int data[4];
uint8_t length;
} epc_t;
typedef struct {
uint8_t core;
size_t heap;
unsigned long uptime;
} ext_t;
__NOINIT_ATTR volatile static backtrace_t backtrace;
__NOINIT_ATTR volatile static epc_t epc;
__NOINIT_ATTR volatile static ext_t ext;
uint8_t backtraceMaxLength = sizeof(backtrace.data) / sizeof(*backtrace.data);
uint8_t epcMaxLength = sizeof(epc.data) / sizeof(*epc.data);
#ifdef ARDUINO_ARCH_ESP32
void IRAM_ATTR panicHandler(arduino_panic_info_t *info, void *arg) {;
ext.core = info->core;
ext.heap = ESP.getFreeHeap();
ext.uptime = millis() / 1000u;
// Backtrace
backtrace.length = info->backtrace_len < backtraceMaxLength ? info->backtrace_len : backtraceMaxLength;
backtrace.continues = false;
for (unsigned int i = 0; i < info->backtrace_len; i++) {
if (i >= backtraceMaxLength) {
backtrace.continues = true;
break;
}
backtrace.data[i] = info->backtrace[i];
}
// EPC
if (info->pc) {
epc.data[0] = (unsigned int) info->pc;
epc.length = 1;
} else {
epc.length = 0;
}
}
#endif
void init() {
if (backtrace.length > backtraceMaxLength) {
backtrace.length = 0;
}
if (epc.length > epcMaxLength) {
epc.length = 0;
}
#ifdef ARDUINO_ARCH_ESP32
set_arduino_panic_handler(panicHandler, nullptr);
#endif
}
}
#ifdef ARDUINO_ARCH_ESP8266
extern "C" void custom_crash_callback(struct rst_info *info, uint32_t stack, uint32_t stack_end) {
uint8_t _length = 0;
CrashRecorder::ext.core = 0;
CrashRecorder::ext.heap = ESP.getFreeHeap();
CrashRecorder::ext.uptime = millis() / 1000u;
// Backtrace
CrashRecorder::backtrace.continues = false;
uint32_t value;
for (uint32_t i = stack; i < stack_end; i += 4) {
value = *((uint32_t*) i);
// keep only addresses in code area
if ((value >= 0x40000000) && (value < 0x40300000)) {
if (_length >= CrashRecorder::backtraceMaxLength) {
CrashRecorder::backtrace.continues = true;
break;
}
CrashRecorder::backtrace.data[_length++] = value;
}
}
CrashRecorder::backtrace.length = _length;
// EPC
_length = 0;
if (info->epc1 > 0) {
CrashRecorder::epc.data[_length++] = info->epc1;
}
if (info->epc2 > 0) {
CrashRecorder::epc.data[_length++] = info->epc2;
}
if (info->epc3 > 0) {
CrashRecorder::epc.data[_length++] = info->epc3;
}
CrashRecorder::epc.length = _length;
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
#include <Blinker.h>
extern Network::Manager* network;
using namespace NetworkUtils;
extern NetworkMgr* network;
extern MqttTask* tMqtt;
extern OpenThermTask* tOt;
extern FileData fsSettings, fsNetworkSettings;
@@ -27,8 +29,6 @@ protected:
enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
Blinker* blinker = nullptr;
bool blinkerInitialized = false;
unsigned long firstFailConnect = 0;
unsigned long lastHeapInfo = 0;
unsigned int minFreeHeap = 0;
unsigned int minMaxFreeBlockHeap = 0;
@@ -38,30 +38,24 @@ protected:
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
unsigned long externalPumpStartTime = 0;
bool telnetStarted = false;
bool emergencyDetected = false;
unsigned long emergencyFlipTime = 0;
const char* getTaskName() {
#if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override {
return "Main";
}
/*int getTaskCore() {
/*BaseType_t getTaskCore() override {
return 1;
}*/
int getTaskPriority() {
int getTaskPriority() override {
return 3;
}
void setup() {
#ifdef LED_STATUS_PIN
pinMode(LED_STATUS_PIN, OUTPUT);
digitalWrite(LED_STATUS_PIN, LOW);
#endif
if (settings.externalPump.pin != 0) {
pinMode(settings.externalPump.pin, OUTPUT);
digitalWrite(settings.externalPump.pin, LOW);
}
}
void setup() {}
void loop() {
network->loop();
@@ -89,31 +83,34 @@ protected:
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
}
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
tOt->enable();
vars.states.mqtt = tMqtt->isConnected();
vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0;
if (settings.system.logLevel >= TinyLogger::Level::SILENT && settings.system.logLevel <= TinyLogger::Level::VERBOSE) {
if (Log.getLevel() != settings.system.logLevel) {
Log.setLevel(static_cast<TinyLogger::Level>(settings.system.logLevel));
}
}
if (network->isConnected()) {
vars.sensors.rssi = WiFi.RSSI();
if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false);
this->telnetStarted = true;
}
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
if (settings.mqtt.enable && !tMqtt->isEnabled()) {
tMqtt->enable();
} else if (!settings.mqtt.enable && tMqtt->isEnabled()) {
tMqtt->disable();
}
if (this->firstFailConnect != 0) {
this->firstFailConnect = 0;
if (settings.sensors.indoor.type == SensorType::MANUAL) {
vars.sensors.indoor.connected = !settings.mqtt.enable || vars.states.mqtt;
}
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);
if (settings.sensors.outdoor.type == SensorType::MANUAL) {
vars.sensors.outdoor.connected = !settings.mqtt.enable || vars.states.mqtt;
}
} else {
@@ -126,23 +123,19 @@ protected:
tMqtt->disable();
}
if (settings.emergency.enable && !vars.states.emergency) {
if (this->firstFailConnect == 0) {
this->firstFailConnect = millis();
if (settings.sensors.indoor.type == SensorType::MANUAL) {
vars.sensors.indoor.connected = false;
}
if (millis() - this->firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
if (settings.sensors.outdoor.type == SensorType::MANUAL) {
vars.sensors.outdoor.connected = false;
}
}
this->yield();
#ifdef LED_STATUS_PIN
this->ledStatus(LED_STATUS_PIN);
#endif
this->emergency();
this->ledStatus();
this->cascadeControl();
this->externalPump();
this->yield();
@@ -183,7 +176,7 @@ protected:
this->restartSignalTime = millis();
}
if (!settings.system.debug) {
if (settings.system.logLevel < TinyLogger::Level::VERBOSE) {
return;
}
@@ -211,16 +204,82 @@ protected:
}
}
void ledStatus(uint8_t ledPin) {
void emergency() {
// flags
uint8_t emergencyFlags = 0b00000000;
// set outdoor sensor flag
if (settings.equitherm.enable && !vars.sensors.outdoor.connected) {
emergencyFlags |= 0b00000001;
}
// set indoor sensor flag
if (!settings.equitherm.enable && settings.pid.enable && !vars.sensors.indoor.connected) {
emergencyFlags |= 0b00000010;
}
// set indoor sensor flag for OT native heating control
if (settings.opentherm.nativeHeatingControl && !vars.sensors.indoor.connected) {
emergencyFlags |= 0b00000100;
}
// if any flags is true
if ((emergencyFlags & 0b00001111) != 0) {
if (!this->emergencyDetected) {
// flip flag
this->emergencyDetected = true;
this->emergencyFlipTime = millis();
} else if (this->emergencyDetected && !vars.states.emergency) {
// enable emergency
if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled (%hhu)"), emergencyFlags);
}
}
} else {
if (this->emergencyDetected) {
// flip flag
this->emergencyDetected = false;
this->emergencyFlipTime = millis();
} else if (!this->emergencyDetected && vars.states.emergency) {
// disable emergency
if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled"));
}
}
}
}
void ledStatus() {
uint8_t errors[4];
uint8_t errCount = 0;
static uint8_t errPos = 0;
static unsigned long endBlinkTime = 0;
static bool ledOn = false;
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
if (!this->blinkerInitialized) {
this->blinker->init(ledPin);
this->blinkerInitialized = true;
if (settings.system.statusLedGpio != configuredGpio) {
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
digitalWrite(configuredGpio, LOW);
}
if (GPIO_IS_VALID(settings.system.statusLedGpio)) {
configuredGpio = settings.system.statusLedGpio;
pinMode(configuredGpio, OUTPUT);
digitalWrite(configuredGpio, LOW);
this->blinker->init(configuredGpio);
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
configuredGpio = GPIO_IS_NOT_CONFIGURED;
}
}
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
return;
}
if (!network->isConnected()) {
@@ -247,14 +306,14 @@ protected:
if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
if (errCount == 0) {
if (!ledOn) {
digitalWrite(ledPin, HIGH);
digitalWrite(configuredGpio, HIGH);
ledOn = true;
}
return;
} else if (ledOn) {
digitalWrite(ledPin, LOW);
digitalWrite(configuredGpio, LOW);
ledOn = false;
endBlinkTime = millis();
return;
@@ -274,7 +333,199 @@ protected:
this->blinker->tick();
}
void cascadeControl() {
static uint8_t configuredInputGpio = GPIO_IS_NOT_CONFIGURED;
static uint8_t configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;
static bool inputTempValue = false;
static unsigned long inputChangedTs = 0;
static bool outputTempValue = false;
static unsigned long outputChangedTs = 0;
// input
if (settings.cascadeControl.input.enable) {
if (settings.cascadeControl.input.gpio != configuredInputGpio) {
if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
pinMode(configuredInputGpio, OUTPUT);
digitalWrite(configuredInputGpio, LOW);
Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Deinitialized on GPIO %hhu"), configuredInputGpio);
}
if (GPIO_IS_VALID(settings.cascadeControl.input.gpio)) {
configuredInputGpio = settings.cascadeControl.input.gpio;
pinMode(configuredInputGpio, INPUT);
Log.sinfoln(FPSTR(L_CASCADE_INPUT), F("Initialized on GPIO %hhu"), configuredInputGpio);
} else if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
configuredInputGpio = GPIO_IS_NOT_CONFIGURED;
Log.swarningln(FPSTR(L_CASCADE_INPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredInputGpio);
}
}
if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) {
bool value;
if (digitalRead(configuredInputGpio) == HIGH) {
value = true ^ settings.cascadeControl.input.invertState;
} else {
value = false ^ settings.cascadeControl.input.invertState;
}
if (value != vars.cascadeControl.input) {
if (value != inputTempValue) {
inputTempValue = value;
inputChangedTs = millis();
} else if (millis() - inputChangedTs >= settings.cascadeControl.input.thresholdTime * 1000u) {
vars.cascadeControl.input = value;
Log.sinfoln(
FPSTR(L_CASCADE_INPUT),
F("State changed to %s"),
value ? F("TRUE") : F("FALSE")
);
}
} else if (value != inputTempValue) {
inputTempValue = value;
}
}
}
if (!settings.cascadeControl.input.enable || configuredInputGpio == GPIO_IS_NOT_CONFIGURED) {
if (!vars.cascadeControl.input) {
vars.cascadeControl.input = true;
Log.sinfoln(
FPSTR(L_CASCADE_INPUT),
F("Disabled, state changed to %s"),
vars.cascadeControl.input ? F("TRUE") : F("FALSE")
);
}
}
// output
if (settings.cascadeControl.output.enable) {
if (settings.cascadeControl.output.gpio != configuredOutputGpio) {
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
pinMode(configuredOutputGpio, OUTPUT);
digitalWrite(configuredOutputGpio, LOW);
Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Deinitialized on GPIO %hhu"), configuredOutputGpio);
}
if (GPIO_IS_VALID(settings.cascadeControl.output.gpio)) {
configuredOutputGpio = settings.cascadeControl.output.gpio;
pinMode(configuredOutputGpio, OUTPUT);
digitalWrite(
configuredOutputGpio,
settings.cascadeControl.output.invertState
? HIGH
: LOW
);
Log.sinfoln(FPSTR(L_CASCADE_OUTPUT), F("Initialized on GPIO %hhu"), configuredOutputGpio);
} else if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
configuredOutputGpio = GPIO_IS_NOT_CONFIGURED;
Log.swarningln(FPSTR(L_CASCADE_OUTPUT), F("Failed initialize: GPIO %hhu is not valid!"), configuredOutputGpio);
}
}
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
bool value = false;
if (settings.cascadeControl.output.onFault && vars.states.fault) {
value = true;
} else if (settings.cascadeControl.output.onLossConnection && !vars.states.otStatus) {
value = true;
} else if (settings.cascadeControl.output.onEnabledHeating && settings.heating.enable && vars.cascadeControl.input) {
value = true;
}
if (value != vars.cascadeControl.output) {
if (value != outputTempValue) {
outputTempValue = value;
outputChangedTs = millis();
} else if (millis() - outputChangedTs >= settings.cascadeControl.output.thresholdTime * 1000u) {
vars.cascadeControl.output = value;
digitalWrite(
configuredOutputGpio,
vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
? HIGH
: LOW
);
Log.sinfoln(
FPSTR(L_CASCADE_OUTPUT),
F("State changed to %s"),
value ? F("TRUE") : F("FALSE")
);
}
} else if (value != outputTempValue) {
outputTempValue = value;
}
}
}
if (!settings.cascadeControl.output.enable || configuredOutputGpio == GPIO_IS_NOT_CONFIGURED) {
if (vars.cascadeControl.output) {
vars.cascadeControl.output = false;
if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) {
digitalWrite(
configuredOutputGpio,
vars.cascadeControl.output ^ settings.cascadeControl.output.invertState
? HIGH
: LOW
);
}
Log.sinfoln(
FPSTR(L_CASCADE_OUTPUT),
F("Disabled, state changed to %s"),
vars.cascadeControl.output ? F("TRUE") : F("FALSE")
);
}
}
}
void externalPump() {
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
if (settings.externalPump.gpio != configuredGpio) {
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
digitalWrite(configuredGpio, LOW);
}
if (GPIO_IS_VALID(settings.externalPump.gpio)) {
configuredGpio = settings.externalPump.gpio;
pinMode(configuredGpio, OUTPUT);
digitalWrite(configuredGpio, LOW);
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
configuredGpio = GPIO_IS_NOT_CONFIGURED;
}
}
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
if (vars.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();
@@ -283,11 +534,9 @@ protected:
this->heatingEnabled = true;
}
if (!settings.externalPump.use || settings.externalPump.pin == 0) {
if (!settings.externalPump.use) {
if (vars.states.externalPump) {
if (settings.externalPump.pin != 0) {
digitalWrite(settings.externalPump.pin, LOW);
}
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
@@ -300,7 +549,7 @@ protected:
if (vars.states.externalPump && !this->heatingEnabled) {
if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
digitalWrite(settings.externalPump.pin, LOW);
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
@@ -308,7 +557,7 @@ protected:
Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time"));
} else if (this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) {
digitalWrite(settings.externalPump.pin, LOW);
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
@@ -324,7 +573,7 @@ protected:
this->externalPumpStartTime = millis();
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
digitalWrite(settings.externalPump.pin, HIGH);
digitalWrite(configuredGpio, HIGH);
Log.sinfoln("EXTPUMP", F("Enabled: heating on"));
@@ -333,7 +582,7 @@ protected:
this->externalPumpStartTime = millis();
this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK;
digitalWrite(settings.externalPump.pin, HIGH);
digitalWrite(configuredGpio, HIGH);
Log.sinfoln("EXTPUMP", F("Enabled: anti stuck"));
}

View File

@@ -20,6 +20,11 @@ public:
if (this->client != nullptr) {
if (this->client->connected()) {
this->client->stop();
if (this->connected) {
this->onDisconnect();
this->connected = false;
}
}
delete this->client;
@@ -31,6 +36,12 @@ public:
void disable() {
this->client->stop();
if (this->connected) {
this->onDisconnect();
this->connected = false;
}
Task::disable();
Log.sinfoln(FPSTR(L_MQTT), F("Disabled"));
@@ -42,16 +53,26 @@ public:
Log.sinfoln(FPSTR(L_MQTT), F("Enabled"));
}
bool isConnected() {
inline bool isConnected() {
return this->connected;
}
inline void resetPublishedSettingsTime() {
this->prevPubSettingsTime = 0;
}
inline void resetPublishedVarsTime() {
this->prevPubVarsTime = 0;
}
protected:
MqttWiFiClient* wifiClient = nullptr;
MqttClient* client = nullptr;
HaHelper* haHelper = nullptr;
MqttWriter* writer = nullptr;
unsigned short readyForSendTime = 15000;
UnitSystem currentUnitSystem = UnitSystem::METRIC;
bool currentHomeAssistantDiscovery = false;
unsigned short readyForSendTime = 30000;
unsigned long lastReconnectTime = 0;
unsigned long connectedTime = 0;
unsigned long disconnectedTime = 0;
@@ -60,19 +81,21 @@ protected:
bool connected = false;
bool newConnection = false;
const char* getTaskName() {
#if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override {
return "Mqtt";
}
/*int getTaskCore() {
/*BaseType_t getTaskCore() override {
return 1;
}*/
int getTaskPriority() {
return 1;
int getTaskPriority() override {
return 2;
}
#endif
bool isReadyForSend() {
inline bool isReadyForSend() {
return millis() - this->connectedTime > this->readyForSendTime;
}
@@ -140,7 +163,7 @@ protected:
// ha helper settings
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
this->haHelper->setDeviceVersion(PROJECT_VERSION);
this->haHelper->setDeviceVersion(BUILD_VERSION);
this->haHelper->setDeviceModel(PROJECT_NAME);
this->haHelper->setDeviceName(PROJECT_NAME);
this->haHelper->setWriter(this->writer);
@@ -175,15 +198,6 @@ protected:
this->onConnect();
}
if (!this->connected && settings.emergency.enable && !vars.states.emergency && millis() - this->disconnectedTime > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
} else if (this->connected && vars.states.emergency && millis() - this->connectedTime > 10000) {
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
}
if (!this->connected) {
return;
}
@@ -213,15 +227,25 @@ protected:
}
// publish ha entities if not published
if (this->newConnection) {
if (settings.mqtt.homeAssistantDiscovery) {
if (this->newConnection || !this->currentHomeAssistantDiscovery || this->currentUnitSystem != settings.system.unitSystem) {
this->publishHaEntities();
this->publishNonStaticHaEntities(true);
this->newConnection = false;
this->currentHomeAssistantDiscovery = true;
this->currentUnitSystem = settings.system.unitSystem;
} else {
// publish non static ha entities
this->publishNonStaticHaEntities();
}
} else if (this->currentHomeAssistantDiscovery) {
this->currentHomeAssistantDiscovery = false;
}
if (this->newConnection) {
this->newConnection = false;
}
}
void onConnect() {
@@ -246,7 +270,7 @@ protected:
return;
}
if (settings.system.debug) {
if (settings.system.logLevel >= TinyLogger::Level::TRACE) {
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
if (Log.lock()) {
for (size_t i = 0; i < length; i++) {
@@ -276,100 +300,83 @@ protected:
Log.swarningln(FPSTR(L_MQTT_MSG), F("Not valid json"));
return;
}
doc.shrinkToFit();
if (this->haHelper->getDeviceTopic("state/set").equals(topic)) {
this->writer->publish(this->haHelper->getDeviceTopic("state/set").c_str(), nullptr, 0, true);
this->updateVariables(doc);
if (jsonToVars(doc, vars)) {
this->resetPublishedVarsTime();
}
} else if (this->haHelper->getDeviceTopic("settings/set").equals(topic)) {
this->writer->publish(this->haHelper->getDeviceTopic("settings/set").c_str(), nullptr, 0, true);
this->updateSettings(doc);
}
}
bool updateSettings(JsonDocument& doc) {
bool changed = safeJsonToSettings(doc, settings);
doc.clear();
doc.shrinkToFit();
if (changed) {
this->prevPubSettingsTime = 0;
if (safeJsonToSettings(doc, settings)) {
this->resetPublishedSettingsTime();
fsSettings.update();
return true;
}
return false;
}
bool updateVariables(JsonDocument& doc) {
bool changed = jsonToVars(doc, vars);
doc.clear();
doc.shrinkToFit();
if (changed) {
this->prevPubVarsTime = 0;
return true;
}
return false;
}
void publishHaEntities() {
// emergency
this->haHelper->publishSwitchEmergency();
this->haHelper->publishNumberEmergencyTarget();
this->haHelper->publishSwitchEmergencyUseEquitherm();
this->haHelper->publishSwitchEmergencyUsePid();
// heating
this->haHelper->publishSwitchHeating(false);
this->haHelper->publishSwitchHeatingTurbo();
this->haHelper->publishNumberHeatingHysteresis();
this->haHelper->publishSensorHeatingSetpoint(false);
this->haHelper->publishSensorBoilerHeatingMinTemp(false);
this->haHelper->publishSensorBoilerHeatingMaxTemp(false);
this->haHelper->publishNumberHeatingMinTemp(false);
this->haHelper->publishNumberHeatingMaxTemp(false);
this->haHelper->publishNumberHeatingMaxModulation(false);
this->haHelper->publishSwitchHeatingTurbo(false);
this->haHelper->publishInputHeatingHysteresis(settings.system.unitSystem);
this->haHelper->publishInputHeatingTurboFactor(false);
this->haHelper->publishInputHeatingMinTemp(settings.system.unitSystem);
this->haHelper->publishInputHeatingMaxTemp(settings.system.unitSystem);
this->haHelper->publishSensorHeatingSetpoint(settings.system.unitSystem, false);
this->haHelper->publishSensorBoilerHeatingMinTemp(settings.system.unitSystem, false);
this->haHelper->publishSensorBoilerHeatingMaxTemp(settings.system.unitSystem, false);
// pid
this->haHelper->publishSwitchPid();
this->haHelper->publishNumberPidFactorP();
this->haHelper->publishNumberPidFactorI();
this->haHelper->publishNumberPidFactorD();
this->haHelper->publishNumberPidDt(false);
this->haHelper->publishNumberPidMinTemp(false);
this->haHelper->publishNumberPidMaxTemp(false);
this->haHelper->publishInputPidFactorP(false);
this->haHelper->publishInputPidFactorI(false);
this->haHelper->publishInputPidFactorD(false);
this->haHelper->publishInputPidDt(false);
this->haHelper->publishInputPidMinTemp(settings.system.unitSystem, false);
this->haHelper->publishInputPidMaxTemp(settings.system.unitSystem, false);
// equitherm
this->haHelper->publishSwitchEquitherm();
this->haHelper->publishNumberEquithermFactorN();
this->haHelper->publishNumberEquithermFactorK();
this->haHelper->publishNumberEquithermFactorT();
// tuning
this->haHelper->publishSwitchTuning();
this->haHelper->publishSelectTuningRegulator();
this->haHelper->publishInputEquithermFactorN(false);
this->haHelper->publishInputEquithermFactorK(false);
this->haHelper->publishInputEquithermFactorT(false);
// states
this->haHelper->publishBinSensorStatus();
this->haHelper->publishBinSensorOtStatus();
this->haHelper->publishBinSensorHeating();
this->haHelper->publishBinSensorFlame();
this->haHelper->publishBinSensorFault();
this->haHelper->publishBinSensorDiagnostic();
this->haHelper->publishStateStatus();
this->haHelper->publishStateEmergency();
this->haHelper->publishStateOtStatus();
this->haHelper->publishStateHeating();
this->haHelper->publishStateFlame();
this->haHelper->publishStateFault();
this->haHelper->publishStateDiagnostic();
this->haHelper->publishStateExtPump(false);
// sensors
this->haHelper->publishSensorModulation(false);
this->haHelper->publishSensorPressure(false);
this->haHelper->publishSensorModulation();
this->haHelper->publishSensorPressure(settings.system.unitSystem, false);
this->haHelper->publishSensorPower();
this->haHelper->publishSensorFaultCode();
this->haHelper->publishSensorDiagnosticCode();
this->haHelper->publishSensorRssi(false);
this->haHelper->publishSensorUptime(false);
this->haHelper->publishOutdoorSensorConnected();
this->haHelper->publishOutdoorSensorRssi(false);
this->haHelper->publishOutdoorSensorBattery(false);
this->haHelper->publishOutdoorSensorHumidity(false);
this->haHelper->publishIndoorSensorConnected();
this->haHelper->publishIndoorSensorRssi(false);
this->haHelper->publishIndoorSensorBattery(false);
this->haHelper->publishIndoorSensorHumidity(false);
// temperatures
this->haHelper->publishNumberIndoorTemp();
this->haHelper->publishSensorHeatingTemp();
this->haHelper->publishSensorHeatingTemp(settings.system.unitSystem);
this->haHelper->publishSensorHeatingReturnTemp(settings.system.unitSystem, false);
this->haHelper->publishSensorExhaustTemp(settings.system.unitSystem, false);
// buttons
this->haHelper->publishButtonRestart(false);
@@ -379,37 +386,46 @@ protected:
bool publishNonStaticHaEntities(bool force = false) {
static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp = 0;
static bool _isStupidMode, _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent = false;
static bool _noRegulators, _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent = false;
bool published = false;
bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable;
byte heatingMinTemp = isStupidMode ? settings.heating.minTemp : 10;
byte heatingMaxTemp = isStupidMode ? settings.heating.maxTemp : 30;
bool editableOutdoorTemp = settings.sensors.outdoor.type == 1;
bool editableIndoorTemp = settings.sensors.indoor.type == 1;
bool noRegulators = !settings.opentherm.nativeHeatingControl && !settings.pid.enable && !settings.equitherm.enable;
byte heatingMinTemp = 0;
byte heatingMaxTemp = 0;
bool editableOutdoorTemp = settings.sensors.outdoor.type == SensorType::MANUAL;
bool editableIndoorTemp = settings.sensors.indoor.type == SensorType::MANUAL;
if (noRegulators) {
heatingMinTemp = settings.heating.minTemp;
heatingMaxTemp = settings.heating.maxTemp;
} else {
heatingMinTemp = convertTemp(THERMOSTAT_INDOOR_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem);
heatingMaxTemp = convertTemp(THERMOSTAT_INDOOR_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem);
}
if (force || _dhwPresent != settings.opentherm.dhwPresent) {
_dhwPresent = settings.opentherm.dhwPresent;
if (_dhwPresent) {
this->haHelper->publishSwitchDhw(false);
this->haHelper->publishSensorBoilerDhwMinTemp(false);
this->haHelper->publishSensorBoilerDhwMaxTemp(false);
this->haHelper->publishNumberDhwMinTemp(false);
this->haHelper->publishNumberDhwMaxTemp(false);
this->haHelper->publishBinSensorDhw();
this->haHelper->publishSensorDhwTemp();
this->haHelper->publishSensorDhwFlowRate(false);
this->haHelper->publishSensorBoilerDhwMinTemp(settings.system.unitSystem, false);
this->haHelper->publishSensorBoilerDhwMaxTemp(settings.system.unitSystem, false);
this->haHelper->publishInputDhwMinTemp(settings.system.unitSystem);
this->haHelper->publishInputDhwMaxTemp(settings.system.unitSystem);
this->haHelper->publishStateDhw();
this->haHelper->publishSensorDhwTemp(settings.system.unitSystem);
this->haHelper->publishSensorDhwFlowRate(settings.system.unitSystem);
} else {
this->haHelper->deleteSwitchDhw();
this->haHelper->deleteSensorBoilerDhwMinTemp();
this->haHelper->deleteSensorBoilerDhwMaxTemp();
this->haHelper->deleteNumberDhwMinTemp();
this->haHelper->deleteNumberDhwMaxTemp();
this->haHelper->deleteBinSensorDhw();
this->haHelper->deleteInputDhwMinTemp();
this->haHelper->deleteInputDhwMaxTemp();
this->haHelper->deleteStateDhw();
this->haHelper->deleteSensorDhwTemp();
this->haHelper->deleteNumberDhwTarget();
this->haHelper->deleteInputDhwTarget();
this->haHelper->deleteClimateDhw();
this->haHelper->deleteSensorDhwFlowRate();
}
@@ -417,30 +433,17 @@ protected:
published = true;
}
if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) {
settings.heating.target = constrain(settings.heating.target, heatingMinTemp, heatingMaxTemp);
}
if (force || _noRegulators != noRegulators || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
_heatingMinTemp = heatingMinTemp;
_heatingMaxTemp = heatingMaxTemp;
_isStupidMode = isStupidMode;
_noRegulators = noRegulators;
this->haHelper->publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false);
this->haHelper->publishInputHeatingTarget(settings.system.unitSystem, heatingMinTemp, heatingMaxTemp, false);
this->haHelper->publishClimateHeating(
settings.system.unitSystem,
heatingMinTemp,
heatingMaxTemp,
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
);
published = true;
} else if (_isStupidMode != isStupidMode) {
_isStupidMode = isStupidMode;
this->haHelper->publishClimateHeating(
heatingMinTemp,
heatingMaxTemp,
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
noRegulators ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
);
published = true;
@@ -450,8 +453,8 @@ protected:
_dhwMinTemp = settings.dhw.minTemp;
_dhwMaxTemp = settings.dhw.maxTemp;
this->haHelper->publishNumberDhwTarget(settings.dhw.minTemp, settings.dhw.maxTemp, false);
this->haHelper->publishClimateDhw(settings.dhw.minTemp, settings.dhw.maxTemp);
this->haHelper->publishInputDhwTarget(settings.system.unitSystem, settings.dhw.minTemp, settings.dhw.maxTemp, false);
this->haHelper->publishClimateDhw(settings.system.unitSystem, settings.dhw.minTemp, settings.dhw.maxTemp);
published = true;
}
@@ -461,10 +464,10 @@ protected:
if (editableOutdoorTemp) {
this->haHelper->deleteSensorOutdoorTemp();
this->haHelper->publishNumberOutdoorTemp();
this->haHelper->publishInputOutdoorTemp(settings.system.unitSystem);
} else {
this->haHelper->deleteNumberOutdoorTemp();
this->haHelper->publishSensorOutdoorTemp();
this->haHelper->deleteInputOutdoorTemp();
this->haHelper->publishSensorOutdoorTemp(settings.system.unitSystem);
}
published = true;
@@ -475,10 +478,10 @@ protected:
if (editableIndoorTemp) {
this->haHelper->deleteSensorIndoorTemp();
this->haHelper->publishNumberIndoorTemp();
this->haHelper->publishInputIndoorTemp(settings.system.unitSystem);
} else {
this->haHelper->deleteNumberIndoorTemp();
this->haHelper->publishSensorIndoorTemp();
this->haHelper->deleteInputIndoorTemp();
this->haHelper->publishSensorIndoorTemp(settings.system.unitSystem);
}
published = true;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
#define PORTAL_CACHE_TIME "max-age=86400"
#define PORTAL_CACHE settings.system.debug ? nullptr : PORTAL_CACHE_TIME
#define PORTAL_CACHE (settings.system.logLevel >= TinyLogger::Level::TRACE ? nullptr : PORTAL_CACHE_TIME)
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WebServer.h>
#include <Updater.h>
@@ -14,7 +14,9 @@ using WebServer = ESP8266WebServer;
#include <UpgradeHandler.h>
#include <DNSServer.h>
extern Network::Manager* network;
using namespace NetworkUtils;
extern NetworkMgr* network;
extern FileData fsSettings, fsNetworkSettings;
extern MqttTask* tMqtt;
@@ -53,17 +55,19 @@ protected:
unsigned long webServerChangeState = 0;
unsigned long dnsServerChangeState = 0;
const char* getTaskName() {
#if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override {
return "Portal";
}
/*int getTaskCore() {
/*BaseType_t getTaskCore() override {
return 1;
}*/
int getTaskPriority() {
return 0;
int getTaskPriority() override {
return 1;
}
#endif
void setup() {
this->dnsServer->setTTL(0);
@@ -73,22 +77,34 @@ protected:
#endif
// index page
/*auto indexPage = (new DynamicPage("/", &LittleFS, "/index.html"))
/*auto indexPage = (new DynamicPage("/", &LittleFS, "/pages/index.html"))
->setTemplateCallback([](const char* var) -> String {
String result;
if (strcmp(var, "ver") == 0) {
result = PROJECT_VERSION;
result = BUILD_VERSION;
}
return result;
});
this->webServer->addHandler(indexPage);*/
this->webServer->addHandler(new StaticPage("/", &LittleFS, "/index.html", PORTAL_CACHE));
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->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
@@ -101,9 +117,9 @@ protected:
});
// network settings page
auto networkPage = (new StaticPage("/network.html", &LittleFS, "/network.html", PORTAL_CACHE))
auto networkPage = (new StaticPage("/network.html", &LittleFS, "/pages/network.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
@@ -113,9 +129,9 @@ protected:
this->webServer->addHandler(networkPage);
// settings page
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/settings.html", PORTAL_CACHE))
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/pages/settings.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
@@ -125,9 +141,9 @@ protected:
this->webServer->addHandler(settingsPage);
// upgrade page
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/upgrade.html", PORTAL_CACHE))
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/pages/upgrade.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
@@ -138,7 +154,7 @@ protected:
// OTA
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->sendHeader("Connection", "close");
this->webServer->send(401);
return false;
@@ -172,7 +188,7 @@ protected:
// backup
this->webServer->on("/api/backup/save", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -196,7 +212,7 @@ protected:
});
this->webServer->on("/api/backup/restore", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -209,7 +225,7 @@ protected:
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
} else if (plain.length() > 2536) {
this->webServer->send(413);
return;
}
@@ -234,6 +250,7 @@ protected:
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,
@@ -253,7 +270,7 @@ protected:
// network
this->webServer->on("/api/network/settings", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -267,7 +284,7 @@ protected:
});
this->webServer->on("/api/network/settings", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -298,12 +315,19 @@ protected:
doc.clear();
doc.shrinkToFit();
networkSettingsToJson(networkSettings, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
if (changed) {
this->webServer->send(201);
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,
@@ -312,33 +336,11 @@ protected:
networkSettings.staticConfig.dns
)
->reconnect();
} else {
this->webServer->send(200);
}
});
this->webServer->on("/api/network/status", HTTP_GET, [this]() {
bool isConnected = network->isConnected();
JsonDocument doc;
doc["hostname"] = networkSettings.hostname;
doc["mac"] = network->getStaMac();
doc["isConnected"] = isConnected;
doc["ssid"] = network->getStaSsid();
doc["signalQuality"] = isConnected ? Network::Manager::rssiToSignalQuality(network->getRssi()) : 0;
doc["channel"] = isConnected ? network->getStaChannel() : 0;
doc["ip"] = isConnected ? network->getStaIp().toString() : "";
doc["subnet"] = isConnected ? network->getStaSubnet().toString() : "";
doc["gateway"] = isConnected ? network->getStaGateway().toString() : "";
doc["dns"] = isConnected ? network->getStaDns().toString() : "";
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/network/scan", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
@@ -348,7 +350,11 @@ protected:
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);
@@ -359,10 +365,16 @@ protected:
for (short int i = 0; i < apCount; i++) {
String ssid = WiFi.SSID(i);
doc[i]["ssid"] = ssid;
doc[i]["signalQuality"] = Network::Manager::rssiToSignalQuality(WiFi.RSSI(i));
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();
doc[i]["encryptionType"] = WiFi.encryptionType(i);
#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();
@@ -374,7 +386,7 @@ protected:
// settings
this->webServer->on("/api/settings", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -388,7 +400,7 @@ protected:
});
this->webServer->on("/api/settings", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -401,7 +413,7 @@ protected:
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
} else if (plain.length() > 2536) {
this->webServer->send(413);
return;
}
@@ -419,12 +431,17 @@ protected:
doc.clear();
doc.shrinkToFit();
if (changed) {
fsSettings.update();
this->webServer->send(201);
settingsToJson(settings, doc);
doc.shrinkToFit();
} else {
this->webServer->send(200);
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
if (changed) {
doc.clear();
doc.shrinkToFit();
fsSettings.update();
tMqtt->resetPublishedSettingsTime();
}
});
@@ -433,24 +450,13 @@ protected:
this->webServer->on("/api/vars", HTTP_GET, [this]() {
JsonDocument doc;
varsToJson(vars, doc);
doc["system"]["version"] = PROJECT_VERSION;
doc["system"]["buildDate"] = __DATE__ " " __TIME__;
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();
doc["system"]["mqttConnected"] = tMqtt->isConnected();
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/vars", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
@@ -463,7 +469,7 @@ protected:
this->webServer->send(406);
return;
} else if (plain.length() > 1024) {
} else if (plain.length() > 1536) {
this->webServer->send(413);
return;
}
@@ -481,14 +487,155 @@ protected:
doc.clear();
doc.shrinkToFit();
if (changed) {
this->webServer->send(201);
varsToJson(vars, doc);
doc.shrinkToFit();
} else {
this->webServer->send(200);
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["system"]["resetReason"] = getResetReason();
doc["system"]["uptime"] = millis() / 1000ul;
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["build"]["version"] = BUILD_VERSION;
doc["build"]["date"] = __DATE__ " " __TIME__;
doc["build"]["env"] = BUILD_ENV;
doc["heap"]["total"] = getTotalHeap();
doc["heap"]["free"] = getFreeHeap();
doc["heap"]["minFree"] = getFreeHeap(true);
doc["heap"]["maxFreeBlock"] = getMaxFreeBlockHeap();
doc["heap"]["minMaxFreeBlock"] = getMaxFreeBlockHeap(true);
#ifdef ARDUINO_ARCH_ESP8266
doc["build"]["core"] = ESP.getCoreVersion();
doc["build"]["sdk"] = ESP.getSdkVersion();
doc["chip"]["model"] = esp_is_8285() ? "ESP8285" : "ESP8266";
doc["chip"]["rev"] = 0;
doc["chip"]["cores"] = 1;
doc["chip"]["freq"] = ESP.getCpuFreqMHz();
doc["flash"]["size"] = ESP.getFlashChipSize();
doc["flash"]["realSize"] = ESP.getFlashChipRealSize();
#elif ARDUINO_ARCH_ESP32
doc["build"]["core"] = ESP.getCoreVersion();
doc["build"]["sdk"] = ESP.getSdkVersion();
doc["chip"]["model"] = ESP.getChipModel();
doc["chip"]["rev"] = ESP.getChipRevision();
doc["chip"]["cores"] = ESP.getChipCores();
doc["chip"]["freq"] = ESP.getCpuFreqMHz();
doc["flash"]["size"] = ESP.getFlashChipSize();
doc["flash"]["realSize"] = doc["flash"]["size"];
#else
doc["build"]["core"] = 0;
doc["build"]["sdk"] = 0;
doc["chip"]["model"] = 0;
doc["chip"]["rev"] = 0;
doc["chip"]["cores"] = 0;
doc["chip"]["freq"] = 0;
doc["flash"]["size"] = 0;
doc["flash"]["realSize"] = 0;
#endif
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/debug", HTTP_GET, [this]() {
JsonDocument doc;
doc["build"]["version"] = BUILD_VERSION;
doc["build"]["date"] = __DATE__ " " __TIME__;
doc["build"]["env"] = BUILD_ENV;
doc["heap"]["total"] = getTotalHeap();
doc["heap"]["free"] = getFreeHeap();
doc["heap"]["minFree"] = getFreeHeap(true);
doc["heap"]["maxFreeBlock"] = getMaxFreeBlockHeap();
doc["heap"]["minMaxFreeBlock"] = getMaxFreeBlockHeap(true);
#if defined(ARDUINO_ARCH_ESP32)
auto reason = esp_reset_reason();
if (reason != ESP_RST_UNKNOWN && reason != ESP_RST_POWERON && reason != ESP_RST_SW) {
#elif defined(ARDUINO_ARCH_ESP8266)
auto reason = ESP.getResetInfoPtr()->reason;
if (reason != REASON_DEFAULT_RST && reason != REASON_SOFT_RESTART && reason != REASON_EXT_SYS_RST) {
#else
if (false) {
#endif
doc["crash"]["reason"] = getResetReason();
doc["crash"]["core"] = CrashRecorder::ext.core;
doc["crash"]["heap"] = CrashRecorder::ext.heap;
doc["crash"]["uptime"] = CrashRecorder::ext.uptime;
if (CrashRecorder::backtrace.length > 0 && CrashRecorder::backtrace.length <= CrashRecorder::backtraceMaxLength) {
String backtraceStr;
arr2str(backtraceStr, CrashRecorder::backtrace.data, CrashRecorder::backtrace.length);
doc["crash"]["backtrace"]["data"] = backtraceStr;
doc["crash"]["backtrace"]["continues"] = CrashRecorder::backtrace.continues;
}
if (CrashRecorder::epc.length > 0 && CrashRecorder::epc.length <= CrashRecorder::epcMaxLength) {
String epcStr;
arr2str(epcStr, CrashRecorder::epc.data, CrashRecorder::epc.length);
doc["crash"]["epc"] = epcStr;
}
}
#ifdef ARDUINO_ARCH_ESP8266
doc["build"]["core"] = ESP.getCoreVersion();
doc["build"]["sdk"] = ESP.getSdkVersion();
doc["chip"]["model"] = esp_is_8285() ? "ESP8285" : "ESP8266";
doc["chip"]["rev"] = 0;
doc["chip"]["cores"] = 1;
doc["chip"]["freq"] = ESP.getCpuFreqMHz();
doc["flash"]["size"] = ESP.getFlashChipSize();
doc["flash"]["realSize"] = ESP.getFlashChipRealSize();
#elif ARDUINO_ARCH_ESP32
doc["build"]["core"] = ESP.getCoreVersion();
doc["build"]["sdk"] = ESP.getSdkVersion();
doc["chip"]["model"] = ESP.getChipModel();
doc["chip"]["rev"] = ESP.getChipRevision();
doc["chip"]["cores"] = ESP.getChipCores();
doc["chip"]["freq"] = ESP.getCpuFreqMHz();
doc["flash"]["size"] = ESP.getFlashChipSize();
doc["flash"]["realSize"] = doc["flash"]["size"];
#else
doc["build"]["core"] = 0;
doc["build"]["sdk"] = 0;
doc["chip"]["model"] = 0;
doc["chip"]["rev"] = 0;
doc["chip"]["cores"] = 0;
doc["chip"]["freq"] = 0;
doc["flash"]["size"] = 0;
doc["flash"]["realSize"] = 0;
#endif
doc.shrinkToFit();
this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"debug.json\""));
this->bufferedWebServer->send(200, "application/json", doc, true);
});
// not found
this->webServer->onNotFound([this]() {
@@ -506,13 +653,17 @@ protected:
}
});
this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/favicon.ico", PORTAL_CACHE);
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) {
#ifdef ARDUINO_ARCH_ESP32
this->delay(250);
#endif
this->startWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected"));
@@ -557,10 +708,14 @@ protected:
if (this->stateWebServer()) {
this->webServer->handleClient();
}
if (!this->stateDnsServer() && !this->stateWebServer()) {
this->delay(250);
}
}
bool isNeedAuth() {
return !network->isApEnabled() && settings.portal.useAuth && strlen(settings.portal.password);
bool isAuthRequired() {
return !network->isApEnabled() && settings.portal.auth && strlen(settings.portal.password);
}
void onCaptivePortal() {
@@ -614,7 +769,7 @@ protected:
return;
}
this->webServer->handleClient();
//this->webServer->handleClient();
this->webServer->stop();
this->webServerEnabled = false;
this->webServerChangeState = millis();
@@ -639,7 +794,7 @@ protected:
return;
}
this->dnsServer->processNextRequest();
//this->dnsServer->processNextRequest();
this->dnsServer->stop();
this->dnsServerEnabled = false;
this->dnsServerChangeState = millis();

View File

@@ -1,10 +1,8 @@
#include <Equitherm.h>
#include <GyverPID.h>
#include <PIDtuner.h>
Equitherm etRegulator;
GyverPID pidRegulator(0, 0, 0);
PIDtuner pidTuner;
class RegulatorTask : public LeanTask {
@@ -12,142 +10,119 @@ 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() {
int getTaskPriority() override {
return 4;
}
#endif
void loop() {
byte newTemp = vars.parameters.heatingSetpoint;
if (!settings.pid.enable && fabs(pidRegulator.integral) > 0.01f) {
pidRegulator.integral = 0.0f;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
if (vars.states.emergency) {
if (settings.heating.turbo) {
if (!settings.heating.enable || vars.states.emergency || !vars.sensors.indoor.connected) {
settings.heating.turbo = false;
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
newTemp = getEmergencyModeTemp();
} else {
if (vars.tuning.enable || tunerInit) {
if (settings.heating.turbo) {
} else if (!settings.pid.enable && !settings.equitherm.enable) {
settings.heating.turbo = false;
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
newTemp = getTuningModeTemp();
if (newTemp == 0) {
vars.tuning.enable = false;
}
}
if (!vars.tuning.enable) {
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || !settings.heating.enable || (settings.equitherm.enable && settings.pid.enable))) {
} else if (fabs(settings.heating.target - vars.temperatures.indoor) <= 1.0f) {
settings.heating.turbo = false;
}
if (!settings.heating.turbo) {
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
newTemp = getNormalModeTemp();
}
}
// Ограничиваем, если до этого не ограничило
if (newTemp < settings.heating.minTemp || newTemp > settings.heating.maxTemp) {
newTemp = constrain(newTemp, settings.heating.minTemp, settings.heating.maxTemp);
}
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
float newTemp = vars.states.emergency
? settings.emergency.target
: this->getNormalModeTemp();
// 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 (fabs(vars.parameters.heatingSetpoint - newTemp) > 0.09f) {
vars.parameters.heatingSetpoint = newTemp;
}
}
byte getEmergencyModeTemp() {
float getNormalModeTemp() {
float newTemp = 0;
// if use equitherm
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != 1) {
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
prevEtResult = etResult;
newTemp += etResult;
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New emergency result: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
} else {
newTemp += prevEtResult;
}
} else if(settings.emergency.usePid && settings.sensors.indoor.type != 1) {
if (vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.heating.minTemp,
settings.heating.maxTemp
);
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
prevPidResult = pidResult;
newTemp += pidResult;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New emergency result: %hhu (%.2f)"), (uint8_t) round(pidResult), pidResult);
} else {
newTemp += prevPidResult;
}
} else if (!vars.parameters.heatingEnabled && prevPidResult != 0) {
newTemp += prevPidResult;
}
} else {
// default temp, manual mode
newTemp = settings.emergency.target;
}
return round(newTemp);
}
byte getNormalModeTemp() {
float newTemp = 0;
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001f) {
prevHeatingTarget = settings.heating.target;
Log.sinfoln(FPSTR(L_REGULATOR), F("New target: %.2f"), settings.heating.target);
if (settings.equitherm.enable && settings.pid.enable) {
pidRegulator.integral = 0;
/*if (settings.pid.enable) {
pidRegulator.integral = 0.0f;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
}*/
}
// if use equitherm
if (settings.equitherm.enable) {
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
unsigned short minTemp = settings.heating.minTemp;
unsigned short maxTemp = settings.heating.maxTemp;
float targetTemp = settings.heating.target;
float indoorTemp = vars.temperatures.indoor;
float outdoorTemp = vars.temperatures.outdoor;
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
minTemp = f2c(minTemp);
maxTemp = f2c(maxTemp);
targetTemp = f2c(targetTemp);
indoorTemp = f2c(indoorTemp);
outdoorTemp = f2c(outdoorTemp);
}
if (!vars.sensors.indoor.connected || settings.pid.enable) {
etRegulator.Kt = 0.0f;
etRegulator.indoorTemp = 0.0f;
} else {
etRegulator.Kt = settings.heating.turbo ? 0.0f : settings.equitherm.t_factor;
etRegulator.indoorTemp = indoorTemp;
}
etRegulator.setLimits(minTemp, maxTemp);
etRegulator.Kn = settings.equitherm.n_factor;
etRegulator.Kk = settings.equitherm.k_factor;
etRegulator.targetTemp = targetTemp;
etRegulator.outdoorTemp = outdoorTemp;
float etResult = etRegulator.getResult();
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
etResult = c2f(etResult);
}
if (fabs(prevEtResult - etResult) > 0.09f) {
prevEtResult = etResult;
newTemp += etResult;
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %.2f"), etResult);
} else {
newTemp += prevEtResult;
@@ -156,191 +131,55 @@ protected:
// if use pid
if (settings.pid.enable) {
if (vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
settings.pid.maxTemp
);
//if (vars.parameters.heatingEnabled) {
if (settings.heating.enable && vars.sensors.indoor.connected) {
pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor;
pidRegulator.Kd = settings.pid.d_factor;
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp);
pidRegulator.setDt(settings.pid.dt * 1000u);
pidRegulator.input = vars.temperatures.indoor;
pidRegulator.setpoint = settings.heating.target;
if (fabs(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) {
pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.integral = 0.0f;
pidRegulator.getResultNow();
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
float pidResult = pidRegulator.getResultTimer();
if (fabs(prevPidResult - pidResult) > 0.09f) {
prevPidResult = pidResult;
newTemp += pidResult;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %hhd (%.2f)"), (int8_t) round(pidResult), pidResult);
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %.2f"), pidResult);
Log.straceln(FPSTR(L_REGULATOR_PID), F("Integral: %.2f"), pidRegulator.integral);
} else {
newTemp += prevPidResult;
}
} else {
newTemp += prevPidResult;
}
}
// Turbo mode
if (settings.heating.turbo && (settings.equitherm.enable || settings.pid.enable)) {
newTemp += constrain(
settings.heating.target - vars.temperatures.indoor,
-3.0f,
3.0f
) * settings.heating.turboFactor;
}
// default temp, manual mode
if (!settings.equitherm.enable && !settings.pid.enable) {
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;
Log.sinfoln("REGULATOR.TUNING", F("Stopped"));
}
if (!vars.tuning.enable) {
return 0;
}
if (vars.tuning.regulator == 0) {
// @TODO дописать
Log.sinfoln("REGULATOR.TUNING.EQUITHERM", F("Not implemented"));
return 0;
} else if (vars.tuning.regulator == 1) {
// PID tuner
float defaultTemp = settings.equitherm.enable
? getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp)
: settings.heating.target;
if (tunerInit && pidTuner.getState() == 3) {
Log.sinfoln("REGULATOR.TUNING.PID", F("Finished"));
for (Stream* stream : Log.getStreams()) {
pidTuner.debugText(stream);
}
pidTuner.reset();
tunerInit = false;
tunerRegulator = 0;
tunerState = 0;
if (pidTuner.getAccuracy() < 90) {
Log.swarningln("REGULATOR.TUNING.PID", F("Bad result, try again..."));
} else {
settings.pid.p_factor = pidTuner.getPID_p();
settings.pid.i_factor = pidTuner.getPID_i();
settings.pid.d_factor = pidTuner.getPID_d();
return 0;
}
}
if (!tunerInit) {
Log.sinfoln("REGULATOR.TUNING.PID", F("Start..."));
float step;
if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) {
step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2);
} else {
step = 5.0f;
}
float startTemp = step;
Log.sinfoln("REGULATOR.TUNING.PID", F("Started. Start value: %f, step: %f"), startTemp, step);
pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000);
tunerInit = true;
tunerRegulator = 1;
}
pidTuner.setInput(vars.temperatures.indoor);
pidTuner.compute();
if (tunerState > 0 && pidTuner.getState() != tunerState) {
Log.sinfoln("REGULATOR.TUNING.PID", F("Log:"));
for (Stream* stream : Log.getStreams()) {
pidTuner.debugText(stream);
}
tunerState = pidTuner.getState();
}
return round(defaultTemp + pidTuner.getOutput());
} else {
return 0;
}
}
float getEquithermTemp(int minTemp, int maxTemp) {
if (vars.states.emergency) {
etRegulator.Kt = 0;
etRegulator.indoorTemp = 0;
etRegulator.outdoorTemp = vars.temperatures.outdoor;
} else if (settings.pid.enable) {
etRegulator.Kt = 0;
etRegulator.indoorTemp = round(vars.temperatures.indoor);
etRegulator.outdoorTemp = round(vars.temperatures.outdoor);
} else {
if (settings.heating.turbo) {
etRegulator.Kt = 10;
} else {
etRegulator.Kt = settings.equitherm.t_factor;
}
etRegulator.indoorTemp = vars.temperatures.indoor;
etRegulator.outdoorTemp = vars.temperatures.outdoor;
}
etRegulator.setLimits(minTemp, maxTemp);
etRegulator.Kn = settings.equitherm.n_factor;
// etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1);
etRegulator.Kk = settings.equitherm.k_factor;
etRegulator.targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target;
return etRegulator.getResult();
}
float getPidTemp(int minTemp, int maxTemp) {
pidRegulator.Kp = settings.pid.p_factor;
pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.Kd = settings.pid.d_factor;
pidRegulator.setLimits(minTemp, maxTemp);
pidRegulator.setDt(settings.pid.dt * 1000u);
pidRegulator.input = vars.temperatures.indoor;
pidRegulator.setpoint = settings.heating.target;
return pidRegulator.getResultTimer();
}
float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) {
static uint32_t _prevIteration = millis();
if (abs(currentTemp - setTemp) < accurateStepAfter) {
if (millis() - _prevIteration < (accurateInterval * 1000)) {
return ratio;
}
if (currentTemp - setTemp > 0.1f) {
ratio -= accurateStep;
} else if (currentTemp - setTemp < -0.1f) {
ratio += accurateStep;
}
} else {
if (millis() - _prevIteration < (dirtyInterval * 1000)) {
return ratio;
}
ratio = ratio * (setTemp / currentTemp);
}
_prevIteration = millis();
return ratio;
}
};

View File

@@ -32,182 +32,564 @@ protected:
DallasTemperature* indoorSensor = nullptr;
bool initOutdoorSensor = false;
unsigned long initOutdoorSensorTime = 0;
unsigned long startOutdoorConversionTime = 0;
float filteredOutdoorTemp = 0;
bool emptyOutdoorTemp = true;
float prevFilteredOutdoorTemp = 0;
bool initIndoorSensor = false;
unsigned long initIndoorSensorTime = 0;
unsigned long startIndoorConversionTime = 0;
float filteredIndoorTemp = 0;
bool emptyIndoorTemp = true;
float prevFilteredIndoorTemp = 0;
#if USE_BLE
BLEClient* pBleClient = nullptr;
bool initBleSensor = false;
bool initBleNotify = false;
#endif
#if defined(ARDUINO_ARCH_ESP32)
#if USE_BLE
unsigned long outdoorConnectedTime = 0;
unsigned long indoorConnectedTime = 0;
#endif
const char* getTaskName() {
const char* getTaskName() override {
return "Sensors";
}
/*int getTaskCore() {
return 1;
}*/
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() {
int getTaskPriority() override {
return 4;
}
#endif
void loop() {
if (settings.sensors.outdoor.type == 2 && settings.sensors.outdoor.pin) {
outdoorTemperatureSensor();
#if USE_BLE
if (!NimBLEDevice::getInitialized() && millis() > 5000) {
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE"));
BLEDevice::init("");
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
}
#endif
if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.outdoor.gpio)) {
outdoorDallasSensor();
}
#if USE_BLE
else if (settings.sensors.outdoor.type == SensorType::BLUETOOTH) {
bool connected = this->bluetoothSensor(
BLEAddress(settings.sensors.outdoor.bleAddress),
&vars.sensors.outdoor.rssi,
&this->filteredOutdoorTemp,
&vars.sensors.outdoor.humidity,
&vars.sensors.outdoor.battery
);
if (connected) {
this->outdoorConnectedTime = millis();
vars.sensors.outdoor.connected = true;
} else if (millis() - this->outdoorConnectedTime > 60000) {
vars.sensors.outdoor.connected = false;
}
}
#endif
if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
indoorDallasSensor();
}
#if USE_BLE
else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) {
bool connected = this->bluetoothSensor(
BLEAddress(settings.sensors.indoor.bleAddress),
&vars.sensors.indoor.rssi,
&this->filteredIndoorTemp,
&vars.sensors.indoor.humidity,
&vars.sensors.indoor.battery
);
if (connected) {
this->indoorConnectedTime = millis();
vars.sensors.indoor.connected = true;
} else if (millis() - this->indoorConnectedTime > 60000) {
vars.sensors.indoor.connected = false;
}
}
#endif
// convert
if (fabs(this->prevFilteredOutdoorTemp - this->filteredOutdoorTemp) >= 0.1f) {
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 (settings.sensors.indoor.type == 2 && settings.sensors.indoor.pin) {
indoorTemperatureSensor();
}
#if USE_BLE
if (settings.sensors.indoor.type == 3) {
bluetoothSensor();
}
#endif
if (fabs(vars.temperatures.outdoor - this->filteredOutdoorTemp) > 0.099) {
vars.temperatures.outdoor = this->filteredOutdoorTemp + settings.sensors.outdoor.offset;
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 (fabs(vars.temperatures.indoor - this->filteredIndoorTemp) > 0.099) {
vars.temperatures.indoor = this->filteredIndoorTemp + settings.sensors.indoor.offset;
this->prevFilteredOutdoorTemp = this->filteredOutdoorTemp;
}
if (fabs(this->prevFilteredIndoorTemp - this->filteredIndoorTemp) > 0.1f) {
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);
}
this->prevFilteredIndoorTemp = this->filteredIndoorTemp;
}
}
#if USE_BLE
void bluetoothSensor() {
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;
bool bluetoothSensor(const BLEAddress& address, int8_t* const pRssi, float* const pTemperature, float* const pHumidity = nullptr, float* const pBattery = nullptr) {
if (!NimBLEDevice::getInitialized()) {
return false;
}
if (!initBleSensor || pBleClient->isConnected()) {
return;
NimBLEClient* pClient = nullptr;
pClient = NimBLEDevice::getClientByPeerAddress(address);
if (pClient == nullptr) {
pClient = NimBLEDevice::getDisconnectedClient();
}
// Reset init notify flag
this->initBleNotify = false;
// Connect to the remote BLE Server.
BLEAddress bleServerAddress(settings.sensors.indoor.bleAddresss);
if (!pBleClient->connect(bleServerAddress)) {
Log.swarningln(FPSTR(L_SENSORS_BLE), "Failed connecting to device at %s", bleServerAddress.toString().c_str());
return;
if (pClient == nullptr) {
if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
return false;
}
Log.sinfoln(FPSTR(L_SENSORS_BLE), "Connected to device at %s", bleServerAddress.toString().c_str());
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;
pClient = NimBLEDevice::createClient();
pClient->setConnectTimeout(5);
}
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found service UUID: %s"), serviceUUID.toString().c_str());
if(pClient->isConnected()) {
*pRssi = pClient->getRssi();
return true;
}
if (!pClient->connect(address)) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Device %s: failed connecting"), address.toString().c_str());
NimBLEDevice::deleteClient(pClient);
return false;
}
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Device %s: connected"), address.toString().c_str());
NimBLERemoteService* pService = nullptr;
NimBLERemoteCharacteristic* pChar = nullptr;
// ENV Service (0x181A)
NimBLEUUID serviceUuid((uint16_t) 0x181AU);
pService = pClient->getService(serviceUuid);
if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to find env service (%s)"),
address.toString().c_str(),
serviceUuid.toString().c_str()
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found env service (%s)"),
address.toString().c_str(),
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());
bool tempNotifyCreated = false;
if (!tempNotifyCreated) {
NimBLEUUID charUuid((uint16_t) 0x2A6E);
pChar = pService->getCharacteristic(charUuid);
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"));
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found temperature char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01);
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;
NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) {
return;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) {
return;
}
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at temperature char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw temp %f"),
pClient->getPeerAddress().toString().c_str(),
rawTemp
);
if (fabs(*pTemperature) < 0.1f) {
*pTemperature = rawTemp;
} else {
*pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K;
}
*pTemperature = floor((*pTemperature) * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
if (tempNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to temperature char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to temperature char (%s) in env service"),
address.toString().c_str(),
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());
if (!tempNotifyCreated) {
NimBLEUUID charUuid((uint16_t) 0x2A1F);
pChar = pService->getCharacteristic(charUuid);
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"));
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found temperature char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
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;
NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) {
return;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) {
return;
}
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at temperature char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw temp %f"),
pClient->getPeerAddress().toString().c_str(),
rawTemp
);
if (fabs(*pTemperature) < 0.1f) {
*pTemperature = rawTemp;
} else {
*pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K;
}
*pTemperature = floor((*pTemperature) * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
if (tempNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to temperature char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to temperature char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
}
}
}
if (!this->initBleNotify) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics"));
pBleClient->disconnect();
if (!tempNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: not found supported temperature chars in env service"),
address.toString().c_str()
);
pClient->disconnect();
return false;
}
// 0x2A6F - Notify about humidity x0.01% (pvvx)
if (pHumidity != nullptr) {
bool humidityNotifyCreated = false;
if (!humidityNotifyCreated) {
NimBLEUUID charUuid((uint16_t) 0x2A6F);
pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found humidity char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
humidityNotifyCreated = pChar->subscribe(true, [pHumidity](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) {
return;
}
NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) {
return;
}
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at humidity char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawHumidity = ((pData[0] | (pData[1] << 8)) * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw humidity %f"),
pClient->getPeerAddress().toString().c_str(),
rawHumidity
);
if (fabs(*pHumidity) < 0.1f) {
*pHumidity = rawHumidity;
} else {
*pHumidity += (rawHumidity - (*pHumidity)) * EXT_SENSORS_FILTER_K;
}
*pHumidity = floor((*pHumidity) * 100) / 100;
});
if (humidityNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to humidity char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to humidity char (%s) in env service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
}
}
}
if (!humidityNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: not found supported humidity chars in env service"),
address.toString().c_str()
);
}
}
}
// Battery Service (0x180F)
if (pBattery != nullptr) {
NimBLEUUID serviceUuid((uint16_t) 0x180F);
pService = pClient->getService(serviceUuid);
if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to find battery service (%s)"),
address.toString().c_str(),
serviceUuid.toString().c_str()
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found battery service (%s)"),
address.toString().c_str(),
serviceUuid.toString().c_str()
);
// 0x2A19 - Notify the battery charge level 0..99% (pvvx)
bool batteryNotifyCreated = false;
if (!batteryNotifyCreated) {
NimBLEUUID charUuid((uint16_t) 0x2A19);
pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found battery char (%s) in battery service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
batteryNotifyCreated = pChar->subscribe(true, [pBattery](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) {
return;
}
NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) {
return;
}
if (length != 1) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at battery char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
uint8_t rawBattery = pData[0];
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw battery %hhu"),
pClient->getPeerAddress().toString().c_str(),
rawBattery
);
if (fabs(*pBattery) < 0.1f) {
*pBattery = rawBattery;
} else {
*pBattery += (rawBattery - (*pBattery)) * EXT_SENSORS_FILTER_K;
}
*pBattery = floor((*pBattery) * 100) / 100;
});
if (batteryNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to battery char (%s) in battery service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to battery char (%s) in battery service"),
address.toString().c_str(),
charUuid.toString().c_str()
);
}
}
}
if (!batteryNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: not found supported battery chars in battery service"),
address.toString().c_str()
);
}
}
}
return true;
}
#endif
void outdoorTemperatureSensor() {
void outdoorDallasSensor() {
if (!this->initOutdoorSensor) {
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Starting on gpio %hhu..."), settings.sensors.outdoor.pin);
if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return;
}
this->oneWireOutdoorSensor->begin(settings.sensors.outdoor.pin);
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),
@@ -225,6 +607,10 @@ protected:
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started"));
} else {
if (vars.sensors.outdoor.connected) {
vars.sensors.outdoor.connected = false;
}
return;
}
}
@@ -254,9 +640,12 @@ protected:
} else {
Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyOutdoorTemp) {
if (!vars.sensors.outdoor.connected) {
vars.sensors.outdoor.connected = true;
}
if (fabs(this->filteredOutdoorTemp) < 0.1f) {
this->filteredOutdoorTemp = rawTemp;
this->emptyOutdoorTemp = false;
} else {
this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
@@ -268,12 +657,18 @@ protected:
}
}
void indoorTemperatureSensor() {
void indoorDallasSensor() {
if (!this->initIndoorSensor) {
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Starting on gpio %hhu..."), settings.sensors.indoor.pin);
if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return;
}
this->oneWireIndoorSensor->begin(settings.sensors.indoor.pin);
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),
@@ -291,6 +686,10 @@ protected:
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started"));
} else {
if (vars.sensors.indoor.connected) {
vars.sensors.indoor.connected = false;
}
return;
}
}
@@ -320,9 +719,12 @@ protected:
} else {
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
if (!vars.sensors.indoor.connected) {
vars.sensors.indoor.connected = true;
}
if (fabs(this->filteredIndoorTemp) < 0.1f) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;

View File

@@ -1,5 +1,5 @@
struct NetworkSettings {
char hostname[25] = HOSTNAME_DEFAULT;
char hostname[25] = DEFAULT_HOSTNAME;
bool useDhcp = true;
struct {
@@ -10,35 +10,53 @@ struct NetworkSettings {
} staticConfig;
struct {
char ssid[33] = AP_SSID_DEFAULT;
char password[65] = AP_PASSWORD_DEFAULT;
char ssid[33] = DEFAULT_AP_SSID;
char password[65] = DEFAULT_AP_PASSWORD;
byte channel = 6;
} ap;
struct {
char ssid[33] = STA_SSID_DEFAULT;
char password[65] = STA_PASSWORD_DEFAULT;
char ssid[33] = DEFAULT_STA_SSID;
char password[65] = DEFAULT_STA_PASSWORD;
byte channel = 0;
} sta;
} networkSettings;
struct Settings {
struct {
bool debug = DEBUG_BY_DEFAULT;
bool useSerial = USE_SERIAL;
bool useTelnet = USE_TELNET;
uint8_t logLevel = DEFAULT_LOG_LEVEL;
struct {
bool enable = DEFAULT_SERIAL_ENABLE;
unsigned int baudrate = DEFAULT_SERIAL_BAUD;
} serial;
struct {
bool enable = DEFAULT_TELNET_ENABLE;
unsigned short port = DEFAULT_TELNET_PORT;
} telnet;
UnitSystem unitSystem = UnitSystem::METRIC;
byte statusLedGpio = DEFAULT_STATUS_LED_GPIO;
} system;
struct {
bool useAuth = false;
char login[13] = PORTAL_LOGIN_DEFAULT;
char password[33] = PORTAL_PASSWORD_DEFAULT;
bool auth = false;
char login[13] = DEFAULT_PORTAL_LOGIN;
char password[33] = DEFAULT_PORTAL_PASSWORD;
} portal;
struct {
byte inPin = OT_IN_PIN_DEFAULT;
byte outPin = OT_OUT_PIN_DEFAULT;
UnitSystem unitSystem = UnitSystem::METRIC;
byte inGpio = DEFAULT_OT_IN_GPIO;
byte outGpio = DEFAULT_OT_OUT_GPIO;
byte rxLedGpio = DEFAULT_OT_RX_LED_GPIO;
unsigned int memberIdCode = 0;
uint8_t maxModulation = 100;
float pressureFactor = 1.0f;
float dhwFlowRateFactor = 1.0f;
float minPower = 0.0f;
float maxPower = 0.0f;
bool dhwPresent = true;
bool summerWinterMode = false;
bool heatingCh2Enabled = true;
@@ -46,49 +64,57 @@ struct Settings {
bool dhwToCh2 = false;
bool dhwBlocking = false;
bool modulationSyncWithHeating = false;
bool getMinMaxTemp = true;
bool nativeHeatingControl = false;
bool immergasFix = false;
struct {
bool enable = false;
float factor = 0.1f;
} filterNumValues;
} opentherm;
struct {
char server[81] = MQTT_SERVER_DEFAULT;
unsigned short port = MQTT_PORT_DEFAULT;
char user[33] = MQTT_USER_DEFAULT;
char password[33] = MQTT_PASSWORD_DEFAULT;
char prefix[33] = MQTT_PREFIX_DEFAULT;
bool 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;
bool useEquitherm = false;
bool usePid = false;
float target = DEFAULT_HEATING_TARGET_TEMP;
unsigned short tresholdTime = 120;
} emergency;
struct {
bool enable = true;
bool turbo = false;
float target = 40.0f;
float target = DEFAULT_HEATING_TARGET_TEMP;
float hysteresis = 0.5f;
float turboFactor = 7.5f;
byte minTemp = DEFAULT_HEATING_MIN_TEMP;
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
byte maxModulation = 100;
} heating;
struct {
bool enable = true;
byte target = 40;
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 = 50;
float i_factor = 0.006f;
float d_factor = 10000;
float p_factor = 2.0f;
float i_factor = 0.0055f;
float d_factor = 0.0f;
unsigned short dt = 180;
byte minTemp = 0;
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
short minTemp = 0;
short maxTemp = DEFAULT_HEATING_MAX_TEMP;
} pid;
struct {
@@ -100,38 +126,51 @@ struct Settings {
struct {
struct {
// 0 - boiler, 1 - manual, 2 - ds18b20
byte type = 0;
byte pin = SENSOR_OUTDOOR_PIN_DEFAULT;
SensorType type = SensorType::BOILER_OUTDOOR;
byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO;
uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float offset = 0.0f;
} outdoor;
struct {
// 1 - manual, 2 - ds18b20, 3 - ble
byte type = 1;
byte pin = SENSOR_INDOOR_PIN_DEFAULT;
uint8_t bleAddresss[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
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 pin = EXT_PUMP_PIN_DEFAULT;
byte gpio = DEFAULT_EXT_PUMP_GPIO;
unsigned short postCirculationTime = 600;
unsigned int antiStuckInterval = 2592000;
unsigned short antiStuckTime = 300;
} externalPump;
struct {
struct {
bool enable = false;
byte gpio = GPIO_IS_NOT_CONFIGURED;
byte invertState = false;
unsigned short thresholdTime = 60;
} input;
struct {
bool enable = false;
byte gpio = GPIO_IS_NOT_CONFIGURED;
byte invertState = false;
unsigned short thresholdTime = 60;
bool onFault = true;
bool onLossConnection = true;
bool onEnabledHeating = false;
} output;
} cascadeControl;
char validationValue[8] = SETTINGS_VALID_VALUE;
} settings;
struct Variables {
struct {
bool enable = false;
byte regulator = 0;
} tuning;
struct {
bool otStatus = false;
bool emergency = false;
@@ -141,32 +180,58 @@ struct Variables {
bool fault = false;
bool diagnostic = false;
bool externalPump = false;
bool mqtt = false;
} states;
struct {
float modulation = 0.0f;
float pressure = 0.0f;
float dhwFlowRate = 0.0f;
float power = 0.0f;
byte faultCode = 0;
unsigned short diagnosticCode = 0;
int8_t rssi = 0;
struct {
bool connected = false;
int8_t rssi = 0;
float battery = 0.0f;
float humidity = 0.0f;
} outdoor;
struct {
bool connected = false;
int8_t rssi = 0;
float battery = 0.0f;
float humidity = 0.0f;
} indoor;
} 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 {
bool input = false;
bool output = false;
} cascadeControl;
struct {
bool heatingEnabled = false;
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
byte heatingSetpoint = 0;
float heatingSetpoint = 0;
unsigned long extPumpLastEnableTime = 0;
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
byte minModulation = 0;
byte maxModulation = 0;
uint8_t maxPower = 0;
uint8_t slaveMemberId = 0;
uint8_t slaveFlags = 0;
uint8_t slaveType = 0;

View File

@@ -1,115 +1,162 @@
#define PROJECT_NAME "OpenTherm Gateway"
#define PROJECT_VERSION "1.4.0-rc.15"
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
#define EMERGENCY_TIME_TRESHOLD 120000
#define MQTT_RECONNECT_INTERVAL 15000
#define MQTT_KEEPALIVE 30
#define OPENTHERM_OFFLINE_TRESHOLD 10
#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 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 WM_DEBUG_MODE
#define WM_DEBUG_MODE WM_DEBUG_NOTIFY
#ifndef BUILD_VERSION
#define BUILD_VERSION "0.0.0"
#endif
#ifndef USE_SERIAL
#define USE_SERIAL true
#ifndef BUILD_ENV
#define BUILD_ENV "undefined"
#endif
#ifndef USE_TELNET
#define USE_TELNET true
#ifndef DEFAULT_SERIAL_ENABLE
#define DEFAULT_SERIAL_ENABLE true
#endif
#ifndef DEFAULT_SERIAL_BAUD
#define DEFAULT_SERIAL_BAUD 115200
#endif
#ifndef DEFAULT_TELNET_ENABLE
#define DEFAULT_TELNET_ENABLE true
#endif
#ifndef DEFAULT_TELNET_PORT
#define DEFAULT_TELNET_PORT 23
#endif
#ifndef USE_BLE
#define USE_BLE false
#endif
#ifndef HOSTNAME_DEFAULT
#define HOSTNAME_DEFAULT "opentherm"
#ifndef DEFAULT_HOSTNAME
#define DEFAULT_HOSTNAME "opentherm"
#endif
#ifndef AP_SSID_DEFAULT
#define AP_SSID_DEFAULT "OpenTherm Gateway"
#ifndef DEFAULT_AP_SSID
#define DEFAULT_AP_SSID "OpenTherm Gateway"
#endif
#ifndef AP_PASSWORD_DEFAULT
#define AP_PASSWORD_DEFAULT "otgateway123456"
#ifndef DEFAULT_AP_PASSWORD
#define DEFAULT_AP_PASSWORD "otgateway123456"
#endif
#ifndef STA_SSID_DEFAULT
#define STA_SSID_DEFAULT ""
#ifndef DEFAULT_STA_SSID
#define DEFAULT_STA_SSID ""
#endif
#ifndef STA_PASSWORD_DEFAULT
#define STA_PASSWORD_DEFAULT ""
#ifndef DEFAULT_STA_PASSWORD
#define DEFAULT_STA_PASSWORD ""
#endif
#ifndef DEBUG_BY_DEFAULT
#define DEBUG_BY_DEFAULT false
#ifndef DEFAULT_LOG_LEVEL
#define DEFAULT_LOG_LEVEL TinyLogger::Level::VERBOSE
#endif
#ifndef PORTAL_LOGIN_DEFAULT
#define PORTAL_LOGIN_DEFAULT ""
#ifndef DEFAULT_STATUS_LED_GPIO
#define DEFAULT_STATUS_LED_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef PORTAL_PASSWORD_DEFAULT
#define PORTAL_PASSWORD_DEFAULT ""
#ifndef DEFAULT_PORTAL_LOGIN
#define DEFAULT_PORTAL_LOGIN ""
#endif
#ifndef MQTT_SERVER_DEFAULT
#define MQTT_SERVER_DEFAULT ""
#ifndef DEFAULT_PORTAL_PASSWORD
#define DEFAULT_PORTAL_PASSWORD ""
#endif
#ifndef MQTT_PORT_DEFAULT
#define MQTT_PORT_DEFAULT 1883
#ifndef DEFAULT_MQTT_SERVER
#define DEFAULT_MQTT_SERVER ""
#endif
#ifndef MQTT_USER_DEFAULT
#define MQTT_USER_DEFAULT ""
#ifndef DEFAULT_MQTT_PORT
#define DEFAULT_MQTT_PORT 1883
#endif
#ifndef MQTT_PASSWORD_DEFAULT
#define MQTT_PASSWORD_DEFAULT ""
#ifndef DEFAULT_MQTT_USER
#define DEFAULT_MQTT_USER ""
#endif
#ifndef MQTT_PREFIX_DEFAULT
#define MQTT_PREFIX_DEFAULT "opentherm"
#ifndef DEFAULT_MQTT_PASSWORD
#define DEFAULT_MQTT_PASSWORD ""
#endif
#ifndef OT_IN_PIN_DEFAULT
#define OT_IN_PIN_DEFAULT 0
#ifndef DEFAULT_MQTT_PREFIX
#define DEFAULT_MQTT_PREFIX "opentherm"
#endif
#ifndef OT_OUT_PIN_DEFAULT
#define OT_OUT_PIN_DEFAULT 0
#ifndef DEFAULT_OT_IN_GPIO
#define DEFAULT_OT_IN_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef SENSOR_OUTDOOR_PIN_DEFAULT
#define SENSOR_OUTDOOR_PIN_DEFAULT 0
#ifndef DEFAULT_OT_OUT_GPIO
#define DEFAULT_OT_OUT_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef SENSOR_INDOOR_PIN_DEFAULT
#define SENSOR_INDOOR_PIN_DEFAULT 0
#ifndef DEFAULT_OT_RX_LED_GPIO
#define DEFAULT_OT_RX_LED_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef EXT_PUMP_PIN_DEFAULT
#define EXT_PUMP_PIN_DEFAULT 0
#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_OUTDOOR = 0,
BOILER_RETURN = 4,
MANUAL = 1,
DS18B20 = 2,
BLUETOOTH = 3
};
enum class UnitSystem : byte {
METRIC,
IMPERIAL
};
char buffer[255];

View File

@@ -1,14 +1,15 @@
#include <Arduino.h>
#include "defines.h"
#include "strings.h"
#include "CrashRecorder.h"
#include <ArduinoJson.h>
#include <FileData.h>
#include <LittleFS.h>
#include "ESPTelnetStream.h"
#include <ESPTelnetStream.h>
#include <TinyLogger.h>
#include <NetworkManager.h>
#include <NetworkMgr.h>
#include "Settings.h"
#include <utils.h>
#include "utils.h"
#if defined(ARDUINO_ARCH_ESP32)
#include <ESP32Scheduler.h>
@@ -27,11 +28,13 @@
#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;
Network::Manager* network = nullptr;
NetworkMgr* network = nullptr;
// Tasks
MqttTask* tMqtt;
@@ -43,6 +46,7 @@ MainTask* tMain;
void setup() {
CrashRecorder::init();
LittleFS.begin();
Log.setLevel(TinyLogger::Level::VERBOSE);
@@ -109,41 +113,53 @@ void setup() {
}
// logs
if (!settings.system.useSerial) {
Log.clearStreams();
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.useTelnet) {
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);
if (settings.system.logLevel >= TinyLogger::Level::SILENT && settings.system.logLevel <= TinyLogger::Level::VERBOSE) {
Log.setLevel(static_cast<TinyLogger::Level>(settings.system.logLevel));
}
// network
network = (new Network::Manager)
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, 750);
tOt = new OpenThermTask(true, 750);
Scheduler.start(tOt);
tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL);

View File

@@ -4,6 +4,9 @@
#endif
const char L_SETTINGS[] PROGMEM = "SETTINGS";
const char L_SETTINGS_OT[] PROGMEM = "SETTINGS.OT";
const char L_SETTINGS_DHW[] PROGMEM = "SETTINGS.DHW";
const char L_SETTINGS_HEATING[] PROGMEM = "SETTINGS.HEATING";
const char L_NETWORK[] PROGMEM = "NETWORK";
const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS";
const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER";
@@ -22,3 +25,5 @@ const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
const char L_REGULATOR[] PROGMEM = "REGULATOR";
const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID";
const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM";
const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT";
const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT";

File diff suppressed because it is too large Load Diff

BIN
src_data/fonts/iconly.eot Normal file

Binary file not shown.

78
src_data/fonts/iconly.svg Normal file
View 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="&#xE000;"
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="&#x70;&#x6C;&#x75;&#x73;"
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="&#xE001;"
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="&#x6D;&#x69;&#x6E;&#x75;&#x73;"
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="&#xE002;"
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="&#x75;&#x6E;&#x6C;&#x6F;&#x63;&#x6B;&#x65;&#x64;"
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="&#xE003;"
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="&#x6C;&#x6F;&#x63;&#x6B;&#x65;&#x64;"
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="&#xE004;"
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="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x31;"
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="&#xE005;"
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="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x30;"
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="&#xE006;"
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="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x32;"
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="&#xE008;"
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="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x33;"
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="&#xE009;"
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="&#x64;&#x6F;&#x77;&#x6E;"
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="&#xE00A;"
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="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x34;"
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="&#xE00C;"
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="&#x75;&#x70;"
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

Binary file not shown.

BIN
src_data/fonts/iconly.woff Normal file

Binary file not shown.

BIN
src_data/fonts/iconly.woff2 Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

385
src_data/locales/en.json Normal file
View File

@@ -0,0 +1,385 @@
{
"values": {
"logo": "OpenTherm Gateway",
"nav": {
"license": "License",
"source": "Source code",
"help": "Help",
"issues": "Issues & questions",
"releases": "Releases"
},
"dbm": "dBm",
"kw": "kW",
"time": {
"days": "d.",
"hours": "h.",
"min": "min.",
"sec": "sec."
},
"button": {
"upgrade": "Upgrade",
"restart": "Restart",
"save": "Save",
"saved": "Saved",
"refresh": "Refresh",
"restore": "Restore",
"restored": "Restored",
"backup": "Backup",
"wait": "Please wait...",
"uploading": "Uploading...",
"success": "Success",
"error": "Error"
},
"index": {
"title": "OpenTherm Gateway",
"section": {
"network": "Network",
"system": "System"
},
"system": {
"build": {
"title": "Build",
"version": "Version",
"date": "Date",
"core": "Core",
"sdk": "SDK"
},
"uptime": "Uptime",
"memory": {
"title": "Free memory",
"maxFreeBlock": "max free block",
"min": "min"
},
"board": "Board",
"chip": {
"model": "Chip",
"cores": "Cores",
"freq": "frequency"
},
"flash": {
"size": "Flash size",
"realSize": "real size"
},
"lastResetReason": "Last reset reason"
}
},
"dashboard": {
"name": "Dashboard",
"title": "Dashboard - OpenTherm Gateway",
"section": {
"control": "Control",
"states": "States 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",
"outdoorSensorConnected": "Outdoor sensor connected",
"outdoorSensorRssi": "Outdoor sensor RSSI",
"outdoorSensorHumidity": "Outdoor sensor humidity",
"outdoorSensorBattery": "Outdoor sensor battery",
"indoorSensorConnected": "Indoor sensor connected",
"cascadeControlInput": "Cascade control (input)",
"cascadeControlOutput": "Cascade control (output)",
"indoorSensorRssi": "Indoor sensor RSSI",
"indoorSensorHumidity": "Indoor sensor humidity",
"indoorSensorBattery": "Indoor sensor battery",
"modulation": "Modulation",
"pressure": "Pressure",
"dhwFlowRate": "DHW flow rate",
"power": "Current power",
"faultCode": "Fault code",
"diagCode": "Diagnostic 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",
"equitherm": "Equitherm settings",
"pid": "PID settings",
"ot": "OpenTherm settings",
"mqtt": "MQTT settings",
"outdorSensor": "Outdoor sensor settings",
"indoorSensor": "Indoor sensor settings",
"extPump": "External pump settings",
"cascadeControl": "Cascade control settings"
},
"enable": "Enable",
"note": {
"restart": "After changing these settings, the device must be restarted for the changes to take effect.",
"blankNotUse": "blank - not use",
"bleDevice": "BLE device can be used <u>only</u> with some ESP32 boards with BLE support!"
},
"temp": {
"min": "Minimum temperature",
"max": "Maximum temperature"
},
"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",
"logLevel": "Log level",
"serial": {
"enable": "Enable Serial port",
"baud": "Serial port baud rate"
},
"telnet": {
"enable": "Enable Telnet",
"port": {
"title": "Telnet port",
"note": "Default: 23"
}
}
},
"heating": {
"hyst": "Hysteresis <small>(in degrees)</small>",
"turboFactor": "Turbo mode coeff."
},
"emergency": {
"desc": "Emergency mode is activated automatically when «PID» or «Equitherm» cannot calculate the heat carrier setpoint:<br />- if «Equitherm» is enabled and the outdoor temperature sensor is disconnected;<br />- if «PID» or OT option <i>«Native heating control»</i> is enabled and the indoor temperature sensor is disconnected.<br /><b>Note:</b> On network fault or MQTT fault, sensors with <i>«Manual via MQTT/API»</i> type will be in DISCONNECTED state.",
"target": {
"title": "Target temperature",
"note": "<b>Important:</b> <u>Target indoor temperature</u> if OT option <i>«Native heating control»</i> is enabled.<br />In all other cases, the <u>target heat carrier temperature</u>."
},
"treshold": "Treshold time <small>(sec)</small>"
},
"equitherm": {
"n": "N factor",
"k": "K factor",
"t": {
"title": "T factor",
"note": "Not used if PID is enabled"
}
},
"pid": {
"p": "P factor",
"i": "I factor",
"d": "D factor",
"dt": "DT <small>in seconds</small>",
"noteMinMaxTemp": "<b>Important:</b> When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.<br />Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from <code>equitherm_result - 15</code> to <code>equitherm_result + 15</code>."
},
"ot": {
"advanced": "Advanced Settings",
"inGpio": "In GPIO",
"outGpio": "Out GPIO",
"ledGpio": "RX LED GPIO",
"memberIdCode": "Master MemberID code",
"maxMod": "Max modulation level",
"pressureFactor": {
"title": "Coeff. pressure correction",
"note": "If the pressure displayed is <b>X10</b> from the real one, set the <b>0.1</b>."
},
"dhwFlowRateFactor": {
"title": "Coeff. DHW flow rate correction",
"note": "If the DHW flow rate displayed is <b>X10</b> from the real one, set the <b>0.1</b>."
},
"minPower": {
"title": "Min boiler power <small>(kW)</small>",
"note": "This value is at 0-1% boiler modulation level. Typically found in the boiler specification as \"minimum useful heat output\"."
},
"maxPower": {
"title": "Max boiler power <small>(kW)</small>",
"note": "<b>0</b> - try detect automatically. Typically found in the boiler specification as \"maximum useful heat output\"."
},
"fnv": {
"desc": "Filtering numeric values",
"enable": {
"title": "Enable filtering",
"note": "It can be useful if there is a lot of sharp noise on the charts. The filter used is \"Running Average\"."
},
"factor": {
"title": "Filtration coeff.",
"note": "The lower the value, the smoother and <u>longer</u> the change in numeric values."
}
},
"options": {
"desc": "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",
"immergasFix": "Fix for Immergas boilers"
},
"nativeHeating": {
"title": "Native heating control (boiler)",
"note": "Works <u>ONLY</u> if the boiler requires the desired room temperature and regulates the temperature of the coolant itself. Not compatible with PID and Equitherm regulators in firmware."
}
},
"mqtt": {
"homeAssistantDiscovery": "Home Assistant Discovery",
"server": "Server",
"port": "Port",
"user": "User",
"password": "Password",
"prefix": "Prefix",
"interval": "Publish interval <small>(sec)</small>"
},
"tempSensor": {
"source": {
"type": "Source type",
"boilerOutdoor": "From boiler via OpenTherm",
"boilerReturn": "Return heat carrier temp via OpenTherm",
"manual": "Manual via MQTT/API",
"ext": "External (DS18B20)",
"ble": "BLE device"
},
"gpio": "GPIO",
"offset": "Temp offset <small>(calibration)</small>",
"bleAddress": "BLE device MAC address"
},
"extPump": {
"use": "Use external pump",
"gpio": "Relay GPIO",
"postCirculationTime": "Post circulation time <small>(min)</small>",
"antiStuckInterval": "Anti stuck interval <small>(days)</small>",
"antiStuckTime": "Anti stuck time <small>(min)</small>"
},
"cascadeControl": {
"input": {
"desc": "Can be used to turn on the heating only if another boiler is faulty. The other boiler controller must change the state of the GPIO input in the event of a fault.",
"enable": "Enable input",
"gpio": "GPIO",
"invertState": "Invert GPIO state",
"thresholdTime": "State change threshold time <small>(sec)</small>"
},
"output": {
"desc": "Can be used to switch on another boiler <u>via relay</u>.",
"enable": "Enable output",
"gpio": "GPIO",
"invertState": "Invert GPIO state",
"thresholdTime": "State change threshold time <small>(sec)</small>",
"events": {
"desc": "Events",
"onFault": "If the fault state is active",
"onLossConnection": "If the connection via Opentherm is lost",
"onEnabledHeating": "If heating is enabled"
}
}
}
},
"upgrade": {
"title": "Upgrade - OpenTherm Gateway",
"name": "Upgrade",
"section": {
"backupAndRestore": "Backup & restore",
"backupAndRestore.desc": "In this section you can save and restore a backup of ALL settings.",
"upgrade": "Upgrade",
"upgrade.desc": "In this section you can upgrade the firmware and filesystem of your device.<br />Latest releases can be downloaded from the <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Releases page</a> of the project repository."
},
"note": {
"disclaimer1": "After a successful upgrade the filesystem, ALL settings will be reset to default values! Save backup before upgrading.",
"disclaimer2": "After a successful upgrade, the device will automatically reboot after 10 seconds."
},
"settingsFile": "Settings file",
"fw": "Firmware",
"fs": "Filesystem"
}
}
}

399
src_data/locales/ru.json Normal file
View File

@@ -0,0 +1,399 @@
{
"values": {
"logo": "OpenTherm Gateway",
"nav": {
"license": "Лицензия",
"source": "Исходный код",
"help": "Помощь",
"issues": "Проблемы и вопросы",
"releases": "Релизы"
},
"dbm": "дБм",
"kw": "кВт",
"time": {
"days": "д.",
"hours": "ч.",
"min": "мин.",
"sec": "сек."
},
"button": {
"upgrade": "Обновить",
"restart": "Перезагрузка",
"save": "Сохранить",
"saved": "Сохранено",
"refresh": "Обновить",
"restore": "Восстановить настройки",
"restored": "Восстановлено",
"backup": "Сохранить настройки",
"wait": "Пожалуйста, подождите...",
"uploading": "Загрузка...",
"success": "Успешно",
"error": "Ошибка"
},
"index": {
"title": "OpenTherm Gateway",
"section": {
"network": "Сеть",
"system": "Система"
},
"system": {
"build": {
"title": "Билд",
"version": "Версия",
"date": "Дата",
"core": "Ядро",
"sdk": "SDK"
},
"uptime": "Аптайм",
"memory": {
"title": "ОЗУ",
"maxFreeBlock": "макс. блок",
"min": "мин."
},
"board": "Плата",
"chip": {
"model": "Чип",
"cores": "Кол-во ядер",
"freq": "частота"
},
"flash": {
"size": "Размер ПЗУ",
"realSize": "реальный размер"
},
"lastResetReason": "Причина перезагрузки"
}
},
"dashboard": {
"name": "Дашборд",
"title": "Дашборд - OpenTherm Gateway",
"section": {
"control": "Управление",
"states": "Состояние и сенсоры",
"otDiag": "Диагностика OpenTherm"
},
"thermostat": {
"heating": "Отопление",
"dhw": "ГВС",
"temp.current": "Текущая",
"enable": "Вкл",
"turbo": "Турбо"
},
"state": {
"ot": "OpenTherm подключение",
"mqtt": "MQTT подключение",
"emergency": "Аварийный режим",
"heating": "Отопление",
"dhw": "ГВС",
"flame": "Пламя",
"fault": "Ошибка",
"diag": "Диагностика",
"extpump": "Внешний насос",
"outdoorSensorConnected": "Датчик наруж. темп.",
"outdoorSensorRssi": "RSSI датчика наруж. темп.",
"outdoorSensorHumidity": "Влажность с наруж. датчика темп.",
"outdoorSensorBattery": "Заряд наруж. датчика темп.",
"indoorSensorConnected": "Датчик внутр. темп.",
"cascadeControlInput": "Каскадное управление (вход)",
"cascadeControlOutput": "Каскадное управление (выход)",
"indoorSensorRssi": "RSSI датчика внутр. темп.",
"indoorSensorHumidity": "Влажность с внутр. датчика темп.",
"indoorSensorBattery": "Заряд внутр. датчика темп.",
"modulation": "Уровень модуляции",
"pressure": "Давление",
"dhwFlowRate": "Расход ГВС",
"power": "Текущая мощность",
"faultCode": "Код ошибки",
"diagCode": "Диагностический код",
"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": "Настройки аварийного режима",
"equitherm": "Настройки ПЗА",
"pid": "Настройки ПИД",
"ot": "Настройки OpenTherm",
"mqtt": "Настройки MQTT",
"outdorSensor": "Настройки наружного датчика температуры",
"indoorSensor": "Настройки внутреннего датчика температуры",
"extPump": "Настройки дополнительного насоса",
"cascadeControl": "Настройки каскадного управления"
},
"enable": "Вкл",
"note": {
"restart": "После изменения этих настроек устройство необходимо перезагрузить, чтобы изменения вступили в силу.",
"blankNotUse": "пусто - не использовать",
"bleDevice": "BLE устройство можно использовать <u>только</u> с некоторыми платами ESP32, которые поддерживают BLE!"
},
"temp": {
"min": "Мин. температура",
"max": "Макс. температура"
},
"portal": {
"login": "Логин",
"password": "Пароль",
"auth": "Требовать аутентификацию"
},
"system": {
"unit": "Система единиц",
"metric": "Метрическая <small>(цильсии, литры, бары)</small>",
"imperial": "Imperial <small>(фаренгейты, галлоны, psi)</small>",
"statusLedGpio": "Статус LED GPIO",
"logLevel": "Уровень логирования",
"serial": {
"enable": "Вкл. Serial порт",
"baud": "Скорость Serial порта"
},
"telnet": {
"enable": "Вкл. Telnet",
"port": {
"title": "Telnet порт",
"note": "По умолчанию: 23"
}
}
},
"heating": {
"hyst": "Гистерезис <small>(в градусах)</small>",
"turboFactor": "Коэфф. турбо режима"
},
"emergency": {
"desc": "Аварийный режим активируется автоматически, если «ПИД» или «ПЗА» не могут рассчитать уставку теплоносителя:<br />- если «ПЗА» включен и датчик наружной температуры отключен;<br />- если включен «ПИД» или OT опция <i>«Передать управление отоплением котлу»</i> и датчик внутренней температуры отключен.<br /><b>Примечание:</b> При сбое сети или MQTT датчики с типом <i>«Вручную через MQTT/API»</i> будут находиться в состоянии ОТКЛЮЧЕН.",
"target": {
"title": "Целевая температура",
"note": "<b>Важно:</b> <u>Целевая температура в помещении</u>, если включена ОТ опция <i>«Передать управление отоплением котлу»</i>.<br />Во всех остальных случаях <u>целевая температура теплоносителя</u>."
},
"treshold": "Пороговое время включения <small>(сек)</small>",
"events": {
"desc": "События",
"network": "При отключении сети",
"mqtt": "При отключении MQTT",
"indoorSensorDisconnect": "При потере связи с датчиком внутренней темп.",
"outdoorSensorDisconnect": "При потере связи с датчиком наружной темп."
},
"regulators": {
"desc": "Используемые регуляторы",
"equitherm": "ПЗА <small>(требуется внешний (DS18B20) или подключенный к котлу датчик <u>наружной</u> температуры)</small>",
"pid": "ПИД <small>(требуется внешний (DS18B20) датчик <u>внутренней</u> температуры)</small>"
}
},
"equitherm": {
"n": "Коэффициент N",
"k": "Коэффициент K",
"t": {
"title": "Коэффициент T",
"note": "Не используется, если ПИД включен"
}
},
"pid": {
"p": "Коэффициент P",
"i": "Коэффициент I",
"d": "Коэффициент D",
"dt": "DT <small>(сек)</small>",
"noteMinMaxTemp": "<b>Важно:</b> При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».<br />Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от <code>equitherm_result - 15</code> до <code>equitherm_result + 15</code>."
},
"ot": {
"advanced": "Дополнительные настройки",
"inGpio": "Вход GPIO",
"outGpio": "Выход GPIO",
"ledGpio": "RX LED GPIO",
"memberIdCode": "Master MemberID код",
"maxMod": "Макс. уровень модуляции",
"pressureFactor": {
"title": "Коэфф. коррекции давления",
"note": "Если давление отображается <b>Х10</b> от реального, установите значение <b>0.1</b>."
},
"dhwFlowRateFactor": {
"title": "Коэфф. коррекции потока ГВС",
"note": "Если поток ГВС отображается <b>Х10</b> от реального, установите значение <b>0.1</b>."
},
"minPower": {
"title": "Мин. мощность котла <small>(кВт)</small>",
"note": "Это значение соответствует уровню модуляции котла 01%. Обычно можно найти в спецификации котла как \"минимальная полезная тепловая мощность\"."
},
"maxPower": {
"title": "Макс. мощность котла <small>(кВт)</small>",
"note": "<b>0</b> - попробовать определить автоматически. Обычно можно найти в спецификации котла как \"максимальная полезная тепловая мощность\"."
},
"fnv": {
"desc": "Фильтрация числовых значений",
"enable": {
"title": "Включить фильтрацию",
"note": "Может быть полезно, если на графиках много резкого шума. В качестве фильтра используется \"бегущее среднее\"."
},
"factor": {
"title": "Коэфф. фильтрации",
"note": "Чем меньше коэф., тем плавнее и <u>дольше</u> изменение числовых значений."
}
},
"options": {
"desc": "Опции",
"dhwPresent": "Контур ГВС",
"summerWinterMode": "Летний/зимний режим",
"heatingCh2Enabled": "Канал 2 отопления всегда вкл.",
"heatingCh1ToCh2": "Дублировать параметры отопления канала 1 в канал 2",
"dhwToCh2": "Дублировать параметры ГВС в канал 2",
"dhwBlocking": "DHW blocking",
"modulationSyncWithHeating": "Синхронизировать модуляцию с отоплением",
"getMinMaxTemp": "Получать мин. и макс. температуру от котла",
"immergasFix": "Фикс для котлов Immergas"
},
"nativeHeating": {
"title": "Передать управление отоплением котлу",
"note": "Работает <u>ТОЛЬКО</u> если котел требует и принимает целевую температуру в помещении и сам регулирует температуру теплоносителя на основе встроенного режима кривых. Несовместимо с ПИД и ПЗА."
}
},
"mqtt": {
"homeAssistantDiscovery": "Home Assistant Discovery",
"server": "Адрес сервера",
"port": "Порт",
"user": "Имя пользователя",
"password": "Пароль",
"prefix": "Префикс",
"interval": "Интервал публикации <small>(сек)</small>"
},
"tempSensor": {
"source": {
"type": "Источник данных",
"boilerOutdoor": "От котла через OpenTherm",
"boilerReturn": "Температура обратки через OpenTherm",
"manual": "Вручную через MQTT/API",
"ext": "Внешний датчик (DS18B20)",
"ble": "BLE устройство"
},
"gpio": "GPIO",
"offset": "Смещение температуры <small>(калибровка)</small>",
"bleAddress": "MAC адрес BLE устройства"
},
"extPump": {
"use": "Использовать доп. насос",
"gpio": "GPIO реле",
"postCirculationTime": "Время постциркуляции <small>(в минутах)</small>",
"antiStuckInterval": "Интервал защиты от блокировки <small>(в днях)</small>",
"antiStuckTime": "Время работы насоса <small>(в минутах)</small>"
},
"cascadeControl": {
"input": {
"desc": "Может использоваться для включения отопления только при неисправности другого котла. Контроллер другого котла должен изменить состояние входа GPIO в случае неисправности.",
"enable": "Включить вход",
"gpio": "GPIO",
"invertState": "Инвертировать состояние GPIO",
"thresholdTime": "Пороговое время изменения состояния <small>(сек)</small>"
},
"output": {
"desc": "Может использоваться для включения другого котла <u>через реле</u>.",
"enable": "Включить выход",
"gpio": "GPIO",
"invertState": "Инвертировать состояние GPIO",
"thresholdTime": "Пороговое время изменения состояния <small>(сек)</small>",
"events": {
"title": "События",
"onFault": "Если состояние fault (ошибки) активно",
"onLossConnection": "Если соединение по OpenTherm потеряно",
"onEnabledHeating": "Если отопление включено"
}
}
}
},
"upgrade": {
"title": "Обновление - OpenTherm Gateway",
"name": "Обновление",
"section": {
"backupAndRestore": "Резервное копирование и восстановление",
"backupAndRestore.desc": "В этом разделе вы можете сохранить и восстановить резервную копию ВСЕХ настроек.",
"upgrade": "Обновление",
"upgrade.desc": "В этом разделе вы можете обновить прошивку и файловую систему вашего устройства.<br />Последнюю версию можно загрузить со страницы <a href=\"https://github.com/Laxilef/OTGateway/releases\" target=\"_blank\">Релизы</a> в репозитории проекта."
},
"note": {
"disclaimer1": "После успешного обновления файловой системы ВСЕ настройки будут сброшены на стандартные! Создайте резервную копию ПЕРЕД обновлением.",
"disclaimer2": "После успешного обновления устройство автоматически перезагрузится через 10 секунд."
},
"settingsFile": "Файл настроек",
"fw": "Прошивка",
"fs": "Файловая система"
}
}
}

View File

@@ -0,0 +1,496 @@
<!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.outdoorSensorConnected</th>
<td><input type="radio" id="outdoor-sensor-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorRssi</th>
<td><b id="outdoor-sensor-rssi"></b> <span data-i18n>dbm</span></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorHumidity</th>
<td><b id="outdoor-sensor-humidity"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorBattery</th>
<td><b id="outdoor-sensor-battery"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorConnected</th>
<td><input type="radio" id="indoor-sensor-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.cascadeControlInput</th>
<td><input type="radio" id="cc-input" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.cascadeControlOutput</th>
<td><input type="radio" id="cc-output" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorRssi</th>
<td><b id="indoor-sensor-rssi"></b> <span data-i18n>dbm</span></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorHumidity</th>
<td><b id="indoor-sensor-humidity"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorBattery</th>
<td><b id="indoor-sensor-battery"></b> %</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.power</th>
<td><b id="ot-power"></b> <span data-i18n>kw</span></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.diagCode</th>
<td><b id="ot-diag-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('#mqtt-connected', result.states.mqtt);
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('#outdoor-sensor-connected', result.sensors.outdoor.connected);
setState('#indoor-sensor-connected', result.sensors.indoor.connected);
setState('#cc-input', result.cascadeControl.input);
setState('#cc-output', result.cascadeControl.output);
setValue('#outdoor-sensor-rssi', result.sensors.outdoor.rssi);
setValue('#outdoor-sensor-humidity', result.sensors.outdoor.humidity);
setValue('#outdoor-sensor-battery', result.sensors.outdoor.battery);
setValue('#indoor-sensor-rssi', result.sensors.indoor.rssi);
setValue('#indoor-sensor-humidity', result.sensors.indoor.humidity);
setValue('#indoor-sensor-battery', result.sensors.indoor.battery);
setValue('#ot-modulation', result.sensors.modulation);
setValue('#ot-pressure', result.sensors.pressure);
setValue('#ot-dhw-flow-rate', result.sensors.dhwFlowRate);
setValue('#ot-power', result.sensors.power);
setValue(
'#ot-fault-code',
result.sensors.faultCode
? (result.sensors.faultCode + " (0x" + dec2hex(result.sensors.faultCode) + ")")
: "-"
);
setValue(
'#ot-diag-code',
result.sensors.diagnosticCode
? (result.sensors.diagnosticCode + " (0x" + dec2hex(result.sensors.diagnosticCode) + ")")
: "-"
);
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>

225
src_data/pages/index.html Normal file
View File

@@ -0,0 +1,225 @@
<!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.core</span>: <b id="build-core"></b><br />
<span data-i18n>index.system.build.sdk</span>: <b id="build-sdk"></b>
</td>
</tr>
<tr>
<th scope="row" data-i18n>index.system.uptime</th>
<td>
<b id="uptime-days"></b> <span data-i18n>time.days</span>,
<b id="uptime-hours"></b> <span data-i18n>time.hours</span>,
<b id="uptime-min"></b> <span data-i18n>time.min</span>,
<b id="uptime-sec"></b> <span data-i18n>time.sec</span>
</td>
</tr>
<tr>
<th scope="row" data-i18n>index.system.memory.title</th>
<td>
<b id="heap-free"></b> of <b id="heap-total"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="heap-min-free"></b> bytes)<br />
<span data-i18n>index.system.memory.maxFreeBlock</span>: <b id="heap-max-free-block"></b> bytes (<span data-i18n>index.system.memory.min</span>: <b id="heap-min-max-free-block"></b> bytes)
</td>
</tr>
<tr>
<th scope="row" data-i18n>index.system.board</th>
<td>
<span data-i18n>index.system.chip.model</span>: <b id="chip-model"></b> (rev. <span id="chip-rev"></span>)<br />
<span data-i18n>index.system.chip.cores</span>: <b id="chip-cores"></b>, <span data-i18n>index.system.chip.freq</span>: <b id="chip-freq"></b> mHz<br />
<span data-i18n>index.system.flash.size</span>: <b id="flash-size"></b> MB (<span data-i18n>index.system.flash.realSize</span>: <b id="flash-real-size"></b> MB)
</td>
</tr>
<tr>
<th scope="row" data-i18n>index.system.lastResetReason</th>
<td>
<b id="reset-reason"></b><br />
<a href="/api/debug" target="_blank"><small>Save debug data</small></a>
</td>
</tr>
</tbody>
</table>
<div class="grid">
<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('#reset-reason', result.system.resetReason);
setValue('#uptime', result.system.uptime);
setValue('#uptime-days', Math.floor(result.system.uptime / 86400));
setValue('#uptime-hours', Math.floor(result.system.uptime % 86400 / 3600));
setValue('#uptime-min', Math.floor(result.system.uptime % 3600 / 60));
setValue('#uptime-sec', Math.floor(result.system.uptime % 60));
setValue('#network-hostname', result.network.hostname);
setValue('#network-mac', result.network.mac);
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.build.version);
setValue('#build-date', result.build.date);
setValue('#build-env', result.build.env);
setValue('#build-core', result.build.core);
setValue('#build-sdk', result.build.sdk);
setValue('#heap-total', result.heap.total);
setValue('#heap-free', result.heap.free);
setValue('#heap-min-free', result.heap.minFree);
setValue('#heap-max-free-block', result.heap.maxFreeBlock);
setValue('#heap-min-max-free-block', result.heap.minMaxFreeBlock);
setValue('#chip-model', result.chip.model);
setValue('#chip-rev', result.chip.rev);
setValue('#chip-cores', result.chip.cores);
setValue('#chip-freq', result.chip.freq);
setValue('#flash-size', result.flash.size / 1024 / 1024);
setValue('#flash-real-size', result.flash.realSize / 1024 / 1024);
setBusy('#system-busy', '#system-table', false);
} catch (error) {
console.log(error);
}
setTimeout(onLoadPage, 10000);
}, 1000);
});
</script>
</body>
</html>

220
src_data/pages/network.html Normal file
View 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>

1003
src_data/pages/settings.html Normal file

File diff suppressed because it is too large Load Diff

111
src_data/pages/upgrade.html Normal file
View 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
View 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
View 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) || 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;
}
}

View File

@@ -1,4 +1,4 @@
function setupForm(formSelector) {
function setupForm(formSelector, onResultCallback = null, noCastItems = []) {
const form = document.querySelector(formSelector);
if (!form) {
return;
@@ -14,22 +14,19 @@ function setupForm(formSelector) {
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
defaultText = button.textContent;
button.textContent = i18n("button.wait");
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (response) => {
const onSuccess = (result) => {
if (button) {
button.textContent = 'Saved';
button.textContent = i18n('button.saved');
button.classList.add('success');
button.removeAttribute('aria-busy');
@@ -41,9 +38,9 @@ function setupForm(formSelector) {
}
};
const onFailed = (response) => {
const onFailed = () => {
if (button) {
button.textContent = 'Error';
button.textContent = i18n('button.error');
button.classList.add('failed');
button.removeAttribute('aria-busy');
@@ -68,18 +65,23 @@ function setupForm(formSelector) {
headers: {
'Content-Type': 'application/json'
},
body: form2json(fd)
body: form2json(fd, noCastItems)
});
if (response.ok) {
onSuccess(response);
if (!response.ok) {
throw new Error('Response not valid');
}
} else {
onFailed(response);
const result = response.status != 204 ? (await response.json()) : null;
onSuccess(result);
if (onResultCallback instanceof Function) {
onResultCallback(result);
}
} catch (err) {
onFailed(false);
console.log(err);
onFailed();
}
});
}
@@ -95,17 +97,14 @@ function setupNetworkScanForm(formSelector, tableSelector) {
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.innerHTML;
}
const onSubmitFn = async (event) => {
if (event) {
event.preventDefault();
}
if (button) {
button.innerHTML = 'Please wait...';
defaultText = button.innerHTML;
button.innerHTML = i18n('button.wait');
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
@@ -134,7 +133,7 @@ function setupNetworkScanForm(formSelector, tableSelector) {
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 input = document.querySelector('input#sta-ssid');
const ssid = this.getAttribute('data-ssid');
if (!input || !ssid) {
return;
@@ -145,19 +144,56 @@ function setupNetworkScanForm(formSelector, tableSelector) {
};
row.insertCell().textContent = "#" + (i + 1);
row.insertCell().innerHTML = result[i].hidden ? '<i>Hidden</i>' : result[i].ssid;
row.insertCell().innerHTML = result[i].hidden ? ("<i>" + result[i].bssid + "</i>") : result[i].ssid;
const signalCell = row.insertCell();
const signalElement = document.createElement("kbd");
signalElement.textContent = result[i].signalQuality + "%";
if (result[i].signalQuality > 60) {
signalElement.classList.add('greatSignal');
// 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) {
signalElement.classList.add('normalSignal');
signalQualityIcon.classList.add('icons-wifi-strength-2');
} else if (result[i].signalQuality > 20) {
signalQualityIcon.classList.add('icons-wifi-strength-1');
} else {
signalElement.classList.add('badSignal');
signalQualityIcon.classList.add('icons-wifi-strength-0');
}
signalCell.appendChild(signalElement);
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) {
@@ -220,22 +256,19 @@ function setupRestoreBackupForm(formSelector) {
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
defaultText = button.textContent;
button.textContent = i18n('button.wait');
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (response) => {
if (button) {
button.textContent = 'Restored';
button.textContent = i18n('button.restored');
button.classList.add('success');
button.removeAttribute('aria-busy');
@@ -249,7 +282,7 @@ function setupRestoreBackupForm(formSelector) {
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.textContent = i18n('button.error');
button.classList.add('failed');
button.removeAttribute('aria-busy');
@@ -307,10 +340,6 @@ function setupUpgradeForm(formSelector) {
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
const statusToText = (status) => {
switch (status) {
case 0:
@@ -395,7 +424,7 @@ function setupUpgradeForm(formSelector) {
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.textContent = i18n('button.error');
button.classList.add('failed');
button.removeAttribute('aria-busy');
@@ -410,8 +439,12 @@ function setupUpgradeForm(formSelector) {
form.addEventListener('submit', async (event) => {
event.preventDefault();
hide('.upgrade-firmware-result');
hide('.upgrade-filesystem-result');
if (button) {
button.textContent = 'Uploading...';
defaultText = button.textContent;
button.textContent = i18n('button.uploading');
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
@@ -437,157 +470,15 @@ function setupUpgradeForm(formSelector) {
});
}
async function loadNetworkStatus() {
let response = await fetch('/api/network/status', { cache: 'no-cache' });
let result = await response.json();
setValue('.network-hostname', result.hostname);
setValue('.network-mac', result.mac);
setState('.network-connected', result.isConnected);
setValue('.network-ssid', result.ssid);
setValue('.network-signal', result.signalQuality);
setValue('.network-ip', result.ip);
setValue('.network-subnet', result.subnet);
setValue('.network-gateway', result.gateway);
setValue('.network-dns', result.dns);
setBusy('.main-busy', '.main-table', false);
}
async function loadNetworkSettings() {
let response = await fetch('/api/network/settings', { cache: 'no-cache' });
let result = await response.json();
setInputValue('.network-hostname', result.hostname);
setCheckboxValue('.network-use-dhcp', result.useDhcp);
setInputValue('.network-static-ip', result.staticConfig.ip);
setInputValue('.network-static-gateway', result.staticConfig.gateway);
setInputValue('.network-static-subnet', result.staticConfig.subnet);
setInputValue('.network-static-dns', result.staticConfig.dns);
setBusy('#network-settings-busy', '#network-settings', false);
setInputValue('.sta-ssid', result.sta.ssid);
setInputValue('.sta-password', result.sta.password);
setInputValue('.sta-channel', result.sta.channel);
setBusy('#sta-settings-busy', '#sta-settings', false);
setInputValue('.ap-ssid', result.ap.ssid);
setInputValue('.ap-password', result.ap.password);
setInputValue('.ap-channel', result.ap.channel);
setBusy('#ap-settings-busy', '#ap-settings', false);
}
async function loadSettings() {
let response = await fetch('/api/settings', { cache: 'no-cache' });
let result = await response.json();
setCheckboxValue('.system-debug', result.system.debug);
setCheckboxValue('.system-use-serial', result.system.useSerial);
setCheckboxValue('.system-use-telnet', result.system.useTelnet);
setBusy('#system-settings-busy', '#system-settings', false);
setCheckboxValue('.portal-use-auth', result.portal.useAuth);
setInputValue('.portal-login', result.portal.login);
setInputValue('.portal-password', result.portal.password);
setBusy('#portal-settings-busy', '#portal-settings', false);
setInputValue('.opentherm-in-pin', result.opentherm.inPin);
setInputValue('.opentherm-out-pin', result.opentherm.outPin);
setInputValue('.opentherm-member-id-code', result.opentherm.memberIdCode);
setCheckboxValue('.opentherm-dhw-present', result.opentherm.dhwPresent);
setCheckboxValue('.opentherm-sw-mode', result.opentherm.summerWinterMode);
setCheckboxValue('.opentherm-heating-ch2-enabled', result.opentherm.heatingCh2Enabled);
setCheckboxValue('.opentherm-heating-ch1-to-ch2', result.opentherm.heatingCh1ToCh2);
setCheckboxValue('.opentherm-dhw-to-ch2', result.opentherm.dhwToCh2);
setCheckboxValue('.opentherm-dhw-blocking', result.opentherm.dhwBlocking);
setCheckboxValue('.opentherm-sync-modulation-with-heating', result.opentherm.modulationSyncWithHeating);
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
setInputValue('.mqtt-server', result.mqtt.server);
setInputValue('.mqtt-port', result.mqtt.port);
setInputValue('.mqtt-user', result.mqtt.user);
setInputValue('.mqtt-password', result.mqtt.password);
setInputValue('.mqtt-prefix', result.mqtt.prefix);
setInputValue('.mqtt-interval', result.mqtt.interval);
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
setRadioValue('.outdoor-sensor-type', result.sensors.outdoor.type);
setInputValue('.outdoor-sensor-pin', result.sensors.outdoor.pin);
setInputValue('.outdoor-sensor-offset', result.sensors.outdoor.offset);
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
setRadioValue('.indoor-sensor-type', result.sensors.indoor.type);
setInputValue('.indoor-sensor-pin', result.sensors.indoor.pin);
setInputValue('.indoor-sensor-offset', result.sensors.indoor.offset);
setInputValue('.indoor-sensor-ble-addresss', result.sensors.indoor.bleAddresss);
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
setCheckboxValue('.extpump-use', result.externalPump.use);
setInputValue('.extpump-pin', result.externalPump.pin);
setInputValue('.extpump-pc-time', result.externalPump.postCirculationTime);
setInputValue('.extpump-as-interval', result.externalPump.antiStuckInterval);
setInputValue('.extpump-as-time', result.externalPump.antiStuckTime);
setBusy('#extpump-settings-busy', '#extpump-settings', false);
}
async function loadVars() {
let response = await fetch('/api/vars');
let result = await response.json();
setState('.ot-connected', result.states.otStatus);
setState('.ot-emergency', result.states.emergency);
setState('.ot-heating', result.states.heating);
setState('.ot-dhw', result.states.dhw);
setState('.ot-flame', result.states.flame);
setState('.ot-fault', result.states.fault);
setState('.ot-diagnostic', result.states.diagnostic);
setState('.ot-external-pump', result.states.externalPump);
setValue('.ot-modulation', result.sensors.modulation);
setValue('.ot-pressure', result.sensors.pressure);
setValue('.ot-dhw-flow-rate', result.sensors.dhwFlowRate);
setValue('.ot-fault-code', result.sensors.faultCode ? ("E" + result.sensors.faultCode) : "-");
setValue('.indoor-temp', result.temperatures.indoor);
setValue('.outdoor-temp', result.temperatures.outdoor);
setValue('.heating-temp', result.temperatures.heating);
setValue('.heating-setpoint-temp', result.parameters.heatingSetpoint);
setValue('.dhw-temp', result.temperatures.dhw);
setBusy('.ot-busy', '.ot-table', false);
setValue('.version', result.system.version);
setValue('.build-date', result.system.buildDate);
setValue('.uptime', result.system.uptime);
setValue('.uptime-days', Math.floor(result.system.uptime / 86400));
setValue('.uptime-hours', Math.floor(result.system.uptime % 86400 / 3600));
setValue('.uptime-min', Math.floor(result.system.uptime % 3600 / 60));
setValue('.uptime-sec', Math.floor(result.system.uptime % 60));
setValue('.total-heap', result.system.totalHeap);
setValue('.free-heap', result.system.freeHeap);
setValue('.min-free-heap', result.system.minFreeHeap);
setValue('.max-free-block-heap', result.system.maxFreeBlockHeap);
setValue('.min-max-free-block-heap', result.system.minMaxFreeBlockHeap);
setValue('.reset-reason', result.system.resetReason);
setState('.mqtt-connected', result.system.mqttConnected);
setBusy('.system-busy', '.system-table', false);
}
function setBusy(busySelector, contentSelector, value) {
let busy = document.querySelector(busySelector);
let content = document.querySelector(contentSelector);
if (!busy || !content) {
return;
}
if (!value) {
busy.classList.add('hidden');
content.classList.remove('hidden');
hide(busySelector);
show(contentSelector);
} else {
busy.classList.remove('hidden');
content.classList.add('hidden');
show(busySelector);
hide(contentSelector);
}
}
@@ -601,12 +492,14 @@ function setState(selector, value) {
}
function setValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.innerHTML = value;
}
}
function setCheckboxValue(selector, value) {
@@ -629,26 +522,132 @@ function setRadioValue(selector, value) {
}
}
function setInputValue(selector, 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 setSelectValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.value = value;
for (let option of item.options) {
option.selected = option.value == value;
}
}
function show(selector) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
function form2json(data) {
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",
2: "AWB/Brink/Viessmann",
4: "ATAG/Baxi/Brötje/ELCO/GEMINOX",
5: "Itho Daalderop",
6: "IDEAL",
8: "Buderus/Bosch/Hoval",
9: "Ferroli",
11: "Remeha/De Dietrich",
16: "Unical",
24: "Vaillant/Bulex",
27: "Baxi",
29: "Itho Daalderop",
33: "Viessmann",
41: "Italtherm/Radiant",
56: "Baxi",
131: "Nefit",
148: "Navien",
173: "Intergas",
247: "Baxi Ampera",
248: "Zota Lux-X"
};
return (memberId in vendorList)
? vendorList[memberId]
: "unknown vendor";
}
function form2json(data, noCastItems = []) {
let method = function (object, pair) {
let keys = pair[0].replace(/\]/g, '').split('[');
let key = keys[0];
let value = pair[1];
if (!noCastItems.includes(keys.join('.'))) {
if (value === 'true' || value === 'false') {
value = value === 'true';
} else if (typeof (value) === 'string' && value.trim() !== '' && !isNaN(value)) {
value = parseFloat(value);
}
}
if (keys.length > 1) {
let i, x, segment;
@@ -678,3 +677,12 @@ function form2json(data) {
let object = Array.from(data).reduce(method, {});
return JSON.stringify(object);
}
function dec2hex(i) {
let hex = parseInt(i).toString(16);
if (hex.length % 2 != 0) {
hex = "0" + hex;
}
return hex.toUpperCase();
}

View File

@@ -1,80 +0,0 @@
@media (min-width: 1280px) {
.container {
max-width: 1000px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1000px;
}
}
.hidden {
display: none !important;
}
header,
main,
footer {
padding-top: 1rem !important;
padding-bottom: 1rem !important;
}
article {
margin-bottom: 1rem;
}
footer {
text-align: center;
}
button.success {
background-color: var(--pico-form-element-valid-border-color);
border-color: var(--pico-form-element-valid-border-color);
}
button.failed {
background-color: var(--pico-form-element-invalid-border-color);
border-color: var(--pico-form-element-invalid-border-color);
}
tr.network:hover {
--pico-background-color: var(--pico-primary-focus);
cursor: pointer;
}
.greatSignal {
background-color: var(--pico-form-element-valid-border-color);
}
.normalSignal {
background-color: #e48500;
}
.badSignal {
background-color: var(--pico-form-element-invalid-border-color);
}
.primary {
border: 0.25rem solid var(--pico-form-element-invalid-border-color);
padding: 1rem;
margin-bottom: 1rem;
}
.logo {
display: inline-block;
padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal);
vertical-align: baseline;
line-height: var(--pico-line-height);
background-color: var(--pico-code-kbd-background-color);
border-radius: var(--pico-border-radius);
color: var(--pico-code-kbd-color);
font-weight: bolder;
font-size: 1.3rem;
font-family: var(--pico-font-family-monospace);
}
nav li a:has(> div.logo) {
margin-bottom: 0;
}

File diff suppressed because one or more lines are too long

203
src_data/styles/app.css Normal file
View 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;
}

View 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

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
import shutil
import gzip
import os
Import("env")
@@ -7,11 +8,36 @@ def post_build(source, target, env):
copy_to_build_dir({
source[0].get_abspath(): "firmware_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
env.subst("$BUILD_DIR/${PROGNAME}.factory.bin"): "firmware_%s_%s.factory.bin" % (env["PIOENV"], env.GetProjectOption("version")),
env.subst("$BUILD_DIR/${PROGNAME}.elf"): "firmware_%s_%s.elf" % (env["PIOENV"], env.GetProjectOption("version"))
}, os.path.join(env["PROJECT_DIR"], "build"));
env.Execute("pio run --target buildfs --environment %s" % env["PIOENV"]);
def before_buildfs(source, target, env):
env.Execute("npm install --silent")
env.Execute("npx gulp build_all --no-deprecation")
"""
src = os.path.join(env["PROJECT_DIR"], "src_data")
dst = os.path.join(env["PROJECT_DIR"], "data")
for root, dirs, files in os.walk(src, topdown=False):
for name in files:
src_path = os.path.join(root, name)
with open(src_path, 'rb') as f_in:
dst_name = name + ".gz"
dst_path = os.path.join(dst, os.path.relpath(root, src), dst_name)
if os.path.exists(os.path.join(dst, os.path.relpath(root, src))) == False:
os.mkdir(os.path.join(dst, os.path.relpath(root, src)))
with gzip.open(dst_path, 'wb', 9) as f_out:
shutil.copyfileobj(f_in, f_out)
print("Compressed '%s' to '%s'" % (src_path, dst_path))
"""
def after_buildfs(source, target, env):
copy_to_build_dir({
source[0].get_abspath(): "filesystem_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
@@ -31,4 +57,6 @@ def copy_to_build_dir(files, build_dir):
env.AddPostAction("buildprog", post_build)
env.AddPreAction("$BUILD_DIR/spiffs.bin", before_buildfs)
env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs)
env.AddPostAction("buildfs", after_buildfs)

View File

@@ -1,12 +0,0 @@
{
"version": 1,
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} }
],
"connections": [
[ "esp:TX0", "$serialMonitor:RX", "", [] ],
[ "esp:RX0", "$serialMonitor:TX", "", [] ]
]
}

View File

@@ -1,8 +0,0 @@
[wokwi]
version = 1
elf = "../../.pio/build/nodemcu_32s/firmware.elf"
firmware = "../../.pio/build/nodemcu_32s/firmware.bin"
[[net.forward]]
from = "localhost:9080"
to = "target:80"