275 Commits

Author SHA1 Message Date
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
Yurii
e829a00355 chore: bump version 2024-02-20 16:21:20 +03:00
Yurii
bee720386a refactor: changed availability conditions for HA entities 2024-02-20 16:17:03 +03:00
Yurii
c4b6eadb81 refactor: using BLE advertising instead of manual requests 2024-02-20 15:29:14 +03:00
Yurii
a5d2b9fcfa refactor: small fixes 2024-02-20 15:27:51 +03:00
Yurii
1a03117257 chore: bump version 2024-02-05 19:58:11 +03:00
Yurii
b421780f7b fix: change channel to 6 for Wifi AP 2024-02-05 19:57:26 +03:00
Yurii
987c101394 fix: set wifi sleep if use ble 2024-02-05 19:55:40 +03:00
Yurii
4002f5b6c2 fix: added board_build.ldscript in platformio.ini for esp8266 boards 2024-02-04 05:37:31 +03:00
Yurii
21edbb7432 chore: bump version 2024-02-04 05:04:51 +03:00
Yurii
88f217abcc refactor: optimization 2024-02-04 05:03:28 +03:00
Yurii
89f3578f27 fix: #37 fixed 2024-02-04 04:48:11 +03:00
Yurii
9c47bf1ddb fix: write empty topics (mqtt) 2024-02-04 04:31:19 +03:00
Yurii
4d199876fb chore: bump lib 2024-02-04 04:28:00 +03:00
Yurii
2ffd19e850 chore: gitignore update 2024-02-04 04:26:20 +03:00
Yurii
d0aabbe82a fix: set policy manual for wifi 13 ch; change timeouts 2024-02-01 21:27:22 +03:00
Yurii
f2e4f2f631 fix: revert 20 ms wait before start bit 2024-02-01 21:24:22 +03:00
Yurii
d374ddc02a refactor: optimization 2024-01-27 18:42:51 +03:00
Yurii
cad9e50a78 fix: hostname pattern in portal fixed 2024-01-27 03:16:27 +03:00
Yurii
f74d0713d7 chore: updated README 2024-01-23 22:38:45 +03:00
Yurii
114b7fb5a7 chore: bump ESP Telnet to 2.2 2024-01-23 01:59:19 +03:00
Yurii
1b969dcb33 style: indents changed 2024-01-23 01:11:13 +03:00
Yurii
0c64c08ff8 fix: changed "accept" attribute for input type=file 2024-01-23 01:10:33 +03:00
Yurii
e6b9a2901c chore: bump GyverPID to 3.3.2 2024-01-23 00:56:41 +03:00
Yurii
ca0ef94478 chore: bump OpenTherm Library 2024-01-19 19:42:29 +03:00
Yurii
335429a52e fix: set mqtt prefix before connection 2024-01-19 03:37:54 +03:00
Yurii
2561e92ab9 feat: added validation for BLE address 2024-01-19 03:37:18 +03:00
Yurii
2a67716f65 fix: http code for StaticPage fixed 2024-01-19 03:19:17 +03:00
Yurii
2adbda6832 feat: added form validation 2024-01-19 03:05:50 +03:00
Yurii
99088fb723 fix: data output via web server on ESP32 fixed 2024-01-18 23:37:12 +03:00
Yurii
5e3751ca03 refactor: code style 2024-01-18 23:34:47 +03:00
Yurii
ef63f48f57 chore: added partition file for esp32 2024-01-18 01:59:58 +03:00
Yurii
4de7119d6c chore: pcb upd 2024-01-17 19:01:48 +03:00
Yurii
280c7f2887 refactor: heap info 2024-01-17 17:51:15 +03:00
Yurii
85ffd4188f chore: bump version 2024-01-17 16:09:24 +03:00
Yurii
133015d7b9 refactor: network management code moved to MainTask (memory optimization); removed stopping DHCP server and client on reset wifi 2024-01-17 16:08:53 +03:00
Yurii
8731311c62 fix: added close tag 2024-01-16 17:00:47 +03:00
Yurii
5856a45d37 chore: bump version 2024-01-15 20:24:28 +03:00
Yurii
827c18513b fix: set default sta password 2024-01-15 20:24:07 +03:00
Yurii
6f08685859 refactor: fix css 2024-01-15 16:19:41 +03:00
Yurii
ccbec44775 feat: saving network settings after FS upgrade 2024-01-15 15:41:19 +03:00
Yurii
5a70403444 refactor: fix css, rename src_data dir 2024-01-15 15:39:31 +03:00
Yurii
a9c9457918 refactor: added doc.clear() doc.shrinkToFit() in some code areas 2024-01-14 19:57:25 +03:00
Yurii
520baa4920 refactor: sensors type settings moved to portal, entities for HA have been deleted; logging settings moved; bump version 2024-01-14 19:16:24 +03:00
Yurii
30ae602ab9 refactor: JsonDocument clear before send response 2024-01-14 16:34:49 +03:00
Yurii
a6098555dc refactor: optimization of connection to MQTT 2024-01-14 16:33:32 +03:00
Yurii
60c860bc26 fix: opentherm polling interval reduced 2024-01-14 15:56:04 +03:00
Yurii
7463687f1b refactor: removed unnecessary ::yield() and added call setNoDelay() for ESP8266 2024-01-14 15:50:11 +03:00
Yurii
5ee1c7029b fix: updated connection logic to MQTT and enable/disable emergency mode 2024-01-14 15:48:57 +03:00
Yurii
70f2760413 refactor: added info about the need to restart after changing the gpio 2024-01-13 20:35:40 +03:00
Yurii
07ce1db304 feat: make fs after build and copy to build dir 2024-01-13 20:34:00 +03:00
Yurii
04a6b4e1b0 refactor: additional checks when initializing sensors 2024-01-13 20:32:55 +03:00
Yurii
4c48dc048a chore: added monitor_filters to platformio.ini 2024-01-13 20:31:41 +03:00
Yurii
feac3bbdf4 feat: display a message on home page if the file system is not flashed 2024-01-13 13:26:24 +03:00
Yurii
1ad1f26d4f feat: automatic restart after restoring settings 2024-01-13 13:02:12 +03:00
Yurii
f22c64e30c feat: human-readable uptime formatting 2024-01-13 13:00:43 +03:00
Yurii
a9db175dba fix: automatic reboot if memory is too low 2024-01-13 12:58:44 +03:00
Yurii
b7c090465b refactor: moving some strings to flash memory 2024-01-12 21:06:32 +03:00
Yurii
50a049915b chore: remove unused files 2024-01-12 21:05:29 +03:00
Yurii
e38bda6b4a fix: calculating pid temperature in float, fixed #23 2024-01-12 18:30:13 +03:00
Yurii
ab1e9c761f * feat: new portal & network manager
* refactor: migrate from PubSubClient to ArduinoMqttClient
* refactor: migrate from EEManager to FileData
* chore: bump ESP Telnet to 2.2
* chore: bump TinyLogger to 1.1.0
2024-01-12 18:29:55 +03:00
Yurii
b36e4dca42 refactoring: moving some strings to flash memory 2024-01-12 18:12:33 +03:00
Yurii
fb01d9f566 chore: removed unused code 2023-12-28 20:41:18 +03:00
Yurii
46999fe61c fix: set device_class and unit_of_measurement for number.pid_dt 2023-12-25 15:59:49 +03:00
Yurii
5846812813 chore: workflow 2023-12-25 15:58:19 +03:00
Yurii
67ae236f25 fix: correction pressure and DHW flow rate, if the received value is x10 2023-12-24 18:50:54 +03:00
Yurii
347723cbba fix: rename entities #26
* Current heating min temp => Boiler heating min temp
* Current heating max temp => Boiler heating max temp
* Current DHW min temp => Boiler DHW min temp
* Current DHW max temp => Boiler DHW max temp
2023-12-22 19:15:00 +03:00
Yurii
06659b749a fix: rounding numbers in MqttTask 2023-12-22 18:54:39 +03:00
Yurii
83347765a8 chore: bump version to 1.4.0-rc.5 2023-12-21 18:56:03 +03:00
Yurii
68f412e670 fix: added timeout for wifi client 2023-12-21 18:54:59 +03:00
Yurii
cf4a60dd2d fix: log messages in RegulatorTask 2023-12-21 18:54:08 +03:00
Yurii
ff5da950c1 fix: correction of PID coefficients limits 2023-12-21 16:34:21 +03:00
Yurii
ab21913aa7 fix: disable turbo mode if heating is off 2023-12-21 16:32:27 +03:00
Yurii
7b2014e7b4 fix: upd ESPTelnet 2023-12-21 12:38:33 +03:00
Yurii
025a185bbf refactoring: timings 2023-12-20 16:47:36 +03:00
Yurii
e9bb3e46c8 bump version to 1.4.0-rc.4 2023-12-20 08:14:46 +03:00
Yurii
f4fe8c7366 refactoring: arp gratuitous for esp8266 2023-12-20 08:12:39 +03:00
Yurii
4e980b6e5b feature: added settings.pid.dt, editable via mqtt 2023-12-20 08:11:08 +03:00
Yurii
2df2205d60 fix: PID interval correction #23 2023-12-20 07:50:57 +03:00
Yurii
4bf3b575db feature: use pid in emergency mode 2023-12-19 16:44:54 +03:00
Yurii
c87e08c6af small fix 2023-12-18 08:05:41 +03:00
Yurii
e4e349ba15 fix: change type for mqtt port in settings 2023-12-18 08:05:13 +03:00
Yurii
2b5d66173e Fix typo #24 2023-12-18 00:55:25 +03:00
Yurii
0236a0dd8a optimization for esp8266 2023-12-17 13:21:25 +03:00
Yurii
8875fd019a upd heap monitoring 2023-12-17 13:18:42 +03:00
Yurii
7149f52d62 Heap fragmentation optimization
Moving object creation to task constructors
2023-12-16 05:05:37 +03:00
Yurii
214e840ec2 fix buildings JsonDocuments 2023-12-16 01:52:17 +03:00
Laxilef
6c23d5032b Merge pull request #17 from mennodegraaf/ot-improvements
Improve opentherm handling
2023-12-16 00:43:20 +03:00
Laxilef
47879d5486 Update OpenThermTask.h 2023-12-16 00:42:17 +03:00
Yurii
315a975aa8 mqtt refactoring, change version to 1.4.0-rc.1
* added MqttWriter
* added MqttWiFiClient (modified WiFiClient for esp8266)
* adaptation HomeAssistantHelper for MqttWriter
* adaptation HaHelper for new HomeAssistantHelper
2023-12-16 00:29:19 +03:00
Yurii
21ed8f2a14 heap monitoring features 2023-12-15 23:59:20 +03:00
Yurii
8d92409d7b Fix Guru Meditation Error on esp32 2023-12-15 23:58:25 +03:00
Menno de Graaf
15645f4d30 Add log info 2023-12-15 14:46:35 +01:00
Menno de Graaf
a5581f3778 Add master/slave set/get to initBoiler() 2023-12-15 12:42:22 +01:00
Menno de Graaf
d5e55cf0ae Improve opentherm handling
* First try setBoilerStatus, if it fails there is no need to request other info
* Do not retry to get/set non-essential parameters that the boiler does not support
2023-12-15 11:36:29 +01:00
Yurii
adbf67ac13 fix heap monitoring on esp32 2023-12-14 06:20:30 +03:00
Yurii
e13984f869 small fix 2023-12-14 06:00:09 +03:00
Yurii
38889bb59d Fix issue #18 2023-12-14 05:43:12 +03:00
Yurii
6a9a069043 removed unused lib 2023-12-14 04:02:23 +03:00
Yurii
468a7dfc02 bump TinyLogger to 1.0.9, added display of time in logs 2023-12-14 03:42:47 +03:00
Yurii
2a28f664cf bump ArduinoJson to 7.x, refactoring MqttTask 2023-12-13 23:23:54 +03:00
Yurii
8e80cecc22 heap monitoring changed 2023-12-13 23:15:51 +03:00
Yurii
a55c521e7b reconnection interval to mqtt increased 2023-12-13 23:13:54 +03:00
Yurii
a47888c17a fix typo 2023-12-13 23:08:36 +03:00
Yurii
e9c91ed6b4 fix reconnect & memory optimization 2023-12-13 00:43:12 +03:00
Yurii
b6276ddb3f ble in HaHelper refactoring 2023-12-10 23:30:45 +03:00
Yurii
17b000bdfd fix build with ble 2023-12-10 23:16:15 +03:00
Yurii
c048f31672 added check for success of PubSubClient::beginPublish() 2023-12-10 19:47:28 +03:00
Laxilef
be5f2b74bc Merge pull request #19 from mennodegraaf/ble-support
Add support for BLE temp sensors
2023-12-10 18:22:00 +03:00
Menno de Graaf
ea86bbff30 Fix merge conflict 2023-12-10 14:24:35 +01:00
Menno de Graaf
b416110d4f Merge branch 'master' into ble-support 2023-12-10 14:23:20 +01:00
Menno de Graaf
84c3859c5d Use USE_BLE flag to enable/disable BLE temp sensors 2023-12-10 14:17:13 +01:00
Yurii
b56146f759 temp replacing ESPTelnet with a fork 2023-12-10 01:31:38 +03:00
Yurii
2db1c5194a revert BufferedTelnetStream to ESPTelnetStream, reduced keep alive timeout for ESPTelnetStream 2023-12-10 01:30:15 +03:00
Yurii
29ff38c285 inc interval for yield in HomeAssistantHelper 2023-12-10 01:27:58 +03:00
Yurii
dce94b0f98 added headers for setup page 2023-12-10 01:26:14 +03:00
Yurii
1f81ec1ba5 fix publish non static ha entities 2023-12-09 08:32:02 +03:00
Yurii
e8f26aff65 fix exception due to mqtt client 2023-12-09 08:14:17 +03:00
Yurii
bc0ba5bdd8 small fix externalPump 2023-12-09 08:12:36 +03:00
Yurii
bc23bbc9f3 fix typo in printf 2023-12-09 08:01:34 +03:00
Yurii
d61b8a8ecb fix BufferedTelnetStream 2023-12-09 04:09:49 +03:00
Yurii
3fbb26fd91 added BufferedTelnetStream 2023-12-09 03:43:14 +03:00
Menno de Graaf
3ed2b22d06 Prevent using deprecated type 2023-12-08 10:42:24 +01:00
Menno de Graaf
5ecbddc929 Add support for BLE temp sensors 2023-12-08 10:32:58 +01:00
Yurii
dbcca514b0 small fix 2023-12-07 22:04:13 +03:00
Yurii
96d506ba57 refactoring 2023-12-07 22:03:41 +03:00
Yurii
03aeaa9441 ha scripts
fix bug and compatibility with older versions
2023-12-07 21:26:16 +03:00
Yurii
5bdc28675f upd readme 2023-12-07 16:29:46 +03:00
Yurii
5633d0d2ee fix ha scripts 2023-12-07 16:04:51 +03:00
Yurii
85cd37c4ae refactoring 2023-12-07 02:57:06 +03:00
Yurii
45630c3be9 fix times ext pump 2023-12-07 02:09:10 +03:00
Yurii
dd293e9802 added external pump control 2023-12-07 01:01:19 +03:00
Yurii
412740c5d2 upd readme 2023-12-06 19:44:34 +03:00
Yurii
c95a19eb42 fix current temperature in climate.heating entity 2023-12-05 19:52:57 +03:00
Yurii
9b32ccca16 small fix 2023-12-04 08:49:57 +03:00
Yurii
60f66a4ead attach OT task to 1 core 2023-12-04 08:49:42 +03:00
Yurii
7740d9c4c7 disable tasks before ota update 2023-12-04 08:48:02 +03:00
Yurii
c5434e0a45 fix sendRequest() 2023-12-03 06:43:55 +03:00
Yurii
cd0e8a6a11 bump ESP32Scheduler to 1.0.1 2023-12-02 23:10:50 +03:00
Yurii
88682eef13 modify task priority for ESP32, remove attach to core 2023-12-02 23:06:32 +03:00
Yurii
c0a181632a changing intervals for some tasks 2023-12-02 23:04:30 +03:00
Yurii
a457ab31be added buffer for writing to mqtt, refactoring 2023-12-02 23:03:19 +03:00
Yurii
3284e84b67 added buffer for writing to mqtt 2023-12-02 23:02:48 +03:00
Yurii
e379df388c bump TinyLogger to 1.0.7 2023-12-01 14:36:06 +03:00
Yurii
ff91e328cb refactoring 2023-12-01 14:35:13 +03:00
Yurii
522f8ec699 bump TinyLogger to 1.0.6 2023-11-29 23:58:05 +03:00
Yurii
ad1fe601b3 fix typo 2023-11-29 23:57:38 +03:00
Yurii
40d9606bea bump TinyLogger to 1.0.5 2023-11-29 18:47:16 +03:00
Yurii
10df1c1d34 fix typo 2023-11-28 17:51:19 +03:00
Yurii
ead8c64e92 upd images 2023-11-28 13:31:38 +03:00
Yurii
9bc4c06cad upd ha scripts 2023-11-28 12:45:49 +03:00
Yurii
7763ee9fa9 ha scripts 2023-11-27 21:28:27 +03:00
Yurii
2c26b1cb92 added interval for OT task 2023-11-27 14:38:19 +03:00
Yurii
02d2f0f524 move USE_TELNET & USE_SERIAL to global env section 2023-11-27 14:37:24 +03:00
Laxilef
df99aae812 Merge pull request #13 from mennodegraaf/patch-1
Support for Wemos D1 mini32
2023-11-26 23:28:27 +03:00
Menno de Graaf
4d0c87f0b5 Support for Wemos D1 mini32 2023-11-26 19:14:47 +01:00
Yurii
46de4e0cfc upd readme 2023-11-26 19:52:24 +03:00
Yurii
83296855ba many changes
* added parameter "Modulation sync with heating"
* refactoring
2023-11-26 19:36:47 +03:00
Yurii
0ded2c53d8 TinyLogger bump to 1.0.4 2023-11-26 19:33:08 +03:00
Yurii
8ec3655340 add method to f8.8, rename method from f8.8 2023-11-26 16:04:56 +03:00
Yurii
eb2dcd7c09 fix when using two sensors, str to flash 2023-11-26 16:02:38 +03:00
Yurii
8a4b598161 many changes
* migrate from jandrassy/TelnetStream to lennarthennigs/ESP Telnet
* ability to turn on/off output logs to telnet and serial
* memory optimization
* added OT parameter DHW blocking
* changed algorithm for setting OpenThermMessageID::MConfigMMemberIDcode
* refactoring
2023-11-26 00:17:47 +03:00
Yurii
0dee4c20ce refactoring 2023-11-23 09:08:02 +03:00
Yurii
761788792f fix state class for uptime 2023-11-23 05:18:44 +03:00
Yurii
70e577e29f states.faultCode moved to sensors.faultCode, states.rssi moved to sensors.rssi, states.uptime moved to sensors.uptime 2023-11-23 05:11:58 +03:00
Yurii
b268ff4007 reformat code 2023-11-23 04:57:23 +03:00
Yurii
92a2cb9d56 added DHW flow rate from OT 2023-11-23 04:41:39 +03:00
Yurii
e82d47e1dc added OT parameter DHW to CH2 2023-11-23 01:57:08 +03:00
Yurii
7bfad224fe fix set min & max dhw temp 2023-11-23 01:56:19 +03:00
Yurii
d07f73edf4 added heating status pin 2023-11-23 00:30:21 +03:00
Yurii
d2271513f2 rename methods 2023-11-23 00:28:23 +03:00
Yurii
b5a0550c72 fixes 2023-11-22 23:29:31 +03:00
Yurii
c556c38cbc small fix settings 2023-11-22 23:07:02 +03:00
Yurii
44fdff61bd fix naming 2023-11-22 23:04:34 +03:00
Yurii
18acf059fc memory optimization 2023-11-22 21:00:19 +03:00
Yurii
227060591f add parameter for max modulation 2023-11-22 20:59:37 +03:00
Yurii
31cefce4bc upd readme 2023-11-22 18:40:37 +03:00
Yurii
5672ff0c3d use TinyLogger lib 2023-11-22 17:53:43 +03:00
Yurii
8bccfcb95d Refactoring and new entities 2023-11-22 17:52:16 +03:00
Yurii
5443ab82ce Merge branch 'master' of https://github.com/Laxilef/OTGateway 2023-11-19 17:59:22 +03:00
Yurii
361628f4f5 fix typo 2023-11-19 17:59:17 +03:00
Yurii
75d31b73ff WM Parameters fix 2023-11-19 17:58:39 +03:00
Laxilef
065155c930 Update README.md 2023-11-17 22:15:46 +03:00
Yurii
ee5c6fc953 upd readme, bump version 2023-11-17 22:14:45 +03:00
Yurii
7c5810e6d1 features
* Added new OT parameters
* Improved compatibility with the boiler ITALTHERM TIME MAX 30F
* Refactoring min/max temp
* Fix port forwarding: disable captive portal after connecting to wifi
* Compatible with WOKWI emulator
* upd README
2023-11-17 21:47:33 +03:00
Yurii
9f24efb0ab fix typo 2023-11-17 21:41:38 +03:00
Yurii
4b9ebeaa40 fix types 2023-11-16 10:34:25 +03:00
Yurii
76eaec10ea wdt fix for esp8266 2023-11-16 10:33:04 +03:00
Yurii
21de692888 small fix 2023-11-16 08:57:14 +03:00
Yurii
ce318b8cde add header to config portal 2023-11-16 08:06:50 +03:00
Yurii
77b0859cc8 fix config portal 2023-11-16 03:30:24 +03:00
Yurii
3fcb17f2c3 upd readme 2023-11-16 03:29:13 +03:00
Yurii
6204b46c17 Update the code for the status led 2023-11-16 03:29:06 +03:00
Yurii
b5760eb314 fixed heap size
fixed core numbers for esp32 tasks
compatible with lolin_c3_mini
delete task main loop() for esp32
2023-11-12 20:54:02 +03:00
Yurii
eedbd7b80a auto restart after changing some settings 2023-11-11 21:40:22 +03:00
Yurii
e9bf4c4bd5 fix default pins 2023-11-11 21:39:15 +03:00
Yurii
a255dda8dd Compatible with ESP32 2023-11-11 05:01:36 +03:00
Yurii
6de6f7d138 Added callback for yield 2023-11-11 05:00:55 +03:00
Yurii
5e8916b254 Refactoring HomeAssistantHelper 2023-11-11 04:59:39 +03:00
Yurii
7db49350a2 Optimization HomeAssistantHelper 2023-11-06 18:38:57 +03:00
Yurii
8e2a70ec04 upd build script 2023-11-06 16:44:00 +03:00
Yurii
b2ff71c59a upd readme 2023-11-06 16:43:47 +03:00
Yurii
3660b89f7c upd readme 2023-11-06 16:42:03 +03:00
Yurii
5c0dfc544e update to 1.3.2 2023-10-21 03:02:38 +03:00
Yurii
5fba94312b added build functions 2023-10-21 03:01:48 +03:00
Yurii
62bea87f8a upd env 2023-10-21 01:48:44 +03:00
Yurii
f52aa8e889 removed unused code 2023-10-20 21:07:12 +03:00
Yurii
df8354866f fix entity climate.heating 2023-10-20 21:06:44 +03:00
Yurii
6242db7a29 removed unused code 2023-10-19 02:20:37 +03:00
Yurii
dc00fdcdb6 Fix Error ''max' must be > 'min'' when processing MQTT 2023-10-19 02:18:39 +03:00
Yurii
2615e9106e upd readme 2023-10-19 00:45:06 +03:00
Yurii
0f60a07a71 upd readme 2023-10-19 00:43:17 +03:00
Yurii
f8750373d4 format code 2023-10-19 00:40:07 +03:00
Yurii
d5a92c47c7 Fixed get current dhw temp 2023-10-19 00:34:11 +03:00
Yurii
bc91168bbf added interval for forced set temperatures 2023-10-17 14:50:43 +03:00
Yurii
96c1a187cd upd readme, get modulation fix 2023-10-17 14:14:24 +03:00
59 changed files with 12674 additions and 5272 deletions

20
.github/workflows/stale.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: "Close stale issues and PR"
on:
schedule:
- cron: "0 * * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: >
This issue is stale because it has been open 15 days with no activity. Remove stale label or comment or this will be closed in 5 days.
close-issue-message: >
This issue was closed because it has been stalled for 5 days with no activity.
days-before-stale: 15
days-before-close: 5
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "documentation,bug,enhancement"

6
.gitignore vendored
View File

@@ -1,2 +1,6 @@
.pio
.vscode
.vscode
build/*.bin
data/**/*.gz
secrets.ini
!.gitkeep

219
README.md
View File

@@ -1,4 +1,13 @@
![logo](/assets/logo.svg)
<div align="center">
![logo](/assets/logo.svg)
<br>
[![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)
</div>
## Features
- Hot water temperature control
@@ -7,200 +16,68 @@
- 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:
- The process of heating the coolant for heating: works/does not work
- The process of heating: works/does not work
- The process of heating water for hot water: working/not working
- Display of boiler errors
- Burner status: on/off
- Burner status (flame): on/off
- Burner modulation level in percent
- Pressure in the heating system
- Gateway status (depending on errors and connection status)
- Boiler connection status via OpenTherm interface
- The current temperature of the heat carrier (usually the return heat carrier)
- Set coolant temperature (depending on the selected mode)
- 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)
## Tested on
| Boiler | Master Member ID | Notes |
| --- | --- | --- |
| BAXI ECO Nova | default | Pressure sensor not supported, modulation level not stable |
| BAXI Ampera | 1028 | Pressure sensor not supported, only heating (DHW not tested) |
| [Remeha Calenta Ace 40C](https://github.com/Laxilef/OTGateway/issues/1#issuecomment-1726081554) | default | - |
## Documentation
All available information and instructions can be found in the wiki:
## PCB
<img src="/assets/pcb.svg" width="27%" /> <img src="/assets/pcb_3d.png" width="30%" /> <img src="/assets/after_assembly.png" width="40%" />
Housing for installation on DIN rail - D2MG. Occupies only 2 DIN modules.<br>
The 220V > 5V power supply is already on the board, so additional power supplies are not needed.<br>
To save money, 2 levels are ordered as one board. After manufacturing, the boards need to be divided into 2 parts - upper and lower. The boards are inexpensively (5pcs for $2) manufactured at JLCPCB (Remove Order Number = Specify a location).<br><br>
Some components can be replaced with similar ones (for example use a fuse and led with legs). Some SMD components (for example optocouplers) can be replaced with similar SOT components.<br>Most of the components can be purchased inexpensively on Aliexpress, the rest in your local stores.<br><br>
The outdoor temperature sensor must be connected to the **TEMP1** connector, the indoor temperature sensor must be connected to the **TEMP2** connector. The power supply for the sensors must be connected to the **3.3V** connector, GND to **GND**.<br>
**The opentherm connection polarity does not matter.**
<!-- **Important!** On this board opentherm IN pin = 5, OUT pin = 4 -->
- [Schematic](/assets/Schematic.pdf)
- [BOM](/assets/BOM.xlsx)
- [Gerber](/assets/gerber.zip)
## Another compatible OpenTherm Adapters
- [Ihor Melnyk OpenTherm Adapter](http://ihormelnyk.com/opentherm_adapter)
- [DIYLESS Master OpenTherm Shield](https://diyless.com/product/master-opentherm-shield)
- [OpenTherm master shield for Wemos/Lolin](https://www.tindie.com/products/thehognl/opentherm-master-shield-for-wemoslolin/)
- And others. It's just that the adapter must implement [the schema](http://ihormelnyk.com/Content/Pages/opentherm_adapter/opentherm_adapter_schematic_o.png)
## Compatible Temperature Sensors
* DS18B20
* DS1822
* DS1820
* MAX31820
* MAX31850
[See more](https://github.com/milesburton/Arduino-Temperature-Control-Library#usage)
# Quick Start
1. Download the latest firmware from the [releases page](https://github.com/Laxilef/OTGateway/releases) (or compile yourself) and flash your ESP8266 board using the [ESP Flash Download Tool](https://www.espressif.com/en/support/download/other-tools) or other software.
2. Connect to *OpenTherm Gateway* hotspot, password: otgateway123456
3. Open configuration page in browser: 192.168.4.1
4. Set up a connection to your wifi network
5. Set up a connection to your MQTT server: ip, port, user, password
6. Set up a **Opentherm pin IN** & **Opentherm pin OUT**. No change for my board. Typically used **IN** = 4, **OUT** = 5
7. Set up a **Outdoor sensor pin** & **Indoor sensor pin**. No change for my board.
8. if necessary, set up a the master member ID ([see more](#tested-on))
9. Restart module (required after changing OT pins and/or sensors pins!)
After connecting to your wifi network, you can go to the setup page at the address that ESP8266 received.
The OTGateway device will be automatically added to homeassistant if MQTT server ip, login and password are correct.
## HomeAsssistant settings
By default, the "Equitherm" and "PID" modes are disabled. In this case, the boiler will simply maintain the temperature you set.
To use "Equitherm" or "PID" modes, the controller needs to know the temperature inside and outside the house.<br><br>
The temperature inside the house can be set using simple automation:
<details>
**sensor.livingroom_temperature** - temperature sensor inside the house.<br>
**number.opentherm_indoor_temp** - an entity that stores the temperature value inside the house. The default does not need to be changed.
```yaml
alias: Set boiler indoor temp
description: ""
trigger:
- platform: state
entity_id:
- sensor.livingroom_temperature
- platform: time_pattern
seconds: /30
condition: []
action:
- if:
- condition: template
value_template: "{{ has_value('number.opentherm_indoor_temp') and (states('sensor.livingroom_temperature')|float(0) - states('number.opentherm_indoor_temp')|float(0)) | abs | round(2) >= 0.01 }}"
then:
- service: number.set_value
data:
value: "{{ states('sensor.livingroom_temperature')|float(0)|round(2) }}"
target:
entity_id: number.opentherm_indoor_temp
mode: single
```
</details>
If your boiler does not support the installation of an outdoor temperature sensor or does not provide this value via the opentherm protocol, then you can use an external sensor or use automation.
<details>
<summary>Simple automation</summary>
**weather.home** - [weather entity](https://www.home-assistant.io/integrations/weather/). It is important that the address of your home is entered correctly in the Home Assistant settings.<br>
**number.opentherm_outdoor_temp** - an entity that stores the temperature value outside the house. The default does not need to be changed.
```yaml
alias: Set boiler outdoor temp
description: ""
trigger:
- platform: state
entity_id:
- weather.home
attribute: temperature
for:
hours: 0
minutes: 1
seconds: 0
- platform: time_pattern
seconds: /30
condition: []
action:
- if:
- condition: template
value_template: "{{ has_value('weather.home') and (state_attr('weather.home', 'temperature')|float(0) - states('number.opentherm_outdoor_temp')|float(0)) | abs | round(2) >= 0.1 }}"
then:
- service: number.set_value
data:
value: "{{ state_attr('weather.home', 'temperature')|float(0)|round(2) }}"
target:
entity_id: number.opentherm_outdoor_temp
mode: single
```
</details>
After these settings, you can enable the "Equitherm" and/or "PID" modes and configure them as described below.
* [Home](https://github.com/Laxilef/OTGateway/wiki)
* [Quick Start](https://github.com/Laxilef/OTGateway/wiki#quick-start)
* [Build firmware](https://github.com/Laxilef/OTGateway/wiki#build-firmware)
* [Flash firmware via ESP Flash Download Tool](https://github.com/Laxilef/OTGateway/wiki#flash-firmware-via-esp-flash-download-tool)
* [HomeAsssistant settings](https://github.com/Laxilef/OTGateway/wiki#homeasssistant-settings)
* [External temperature sensors](https://github.com/Laxilef/OTGateway/wiki#external-temperature-sensors)
* [Reporting indoor/outdoor temperature from any Home Assistant sensor](https://github.com/Laxilef/OTGateway/wiki#reporting-indooroutdoor-temperature-from-any-home-assistant-sensor)
* [Reporting outdoor temperature from Home Assistant weather integration](https://github.com/Laxilef/OTGateway/wiki#reporting-outdoor-temperature-from-home-assistant-weather-integration)
* [DHW meter](https://github.com/Laxilef/OTGateway/wiki#dhw-meter)
* [Advanced Settings](https://github.com/Laxilef/OTGateway/wiki#advanced-settings)
* [Equitherm mode](https://github.com/Laxilef/OTGateway/wiki#equitherm-mode)
* [Ratios](https://github.com/Laxilef/OTGateway/wiki#ratios)
* [Fit coefficients](https://github.com/Laxilef/OTGateway/wiki#fit-coefficients)
* [PID mode](https://github.com/Laxilef/OTGateway/wiki#pid-mode)
* [Compatibility](https://github.com/Laxilef/OTGateway/wiki/Compatibility)
* [Boilers](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boilers)
* [Boards](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boards)
* [Temperature sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#temperature-sensors)
* [FAQ & Troubleshooting](https://github.com/Laxilef/OTGateway/wiki/FAQ-&-Troubleshooting)
* [OT adapters](https://github.com/Laxilef/OTGateway/wiki/OT-adapters)
* [Adapters on sale](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#adapters-on-sale)
* [DIY](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#diy)
* [Files for production](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#files-for-production)
* [Connection](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#connection)
* [Leds on board](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#leds-on-board)
## About modes
### Equitherm
Weather-compensated temperature control maintains a comfortable set temperature in the house. The algorithm requires temperature sensors in the house and outside.<br> Instead of an outdoor sensor, you can use the weather forecast and automation for HA.
#### Ratios:
***N*** - heating curve coefficient. The coefficient is selected individually, depending on the insulation of the room, the heated area, etc.<br>
Range: 0.3...10, default: 0.7, step 0.01
***K*** - сorrection for desired room temperature.<br>
Range: 0...10, default: 3, step 0.01
***T*** - thermostat correction.<br>
Range: 0...10, default: 2, step 0.01
#### Instructions for fit coefficients:
**Tip.** I created a [table in Excel](/assets/equitherm_calc.xlsx) in which you can enter temperature parameters inside and outside the house and select coefficients. On the graph you can see the temperature that the boiler will set.
1. Set the ***K*** and ***T*** coefficients to 0.
2. The first thing you need to do is to fit the curve (***N*** coefficient). If your home has low heat loss, then start with 0.5. Otherwise start at 0.7. When the temperature inside the house stops changing, increase or decrease the coefficient value in increments of 0.1 to select the optimal curve.<br>
Please note that passive heating (sun) will affect the house temperature during curve fitting. This process is not fast and will take you 1-2 days.
Important. During curve fitting, the temperature must be kept stable as the outside temperature changes.<br>
At this stage, it is important for you to stabilize the indoor temperature at exactly 20 (+- 0.5) degrees.<br>
For example. You fit curve 0.67; set temperature 20; the temperature in the house is 20.1 degrees while the outside temperature is -10 degrees and -5 degrees. This is good.
3. After fitting the curve, you must select the ***K*** coefficient. It influences the boiler temperature correction to maintain the set temperature.
For example. Set temperature: 23 degrees; temperature in the house: 20 degrees. Try setting it to 2 and see how the temperature in the house changes after stabilization. Select the value so that the temperature in the house is close to the set.
4. Now you can choose the ***T*** coefficient. Simply put, it affects the sharpness of the temperature change. If you want fast heating, then set a high value (6-10), but then the room may overheat. If you want smooth heating, set 1-5. Choose the optimal value for yourself.
5. Check to see if it works correctly at different set temperatures over several days.
Read more about the algorithm [here](https://wdn.su/blog/1154).
### PID
See [Wikipedia](https://en.wikipedia.org/wiki/PID_controller).
![PID example](https://upload.wikimedia.org/wikipedia/commons/3/33/PID_Compensation_Animated.gif)
In Google you can find instructions for tuning the PID controller.
### Use Equitherm mode + PID mode
@todo
## Dependencies
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler)
- [NTPClient](https://github.com/arduino-libraries/NTPClient)
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler) (for ESP8266)
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
- [PubSubClient](https://github.com/knolleary/pubsubclient)
- [TelnetStream](https://github.com/jandrassy/TelnetStream)
- [EEManager](https://github.com/GyverLibs/EEManager)
- [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient)
- [ESPTelnet](https://github.com/LennartHennigs/ESPTelnet)
- [FileData](https://github.com/GyverLibs/FileData)
- [GyverPID](https://github.com/GyverLibs/GyverPID)
- [GyverBlinker](https://github.com/GyverLibs/GyverBlinker)
- [DallasTemperature](https://github.com/milesburton/Arduino-Temperature-Control-Library)
- [WiFiManager](https://github.com/tzapu/WiFiManager)
- [TinyLogger](https://github.com/laxilef/TinyLogger)
## Debug
To display DEBUG messages you must enable debug in settings (switch is disabled by default).

BIN
assets/CPL.csv Normal file

Binary file not shown.
1 Designator Footprint Mid X Mid Y Ref X Ref Y Pad X Pad Y Layer Rotation Comment
2 C1 CAP-SMD_BD6.3-L6.6-W6.6-LS7.2-FD 241.22mil -1517.21mil 241.22mil -1517.2mil 346.34mil -1517.21mil T 180 100uF
3 C2 C0805 306.22mil -1727.21mil 306.22mil -1727.2mil 266.85mil -1727.21mil T 0 100nF
4 C3 CAP-SMD_BD5.0-L5.3-W5.3-LS6.3-FD 200mil -2205mil 200mil -2205mil 294.49mil -2205mil T 180 22uF
5 D1 SOD-323F 1919.17mil -2742.21mil 1919.17mil -2742.2mil 1919.17mil -2698.21mil T 270 1N4148
6 D2 SOD-323F 2319.17mil -2742.21mil 2319.17mil -2742.2mil 2319.17mil -2698.21mil T 270 1N4148
7 D3 SOD-323F 1719.17mil -2742.21mil 1719.17mil -2742.2mil 1719.17mil -2786.21mil T 90 1N4148
8 D4 SOD-323F 2119.17mil -2742.21mil 2119.17mil -2742.2mil 2119.17mil -2786.21mil T 90 1N4148
9 F1 FUSE-7.4*4.5-5.1MM 510mil -700mil 510mil -700mil 510mil -599.61mil T 270 0.5A
10 F2 F1812 216.5mil -1200mil 216.5mil -1200mil 289.47mil -1200mil T 180 MF-MSMF110/16-2
11 L1 LED0805-RD 2527.17mil -2014.71mil 2527.17mil -2014.7mil 2527.17mil -2058.02mil T 90 LED0805
12 L2 LED0805-RD 1727.17mil -2014.71mil 1727.17mil -2014.7mil 1727.17mil -2058.02mil T 90 LED0805
13 L3 LED0805-RD 2164.17mil -2014.71mil 2164.17mil -2014.7mil 2164.17mil -2058.02mil T 90 LED0805
14 PCB_A D2MG_PCB_A_WITHOUT_TERMINALS 647.96mil -1682.91mil 647.95mil -1708.89mil 60.55mil -896.69mil T 0 D2MG_PCB_A_WITHOUT_TERMINALS
15 PCB_B D2MG_PCB_B_WITHOUT_TERMINALS 2026.57mil -1682.91mil 1389.17mil -536.69mil 1439.17mil -896.69mil T 0 D2MG_PCB_B_WITHOUT_TERMINALS
16 Q1 SOT-23(SOT-23-3) 1953.37mil -2544.81mil 1953.37mil -2544.81mil 1904.17mil -2507.41mil T 270 BC858A
17 R1 R0805 2423.67mil -2017.2mil 2423.67mil -2017.2mil 2423.67mil -1977.83mil T 270 1K
18 R2 R0805 420mil -2515mil 420mil -2515mil 420mil -2554.37mil T 90 4.7K
19 R3 R0805 1793.37mil -2559.81mil 1793.37mil -2559.81mil 1793.37mil -2599.18mil T 90 330
20 R4 R0805 2128.37mil -2529.81mil 2128.37mil -2529.81mil 2128.37mil -2569.18mil T 90 220
21 R5 R0805 2233.37mil -2529.81mil 2233.37mil -2529.81mil 2233.37mil -2490.43mil T 270 100
22 R6 R0805 1838.37mil -2209.8mil 1838.37mil -2209.81mil 1799mil -2209.8mil T 0 330
23 R7 R0805 2213.37mil -2209.8mil 2213.37mil -2209.81mil 2252.75mil -2209.8mil T 180 1.5k
24 R8 R0805 2060.67mil -2017.2mil 2060.67mil -2017.2mil 2060.67mil -2056.58mil T 90 1K
25 R9 R0805 1624.17mil -2017.2mil 1624.17mil -2017.2mil 1624.17mil -2056.58mil T 90 1K
26 R10 R0805 575mil -2515mil 575mil -2515mil 575mil -2475.63mil T 270 4.7K
27 U1 DIP-16_WEMOS_D1_MINI_PRO4 2045mil -1245mil 2045mil -1245mil 1595mil -1595mil T 180 WEMOS_D1_MINI
28 U2 PWRM-TH_HLK-PM01 881.22mil -1682.21mil 881.22mil -1682.2mil 979.65mil -1103.47mil T 270 HLK-PM01
29 U3 SOT-223-3_L6.4-W3.5-P2.30-LS7.0-BR 231.22mil -1932.21mil 231.22mil -1932.2mil 348.15mil -2022.75mil T 0 AMS1117-3.3
30 U4 SMD-4(6.5X4.58) 1628.37mil -2404.8mil 1628.37mil -2404.81mil 1678.37mil -2207.96mil T 270 PC817
31 U5 SMD-4(6.5X4.58) 2423.37mil -2404.8mil 2423.37mil -2404.81mil 2373.37mil -2601.65mil T 90 PC817
32 U6 KLS2-300-5.00-03P-2S 353.15mil -168mil 550mil -168mil 550mil -168mil T 180 KLS2-300-5.00-03P-2S
33 U7 KLS2-300-5.00-03P-2S 944.65mil -168mil 1141.5mil -168mil 1141.5mil -168mil T 180 KLS2-300-5.00-03P-2S
34 U8 KLS2-300-5.00-03P-2S 944.35mil -3198.5mil 747.5mil -3198.5mil 747.5mil -3198.5mil T 0 KLS2-300-5.00-03P-2S
35 U9 KLS2-300-5.00-03P-2S 353.85mil -3198.5mil 157mil -3198.5mil 157mil -3198.5mil T 0 KLS2-300-5.00-03P-2S
36 VAR RES-TH_L12.5-W8.0-P7.50-D0.8-S3.10 845mil -730.01mil 845mil -730mil 906.02mil -582.37mil T 270 B72210S2511K101
37 ZD1 SOD-80_L3.5-W1.5-RD 2181.38mil -2344.8mil 2181.37mil -2344.81mil 2111.89mil -2344.8mil T 0 4V7
38 ZD2 SOD-80_L3.5-W1.5-RD 1863.37mil -2407.21mil 1863.37mil -2407.2mil 1793.89mil -2407.21mil T 0 15V
39 ZD3 SOD-80_L3.5-W1.5-RD 1863.37mil -2299.8mil 1863.37mil -2299.81mil 1793.89mil -2299.8mil T 0 4V3

File diff suppressed because it is too large Load Diff

BIN
assets/dhw_meter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

16
assets/ha/dhw_meter.yaml Normal file
View File

@@ -0,0 +1,16 @@
dhw_meter:
sensor:
- platform: integration
unique_id: hot_water_meter
name: hot_water_meter
source: sensor.opentherm_dhw_flow_rate
unit_time: min
method: left
round: 2
homeassistant:
customize:
sensor.hot_water_meter:
friendly_name: "Hot water"
device_class: "water"
icon: "mdi:water-pump"

View File

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

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 716 KiB

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 154 KiB

0
build/.gitkeep Normal file
View File

0
data/.gitkeep Normal file
View File

0
data/static/.gitkeep Normal file
View File

7
esp32_partitions.csv Normal file
View File

@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x10000, 0x1B0000,
app1, app, ota_1, 0x1C0000, 0x1B0000,
spiffs, data, spiffs, 0x370000, 0x80000,
coredump, data, coredump, 0x3F0000, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xE000 0x2000
4 app0 app ota_0 0x10000 0x1B0000
5 app1 app ota_1 0x1C0000 0x1B0000
6 spiffs data spiffs 0x370000 0x80000
7 coredump data coredump 0x3F0000 0x10000

View File

@@ -0,0 +1,85 @@
class BufferedWebServer {
public:
BufferedWebServer(WebServer* webServer, size_t bufferSize = 64) {
this->webServer = webServer;
this->bufferSize = bufferSize;
this->buffer = (uint8_t*)malloc(bufferSize * sizeof(*this->buffer));
}
~BufferedWebServer() {
free(this->buffer);
}
void send(int code, const char* contentType, JsonDocument& content) {
#ifdef ARDUINO_ARCH_ESP8266
if (!this->webServer->chunkedResponseModeStart(code, contentType)) {
this->webServer->send(505, F("text/html"), F("HTTP1.1 required"));
return;
}
this->webServer->setContentLength(measureJson(content));
#else
this->webServer->setContentLength(CONTENT_LENGTH_UNKNOWN);
this->webServer->sendHeader(F("Content-Length"), String(measureJson(content)));
this->webServer->send(code, contentType, emptyString);
#endif
serializeJson(content, *this);
this->flush();
#ifdef ARDUINO_ARCH_ESP8266
this->webServer->chunkedResponseFinalize();
#else
this->webServer->sendContent(emptyString);
#endif
}
size_t write(uint8_t c) {
this->buffer[this->bufferPos++] = c;
if (this->bufferPos >= this->bufferSize) {
this->flush();
}
return 1;
}
size_t write(const uint8_t* buffer, size_t length) {
size_t written = 0;
while (written < length) {
size_t copySize = this->bufferSize - this->bufferPos;
if (written + copySize > length) {
copySize = length - written;
}
memcpy(this->buffer + this->bufferPos, buffer + written, copySize);
this->bufferPos += copySize;
if (this->bufferPos >= this->bufferSize) {
this->flush();
}
written += copySize;
}
return written;
}
void flush() {
if (this->bufferPos == 0) {
return;
}
this->webServer->sendContent((const char*)this->buffer, this->bufferPos);
this->bufferPos = 0;
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
}
protected:
WebServer* webServer = nullptr;
uint8_t* buffer;
size_t bufferSize = 64;
size_t bufferPos = 0;
};

View File

@@ -1,106 +1,199 @@
#include <Arduino.h>
#include <OpenTherm.h>
extern SchedulerClass Scheduler;
class CustomOpenTherm : public OpenTherm {
private:
unsigned long send_ts = millis();
void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt);
public:
typedef std::function<void()> YieldCallback;
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
void setHandleSendRequestCallback(void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt)) {
this->handleSendRequestCallback = handleSendRequestCallback;
~CustomOpenTherm() {}
CustomOpenTherm* setYieldCallback(YieldCallback callback = nullptr) {
this->yieldCallback = callback;
return this;
}
CustomOpenTherm* setBeforeSendRequestCallback(BeforeSendRequestCallback callback = nullptr) {
this->beforeSendRequestCallback = callback;
return this;
}
CustomOpenTherm* setAfterSendRequestCallback(AfterSendRequestCallback callback = nullptr) {
this->afterSendRequestCallback = callback;
return this;
}
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
_attempt++;
while (send_ts > 0 && millis() - send_ts < 200) {
Scheduler.yield();
while (!this->isReady()) {
if (this->yieldCallback) {
this->yieldCallback();
} else {
::yield();
}
this->process();
}
if (this->beforeSendRequestCallback) {
this->beforeSendRequestCallback(request, _attempt);
}
unsigned long _response;
if (!sendRequestAync(request)) {
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
if (!this->sendRequestAsync(request)) {
_response = 0;
} else {
while (!isReady()) {
Scheduler.yield();
process();
while (true) {
this->process();
if (this->status == OpenThermStatus::READY || this->status == OpenThermStatus::DELAY) {
break;
} else if (this->yieldCallback) {
this->yieldCallback();
} else {
::yield();
}
}
_response = getLastResponse();
_response = this->getLastResponse();
_responseStatus = this->getLastResponseStatus();
}
if (handleSendRequestCallback != NULL) {
handleSendRequestCallback(request, _response, getLastResponseStatus(), _attempt);
if (this->afterSendRequestCallback) {
this->afterSendRequestCallback(request, _response, _responseStatus, _attempt);
}
send_ts = millis();
if (getLastResponseStatus() == OpenThermResponseStatus::SUCCESS || _attempt >= attempts) {
if (_responseStatus == OpenThermResponseStatus::SUCCESS || _responseStatus == OpenThermResponseStatus::INVALID || _attempt >= attempts) {
return _response;
} else {
return sendRequest(request, attempts, _attempt);
return this->sendRequest(request, attempts, _attempt);
}
}
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking) {
return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, summerWinterMode, dhwBlocking));
}
unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking) {
unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (summerWinterMode << 5) | (dhwBlocking << 6);
unsigned int data = enableCentralHeating
| (enableHotWater << 1)
| (enableCooling << 2)
| (enableOutsideTemperatureCompensation << 3)
| (enableCentralHeating2 << 4)
| (summerWinterMode << 5)
| (dhwBlocking << 6);
data <<= 8;
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data);
return this->sendRequest(buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Status,
data
));
}
bool setBoilerTemperature(float temperature) {
unsigned int data = temperatureToData(temperature);
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data));
bool setHeatingCh1Temp(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TSet,
temperatureToData(temperature)
));
return isValidResponse(response);
}
bool setBoilerTemperature2(float temperature) {
unsigned int data = temperatureToData(temperature);
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TsetCH2, data));
bool setHeatingCh2Temp(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TsetCH2,
temperatureToData(temperature)
));
return isValidResponse(response);
}
bool setDhwTemp(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TdhwSet,
temperatureToData(temperature)
));
return isValidResponse(response);
}
bool setRoomSetpoint(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::TrSet,
temperatureToData(temperature)
));
return isValidResponse(response);
}
bool setRoomTemp(float temperature) {
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::Tr,
temperatureToData(temperature)
));
return isValidResponse(response);
}
bool sendBoilerReset() {
unsigned int data = 1;
data <<= 8;
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
}
bool sendServiceReset() {
unsigned int data = 10;
data <<= 8;
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
}
bool sendWaterFilling() {
unsigned int data = 2;
data <<= 8;
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::Command, data));
unsigned long response = this->sendRequest(buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::RemoteRequest,
data
));
return isValidResponse(response);
}
// converters
float f88(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
float value = (int8_t) valueHB;
return value + (float)valueLB / 256.0;
template <class T>
static unsigned int toFloat(const T val) {
return (unsigned int)(val * 256);
}
int16_t s16(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
int16_t value = valueHB;
return ((value << 8) + valueLB);
static short getInt(const unsigned long response) {
return response & 0xffff;
}
protected:
YieldCallback yieldCallback;
BeforeSendRequestCallback beforeSendRequestCallback;
AfterSendRequestCallback afterSendRequestCallback;
};

View File

@@ -17,7 +17,7 @@ public:
float Kk = 0.0;
float Kt = 0.0;
Equitherm() {}
Equitherm() = default;
// kn, kk, kt
Equitherm(float new_kn, float new_kk, float new_kt) {

View File

@@ -0,0 +1,145 @@
#pragma once
#include <Arduino.h>
#include "strings.h"
class HomeAssistantHelper {
public:
typedef std::function<void(const char*, bool)> PublishEventCallback;
HomeAssistantHelper() = default;
void setWriter() {
this->writer = nullptr;
}
void setWriter(MqttWriter* writer) {
this->writer = writer;
}
void setPublishEventCallback(PublishEventCallback callback) {
this->publishEventCallback = callback;
}
void setDevicePrefix(const char* value) {
this->devicePrefix = value;
}
void setDeviceVersion(const char* value) {
this->deviceVersion = value;
}
void setDeviceManufacturer(const char* value) {
this->deviceManufacturer = value;
}
void setDeviceModel(const char* value) {
this->deviceModel = value;
}
void setDeviceName(const char* value) {
this->deviceName = value;
}
void setDeviceConfigUrl(const char* value) {
this->deviceConfigUrl = value;
}
bool publish(const char* topic, JsonDocument& doc) {
if (this->writer == nullptr) {
if (this->publishEventCallback) {
this->publishEventCallback(topic, false);
}
return false;
}
doc[FPSTR(HA_DEVICE)][FPSTR(HA_IDENTIFIERS)][0] = this->devicePrefix;
doc[FPSTR(HA_DEVICE)][FPSTR(HA_SW_VERSION)] = this->deviceVersion;
if (this->deviceManufacturer != nullptr) {
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MANUFACTURER)] = this->deviceManufacturer;
}
if (this->deviceModel != nullptr) {
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MODEL)] = this->deviceModel;
}
if (this->deviceName != nullptr) {
doc[FPSTR(HA_DEVICE)][FPSTR(HA_NAME)] = this->deviceName;
}
if (this->deviceConfigUrl != nullptr) {
doc[FPSTR(HA_DEVICE)][FPSTR(HA_CONF_URL)] = this->deviceConfigUrl;
}
bool result = this->writer->publish(topic, doc, true);
doc.clear();
doc.shrinkToFit();
if (this->publishEventCallback) {
this->publishEventCallback(topic, result);
}
return result;
}
bool publish(const char* topic) {
if (this->writer == nullptr) {
if (this->publishEventCallback) {
this->publishEventCallback(topic, false);
}
return false;
}
bool result = writer->publish(topic, nullptr, 0, true);
if (this->publishEventCallback) {
this->publishEventCallback(topic, result);
}
return result;
}
template <class T>
String getTopic(T category, T name, char nameSeparator = '/') {
String topic = "";
topic.concat(this->prefix);
topic.concat('/');
topic.concat(category);
topic.concat('/');
topic.concat(this->devicePrefix);
topic.concat(nameSeparator);
topic.concat(name);
topic.concat("/config");
return topic;
}
template <class T>
String getDeviceTopic(T value, char separator = '/') {
String topic = "";
topic.concat(this->devicePrefix);
topic.concat(separator);
topic.concat(value);
return topic;
}
template <class T>
String getObjectId(T value, char separator = '_') {
String topic = "";
topic.concat(this->devicePrefix);
topic.concat(separator);
topic.concat(value);
return topic;
}
protected:
PublishEventCallback publishEventCallback;
MqttWriter* writer = nullptr;
const char* prefix = "homeassistant";
const char* devicePrefix = "";
const char* deviceVersion = "1.0";
const char* deviceManufacturer = nullptr;
const char* deviceModel = nullptr;
const char* deviceName = nullptr;
const char* deviceConfigUrl = nullptr;
};

View File

@@ -0,0 +1,70 @@
#pragma once
#ifndef PROGMEM
#define PROGMEM
#endif
const char HA_ENTITY_BINARY_SENSOR[] PROGMEM = "binary_sensor";
const char HA_ENTITY_BUTTON[] PROGMEM = "button";
const char HA_ENTITY_FAN[] PROGMEM = "fan";
const char HA_ENTITY_CLIMATE[] PROGMEM = "climate";
const char HA_ENTITY_NUMBER[] PROGMEM = "number";
const char HA_ENTITY_SELECT[] PROGMEM = "select";
const char HA_ENTITY_SENSOR[] PROGMEM = "sensor";
const char HA_ENTITY_SWITCH[] PROGMEM = "switch";
const char HA_DEVICE[] PROGMEM = "device";
const char HA_IDENTIFIERS[] PROGMEM = "identifiers";
const char HA_SW_VERSION[] PROGMEM = "sw_version";
const char HA_MANUFACTURER[] PROGMEM = "manufacturer";
const char HA_MODEL[] PROGMEM = "model";
const char HA_NAME[] PROGMEM = "name";
const char HA_CONF_URL[] PROGMEM = "configuration_url";
const char HA_COMMAND_TOPIC[] PROGMEM = "command_topic";
const char HA_COMMAND_TEMPLATE[] PROGMEM = "command_template";
const char HA_ENABLED_BY_DEFAULT[] PROGMEM = "enabled_by_default";
const char HA_UNIQUE_ID[] PROGMEM = "unique_id";
const char HA_OBJECT_ID[] PROGMEM = "object_id";
const char HA_ENTITY_CATEGORY[] PROGMEM = "entity_category";
const char HA_STATE_TOPIC[] PROGMEM = "state_topic";
const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template";
const char HA_OPTIONS[] PROGMEM = "options";
const char HA_AVAILABILITY[] PROGMEM = "availability";
const char HA_AVAILABILITY_MODE[] PROGMEM = "availability_mode";
const char HA_TOPIC[] PROGMEM = "topic";
const char HA_DEVICE_CLASS[] PROGMEM = "device_class";
const char HA_UNIT_OF_MEASUREMENT[] PROGMEM = "unit_of_measurement";
const char HA_UNIT_OF_MEASUREMENT_C[] PROGMEM = "°C";
const char HA_UNIT_OF_MEASUREMENT_F[] PROGMEM = "°F";
const char HA_ICON[] PROGMEM = "icon";
const char HA_MIN[] PROGMEM = "min";
const char HA_MAX[] PROGMEM = "max";
const char HA_STEP[] PROGMEM = "step";
const char HA_MODE[] PROGMEM = "mode";
const char HA_STATE_ON[] PROGMEM = "state_on";
const char HA_STATE_OFF[] PROGMEM = "state_off";
const char HA_PAYLOAD_ON[] PROGMEM = "payload_on";
const char HA_PAYLOAD_OFF[] PROGMEM = "payload_off";
const char HA_STATE_CLASS[] PROGMEM = "state_class";
const char HA_EXPIRE_AFTER[] PROGMEM = "expire_after";
const char HA_CURRENT_TEMPERATURE_TOPIC[] PROGMEM = "current_temperature_topic";
const char HA_CURRENT_TEMPERATURE_TEMPLATE[] PROGMEM = "current_temperature_template";
const char HA_TEMPERATURE_COMMAND_TOPIC[] PROGMEM = "temperature_command_topic";
const char HA_TEMPERATURE_COMMAND_TEMPLATE[] PROGMEM = "temperature_command_template";
const char HA_TEMPERATURE_STATE_TOPIC[] PROGMEM = "temperature_state_topic";
const char HA_TEMPERATURE_STATE_TEMPLATE[] PROGMEM = "temperature_state_template";
const char HA_TEMPERATURE_UNIT[] PROGMEM = "temperature_unit";
const char HA_MODE_COMMAND_TOPIC[] PROGMEM = "mode_command_topic";
const char HA_MODE_COMMAND_TEMPLATE[] PROGMEM = "mode_command_template";
const char HA_MODE_STATE_TOPIC[] PROGMEM = "mode_state_topic";
const char HA_MODE_STATE_TEMPLATE[] PROGMEM = "mode_state_template";
const char HA_MODES[] PROGMEM = "modes";
const char HA_ACTION_TOPIC[] PROGMEM = "action_topic";
const char HA_ACTION_TEMPLATE[] PROGMEM = "action_template";
const char HA_MIN_TEMP[] PROGMEM = "min_temp";
const char HA_MAX_TEMP[] PROGMEM = "max_temp";
const char HA_TEMP_STEP[] PROGMEM = "temp_step";
const char HA_PRESET_MODE_COMMAND_TOPIC[] PROGMEM = "preset_mode_command_topic";
const char HA_PRESET_MODE_COMMAND_TEMPLATE[] PROGMEM = "preset_mode_command_template";
const char HA_PRESET_MODE_STATE_TOPIC[] PROGMEM = "preset_mode_state_topic";
const char HA_PRESET_MODE_VALUE_TEMPLATE[] PROGMEM = "preset_mode_value_template";
const char HA_PRESET_MODES[] PROGMEM = "preset_modes";

View File

@@ -0,0 +1,24 @@
#include <WiFiClient.h>
class MqttWiFiClient : public WiFiClient {
public:
#ifdef ARDUINO_ARCH_ESP8266
void flush() override {
if (this->connected()) {
WiFiClient::flush(0);
}
}
void stop() override {
this->abort();
}
#endif
#ifdef ARDUINO_ARCH_ESP32
void setSync(bool) {}
bool getSync() {
return false;
}
#endif
};

220
lib/MqttWriter/MqttWriter.h Normal file
View File

@@ -0,0 +1,220 @@
#pragma once
#include <Arduino.h>
#include <MqttClient.h>
#ifdef ARDUINO_ARCH_ESP32
#include <mutex>
#endif
class MqttWriter {
public:
typedef std::function<void()> YieldCallback;
typedef std::function<void(const char*, size_t, size_t, bool)> PublishEventCallback;
typedef std::function<void(size_t, size_t)> FlushEventCallback;
MqttWriter(MqttClient* client, size_t bufferSize = 64) {
this->client = client;
this->bufferSize = bufferSize;
this->buffer = (uint8_t*) malloc(bufferSize * sizeof(*this->buffer));
#ifdef ARDUINO_ARCH_ESP32
this->mutex = new std::mutex();
#endif
}
~MqttWriter() {
free(this->buffer);
#ifdef ARDUINO_ARCH_ESP32
delete this->mutex;
#endif
}
MqttWriter* setYieldCallback(YieldCallback callback = nullptr) {
this->yieldCallback = callback;
return this;
}
MqttWriter* setPublishEventCallback(PublishEventCallback callback) {
this->publishEventCallback = callback;
return this;
}
MqttWriter* setFlushEventCallback(FlushEventCallback callback) {
this->flushEventCallback = callback;
return this;
}
bool lock() {
#ifdef ARDUINO_ARCH_ESP32
if (!this->mutex->try_lock()) {
return false;
}
#else
if (this->isLocked()) {
return false;
}
#endif
this->locked = true;
this->writeAfterLock = 0;
this->lockedTime = millis();
return true;
}
bool isLocked() {
return this->locked;
}
void unlock() {
this->locked = false;
#if defined(ARDUINO_ARCH_ESP32)
this->mutex->unlock();
#endif
}
bool publish(const char* topic, JsonDocument& doc, bool retained = false) {
if (!this->client->connected()) {
this->bufferPos = 0;
return false;
}
while (!this->lock()) {
if (this->yieldCallback) {
this->yieldCallback();
}
}
this->bufferPos = 0;
size_t docSize = measureJson(doc);
size_t written = 0;
if (this->client->beginMessage(topic, docSize, retained)) {
serializeJson(doc, *this);
this->flush();
this->client->endMessage();
written = this->writeAfterLock;
}
this->unlock();
if (this->publishEventCallback) {
this->publishEventCallback(topic, written, docSize, written == docSize);
}
return written == docSize;
}
bool publish(const char* topic, const char* buffer, bool retained = false) {
return this->publish(topic, (const uint8_t*) buffer, strlen(buffer), retained);
}
bool publish(const char* topic, const uint8_t* buffer, size_t length, bool retained = false) {
if (!this->client->connected()) {
this->bufferPos = 0;
return false;
}
while (!this->lock()) {
if (this->yieldCallback) {
this->yieldCallback();
}
}
this->bufferPos = 0;
size_t written = 0;
bool result = false;
if (!length || buffer == nullptr) {
result = this->client->beginMessage(topic, retained) && this->client->endMessage();
} else if (this->client->beginMessage(topic, length, retained)) {
this->write(buffer, length);
this->flush();
this->client->endMessage();
written = this->writeAfterLock;
result = written == length;
}
this->unlock();
if (this->publishEventCallback) {
this->publishEventCallback(topic, written, length, result);
}
return result;
}
size_t write(uint8_t c) {
this->buffer[this->bufferPos++] = c;
if (this->bufferPos >= this->bufferSize) {
this->flush();
}
return 1;
}
size_t write(const uint8_t* buffer, size_t length) {
size_t written = 0;
while (written < length) {
size_t copySize = this->bufferSize - this->bufferPos;
if (written + copySize > length) {
copySize = length - written;
}
memcpy(this->buffer + this->bufferPos, buffer + written, copySize);
this->bufferPos += copySize;
if (this->bufferPos >= this->bufferSize) {
this->flush();
}
written += copySize;
}
return written;
}
bool flush() {
if (this->bufferPos == 0) {
return false;
}
if (!this->client->connected()) {
this->bufferPos = 0;
}
size_t length = this->bufferPos;
size_t written = this->client->write(this->buffer, length);
this->client->flush();
this->bufferPos = 0;
if (this->isLocked()) {
this->writeAfterLock += written;
}
if (this->flushEventCallback) {
this->flushEventCallback(written, length);
}
return written == length;
}
protected:
YieldCallback yieldCallback;
PublishEventCallback publishEventCallback;
FlushEventCallback flushEventCallback;
MqttClient* client;
uint8_t* buffer;
size_t bufferSize = 64;
size_t bufferPos = 0;
bool locked = false;
#ifdef ARDUINO_ARCH_ESP32
mutable std::mutex* mutex;
#endif
unsigned long lockedTime = 0;
size_t writeAfterLock = 0;
};

View File

@@ -0,0 +1,150 @@
#include "NetworkConnection.h"
using namespace Network;
void Connection::setup(bool useDhcp) {
setUseDhcp(useDhcp);
#if defined(ARDUINO_ARCH_ESP8266)
wifi_set_event_handler_cb(Connection::onEvent);
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.onEvent(Connection::onEvent);
#endif
}
void Connection::reset() {
status = Status::NONE;
disconnectReason = DisconnectReason::NONE;
}
void Connection::setUseDhcp(bool value) {
useDhcp = value;
}
Connection::Status Connection::getStatus() {
return status;
}
Connection::DisconnectReason Connection::getDisconnectReason() {
return disconnectReason;
}
#if defined(ARDUINO_ARCH_ESP8266)
void Connection::onEvent(System_Event_t *event) {
switch (event->event) {
case EVENT_STAMODE_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_GOT_IP:
status = Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_DHCP_TIMEOUT:
status = Status::DISCONNECTED;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case EVENT_STAMODE_DISCONNECTED:
status = Status::DISCONNECTED;
disconnectReason = convertDisconnectReason(event->event_info.disconnected.reason);
// https://github.com/esp8266/Arduino/blob/d5eb265f78bff9deb7063d10030a02d021c8c66c/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp#L231
if ((wifi_station_get_connect_status() == STATION_GOT_IP) && !wifi_station_get_reconnect_policy()) {
wifi_station_disconnect();
}
break;
case EVENT_STAMODE_AUTHMODE_CHANGE:
// https://github.com/esp8266/Arduino/blob/d5eb265f78bff9deb7063d10030a02d021c8c66c/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp#L241
{
auto& src = event->event_info.auth_change;
if ((src.old_mode != AUTH_OPEN) && (src.new_mode == AUTH_OPEN)) {
status = Status::DISCONNECTED;
disconnectReason = DisconnectReason::OTHER;
wifi_station_disconnect();
}
}
break;
default:
break;
}
}
#elif defined(ARDUINO_ARCH_ESP32)
void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
status = Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
status = Status::DISCONNECTED;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
status = Status::DISCONNECTED;
disconnectReason = convertDisconnectReason(info.wifi_sta_disconnected.reason);
break;
default:
break;
}
}
#endif
Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason) {
switch (reason) {
#if defined(ARDUINO_ARCH_ESP8266)
case REASON_BEACON_TIMEOUT:
return DisconnectReason::BEACON_TIMEOUT;
case REASON_NO_AP_FOUND:
return DisconnectReason::NO_AP_FOUND;
case REASON_AUTH_FAIL:
return DisconnectReason::AUTH_FAIL;
case REASON_ASSOC_FAIL:
return DisconnectReason::ASSOC_FAIL;
case REASON_HANDSHAKE_TIMEOUT:
return DisconnectReason::HANDSHAKE_TIMEOUT;
#elif defined(ARDUINO_ARCH_ESP32)
case WIFI_REASON_BEACON_TIMEOUT:
return DisconnectReason::BEACON_TIMEOUT;
case WIFI_REASON_NO_AP_FOUND:
return DisconnectReason::NO_AP_FOUND;
case WIFI_REASON_AUTH_FAIL:
return DisconnectReason::AUTH_FAIL;
case WIFI_REASON_ASSOC_FAIL:
return DisconnectReason::ASSOC_FAIL;
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return DisconnectReason::HANDSHAKE_TIMEOUT;
#endif
default:
return DisconnectReason::OTHER;
}
}
bool Connection::useDhcp = false;
Connection::Status Connection::status = Status::NONE;
Connection::DisconnectReason Connection::disconnectReason = DisconnectReason::NONE;

View File

@@ -0,0 +1,46 @@
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include "lwip/etharp.h"
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#endif
namespace Network {
struct Connection {
enum class Status {
CONNECTED,
CONNECTING,
DISCONNECTED,
NONE
};
enum class DisconnectReason {
BEACON_TIMEOUT,
NO_AP_FOUND,
AUTH_FAIL,
ASSOC_FAIL,
HANDSHAKE_TIMEOUT,
DHCP_TIMEOUT,
OTHER,
NONE
};
static Status status;
static DisconnectReason disconnectReason;
static void setup(bool useDhcp);
static void setUseDhcp(bool value);
static void reset();
static Status getStatus();
static DisconnectReason getDisconnectReason();
#if defined(ARDUINO_ARCH_ESP8266)
static void onEvent(System_Event_t *evt);
#elif defined(ARDUINO_ARCH_ESP32)
static void onEvent(WiFiEvent_t event, WiFiEventInfo_t info);
#endif
protected:
static DisconnectReason convertDisconnectReason(uint8_t reason);
static bool useDhcp;
};
}

View File

@@ -0,0 +1,443 @@
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include "lwip/etharp.h"
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <esp_wifi.h>
#endif
#include <NetworkConnection.h>
namespace Network {
class Manager {
public:
typedef std::function<void()> YieldCallback;
typedef std::function<void(unsigned int)> DelayCallback;
Manager() {
Connection::setup(this->useDhcp);
this->resetWifi();
}
Manager* setYieldCallback(YieldCallback callback = nullptr) {
this->yieldCallback = callback;
return this;
}
Manager* setDelayCallback(DelayCallback callback = nullptr) {
this->delayCallback = callback;
return this;
}
Manager* setHostname(const char* value) {
this->hostname = value;
return this;
}
Manager* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
this->apName = ssid;
this->apPassword = password;
this->apChannel = channel;
return this;
}
Manager* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
this->staSsid = ssid;
this->staPassword = password;
this->staChannel = channel;
return this;
}
Manager* setUseDhcp(bool value) {
this->useDhcp = value;
Connection::setup(this->useDhcp);
return this;
}
Manager* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
this->staticIp.fromString(ip);
this->staticGateway.fromString(gateway);
this->staticSubnet.fromString(subnet);
this->staticDns.fromString(dns);
return this;
}
Manager* setStaticConfig(IPAddress& ip, IPAddress& gateway, IPAddress& subnet, IPAddress& dns) {
this->staticIp = ip;
this->staticGateway = gateway;
this->staticSubnet = subnet;
this->staticDns = dns;
return this;
}
bool hasStaCredentials() {
return this->staSsid != nullptr;
}
bool isConnected() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTED;
}
bool isConnecting() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTING;
}
bool isStaEnabled() {
return (WiFi.getMode() & WIFI_STA) != 0;
}
bool isApEnabled() {
return (WiFi.getMode() & WIFI_AP) != 0;
}
bool hasApClients() {
if (!this->isApEnabled()) {
return false;
}
return WiFi.softAPgetStationNum() > 0;
}
short int getRssi() {
return WiFi.RSSI();
}
IPAddress getApIp() {
return WiFi.softAPIP();
}
IPAddress getStaIp() {
return WiFi.localIP();
}
IPAddress getStaSubnet() {
return WiFi.subnetMask();
}
IPAddress getStaGateway() {
return WiFi.gatewayIP();
}
IPAddress getStaDns() {
return WiFi.dnsIP();
}
String getStaMac() {
return WiFi.macAddress();
}
const char* getStaSsid() {
return this->staSsid;
}
const char* getStaPassword() {
return this->staPassword;
}
byte getStaChannel() {
return this->staChannel;
}
bool resetWifi() {
// set policy manual for work 13 ch
{
wifi_country_t country = {"CN", 1, 13, WIFI_COUNTRY_POLICY_MANUAL};
#ifdef ARDUINO_ARCH_ESP8266
wifi_set_country(&country);
#elif defined(ARDUINO_ARCH_ESP32)
esp_wifi_set_country(&country);
#endif
}
WiFi.persistent(false);
WiFi.setAutoConnect(false);
WiFi.setAutoReconnect(false);
#ifdef ARDUINO_ARCH_ESP8266
WiFi.setSleepMode(WIFI_NONE_SLEEP);
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.setSleep(USE_BLE ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE);
#endif
WiFi.softAPdisconnect();
#ifdef ARDUINO_ARCH_ESP8266
/*if (wifi_softap_dhcps_status() == DHCP_STARTED) {
wifi_softap_dhcps_stop();
}*/
#endif
WiFi.disconnect(false, true);
#ifdef ARDUINO_ARCH_ESP8266
/*if (wifi_station_dhcpc_status() == DHCP_STARTED) {
wifi_station_dhcpc_stop();
}*/
wifi_station_dhcpc_set_maxtry(5);
#endif
#ifdef ARDUINO_ARCH_ESP32
// Nothing. Because memory leaks when turn off WiFi on ESP32, bug?
return true;
#else
return WiFi.mode(WIFI_OFF);
#endif
}
void reconnect() {
this->reconnectFlag = true;
}
bool connect(bool force = false, unsigned int timeout = 1000u) {
if (this->isConnected() && !force) {
return true;
}
if (force && !this->isApEnabled()) {
this->resetWifi();
} else {
/*#ifdef ARDUINO_ARCH_ESP8266
if (wifi_station_dhcpc_status() == DHCP_STARTED) {
wifi_station_dhcpc_stop();
}
#endif*/
WiFi.disconnect(false, true);
}
if (!this->hasStaCredentials()) {
return false;
}
this->delayCallback(200);
#ifdef ARDUINO_ARCH_ESP32
if (this->setWifiHostname(this->hostname)) {
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
} else {
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
}
#endif
if (!WiFi.mode((WiFiMode_t)(WiFi.getMode() | WIFI_STA))) {
return false;
}
this->delayCallback(200);
#ifdef ARDUINO_ARCH_ESP8266
if (this->setWifiHostname(this->hostname)) {
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
} else {
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
}
this->delayCallback(200);
#endif
if (!this->useDhcp) {
WiFi.config(this->staticIp, this->staticGateway, this->staticSubnet, this->staticDns);
}
WiFi.begin(this->staSsid, this->staPassword, this->staChannel);
unsigned long beginConnectionTime = millis();
while (millis() - beginConnectionTime < timeout) {
this->delayCallback(100);
Connection::Status status = Connection::getStatus();
if (status != Connection::Status::CONNECTING && status != Connection::Status::NONE) {
return status == Connection::Status::CONNECTED;
}
}
return false;
}
void loop() {
if (this->isConnected() && !this->hasStaCredentials()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Reset"));
this->resetWifi();
Connection::reset();
this->delayCallback(200);
} else if (this->isConnected() && !this->reconnectFlag) {
if (!this->connected) {
this->connectedTime = millis();
this->connected = true;
Log.sinfoln(
FPSTR(L_NETWORK),
F("Connected, downtime: %lu s., IP: %s, RSSI: %hhd"),
(millis() - this->disconnectedTime) / 1000,
WiFi.localIP().toString().c_str(),
WiFi.RSSI()
);
}
if (this->isApEnabled() && millis() - this->connectedTime > this->reconnectInterval && !this->hasApClients()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because connected, start only STA"));
WiFi.mode(WIFI_STA);
return;
}
#ifdef ARDUINO_ARCH_ESP8266
if (millis() - this->prevArpGratuitous > 60000) {
this->stationKeepAliveNow();
this->prevArpGratuitous = millis();
}
#endif
} else {
if (this->connected) {
this->disconnectedTime = millis();
this->connected = false;
Log.sinfoln(
FPSTR(L_NETWORK),
F("Disconnected, reason: %d, uptime: %lu s."),
Connection::getDisconnectReason(),
(millis() - this->connectedTime) / 1000
);
}
if (!this->hasStaCredentials() && !this->isApEnabled()) {
Log.sinfoln(FPSTR(L_NETWORK), F("No STA credentials, start AP"));
WiFi.mode(WIFI_AP_STA);
this->delayCallback(250);
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
} else if (!this->isApEnabled() && millis() - this->disconnectedTime > this->failedConnectTimeout) {
Log.sinfoln(FPSTR(L_NETWORK), F("Disconnected for a long time, start AP"));
WiFi.mode(WIFI_AP_STA);
this->delayCallback(250);
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
} else if (this->isConnecting() && millis() - this->prevReconnectingTime > this->resetConnectionTimeout) {
Log.swarningln(FPSTR(L_NETWORK), F("Connection timeout, reset wifi..."));
this->resetWifi();
Connection::reset();
this->delayCallback(200);
} 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();
if (!this->connect(true, this->connectionTimeout)) {
Log.straceln(FPSTR(L_NETWORK), F("Connection failed. Status: %d, reason: %d"), Connection::getStatus(), Connection::getDisconnectReason());
}
this->prevReconnectingTime = millis();
}
}
}
static byte rssiToSignalQuality(short int rssi) {
return constrain(map(rssi, -100, -50, 0, 100), 0, 100);
}
protected:
const unsigned int reconnectInterval = 5000;
const unsigned int failedConnectTimeout = 120000;
const unsigned int connectionTimeout = 15000;
const unsigned int resetConnectionTimeout = 30000;
YieldCallback yieldCallback = []() {
::yield();
};
DelayCallback delayCallback = [](unsigned int time) {
::delay(time);
};
const char* hostname = "esp";
const char* apName = "ESP";
const char* apPassword = nullptr;
byte apChannel = 1;
const char* staSsid = nullptr;
const char* staPassword = nullptr;
byte staChannel = 0;
bool useDhcp = true;
IPAddress staticIp;
IPAddress staticGateway;
IPAddress staticSubnet;
IPAddress staticDns;
bool connected = false;
bool reconnectFlag = false;
unsigned long prevArpGratuitous = 0;
unsigned long prevReconnectingTime = 0;
unsigned long connectedTime = 0;
unsigned long disconnectedTime = 0;
bool setWifiHostname(const char* hostname) {
if (!this->isHostnameValid(hostname)) {
return false;
}
if (strcmp(WiFi.getHostname(), hostname) == 0) {
return true;
}
return WiFi.setHostname(hostname);
}
#ifdef ARDUINO_ARCH_ESP8266
/**
* @brief
* https://github.com/arendst/Tasmota/blob/e6515883f0ee5451931b6280ff847b117de5a231/tasmota/tasmota_support/support_wifi.ino#L1196
*/
static void stationKeepAliveNow(void) {
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
if (
(interface->flags & NETIF_FLAG_LINK_UP)
&& (interface->flags & NETIF_FLAG_UP)
&& interface->num == STATION_IF
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
) {
etharp_gratuitous(interface);
break;
}
}
}
#endif
/**
* @brief check RFC compliance
*
* @param value
* @return true
* @return false
*/
static bool isHostnameValid(const char* value) {
size_t len = strlen(value);
if (len > 24) {
return false;
} else if (value[len - 1] == '-') {
return false;
}
for (size_t i = 0; i < len; i++) {
if (!isalnum(value[i]) && value[i] != '-') {
return false;
}
}
return true;
}
};
}

View File

@@ -0,0 +1,227 @@
#include <FS.h>
class DynamicPage : public RequestHandler {
public:
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
typedef std::function<bool()> BeforeSendCallback;
typedef std::function<String(const char*)> TemplateCallback;
DynamicPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) {
this->uri = uri;
this->fs = fs;
this->path = path;
this->cacheHeader = cacheHeader;
}
DynamicPage* setCanHandleCallback(CanHandleCallback callback = nullptr) {
this->canHandleCallback = callback;
return this;
}
DynamicPage* setBeforeSendCallback(BeforeSendCallback callback = nullptr) {
this->beforeSendCallback = callback;
return this;
}
DynamicPage* setTemplateCallback(TemplateCallback callback = nullptr) {
this->templateCallback = callback;
return this;
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
#endif
return uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (!this->canHandle(method, uri)) {
return false;
}
if (this->beforeSendCallback && !this->beforeSendCallback()) {
return true;
}
File file = this->fs->open(this->path, "r");
if (!file) {
return false;
} else if (file.isDirectory()) {
file.close();
return false;
}
if (this->cacheHeader != nullptr) {
server.sendHeader("Cache-Control", this->cacheHeader);
}
#ifdef ARDUINO_ARCH_ESP8266
if (!server.chunkedResponseModeStart(200, F("text/html"))) {
server.send(505, F("text/html"), F("HTTP1.1 required"));
return true;
}
#else
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", emptyString);
#endif
uint8_t* argStartPos = nullptr;
uint8_t* argEndPos = nullptr;
uint8_t argName[16];
size_t sizeArgName = 0;
bool argNameProcess = false;
while (file.available()) {
uint8_t buf[64];
size_t length = file.read(buf, sizeof(buf));
size_t offset = 0;
if (argNameProcess) {
argEndPos = (uint8_t*) memchr(buf, '}', length);
if (argEndPos != nullptr) {
size_t fullSizeArgName = sizeArgName + (argEndPos - buf);
if (fullSizeArgName < sizeof(argName)) {
// copy full arg name
if (argEndPos - buf > 0) {
memcpy(argName + sizeArgName, buf, argEndPos - buf);
}
argName[fullSizeArgName] = '\0';
// send arg value
String argValue = this->templateCallback((const char*) argName);
if (argValue.length()) {
server.sendContent(argValue.c_str());
} else if (fullSizeArgName > 0) {
server.sendContent("{");
server.sendContent((const char*) argName);
server.sendContent("}");
}
offset = size_t(argEndPos - buf + 1);
sizeArgName = 0;
argNameProcess = false;
}
}
if (argNameProcess) {
server.sendContent("{");
if (sizeArgName > 0) {
argName[sizeArgName] = '\0';
server.sendContent((const char*) argName);
}
argNameProcess = false;
}
}
do {
uint8_t* currentBuf = buf + offset;
size_t currentLength = length - offset;
argStartPos = (uint8_t*) memchr(currentBuf, '{', currentLength);
// send all content
if (argStartPos == nullptr) {
if (currentLength > 0) {
server.sendContent((const char*) currentBuf, currentLength);
}
break;
}
argEndPos = (uint8_t*) memchr(argStartPos, '}', length - (argStartPos - buf));
if (argEndPos != nullptr) {
sizeArgName = argEndPos - argStartPos - 1;
// send all content if arg len > space
if (sizeArgName >= sizeof(argName)) {
if (currentLength > 0) {
server.sendContent((const char*) currentBuf, currentLength);
}
break;
}
// arg name
memcpy(argName, argStartPos + 1, sizeArgName);
argName[sizeArgName] = '\0';
// send arg value
String argValue = this->templateCallback((const char*) argName);
if (argValue.length()) {
// send content before var
if (argStartPos - buf > 0) {
server.sendContent((const char*) currentBuf, argStartPos - buf);
}
server.sendContent(argValue.c_str());
} else {
server.sendContent((const char*) currentBuf, argEndPos - currentBuf + 1);
}
offset = size_t(argEndPos - currentBuf + 1);
} else {
sizeArgName = length - size_t(argStartPos - currentBuf) - 1;
// send all content if arg len > space
if (sizeArgName >= sizeof(argName)) {
if (currentLength) {
server.sendContent((const char*) currentBuf, currentLength);
}
break;
}
// send content before var
if (argStartPos - buf > 0) {
server.sendContent((const char*) currentBuf, argStartPos - buf);
}
// copy arg name chunk
if (sizeArgName > 0) {
memcpy(argName, argStartPos + 1, sizeArgName);
}
argNameProcess = true;
break;
}
} while(true);
}
file.close();
#ifdef ARDUINO_ARCH_ESP8266
server.chunkedResponseFinalize();
#else
server.sendContent(emptyString);
#endif
return true;
}
protected:
FS* fs = nullptr;
CanHandleCallback canHandleCallback;
BeforeSendCallback beforeSendCallback;
TemplateCallback templateCallback;
String eTag;
const char* uri = nullptr;
const char* path = nullptr;
const char* cacheHeader = nullptr;
};

View File

@@ -0,0 +1,109 @@
#include <FS.h>
#include <detail/mimetable.h>
using namespace mime;
class StaticPage : public RequestHandler {
public:
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
typedef std::function<bool()> BeforeSendCallback;
StaticPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) {
this->uri = uri;
this->fs = fs;
this->path = path;
this->cacheHeader = cacheHeader;
}
StaticPage* setCanHandleCallback(CanHandleCallback callback = nullptr) {
this->canHandleCallback = callback;
return this;
}
StaticPage* setBeforeSendCallback(BeforeSendCallback callback = nullptr) {
this->beforeSendCallback = callback;
return this;
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
#endif
return method == HTTP_GET && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (!this->canHandle(method, uri)) {
return false;
}
if (this->beforeSendCallback && !this->beforeSendCallback()) {
return true;
}
#if defined(ARDUINO_ARCH_ESP8266)
if (server._eTagEnabled) {
if (server._eTagFunction) {
this->eTag = (server._eTagFunction)(*this->fs, this->path);
} else if (this->eTag.isEmpty()) {
this->eTag = esp8266webserver::calcETag(*this->fs, this->path);
}
if (server.header("If-None-Match").equals(this->eTag.c_str())) {
server.send(304);
return true;
}
}
#endif
if (!this->path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !this->fs->exists(path)) {
String pathWithGz = this->path + FPSTR(mimeTable[gz].endsWith);
if (this->fs->exists(pathWithGz)) {
this->path += FPSTR(mimeTable[gz].endsWith);
}
}
File file = this->fs->open(this->path, "r");
if (!file) {
return false;
} else if (file.isDirectory()) {
file.close();
return false;
}
if (this->cacheHeader != nullptr) {
server.sendHeader("Cache-Control", this->cacheHeader);
}
#if defined(ARDUINO_ARCH_ESP8266)
if (server._eTagEnabled && this->eTag.length() > 0) {
server.sendHeader("ETag", this->eTag);
}
server.streamFile(file, F("text/html"), method);
#else
server.streamFile(file, F("text/html"), 200);
#endif
return true;
}
protected:
FS* fs = nullptr;
CanHandleCallback canHandleCallback;
BeforeSendCallback beforeSendCallback;
String eTag;
const char* uri = nullptr;
String path;
const char* cacheHeader = nullptr;
};

View File

@@ -0,0 +1,218 @@
#include <Arduino.h>
class UpgradeHandler : public RequestHandler {
public:
enum class UpgradeType {
FIRMWARE = 0,
FILESYSTEM = 1
};
enum class UpgradeStatus {
NONE,
NO_FILE,
SUCCESS,
PROHIBITED,
ABORTED,
ERROR_ON_START,
ERROR_ON_WRITE,
ERROR_ON_FINISH
};
typedef struct {
UpgradeType type;
UpgradeStatus status;
String error;
} UpgradeResult;
typedef std::function<bool(HTTPMethod, const String&)> CanHandleCallback;
typedef std::function<bool(const String&)> CanUploadCallback;
typedef std::function<bool(UpgradeType)> BeforeUpgradeCallback;
typedef std::function<void(const UpgradeResult&, const UpgradeResult&)> AfterUpgradeCallback;
UpgradeHandler(const char* uri) {
this->uri = uri;
}
UpgradeHandler* setCanHandleCallback(CanHandleCallback callback = nullptr) {
this->canHandleCallback = callback;
return this;
}
UpgradeHandler* setCanUploadCallback(CanUploadCallback callback = nullptr) {
this->canUploadCallback = callback;
return this;
}
UpgradeHandler* setBeforeUpgradeCallback(BeforeUpgradeCallback callback = nullptr) {
this->beforeUpgradeCallback = callback;
return this;
}
UpgradeHandler* setAfterUpgradeCallback(AfterUpgradeCallback callback = nullptr) {
this->afterUpgradeCallback = callback;
return this;
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
#endif
return method == HTTP_POST && uri.equals(this->uri) && (!this->canHandleCallback || this->canHandleCallback(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool canUpload(const String uri) override {
#else
bool canUpload(const String& uri) override {
#endif
return uri.equals(this->uri) && (!this->canUploadCallback || this->canUploadCallback(uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (this->afterUpgradeCallback) {
this->afterUpgradeCallback(this->firmwareResult, this->filesystemResult);
}
this->firmwareResult.status = UpgradeStatus::NONE;
this->firmwareResult.error.clear();
this->filesystemResult.status = UpgradeStatus::NONE;
this->filesystemResult.error.clear();
return true;
}
#if defined(ARDUINO_ARCH_ESP32)
void upload(WebServer& server, const String uri, HTTPUpload& upload) override {
#else
void upload(WebServer& server, const String& uri, HTTPUpload& upload) override {
#endif
UpgradeResult* result;
if (upload.name.equals("firmware")) {
result = &this->firmwareResult;
} else if (upload.name.equals("filesystem")) {
result = &this->filesystemResult;
} else {
return;
}
if (result->status != UpgradeStatus::NONE) {
return;
}
if (this->beforeUpgradeCallback && !this->beforeUpgradeCallback(result->type)) {
result->status = UpgradeStatus::PROHIBITED;
return;
}
if (!upload.filename.length()) {
result->status = UpgradeStatus::NO_FILE;
return;
}
if (upload.status == UPLOAD_FILE_START) {
// reset
if (Update.isRunning()) {
Update.end(false);
Update.clearError();
}
bool begin = false;
#ifdef ARDUINO_ARCH_ESP8266
Update.runAsync(true);
if (result->type == UpgradeType::FIRMWARE) {
begin = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000, U_FLASH);
} else if (result->type == UpgradeType::FILESYSTEM) {
close_all_fs();
begin = Update.begin((size_t)FS_end - (size_t)FS_start, U_FS);
}
#elif defined(ARDUINO_ARCH_ESP32)
if (result->type == UpgradeType::FIRMWARE) {
begin = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
} else if (result->type == UpgradeType::FILESYSTEM) {
begin = Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS);
}
#endif
if (!begin || Update.hasError()) {
result->status = UpgradeStatus::ERROR_ON_START;
#ifdef ARDUINO_ARCH_ESP8266
result->error = Update.getErrorString();
#else
result->error = Update.errorString();
#endif
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s', on start: %s"), upload.filename.c_str(), result->error.c_str());
return;
}
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s', started"), upload.filename.c_str());
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.end(false);
result->status = UpgradeStatus::ERROR_ON_WRITE;
#ifdef ARDUINO_ARCH_ESP8266
result->error = Update.getErrorString();
#else
result->error = Update.errorString();
#endif
Log.serrorln(
FPSTR(L_PORTAL_OTA),
F("File '%s', on writing %d bytes: %s"),
upload.filename.c_str(), upload.totalSize, result->error.c_str()
);
} else {
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s', writed %d bytes"), upload.filename.c_str(), upload.totalSize);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
result->status = UpgradeStatus::SUCCESS;
Log.sinfoln(FPSTR(L_PORTAL_OTA), F("File '%s': finish"), upload.filename.c_str());
} else {
result->status = UpgradeStatus::ERROR_ON_FINISH;
#ifdef ARDUINO_ARCH_ESP8266
result->error = Update.getErrorString();
#else
result->error = Update.errorString();
#endif
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s', on finish: %s"), upload.filename.c_str(), result->error);
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
Update.end(false);
result->status = UpgradeStatus::ABORTED;
Log.serrorln(FPSTR(L_PORTAL_OTA), F("File '%s': aborted"), upload.filename.c_str());
}
}
protected:
CanHandleCallback canHandleCallback;
CanUploadCallback canUploadCallback;
BeforeUpgradeCallback beforeUpgradeCallback;
AfterUpgradeCallback afterUpgradeCallback;
const char* uri = nullptr;
UpgradeResult firmwareResult{UpgradeType::FIRMWARE, UpgradeStatus::NONE};
UpgradeResult filesystemResult{UpgradeType::FILESYSTEM, UpgradeStatus::NONE};
};

View File

@@ -1,38 +0,0 @@
class IntParameter : public WiFiManagerParameter {
public:
IntParameter(const char* id, const char* label, int value, const uint8_t length = 10) : WiFiManagerParameter("") {
init(id, label, String(value).c_str(), length, "", WFM_LABEL_DEFAULT);
}
int getValue() {
return atoi(WiFiManagerParameter::getValue());
}
};
class CheckboxParameter : public WiFiManagerParameter {
public:
const char* checked = "type=\"checkbox\" checked";
const char* noChecked = "type=\"checkbox\"";
const char* trueVal = "T";
CheckboxParameter(const char* id, const char* label, bool value): WiFiManagerParameter("") {
init(id, label, value ? trueVal : "0", 1, "", WFM_LABEL_AFTER);
}
const char* getValue() const override {
return trueVal;
}
const char* getCustomHTML() const override {
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? checked : noChecked;
}
bool getCheckboxValue() {
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? true : false;
}
};
class SeparatorParameter : public WiFiManagerParameter {
public:
SeparatorParameter(): WiFiManagerParameter("<hr>") {}
};

View File

@@ -8,22 +8,234 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:d1_mini_pro]
platform = espressif8266
board = d1_mini_pro
[platformio]
;extra_configs = secrets.ini
extra_configs = secrets.default.ini
[env]
framework = arduino
lib_deps =
nrwiersma/ESP8266Scheduler@^1.0
arduino-libraries/NTPClient@^3.2.1
bblanchon/ArduinoJson@^6.20.0
ihormelnyk/OpenTherm Library@^1.1.4
knolleary/PubSubClient@^2.8
jandrassy/TelnetStream@^1.2.4
gyverlibs/EEManager@^2.0
gyverlibs/GyverPID@^3.3
bblanchon/ArduinoJson@^7.0.4
;ihormelnyk/OpenTherm Library@^1.1.5
https://github.com/Laxilef/opentherm_library/archive/refs/heads/fix_lambda.zip
arduino-libraries/ArduinoMqttClient@^0.1.8
lennarthennigs/ESP Telnet@^2.2
gyverlibs/FileData@^1.0.2
gyverlibs/GyverPID@^3.3.2
gyverlibs/GyverBlinker@^1.0
milesburton/DallasTemperature@^3.11.0
https://github.com/Laxilef/WiFiManager/archive/refs/heads/patch-1.zip
; https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
laxilef/TinyLogger@^1.1.0
build_flags =
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
-D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305
-mtext-section-literals
-D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_HTTP_SERVER -D DEBUG_ESP_PORT=Serial
-D USE_SERIAL=${secrets.use_serial}
-D USE_TELNET=${secrets.use_telnet}
-D DEBUG_BY_DEFAULT=${secrets.debug}
-D DEFAULT_HOSTNAME='"${secrets.hostname}"'
-D DEFAULT_AP_SSID='"${secrets.ap_ssid}"'
-D DEFAULT_AP_PASSWORD='"${secrets.ap_password}"'
-D DEFAULT_STA_SSID='"${secrets.sta_ssid}"'
-D DEFAULT_STA_PASSWORD='"${secrets.sta_password}"'
-D DEFAULT_PORTAL_LOGIN='"${secrets.portal_login}"'
-D DEFAULT_PORTAL_PASSWORD='"${secrets.portal_password}"'
-D DEFAULT_MQTT_SERVER='"${secrets.mqtt_server}"'
-D DEFAULT_MQTT_PORT=${secrets.mqtt_port}
-D DEFAULT_MQTT_USER='"${secrets.mqtt_user}"'
-D DEFAULT_MQTT_PASSWORD='"${secrets.mqtt_password}"'
-D DEFAULT_MQTT_PREFIX='"${secrets.mqtt_prefix}"'
upload_speed = 921600
monitor_speed = 115200
monitor_filters = direct
board_build.flash_mode = dio
board_build.filesystem = littlefs
version = 1.4.0-rc.24
; Defaults
[esp8266_defaults]
platform = espressif8266
lib_deps =
${env.lib_deps}
nrwiersma/ESP8266Scheduler@^1.2
lib_ignore =
extra_scripts =
post:tools/build.py
build_flags = ${env.build_flags}
board_build.ldscript = eagle.flash.4m1m.ld
[esp32_defaults]
platform = espressif32@^6.6
platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/archive/refs/tags/2.0.15.zip
board_build.partitions = esp32_partitions.csv
lib_deps =
${env.lib_deps}
laxilef/ESP32Scheduler@^1.0.1
lib_ignore =
extra_scripts =
post:tools/esp32.py
post:tools/build.py
build_flags =
${env.build_flags}
-D CORE_DEBUG_LEVEL=0
; Boards
[env:d1_mini]
platform = ${esp8266_defaults.platform}
board = d1_mini
lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_flags =
${esp8266_defaults.build_flags}
-D 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}
board = d1_mini_lite
lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_flags =
${esp8266_defaults.build_flags}
-D 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}
board = d1_mini_pro
lib_deps = ${esp8266_defaults.lib_deps}
lib_ignore = ${esp8266_defaults.lib_ignore}
extra_scripts = ${esp8266_defaults.extra_scripts}
board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
build_flags =
${esp8266_defaults.build_flags}
-D 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:s2_mini]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = lolin_s2_mini
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps = ${esp32_defaults.lib_deps}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-DARDUINO_USB_MODE=1
build_flags =
${esp32_defaults.build_flags}
-D 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
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-DARDUINO_USB_MODE=1
build_flags =
${esp32_defaults.build_flags}
-D ARDUINO_USB_MODE=0
-D ARDUINO_USB_CDC_ON_BOOT=1
-D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=35
-D DEFAULT_OT_OUT_GPIO=36
-D DEFAULT_SENSOR_OUTDOOR_GPIO=13
-D DEFAULT_SENSOR_INDOOR_GPIO=12
-D DEFAULT_STATUS_LED_GPIO=11
-D DEFAULT_OT_RX_LED_GPIO=10
[env:c3_mini]
platform = ${esp32_defaults.platform}
platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/archive/refs/tags/2.0.15.zip
board = lolin_c3_mini
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-mtext-section-literals
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=8
-D DEFAULT_OT_OUT_GPIO=10
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
-D DEFAULT_SENSOR_INDOOR_GPIO=1
-D DEFAULT_STATUS_LED_GPIO=4
-D DEFAULT_OT_RX_LED_GPIO=5
[env:nodemcu_32s]
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
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=21
-D DEFAULT_OT_OUT_GPIO=22
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=13
-D DEFAULT_STATUS_LED_GPIO=2 ; 18
-D DEFAULT_OT_RX_LED_GPIO=19
;-D WOKWI=1
[env:d1_mini32]
platform = ${esp32_defaults.platform}
platform_packages = ${esp32_defaults.platform_packages}
board = wemos_d1_mini32
board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps =
${esp32_defaults.lib_deps}
h2zero/NimBLE-Arduino@^1.4.1
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=21
-D DEFAULT_OT_OUT_GPIO=22
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
-D DEFAULT_SENSOR_INDOOR_GPIO=18
-D DEFAULT_STATUS_LED_GPIO=2
-D DEFAULT_OT_RX_LED_GPIO=19

20
secrets.default.ini Normal file
View File

@@ -0,0 +1,20 @@
[secrets]
use_serial = true
use_telnet = true
debug = true
hostname = opentherm
ap_ssid = OpenTherm Gateway
ap_password = otgateway123456
sta_ssid =
sta_password =
portal_login = admin
portal_password = admin
mqtt_server =
mqtt_port = 1883
mqtt_user =
mqtt_password =
mqtt_prefix = opentherm

1472
src/HaHelper.h Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,134 +1,381 @@
#include <Blinker.h>
extern Network::Manager* network;
extern MqttTask* tMqtt;
extern SensorsTask* tSensors;
extern OpenThermTask* tOt;
extern FileData fsSettings, fsNetworkSettings;
extern ESPTelnetStream* telnetStream;
class MainTask: public Task {
class MainTask : public Task {
public:
MainTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
this->blinker = new Blinker();
protected:
unsigned long lastHeapInfo = 0;
unsigned long firstFailConnect = 0;
unsigned short minFreeHeapSize = 65535;
void setup() {
pinMode(LED_STATUS_PIN, OUTPUT);
network->setDelayCallback([this](unsigned int time) {
this->delay(time);
})->setYieldCallback([this]() {
this->yield();
});
}
~MainTask() {
delete this->blinker;
}
protected:
enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
Blinker* blinker = nullptr;
unsigned long firstFailConnect = 0;
unsigned long lastHeapInfo = 0;
unsigned int minFreeHeap = 0;
unsigned int minMaxFreeBlockHeap = 0;
unsigned long restartSignalTime = 0;
bool heatingEnabled = false;
unsigned long heatingDisabledTime = 0;
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
unsigned long externalPumpStartTime = 0;
bool telnetStarted = false;
const char* getTaskName() {
return "Main";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 3;
}
void setup() {}
void loop() {
if (eeSettings.tick()) {
INFO("Settings updated (EEPROM)");
network->loop();
if (fsSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
}
if (WiFi.status() == WL_CONNECTED) {
if (!tMqtt->isEnabled()) {
if (fsNetworkSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
}
if (vars.actions.restart) {
vars.actions.restart = false;
this->restartSignalTime = millis();
// save settings
fsSettings.updateNow();
// force save network settings
if (fsNetworkSettings.updateNow() == FD_FILE_ERR && LittleFS.begin()) {
fsNetworkSettings.write();
}
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
}
vars.states.mqtt = tMqtt->isConnected();
vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0;
if (vars.states.emergency && !settings.emergency.enable) {
vars.states.emergency = false;
}
if (network->isConnected()) {
if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false);
this->telnetStarted = true;
}
if (settings.mqtt.enable && !tMqtt->isEnabled()) {
tMqtt->enable();
} else if (!settings.mqtt.enable && tMqtt->isEnabled()) {
tMqtt->disable();
}
if ( firstFailConnect != 0 ) {
firstFailConnect = 0;
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onMqttFault && !tMqtt->isEnabled()) {
vars.states.emergency = true;
} else if (vars.states.emergency && !settings.emergency.onMqttFault) {
vars.states.emergency = false;
}
vars.states.rssi = WiFi.RSSI();
if (this->firstFailConnect != 0) {
this->firstFailConnect = 0;
}
if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.system.debug ) {
Log.setLevel(TinyLogger::Level::INFO);
} else if ( Log.getLevel() != TinyLogger::Level::VERBOSE && settings.system.debug ) {
Log.setLevel(TinyLogger::Level::VERBOSE);
}
} else {
if (this->telnetStarted) {
telnetStream->stop();
this->telnetStarted = false;
}
if (tMqtt->isEnabled()) {
tMqtt->disable();
}
if (settings.emergency.enable && !vars.states.emergency) {
if (firstFailConnect == 0) {
firstFailConnect = millis();
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onNetworkFault) {
if (this->firstFailConnect == 0) {
this->firstFailConnect = millis();
}
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
if (millis() - this->firstFailConnect > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = true;
INFO("Emergency mode enabled");
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
}
}
this->yield();
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
tOt->enable();
this->ledStatus();
this->externalPump();
this->yield();
// telnet
if (this->telnetStarted) {
telnetStream->loop();
this->yield();
}
ledStatus();
#ifdef USE_TELNET
yield();
// anti memory leak
TelnetStream.flush();
while (TelnetStream.available() > 0) {
TelnetStream.read();
for (Stream* stream : Log.getStreams()) {
while (stream->available() > 0) {
stream->read();
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
}
}
#endif
if (settings.debug) {
unsigned short freeHeapSize = ESP.getFreeHeap();
unsigned short minFreeHeapSizeDiff = 0;
// heap info
this->heap();
if (freeHeapSize < minFreeHeapSize) {
minFreeHeapSizeDiff = minFreeHeapSize - freeHeapSize;
minFreeHeapSize = freeHeapSize;
}
if (millis() - lastHeapInfo > 10000 || minFreeHeapSizeDiff > 0) {
DEBUG_F("Free heap size: %hu bytes, min: %hu bytes (diff: %hu bytes)\n", freeHeapSize, minFreeHeapSize, minFreeHeapSizeDiff);
lastHeapInfo = millis();
}
// restart
if (this->restartSignalTime > 0 && millis() - this->restartSignalTime > 10000) {
this->restartSignalTime = 0;
ESP.restart();
}
}
void heap() {
unsigned int freeHeap = getFreeHeap();
unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap();
if (!this->restartSignalTime && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
this->restartSignalTime = millis();
}
if (!settings.system.debug) {
return;
}
size_t minFreeHeap = getFreeHeap(true);
size_t minFreeHeapDiff = 0;
if (minFreeHeap < this->minFreeHeap || this->minFreeHeap == 0) {
minFreeHeapDiff = this->minFreeHeap - minFreeHeap;
this->minFreeHeap = minFreeHeap;
}
size_t minMaxFreeBlockHeap = getMaxFreeBlockHeap(true);
size_t minMaxFreeBlockHeapDiff = 0;
if (minMaxFreeBlockHeap < this->minMaxFreeBlockHeap || this->minMaxFreeBlockHeap == 0) {
minMaxFreeBlockHeapDiff = this->minMaxFreeBlockHeap - minMaxFreeBlockHeap;
this->minMaxFreeBlockHeap = minMaxFreeBlockHeap;
}
if (millis() - this->lastHeapInfo > 20000 || minFreeHeapDiff > 0 || minMaxFreeBlockHeapDiff > 0) {
Log.sverboseln(
FPSTR(L_MAIN),
F("Free heap size: %u of %u bytes (min: %u, diff: %u), max free block: %u (min: %u, diff: %u, frag: %hhu%%)"),
freeHeap, getTotalHeap(), this->minFreeHeap, minFreeHeapDiff, maxFreeBlockHeap, this->minMaxFreeBlockHeap, minMaxFreeBlockHeapDiff, getHeapFrag()
);
this->lastHeapInfo = millis();
}
}
void ledStatus() {
static byte blinkLeft = 0;
uint8_t errors[4];
uint8_t errCount = 0;
static uint8_t errPos = 0;
static unsigned long endBlinkTime = 0;
static bool ledOn = false;
static unsigned long changeTime = 0;
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
byte errNo = 0;
if (!vars.states.otStatus) {
errNo = 1;
} else if (vars.states.fault) {
errNo = 2;
} else if (vars.states.emergency) {
errNo = 3;
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 (errNo == 0) {
if (!ledOn) {
digitalWrite(LED_STATUS_PIN, true);
ledOn = true;
}
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
return;
}
if (blinkLeft > 0) {
blinkLeft = 0;
}
if (!network->isConnected()) {
errors[errCount++] = 2;
}
} else {
if (blinkLeft == 0) {
if (ledOn) {
digitalWrite(LED_STATUS_PIN, false);
ledOn = false;
changeTime = millis();
}
if (!vars.states.otStatus) {
errors[errCount++] = 3;
}
if (millis() - changeTime >= 3000) {
blinkLeft = errNo;
}
}
if (vars.states.fault) {
errors[errCount++] = 4;
}
if (blinkLeft > 0 && millis() - changeTime >= 500) {
if (ledOn) {
digitalWrite(LED_STATUS_PIN, false);
ledOn = false;
blinkLeft--;
if (vars.states.emergency) {
errors[errCount++] = 5;
}
} else {
digitalWrite(LED_STATUS_PIN, true);
if (this->blinker->ready()) {
endBlinkTime = millis();
}
if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
if (errCount == 0) {
if (!ledOn) {
digitalWrite(configuredGpio, HIGH);
ledOn = true;
}
changeTime = millis();
return;
} else if (ledOn) {
digitalWrite(configuredGpio, LOW);
ledOn = false;
endBlinkTime = millis();
return;
}
if (errPos >= errCount) {
errPos = 0;
// end of error list
this->blinker->blink(10, 50, 50);
} else {
this->blinker->blink(errors[errPos++], 300, 300);
}
}
this->blinker->tick();
}
void externalPump() {
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
if (settings.externalPump.gpio != configuredGpio) {
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
digitalWrite(configuredGpio, LOW);
}
if (GPIO_IS_VALID(settings.externalPump.gpio)) {
configuredGpio = settings.externalPump.gpio;
pinMode(configuredGpio, OUTPUT);
digitalWrite(configuredGpio, LOW);
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
configuredGpio = GPIO_IS_NOT_CONFIGURED;
}
}
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
if (vars.states.externalPump) {
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
}
return;
}
if (!vars.states.heating && this->heatingEnabled) {
this->heatingEnabled = false;
this->heatingDisabledTime = millis();
} else if (vars.states.heating && !this->heatingEnabled) {
this->heatingEnabled = true;
}
if (!settings.externalPump.use) {
if (vars.states.externalPump) {
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
}
return;
}
if (vars.states.externalPump && !this->heatingEnabled) {
if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) {
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time"));
} else if (this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) {
digitalWrite(configuredGpio, LOW);
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time"));
}
} else if (vars.states.externalPump && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) {
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
} else if (!vars.states.externalPump && this->heatingEnabled) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->extPumpStartReason = MainTask::PumpStartReason::HEATING;
digitalWrite(configuredGpio, HIGH);
Log.sinfoln("EXTPUMP", F("Enabled: heating on"));
} else if (!vars.states.externalPump && (vars.parameters.extPumpLastEnableTime == 0 || millis() - vars.parameters.extPumpLastEnableTime >= (settings.externalPump.antiStuckInterval * 1000ul))) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK;
digitalWrite(configuredGpio, HIGH);
Log.sinfoln("EXTPUMP", F("Enabled: anti stuck"));
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

675
src/PortalTask.h Normal file
View File

@@ -0,0 +1,675 @@
#define PORTAL_CACHE_TIME "max-age=86400"
#define PORTAL_CACHE settings.system.debug ? nullptr : PORTAL_CACHE_TIME
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WebServer.h>
#include <Updater.h>
using WebServer = ESP8266WebServer;
#else
#include <WebServer.h>
#include <Update.h>
#endif
#include <BufferedWebServer.h>
#include <StaticPage.h>
#include <DynamicPage.h>
#include <UpgradeHandler.h>
#include <DNSServer.h>
extern Network::Manager* network;
extern FileData fsSettings, fsNetworkSettings;
extern MqttTask* tMqtt;
class PortalTask : public LeanTask {
public:
PortalTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
this->webServer = new WebServer(80);
this->bufferedWebServer = new BufferedWebServer(this->webServer, 32u);
this->dnsServer = new DNSServer();
}
~PortalTask() {
delete this->bufferedWebServer;
if (this->webServer != nullptr) {
this->stopWebServer();
delete this->webServer;
}
if (this->dnsServer != nullptr) {
this->stopDnsServer();
delete this->dnsServer;
}
}
protected:
const unsigned int changeStateInterval = 5000;
WebServer* webServer = nullptr;
BufferedWebServer* bufferedWebServer = nullptr;
DNSServer* dnsServer = nullptr;
bool webServerEnabled = false;
bool dnsServerEnabled = false;
unsigned long webServerChangeState = 0;
unsigned long dnsServerChangeState = 0;
const char* getTaskName() {
return "Portal";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 1;
}
void setup() {
this->dnsServer->setTTL(0);
this->dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
#ifdef ARDUINO_ARCH_ESP8266
this->webServer->enableETag(true);
#endif
// index page
/*auto indexPage = (new DynamicPage("/", &LittleFS, "/index.html"))
->setTemplateCallback([](const char* var) -> String {
String result;
if (strcmp(var, "ver") == 0) {
result = PROJECT_VERSION;
}
return result;
});
this->webServer->addHandler(indexPage);*/
this->webServer->addHandler(new StaticPage("/", &LittleFS, "/index.html", PORTAL_CACHE));
// dashboard page
auto dashboardPage = (new StaticPage("/dashboard.html", &LittleFS, "/dashboard.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(dashboardPage);
// restart
this->webServer->on("/restart.html", HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
}
vars.actions.restart = true;
this->webServer->sendHeader("Location", "/");
this->webServer->send(302);
});
// network settings page
auto networkPage = (new StaticPage("/network.html", &LittleFS, "/network.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(networkPage);
// settings page
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/settings.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(settingsPage);
// upgrade page
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/upgrade.html", PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(upgradePage);
// OTA
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->sendHeader("Connection", "close");
this->webServer->send(401);
return false;
}
return true;
})->setBeforeUpgradeCallback([](UpgradeHandler::UpgradeType type) -> bool {
return true;
})->setAfterUpgradeCallback([this](const UpgradeHandler::UpgradeResult& fwResult, const UpgradeHandler::UpgradeResult& fsResult) {
unsigned short status = 200;
if (fwResult.status == UpgradeHandler::UpgradeStatus::SUCCESS || fsResult.status == UpgradeHandler::UpgradeStatus::SUCCESS) {
vars.actions.restart = true;
} else {
status = 400;
}
String response = "{\"firmware\": {\"status\": ";
response.concat((short int) fwResult.status);
response.concat(", \"error\": \"");
response.concat(fwResult.error);
response.concat("\"}, \"filesystem\": {\"status\": ");
response.concat((short int) fsResult.status);
response.concat(", \"error\": \"");
response.concat(fsResult.error);
response.concat("\"}}");
this->webServer->send(status, "application/json", response);
});
this->webServer->addHandler(upgradeHandler);
// backup
this->webServer->on("/api/backup/save", HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument networkSettingsDoc;
networkSettingsToJson(networkSettings, networkSettingsDoc);
networkSettingsDoc.shrinkToFit();
JsonDocument settingsDoc;
settingsToJson(settings, settingsDoc);
settingsDoc.shrinkToFit();
JsonDocument doc;
doc["network"] = networkSettingsDoc;
doc["settings"] = settingsDoc;
doc.shrinkToFit();
this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"backup.json\""));
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/backup/restore", HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/backup/restore %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 5) {
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
bool changed = false;
if (doc["settings"] && jsonToSettings(doc["settings"], settings)) {
vars.actions.restart = true;
fsSettings.update();
changed = true;
}
if (doc["network"] && jsonToNetworkSettings(doc["network"], networkSettings)) {
fsNetworkSettings.update();
network->setHostname(networkSettings.hostname)
->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel)
->setUseDhcp(networkSettings.useDhcp)
->setStaticConfig(
networkSettings.staticConfig.ip,
networkSettings.staticConfig.gateway,
networkSettings.staticConfig.subnet,
networkSettings.staticConfig.dns
)
->reconnect();
changed = true;
}
doc.clear();
doc.shrinkToFit();
this->webServer->send(changed ? 201 : 200);
});
// network
this->webServer->on("/api/network/settings", HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument doc;
networkSettingsToJson(networkSettings, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/network/settings", HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/network/settings %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 5) {
this->webServer->send(406);
return;
} else if (plain.length() > 512) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
bool changed = jsonToNetworkSettings(doc, networkSettings);
doc.clear();
doc.shrinkToFit();
networkSettingsToJson(networkSettings, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
if (changed) {
doc.clear();
doc.shrinkToFit();
fsNetworkSettings.update();
network->setHostname(networkSettings.hostname)
->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel)
->setUseDhcp(networkSettings.useDhcp)
->setStaticConfig(
networkSettings.staticConfig.ip,
networkSettings.staticConfig.gateway,
networkSettings.staticConfig.subnet,
networkSettings.staticConfig.dns
)
->reconnect();
}
});
this->webServer->on("/api/network/scan", HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
}
auto apCount = WiFi.scanComplete();
if (apCount <= 0) {
if (apCount != WIFI_SCAN_RUNNING) {
#ifdef ARDUINO_ARCH_ESP8266
WiFi.scanNetworks(true, true);
#else
WiFi.scanNetworks(true, true, true);
#endif
}
this->webServer->send(404);
return;
}
JsonDocument doc;
for (short int i = 0; i < apCount; i++) {
String ssid = WiFi.SSID(i);
doc[i]["ssid"] = ssid;
doc[i]["signalQuality"] = Network::Manager::rssiToSignalQuality(WiFi.RSSI(i));
doc[i]["channel"] = WiFi.channel(i);
doc[i]["hidden"] = !ssid.length();
doc[i]["encryptionType"] = WiFi.encryptionType(i);
}
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
WiFi.scanDelete();
});
// settings
this->webServer->on("/api/settings", HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument doc;
settingsToJson(settings, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/settings", HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/settings %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 5) {
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
bool changed = jsonToSettings(doc, settings);
doc.clear();
doc.shrinkToFit();
settingsToJson(settings, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
if (changed) {
doc.clear();
doc.shrinkToFit();
fsSettings.update();
tMqtt->resetPublishedSettingsTime();
}
});
// vars
this->webServer->on("/api/vars", HTTP_GET, [this]() {
JsonDocument doc;
varsToJson(vars, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/vars", HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/vars %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 5) {
this->webServer->send(406);
return;
} else if (plain.length() > 1024) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
bool changed = jsonToVars(doc, vars);
doc.clear();
doc.shrinkToFit();
varsToJson(vars, doc);
doc.shrinkToFit();
this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc);
if (changed) {
doc.clear();
doc.shrinkToFit();
tMqtt->resetPublishedVarsTime();
}
});
this->webServer->on("/api/info", HTTP_GET, [this]() {
bool isConnected = network->isConnected();
JsonDocument doc;
doc["network"]["hostname"] = networkSettings.hostname;
doc["network"]["mac"] = network->getStaMac();
doc["network"]["connected"] = isConnected;
doc["network"]["ssid"] = network->getStaSsid();
doc["network"]["signalQuality"] = isConnected ? Network::Manager::rssiToSignalQuality(network->getRssi()) : 0;
doc["network"]["channel"] = isConnected ? network->getStaChannel() : 0;
doc["network"]["ip"] = isConnected ? network->getStaIp().toString() : "";
doc["network"]["subnet"] = isConnected ? network->getStaSubnet().toString() : "";
doc["network"]["gateway"] = isConnected ? network->getStaGateway().toString() : "";
doc["network"]["dns"] = isConnected ? network->getStaDns().toString() : "";
doc["system"]["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.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
// not found
this->webServer->onNotFound([this]() {
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Page not found, uri: %s"), this->webServer->uri().c_str());
const String uri = this->webServer->uri();
if (uri.equals("/")) {
this->webServer->send(200, "text/plain", F("The file system is not flashed!"));
} else if (network->isApEnabled()) {
this->onCaptivePortal();
} else {
this->webServer->send(404, "text/plain", F("Page not found"));
}
});
this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/favicon.ico", PORTAL_CACHE);
this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE);
}
void loop() {
// web server
if (!this->stateWebServer() && (network->isApEnabled() || network->isConnected()) && millis() - this->webServerChangeState >= this->changeStateInterval) {
this->startWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected"));
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
} else if (this->stateWebServer() && !network->isApEnabled() && !network->isStaEnabled()) {
this->stopWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down"));
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
}
// dns server
if (!this->stateDnsServer() && this->stateWebServer() && network->isApEnabled() && network->hasApClients() && millis() - this->dnsServerChangeState >= this->changeStateInterval) {
this->startDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up"));
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
} else if (this->stateDnsServer() && (!network->isApEnabled() || !this->stateWebServer())) {
this->stopDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down"));
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
}
if (this->stateDnsServer()) {
this->dnsServer->processNextRequest();
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
}
if (this->stateWebServer()) {
this->webServer->handleClient();
}
}
bool isAuthRequired() {
return !network->isApEnabled() && settings.portal.auth && strlen(settings.portal.password);
}
void onCaptivePortal() {
const String uri = this->webServer->uri();
if (uri.equals("/connecttest.txt")) {
this->webServer->sendHeader(F("Location"), F("http://logout.net"));
this->webServer->send(302);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to http://logout.net with 302 code"));
} else if (uri.equals("/wpad.dat")) {
this->webServer->send(404);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 404 code"));
} else if (uri.equals("/success.txt")) {
this->webServer->send(200);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 200 code"));
} else {
String portalUrl = "http://" + network->getApIp().toString() + '/';
this->webServer->sendHeader("Location", portalUrl.c_str());
this->webServer->send(302);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to portal page with 302 code"));
}
}
bool stateWebServer() {
return this->webServerEnabled;
}
void startWebServer() {
if (this->stateWebServer()) {
return;
}
this->webServer->begin();
#ifdef ARDUINO_ARCH_ESP8266
this->webServer->getServer().setNoDelay(true);
#endif
this->webServerEnabled = true;
this->webServerChangeState = millis();
}
void stopWebServer() {
if (!this->stateWebServer()) {
return;
}
this->webServer->handleClient();
this->webServer->stop();
this->webServerEnabled = false;
this->webServerChangeState = millis();
}
bool stateDnsServer() {
return this->dnsServerEnabled;
}
void startDnsServer() {
if (this->stateDnsServer()) {
return;
}
this->dnsServer->start(53, "*", network->getApIp());
this->dnsServerEnabled = true;
this->dnsServerChangeState = millis();
}
void stopDnsServer() {
if (!this->stateDnsServer()) {
return;
}
this->dnsServer->processNextRequest();
this->dnsServer->stop();
this->dnsServerEnabled = false;
this->dnsServerChangeState = millis();
}
};

View File

@@ -1,121 +1,134 @@
#include <Equitherm.h>
#include <GyverPID.h>
#include <PIDtuner.h>
Equitherm etRegulator;
GyverPID pidRegulator(0, 0, 0);
PIDtuner pidTuner;
class RegulatorTask: public LeanTask {
class RegulatorTask : public LeanTask {
public:
RegulatorTask(bool _enabled = false, unsigned long _interval = 0): LeanTask(_enabled, _interval) {}
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() {
return "Regulator";
}
/*int getTaskCore() {
return 1;
}*/
void setup() {}
int getTaskPriority() {
return 4;
}
void loop() {
byte newTemp = vars.parameters.heatingSetpoint;
float newTemp = vars.parameters.heatingSetpoint;
if (vars.states.emergency) {
if (settings.heating.turbo) {
settings.heating.turbo = false;
INFO("[REGULATOR] Turbo mode auto disabled");
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
newTemp = getEmergencyModeTemp();
newTemp = this->getEmergencyModeTemp();
} else {
if (vars.tuning.enable || tunerInit) {
if (settings.heating.turbo) {
settings.heating.turbo = false;
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || !settings.heating.enable || (settings.equitherm.enable && settings.pid.enable))) {
settings.heating.turbo = false;
INFO("[REGULATOR] Turbo mode auto disabled");
}
newTemp = getTuningModeTemp();
if (newTemp == 0) {
vars.tuning.enable = false;
}
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
if (!vars.tuning.enable) {
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || (settings.equitherm.enable && settings.pid.enable))) {
settings.heating.turbo = false;
INFO("[REGULATOR] Turbo mode auto disabled");
}
newTemp = getNormalModeTemp();
}
newTemp = this->getNormalModeTemp();
}
// Ограничиваем, если до этого не ограничило
if (newTemp < vars.parameters.heatingMinTemp || newTemp > vars.parameters.heatingMaxTemp) {
newTemp = constrain(newTemp, vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
}
// Limits
newTemp = constrain(
newTemp,
!settings.opentherm.nativeHeatingControl ? settings.heating.minTemp : THERMOSTAT_INDOOR_MIN_TEMP,
!settings.opentherm.nativeHeatingControl ? settings.heating.maxTemp : THERMOSTAT_INDOOR_MAX_TEMP
);
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
if (fabs(vars.parameters.heatingSetpoint - newTemp) > 0.4999f) {
vars.parameters.heatingSetpoint = newTemp;
}
}
byte getEmergencyModeTemp() {
float getEmergencyModeTemp() {
float newTemp = 0;
// if use equitherm
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != 1) {
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
if (settings.emergency.useEquitherm) {
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
if (fabs(prevEtResult - etResult) > 0.4999f) {
prevEtResult = etResult;
newTemp += etResult;
INFO_F("[REGULATOR][EQUITHERM] New emergency result: %u (%f) \n", (int)round(etResult), etResult);
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New emergency result: %.2f"), etResult);
} else {
newTemp += prevEtResult;
}
} else if(settings.emergency.usePid) {
if (vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.heating.minTemp,
settings.heating.maxTemp
);
if (fabs(prevPidResult - pidResult) > 0.4999f) {
prevPidResult = pidResult;
newTemp += pidResult;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New emergency result: %.2f"), pidResult);
} else {
newTemp += prevPidResult;
}
} else if (!vars.parameters.heatingEnabled && prevPidResult != 0) {
newTemp += prevPidResult;
}
} else {
// default temp, manual mode
newTemp = settings.emergency.target;
}
return round(newTemp);
return newTemp;
}
byte getNormalModeTemp() {
float getNormalModeTemp() {
float newTemp = 0;
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001f) {
prevHeatingTarget = settings.heating.target;
INFO_F("[REGULATOR] New target: %f \n", settings.heating.target);
Log.sinfoln(FPSTR(L_REGULATOR), F("New target: %.2f"), settings.heating.target);
if (settings.equitherm.enable && settings.pid.enable) {
if (/*settings.equitherm.enable && */settings.pid.enable) {
pidRegulator.integral = 0;
INFO_F("[REGULATOR][PID] Integral sum has been reset");
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
}
// if use equitherm
if (settings.equitherm.enable) {
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
if (fabs(prevEtResult - etResult) > 0.4999f) {
prevEtResult = etResult;
newTemp += etResult;
INFO_F("[REGULATOR][EQUITHERM] New result: %u (%f) \n", (int)round(etResult), etResult);
Log.sinfoln(FPSTR(L_REGULATOR_EQUITHERM), F("New result: %.2f"), etResult);
} else {
newTemp += prevEtResult;
@@ -123,24 +136,30 @@ protected:
}
// if use pid
if (settings.pid.enable && vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
settings.equitherm.enable ? settings.pid.maxTemp : settings.pid.maxTemp
);
if (settings.pid.enable) {
if (vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
settings.pid.maxTemp
);
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
prevPidResult = pidResult;
newTemp += pidResult;
if (fabs(prevPidResult - pidResult) > 0.4999f) {
prevPidResult = pidResult;
newTemp += pidResult;
INFO_F("[REGULATOR][PID] New result: %d (%f) \n", (int)round(pidResult), pidResult);
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %.2f"), pidResult);
Log.straceln(FPSTR(L_REGULATOR_PID), F("Integral: %.2f"), pidRegulator.integral);
} else {
newTemp += prevPidResult;
}
} else {
newTemp += prevPidResult;
}
} else if ( settings.pid.enable && !vars.parameters.heatingEnabled && prevPidResult != 0 ) {
newTemp += prevPidResult;
} else if (fabs(pidRegulator.integral) > 0.0001f) {
pidRegulator.integral = 0;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
// default temp, manual mode
@@ -148,103 +167,39 @@ protected:
newTemp = settings.heating.target;
}
newTemp = round(newTemp);
newTemp = constrain(newTemp, 0, 100);
return newTemp;
}
byte getTuningModeTemp() {
if (tunerInit && (!vars.tuning.enable || vars.tuning.regulator != tunerRegulator)) {
if (tunerRegulator == 0) {
pidTuner.reset();
}
tunerInit = false;
tunerRegulator = 0;
tunerState = 0;
INFO(F("[REGULATOR][TUNING] Stopped"));
}
if (!vars.tuning.enable) {
return 0;
}
if (vars.tuning.regulator == 0) {
// @TODO дописать
INFO(F("[REGULATOR][TUNING][EQUITHERM] Not implemented"));
return 0;
} else if (vars.tuning.regulator == 1) {
// PID tuner
float defaultTemp = settings.equitherm.enable
? getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp)
: settings.heating.target;
if (tunerInit && pidTuner.getState() == 3) {
INFO(F("[REGULATOR][TUNING][PID] Finished"));
pidTuner.debugText(&INFO_STREAM);
pidTuner.reset();
tunerInit = false;
tunerRegulator = 0;
tunerState = 0;
if (pidTuner.getAccuracy() < 90) {
WARN(F("[REGULATOR][TUNING][PID] Bad result, try again..."));
} else {
settings.pid.p_factor = pidTuner.getPID_p();
settings.pid.i_factor = pidTuner.getPID_i();
settings.pid.d_factor = pidTuner.getPID_d();
return 0;
}
}
if (!tunerInit) {
INFO(F("[REGULATOR][TUNING][PID] Start..."));
float step;
if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) {
step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2);
} else {
step = 5.0f;
}
float startTemp = step;
INFO_F("[REGULATOR][TUNING][PID] Started. Start value: %f, step: %f \n", startTemp, step);
pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000);
tunerInit = true;
tunerRegulator = 1;
}
pidTuner.setInput(vars.temperatures.indoor);
pidTuner.compute();
if (tunerState > 0 && pidTuner.getState() != tunerState) {
INFO(F("[REGULATOR][TUNING][PID] Log:"));
pidTuner.debugText(&INFO_STREAM);
tunerState = pidTuner.getState();
}
return round(defaultTemp + pidTuner.getOutput());
} else {
return 0;
}
}
/**
* @brief Get the Equitherm Temp
* Calculations in degrees C, conversion occurs when using F
*
* @param minTemp
* @param maxTemp
* @return float
*/
float getEquithermTemp(int minTemp, int maxTemp) {
float targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target;
float indoorTemp = vars.temperatures.indoor;
float outdoorTemp = vars.temperatures.outdoor;
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
minTemp = f2c(minTemp);
maxTemp = f2c(maxTemp);
targetTemp = f2c(targetTemp);
indoorTemp = f2c(indoorTemp);
outdoorTemp = f2c(outdoorTemp);
}
if (vars.states.emergency) {
etRegulator.Kt = 0;
etRegulator.indoorTemp = 0;
etRegulator.outdoorTemp = vars.temperatures.outdoor;
etRegulator.outdoorTemp = outdoorTemp;
} else if (settings.pid.enable) {
etRegulator.Kt = 0;
etRegulator.indoorTemp = round(vars.temperatures.indoor);
etRegulator.outdoorTemp = round(vars.temperatures.outdoor);
etRegulator.indoorTemp = round(indoorTemp);
etRegulator.outdoorTemp = round(outdoorTemp);
} else {
if (settings.heating.turbo) {
@@ -252,56 +207,47 @@ protected:
} else {
etRegulator.Kt = settings.equitherm.t_factor;
}
etRegulator.indoorTemp = vars.temperatures.indoor;
etRegulator.outdoorTemp = vars.temperatures.outdoor;
etRegulator.indoorTemp = indoorTemp;
etRegulator.outdoorTemp = outdoorTemp;
}
etRegulator.setLimits(minTemp, maxTemp);
etRegulator.Kn = settings.equitherm.n_factor;
// etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1);
etRegulator.Kk = settings.equitherm.k_factor;
etRegulator.targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target;
etRegulator.targetTemp = targetTemp;
float result = etRegulator.getResult();
return etRegulator.getResult();
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
result = c2f(result);
}
return result;
}
float getPidTemp(int minTemp, int maxTemp) {
pidRegulator.Kp = settings.pid.p_factor;
pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.Kd = settings.pid.d_factor;
if (fabs(pidRegulator.Kp - settings.pid.p_factor) >= 0.0001f) {
pidRegulator.Kp = settings.pid.p_factor;
pidRegulator.integral = 0;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
if (fabs(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) {
pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.integral = 0;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
if (fabs(pidRegulator.Kd - settings.pid.d_factor) >= 0.0001f) {
pidRegulator.Kd = settings.pid.d_factor;
pidRegulator.integral = 0;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}
pidRegulator.setLimits(minTemp, maxTemp);
pidRegulator.setDt(settings.pid.dt * 1000u);
pidRegulator.input = vars.temperatures.indoor;
pidRegulator.setpoint = settings.heating.target;
return pidRegulator.getResultNow();
return pidRegulator.getResultTimer();
}
float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) {
static uint32_t _prevIteration = millis();
if (abs(currentTemp - setTemp) < accurateStepAfter) {
if (millis() - _prevIteration < (accurateInterval * 1000)) {
return ratio;
}
if (currentTemp - setTemp > 0.1f) {
ratio -= accurateStep;
} else if (currentTemp - setTemp < -0.1f) {
ratio += accurateStep;
}
} else {
if (millis() - _prevIteration < (dirtyInterval * 1000)) {
return ratio;
}
ratio = ratio * (setTemp / currentTemp);
}
_prevIteration = millis();
return ratio;
}
};

View File

@@ -1,149 +1,375 @@
#include <OneWire.h>
#include <DallasTemperature.h>
class SensorsTask: public LeanTask {
#if USE_BLE
#include <NimBLEDevice.h>
#endif
class SensorsTask : public LeanTask {
public:
SensorsTask(bool _enabled = false, unsigned long _interval = 0): LeanTask(_enabled, _interval) {}
SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
this->oneWireOutdoorSensor = new OneWire();
this->outdoorSensor = new DallasTemperature(this->oneWireOutdoorSensor);
this->outdoorSensor->setWaitForConversion(false);
this->oneWireIndoorSensor = new OneWire();
this->indoorSensor = new DallasTemperature(this->oneWireIndoorSensor);
this->indoorSensor->setWaitForConversion(false);
}
~SensorsTask() {
delete this->outdoorSensor;
delete this->oneWireOutdoorSensor;
delete this->indoorSensor;
delete this->oneWireIndoorSensor;
}
protected:
OneWire* oneWireOutdoorSensor;
OneWire* oneWireIndoorSensor;
OneWire* oneWireOutdoorSensor = nullptr;
OneWire* oneWireIndoorSensor = nullptr;
DallasTemperature* outdoorSensor;
DallasTemperature* indoorSensor;
DallasTemperature* outdoorSensor = nullptr;
DallasTemperature* indoorSensor = nullptr;
bool initOutdoorSensor = false;
unsigned long startConversionTime = 0;
unsigned long initOutdoorSensorTime = 0;
unsigned long startOutdoorConversionTime = 0;
float filteredOutdoorTemp = 0;
bool emptyOutdoorTemp = true;
bool initIndoorSensor = false;
unsigned long initIndoorSensorTime = 0;
unsigned long startIndoorConversionTime = 0;
float filteredIndoorTemp = 0;
bool emptyIndoorTemp = true;
#if USE_BLE
BLEClient* pBleClient = nullptr;
bool initBleSensor = false;
bool initBleNotify = false;
#endif
const char* getTaskName() {
return "Sensors";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 4;
}
void setup() {}
void loop() {
if ( settings.sensors.outdoor.type == 2 ) {
bool indoorTempUpdated = false;
bool outdoorTempUpdated = false;
if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
outdoorTemperatureSensor();
outdoorTempUpdated = true;
}
if ( settings.sensors.indoor.type == 2 ) {
if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
indoorTemperatureSensor();
indoorTempUpdated = true;
}
#if USE_BLE
else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) {
indoorTemperatureBluetoothSensor();
indoorTempUpdated = true;
}
#endif
if (outdoorTempUpdated) {
float newTemp = settings.sensors.outdoor.offset;
if (settings.system.unitSystem == UnitSystem::METRIC) {
newTemp += this->filteredOutdoorTemp;
} else if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
newTemp += c2f(this->filteredOutdoorTemp);
}
if (fabs(vars.temperatures.outdoor - newTemp) > 0.099f) {
vars.temperatures.outdoor = newTemp;
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor);
}
}
if (indoorTempUpdated) {
float newTemp = settings.sensors.indoor.offset;
if (settings.system.unitSystem == UnitSystem::METRIC) {
newTemp += this->filteredIndoorTemp;
} else if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
newTemp += c2f(this->filteredIndoorTemp);
}
if (fabs(vars.temperatures.indoor - newTemp) > 0.099f) {
vars.temperatures.indoor = newTemp;
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor);
}
}
}
#if USE_BLE
void indoorTemperatureBluetoothSensor() {
static bool initBleNotify = false;
if (!initBleSensor && millis() > 5000) {
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE"));
BLEDevice::init("");
pBleClient = BLEDevice::createClient();
pBleClient->setConnectTimeout(5);
initBleSensor = true;
}
if (!initBleSensor || pBleClient->isConnected()) {
return;
}
// 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;
}
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;
}
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found service UUID: %s"), serviceUUID.toString().c_str());
// 0x2A6E - Notify temperature x0.01C (pvvx)
if (!this->initBleNotify) {
NimBLEUUID charUUID((uint16_t) 0x2A6E);
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) {
if (length != 2) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data"));
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f);
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
}
}
}
// 0x2A1F - Notify temperature x0.1C (atc1441/pvvx)
if (!this->initBleNotify) {
NimBLEUUID charUUID((uint16_t) 0x2A1F);
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) {
if (length != 2) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data"));
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1);
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
}
}
}
if (!this->initBleNotify) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics"));
pBleClient->disconnect();
}
}
#endif
void outdoorTemperatureSensor() {
if ( !initOutdoorSensor ) {
oneWireOutdoorSensor = new OneWire(settings.sensors.outdoor.pin);
outdoorSensor = new DallasTemperature(oneWireOutdoorSensor);
outdoorSensor->begin();
outdoorSensor->setResolution(12);
outdoorSensor->setWaitForConversion(false);
outdoorSensor->requestTemperatures();
startConversionTime = millis();
initOutdoorSensor = true;
}
if (!this->initOutdoorSensor) {
if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return;
}
unsigned long estimateConversionTime = millis() - startConversionTime;
if ( estimateConversionTime < outdoorSensor->millisToWaitForConversion() ) {
return;
}
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Starting on GPIO %hhu..."), settings.sensors.outdoor.gpio);
bool completed = outdoorSensor->isConversionComplete();
if ( !completed && estimateConversionTime >= 1000 ) {
// fail, retry
outdoorSensor->requestTemperatures();
startConversionTime = millis();
this->oneWireOutdoorSensor->begin(settings.sensors.outdoor.gpio);
this->oneWireOutdoorSensor->reset();
this->outdoorSensor->begin();
this->initOutdoorSensorTime = millis();
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (no response)");
}
Log.straceln(
FPSTR(L_SENSORS_OUTDOOR),
F("Devices on bus: %hhu, DS18* devices: %hhu"),
this->outdoorSensor->getDeviceCount(),
this->outdoorSensor->getDS18Count()
);
if ( !completed ) {
return;
}
if (this->outdoorSensor->getDeviceCount() > 0) {
this->initOutdoorSensor = true;
this->outdoorSensor->setResolution(12);
this->outdoorSensor->requestTemperatures();
this->startOutdoorConversionTime = millis();
float rawTemp = outdoorSensor->getTempCByIndex(0);
if (rawTemp == DEVICE_DISCONNECTED_C) {
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (not connected)");
} else {
DEBUG_F("[SENSORS][OUTDOOR] Raw temp: %f \n", rawTemp);
if (emptyOutdoorTemp) {
filteredOutdoorTemp = rawTemp;
emptyOutdoorTemp = false;
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started"));
} else {
filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
}
filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100;
if (fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099) {
vars.temperatures.outdoor = filteredOutdoorTemp + settings.sensors.outdoor.offset;
INFO_F("[SENSORS][OUTDOOR] New temp: %f \n", filteredOutdoorTemp);
return;
}
}
outdoorSensor->requestTemperatures();
startConversionTime = millis();
unsigned long estimateConversionTime = millis() - this->startOutdoorConversionTime;
if (estimateConversionTime < this->outdoorSensor->millisToWaitForConversion()) {
return;
}
bool completed = this->outdoorSensor->isConversionComplete();
if (!completed && estimateConversionTime >= 1000) {
this->initOutdoorSensor = false;
Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (no response)"));
}
if (!completed) {
return;
}
float rawTemp = this->outdoorSensor->getTempCByIndex(0);
if (rawTemp == DEVICE_DISCONNECTED_C) {
this->initOutdoorSensor = false;
Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (not connected)"));
} else {
Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyOutdoorTemp) {
this->filteredOutdoorTemp = rawTemp;
this->emptyOutdoorTemp = false;
} else {
this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredOutdoorTemp = floor(this->filteredOutdoorTemp * 100) / 100;
this->outdoorSensor->requestTemperatures();
this->startOutdoorConversionTime = millis();
}
}
void indoorTemperatureSensor() {
if ( !initIndoorSensor ) {
oneWireIndoorSensor = new OneWire(settings.sensors.indoor.pin);
indoorSensor = new DallasTemperature(oneWireIndoorSensor);
indoorSensor->begin();
indoorSensor->setResolution(12);
indoorSensor->setWaitForConversion(false);
indoorSensor->requestTemperatures();
startConversionTime = millis();
initIndoorSensor = true;
}
if (!this->initIndoorSensor) {
if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return;
}
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Starting on GPIO %hhu..."), settings.sensors.indoor.gpio);
unsigned long estimateConversionTime = millis() - startConversionTime;
if ( estimateConversionTime < indoorSensor->millisToWaitForConversion() ) {
return;
}
this->oneWireIndoorSensor->begin(settings.sensors.indoor.gpio);
this->oneWireIndoorSensor->reset();
this->indoorSensor->begin();
this->initIndoorSensorTime = millis();
bool completed = indoorSensor->isConversionComplete();
if ( !completed && estimateConversionTime >= 1000 ) {
// fail, retry
indoorSensor->requestTemperatures();
startConversionTime = millis();
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Devices on bus: %hhu, DS18* devices: %hhu"),
this->indoorSensor->getDeviceCount(),
this->indoorSensor->getDS18Count()
);
ERROR("[SENSORS][INDOOR] Could not read temperature data (no response)");
}
if (this->indoorSensor->getDeviceCount() > 0) {
this->initIndoorSensor = true;
this->indoorSensor->setResolution(12);
this->indoorSensor->requestTemperatures();
this->startIndoorConversionTime = millis();
if ( !completed ) {
return;
}
float rawTemp = indoorSensor->getTempCByIndex(0);
if (rawTemp == DEVICE_DISCONNECTED_C) {
ERROR("[SENSORS][INDOOR] Could not read temperature data (not connected)");
} else {
DEBUG_F("[SENSORS][INDOOR] Raw temp: %f \n", rawTemp);
if (emptyIndoorTemp) {
filteredIndoorTemp = rawTemp;
emptyIndoorTemp = false;
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started"));
} else {
filteredIndoorTemp += (rawTemp - filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
filteredIndoorTemp = floor(filteredIndoorTemp * 100) / 100;
if (fabs(vars.temperatures.indoor - filteredIndoorTemp) > 0.099) {
vars.temperatures.indoor = filteredIndoorTemp + settings.sensors.indoor.offset;
INFO_F("[SENSORS][INDOOR] New temp: %f \n", filteredIndoorTemp);
return;
}
}
indoorSensor->requestTemperatures();
startConversionTime = millis();
unsigned long estimateConversionTime = millis() - this->startIndoorConversionTime;
if (estimateConversionTime < this->indoorSensor->millisToWaitForConversion()) {
return;
}
bool completed = this->indoorSensor->isConversionComplete();
if (!completed && estimateConversionTime >= 1000) {
this->initIndoorSensor = false;
Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (no response)"));
}
if (!completed) {
return;
}
float rawTemp = this->indoorSensor->getTempCByIndex(0);
if (rawTemp == DEVICE_DISCONNECTED_C) {
this->initIndoorSensor = false;
Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (not connected)"));
} else {
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
this->indoorSensor->requestTemperatures();
this->startIndoorConversionTime = millis();
}
}
};

View File

@@ -1,52 +1,114 @@
struct Settings {
bool debug = false;
char hostname[80] = "opentherm";
struct NetworkSettings {
char hostname[25] = DEFAULT_HOSTNAME;
bool useDhcp = true;
struct {
byte inPin = 4;
byte outPin = 5;
char ip[16] = "192.168.0.100";
char gateway[16] = "192.168.0.1";
char subnet[16] = "255.255.255.0";
char dns[16] = "192.168.0.1";
} staticConfig;
struct {
char ssid[33] = DEFAULT_AP_SSID;
char password[65] = DEFAULT_AP_PASSWORD;
byte channel = 6;
} ap;
struct {
char ssid[33] = DEFAULT_STA_SSID;
char password[65] = DEFAULT_STA_PASSWORD;
byte channel = 0;
} sta;
} networkSettings;
struct Settings {
struct {
bool debug = DEBUG_BY_DEFAULT;
struct {
bool enable = USE_SERIAL;
unsigned int baudrate = 115200;
} serial;
struct {
bool enable = USE_TELNET;
unsigned short port = 23;
} telnet;
UnitSystem unitSystem = UnitSystem::METRIC;
byte statusLedGpio = DEFAULT_STATUS_LED_GPIO;
} system;
struct {
bool auth = false;
char login[13] = DEFAULT_PORTAL_LOGIN;
char password[33] = DEFAULT_PORTAL_PASSWORD;
} portal;
struct {
UnitSystem unitSystem = UnitSystem::METRIC;
byte inGpio = DEFAULT_OT_IN_GPIO;
byte outGpio = DEFAULT_OT_OUT_GPIO;
byte rxLedGpio = DEFAULT_OT_RX_LED_GPIO;
unsigned int memberIdCode = 0;
bool dhwPresent = true;
bool summerWinterMode = false;
bool heatingCh2Enabled = true;
bool heatingCh1ToCh2 = false;
bool dhwToCh2 = false;
bool dhwBlocking = false;
bool modulationSyncWithHeating = false;
bool getMinMaxTemp = true;
bool nativeHeatingControl = false;
} opentherm;
struct {
char server[80];
int port = 1883;
char user[32];
char password[32];
char prefix[80] = "opentherm";
unsigned int interval = 5000;
bool enable = false;
char server[81] = DEFAULT_MQTT_SERVER;
unsigned short port = DEFAULT_MQTT_PORT;
char user[33] = DEFAULT_MQTT_USER;
char password[33] = DEFAULT_MQTT_PASSWORD;
char prefix[33] = DEFAULT_MQTT_PREFIX;
unsigned short interval = 5;
bool homeAssistantDiscovery = true;
} mqtt;
struct {
bool enable = true;
float target = 40.0f;
float target = DEFAULT_HEATING_TARGET_TEMP;
unsigned short tresholdTime = 120;
bool useEquitherm = false;
bool usePid = false;
bool onNetworkFault = true;
bool onMqttFault = true;
} emergency;
struct {
bool enable = true;
bool turbo = false;
float target = 40.0f;
float target = DEFAULT_HEATING_TARGET_TEMP;
float hysteresis = 0.5f;
byte minTemp = 20.0f;
byte maxTemp = 90.0f;
byte minTemp = DEFAULT_HEATING_MIN_TEMP;
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
byte maxModulation = 100;
} heating;
struct {
bool enable = true;
byte target = 40;
byte minTemp = 30.0f;
byte maxTemp = 60.0f;
float target = DEFAULT_DHW_TARGET_TEMP;
byte minTemp = DEFAULT_DHW_MIN_TEMP;
byte maxTemp = DEFAULT_DHW_MAX_TEMP;
} dhw;
struct {
bool enable = false;
float p_factor = 3;
float i_factor = 0.2f;
float p_factor = 2;
float i_factor = 0.0055f;
float d_factor = 0;
byte minTemp = 0.0f;
byte maxTemp = 90.0f;
unsigned short dt = 180;
byte minTemp = 0;
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
} pid;
struct {
@@ -58,29 +120,31 @@ struct Settings {
struct {
struct {
// 0 - boiler, 1 - manual, 2 - ds18b20
byte type = 0;
byte pin = 12;
SensorType type = SensorType::BOILER;
byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO;
float offset = 0.0f;
} outdoor;
struct {
// 1 - manual, 2 - ds18b20
byte type = 1;
byte pin = 14;
SensorType type = SensorType::MANUAL;
byte gpio = DEFAULT_SENSOR_INDOOR_GPIO;
uint8_t bleAddresss[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float offset = 0.0f;
} indoor;
} sensors;
struct {
bool use = false;
byte gpio = DEFAULT_EXT_PUMP_GPIO;
unsigned short postCirculationTime = 600;
unsigned int antiStuckInterval = 2592000;
unsigned short antiStuckTime = 300;
} externalPump;
char validationValue[8] = SETTINGS_VALID_VALUE;
} settings;
struct Variables {
struct {
bool enable = false;
byte regulator = 0;
} tuning;
struct {
bool otStatus = false;
bool emergency = false;
@@ -89,33 +153,51 @@ struct Variables {
bool flame = false;
bool fault = false;
bool diagnostic = false;
byte faultCode = 0;
int8_t rssi = 0;
bool externalPump = false;
bool mqtt = false;
} states;
struct {
float modulation = 0.0f;
float pressure = 0.0f;
float dhwFlowRate = 0.0f;
byte faultCode = 0;
int8_t rssi = 0;
} sensors;
struct {
float indoor = 0.0f;
float outdoor = 0.0f;
float heating = 0.0f;
float heatingReturn = 0.0f;
float dhw = 0.0f;
float exhaust = 0.0f;
} temperatures;
struct {
bool heatingEnabled = false;
byte heatingMinTemp = 0;
byte heatingMaxTemp = 0;
byte heatingSetpoint = 0.0f;
byte dhwMinTemp = 0;
byte dhwMaxTemp = 0;
uint8_t slaveMemberIdCode;
uint8_t slaveType;
uint8_t slaveVersion;
uint8_t masterType;
uint8_t masterVersion;
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
float heatingSetpoint = 0;
unsigned long extPumpLastEnableTime = 0;
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
byte maxModulation = 0;
uint8_t slaveMemberId = 0;
uint8_t slaveFlags = 0;
uint8_t slaveType = 0;
uint8_t slaveVersion = 0;
float slaveOtVersion = 0.0f;
uint8_t masterMemberId = 0;
uint8_t masterFlags = 0;
uint8_t masterType = 0;
uint8_t masterVersion = 0;
float masterOtVersion = 0;
} parameters;
struct {
bool restart = false;
bool resetFault = false;
bool resetDiagnostic = false;
} actions;
} vars;

View File

@@ -1,165 +0,0 @@
#include <WiFiManager.h>
#include <WiFiManagerParameters.h>
// Wifimanager
WiFiManager wm;
WiFiManagerParameter* wmHostname;
WiFiManagerParameter* wmMqttServer;
IntParameter* wmMqttPort;
WiFiManagerParameter* wmMqttUser;
WiFiManagerParameter* wmMqttPassword;
WiFiManagerParameter* wmMqttPrefix;
IntParameter* wmMqttPublishInterval;
IntParameter* wmOtInPin;
IntParameter* wmOtOutPin;
IntParameter* wmOtMemberIdCode;
CheckboxParameter* wmOtDHWPresent;
IntParameter* wmOutdoorSensorPin;
IntParameter* wmIndoorSensorPin;
SeparatorParameter* wmSep1;
SeparatorParameter* wmSep2;
class WifiManagerTask : public Task {
public:
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
protected:
void setup() {
//WiFi.mode(WIFI_STA);
wm.setDebugOutput(settings.debug);
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
wm.addParameter(wmHostname);
wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80);
wm.addParameter(wmMqttServer);
wmMqttPort = new IntParameter("mqtt_port", "MQTT port", settings.mqtt.port, 6);
wm.addParameter(wmMqttPort);
wmMqttUser = new WiFiManagerParameter("mqtt_user", "MQTT username", settings.mqtt.user, 32);
wm.addParameter(wmMqttUser);
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32, "type=\"password\"");
wm.addParameter(wmMqttPassword);
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32);
wm.addParameter(wmMqttPrefix);
wmMqttPublishInterval = new IntParameter("mqtt_publish_interval", "MQTT publish interval", settings.mqtt.interval, 5);
wm.addParameter(wmMqttPublishInterval);
wmSep1 = new SeparatorParameter();
wm.addParameter(wmSep1);
wmOtInPin = new IntParameter("ot_in_pin", "Opentherm pin IN", settings.opentherm.inPin, 2);
wm.addParameter(wmOtInPin);
wmOtOutPin = new IntParameter("ot_out_pin", "Opentherm pin OUT", settings.opentherm.outPin, 2);
wm.addParameter(wmOtOutPin);
wmOtMemberIdCode = new IntParameter("ot_member_id_code", "Opentherm member id", settings.opentherm.memberIdCode, 5);
wm.addParameter(wmOtMemberIdCode);
wmOtDHWPresent = new CheckboxParameter("ot_dhw_present", "Opentherm DHW present", settings.opentherm.dhwPresent);
wm.addParameter(wmOtDHWPresent);
wmSep2 = new SeparatorParameter();
wm.addParameter(wmSep2);
wmOutdoorSensorPin = new IntParameter("outdoor_sensor_pin", "Outdoor sensor pin", settings.sensors.outdoor.pin, 2);
wm.addParameter(wmOutdoorSensorPin);
wmIndoorSensorPin = new IntParameter("indoor_sensor_pin", "Indoor sensor pin", settings.sensors.indoor.pin, 2);
wm.addParameter(wmIndoorSensorPin);
//wm.setCleanConnect(true);
wm.setRestorePersistent(false);
wm.setHostname(settings.hostname);
wm.setWiFiAutoReconnect(true);
wm.setAPClientCheck(true);
wm.setConfigPortalBlocking(false);
wm.setSaveParamsCallback(saveParamsCallback);
wm.setConfigPortalTimeout(180);
//wm.setDisableConfigPortal(false);
wm.autoConnect(AP_SSID, AP_PASSWORD);
}
void loop() {
/*if (WiFi.status() != WL_CONNECTED && !wm.getWebPortalActive() && !wm.getConfigPortalActive()) {
wm.autoConnect(AP_SSID);
}*/
if (connected && WiFi.status() != WL_CONNECTED) {
connected = false;
INFO("[wifi] Disconnected");
} else if (!connected && WiFi.status() == WL_CONNECTED) {
connected = true;
if (wm.getConfigPortalActive()) {
wm.stopConfigPortal();
}
INFO_F("[wifi] Connected. IP address: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
}
if (WiFi.status() == WL_CONNECTED && !wm.getWebPortalActive() && !wm.getConfigPortalActive()) {
wm.startWebPortal();
}
wm.process();
}
void static saveParamsCallback() {
strcpy(settings.hostname, wmHostname->getValue());
strcpy(settings.mqtt.server, wmMqttServer->getValue());
settings.mqtt.port = wmMqttPort->getValue();
strcpy(settings.mqtt.user, wmMqttUser->getValue());
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
settings.mqtt.interval = wmMqttPublishInterval->getValue();
settings.opentherm.inPin = wmOtInPin->getValue();
settings.opentherm.outPin = wmOtOutPin->getValue();
settings.opentherm.memberIdCode = wmOtMemberIdCode->getValue();
settings.opentherm.dhwPresent = wmOtDHWPresent->getCheckboxValue();
settings.sensors.outdoor.pin = wmOutdoorSensorPin->getValue();
settings.sensors.indoor.pin = wmIndoorSensorPin->getValue();
INFO_F(
"New settings:\r\n"
" Hostname: %s\r\n"
" Mqtt server: %s:%d\r\n"
" Mqtt user: %s\r\n"
" Mqtt pass: %s\r\n"
" Mqtt prefix: %s\r\n"
" Mqtt publish interval: %d\r\n"
" OT in pin: %d\r\n"
" OT out pin: %d\r\n"
" OT member id code: %d\r\n"
" OT DHW present: %d\r\n"
" Outdoor sensor pin: %d\r\n"
" Indoor sensor pin: %d\r\n",
settings.hostname,
settings.mqtt.server,
settings.mqtt.port,
settings.mqtt.user,
settings.mqtt.password,
settings.mqtt.prefix,
settings.mqtt.interval,
settings.opentherm.inPin,
settings.opentherm.outPin,
settings.opentherm.memberIdCode,
settings.opentherm.dhwPresent,
settings.sensors.outdoor.pin,
settings.sensors.indoor.pin
);
eeSettings.updateNow();
INFO(F("Settings saved"));
}
bool connected = false;
};

View File

@@ -1,46 +1,140 @@
#define OT_GATEWAY_VERSION "1.3.1"
#define AP_SSID "OpenTherm Gateway"
#define AP_PASSWORD "otgateway123456"
#define USE_TELNET
#define PROJECT_NAME "OpenTherm Gateway"
#define PROJECT_VERSION "1.4.0-rc.24"
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
#define EMERGENCY_TIME_TRESHOLD 120000
#define MQTT_RECONNECT_INTERVAL 5000
#define MQTT_KEEPALIVE 30
#define MQTT_RECONNECT_INTERVAL 15000
#define OPENTHERM_OFFLINE_TRESHOLD 10
#define EXT_SENSORS_INTERVAL 5000
#define EXT_SENSORS_FILTER_K 0.15
#define EXT_SENSORS_INTERVAL 5000
#define EXT_SENSORS_FILTER_K 0.15
#define DS_CHECK_CRC true
#define DS_CRC_USE_TABLE true
#define CONFIG_URL "http://%s/"
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
#define GPIO_IS_NOT_CONFIGURED 0xff
#define LED_STATUS_PIN 13
#define LED_OT_RX_PIN 15
#define DEFAULT_HEATING_TARGET_TEMP 40
#define DEFAULT_HEATING_MIN_TEMP 20
#define DEFAULT_HEATING_MAX_TEMP 90
#define CONFIG_URL "http://%s/"
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
#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
#ifdef USE_TELNET
#define INFO_STREAM TelnetStream
#define WARN_STREAM TelnetStream
#define ERROR_STREAM TelnetStream
#define DEBUG_STREAM if (settings.debug) TelnetStream
#define WM_DEBUG_PORT TelnetStream
#else
#define INFO_STREAM Serial
#define WARN_STREAM Serial
#define ERROR_STREAM Serial
#define DEBUG_STREAM if (settings.debug) Serial
#define WM_DEBUG_PORT Serial
#ifndef USE_SERIAL
#define USE_SERIAL true
#endif
#define INFO(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.println(__VA_ARGS__);
#define INFO_F(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.printf(__VA_ARGS__);
#define WARN(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.println(__VA_ARGS__);
#define WARN_F(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.printf(__VA_ARGS__);
#define ERROR(...) ERROR_STREAM.print("\r[ERROR] "); ERROR_STREAM.println(__VA_ARGS__);
#define DEBUG(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.println(__VA_ARGS__);
#define DEBUG_F(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.printf(__VA_ARGS__);
#ifndef USE_TELNET
#define USE_TELNET true
#endif
char buffer[120];
#ifndef USE_BLE
#define USE_BLE false
#endif
#ifndef DEFAULT_HOSTNAME
#define DEFAULT_HOSTNAME "opentherm"
#endif
#ifndef DEFAULT_AP_SSID
#define DEFAULT_AP_SSID "OpenTherm Gateway"
#endif
#ifndef DEFAULT_AP_PASSWORD
#define DEFAULT_AP_PASSWORD "otgateway123456"
#endif
#ifndef DEFAULT_STA_SSID
#define DEFAULT_STA_SSID ""
#endif
#ifndef DEFAULT_STA_PASSWORD
#define DEFAULT_STA_PASSWORD ""
#endif
#ifndef DEBUG_BY_DEFAULT
#define DEBUG_BY_DEFAULT false
#endif
#ifndef DEFAULT_STATUS_LED_GPIO
#define DEFAULT_STATUS_LED_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_PORTAL_LOGIN
#define DEFAULT_PORTAL_LOGIN ""
#endif
#ifndef DEFAULT_PORTAL_PASSWORD
#define DEFAULT_PORTAL_PASSWORD ""
#endif
#ifndef DEFAULT_MQTT_SERVER
#define DEFAULT_MQTT_SERVER ""
#endif
#ifndef DEFAULT_MQTT_PORT
#define DEFAULT_MQTT_PORT 1883
#endif
#ifndef DEFAULT_MQTT_USER
#define DEFAULT_MQTT_USER ""
#endif
#ifndef DEFAULT_MQTT_PASSWORD
#define DEFAULT_MQTT_PASSWORD ""
#endif
#ifndef DEFAULT_MQTT_PREFIX
#define DEFAULT_MQTT_PREFIX "opentherm"
#endif
#ifndef DEFAULT_OT_IN_GPIO
#define DEFAULT_OT_IN_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_OT_OUT_GPIO
#define DEFAULT_OT_OUT_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_OT_RX_LED_GPIO
#define DEFAULT_OT_RX_LED_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_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
#ifndef GPIO_IS_VALID_GPIO
#define GPIO_IS_VALID_GPIO(gpioNum) (gpioNum >= 0 && gpioNum <= 16)
#endif
#define GPIO_IS_VALID(gpioNum) (gpioNum != GPIO_IS_NOT_CONFIGURED && GPIO_IS_VALID_GPIO(gpioNum))
enum class SensorType : byte {
BOILER,
MANUAL,
DS18B20,
BLUETOOTH
};
enum class UnitSystem : byte {
METRIC,
IMPERIAL
};
char buffer[255];

View File

@@ -1,66 +1,156 @@
#include <Arduino.h>
#include "defines.h"
#include "strings.h"
#include <ArduinoJson.h>
#include <TelnetStream.h>
#include <EEManager.h>
#include <Scheduler.h>
#include <FileData.h>
#include <LittleFS.h>
#include "ESPTelnetStream.h"
#include <TinyLogger.h>
#include <NetworkManager.h>
#include "Settings.h"
#include <utils.h>
#if defined(ARDUINO_ARCH_ESP32)
#include <ESP32Scheduler.h>
#elif defined(ARDUINO_ARCH_ESP8266)
#include <Scheduler.h>
#else
#error Wrong board. Supported boards: esp8266, esp32
#endif
#include <Task.h>
#include <LeanTask.h>
#include "Settings.h"
EEManager eeSettings(settings, 30000);
#include "WifiManagerTask.h"
#include "MqttTask.h"
#include "OpenThermTask.h"
#include "SensorsTask.h"
#include "RegulatorTask.h"
#include "PortalTask.h"
#include "MainTask.h"
// 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;
// Tasks
WifiManagerTask* tWm;
MqttTask* tMqtt;
OpenThermTask* tOt;
SensorsTask* tSensors;
RegulatorTask* tRegulator;
PortalTask* tPortal;
MainTask* tMain;
void setup() {
#ifdef USE_TELNET
TelnetStream.begin();
delay(1000);
#else
LittleFS.begin();
Log.setLevel(TinyLogger::Level::VERBOSE);
Log.setServiceTemplate("\033[1m[%s]\033[22m");
Log.setLevelTemplate("\033[1m[%s]\033[22m");
Log.setMsgPrefix("\033[m ");
Log.setDateTemplate("\033[1m[%H:%M:%S]\033[22m");
Log.setDateCallback([] {
unsigned int time = millis() / 1000;
int sec = time % 60;
int min = time % 3600 / 60;
int hour = time / 3600;
return tm{sec, min, hour};
});
Serial.begin(115200);
Serial.println("\n\n");
#endif
Log.addStream(&Serial);
Log.print("\n\n\r");
EEPROM.begin(eeSettings.blockSize());
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
if (eeSettingsResult == 0) {
INFO("Settings loaded");
if ( strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0 ) {
INFO("Settings not valid, reset and restart...");
eeSettings.reset();
delay(1000);
ESP.restart();
}
} else if (eeSettingsResult == 1) {
INFO("Settings NOT loaded, first start");
} else if (eeSettingsResult == 2) {
INFO("Settings NOT loaded (error)");
// network settings
switch (fsNetworkSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Loaded"));
break;
default:
break;
}
tWm = new WifiManagerTask(true);
Scheduler.start(tWm);
// settings
switch (fsSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_SETTINGS), F("Loaded"));
tMqtt = new MqttTask(false);
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
Log.swarningln(FPSTR(L_SETTINGS), F("Not valid, set default and restart..."));
fsSettings.reset();
::delay(5000);
ESP.restart();
}
break;
default:
break;
}
// logs
if (!settings.system.serial.enable) {
Serial.end();
Log.clearStreams();
} else if (settings.system.serial.baudrate != 115200) {
Serial.end();
Log.clearStreams();
Serial.begin(settings.system.serial.baudrate);
Log.addStream(&Serial);
}
if (settings.system.telnet.enable) {
telnetStream = new ESPTelnetStream;
telnetStream->setKeepAliveInterval(500);
Log.addStream(telnetStream);
}
Log.setLevel(settings.system.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
// network
network = (new Network::Manager)
->setHostname(networkSettings.hostname)
->setStaCredentials(
#ifdef WOKWI
"Wokwi-GUEST", nullptr, 6
#else
strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr,
strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr,
networkSettings.sta.channel
#endif
)->setApCredentials(
strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr,
strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr,
networkSettings.ap.channel
);
// tasks
tMqtt = new MqttTask(false, 500);
Scheduler.start(tMqtt);
tOt = new OpenThermTask(false);
tOt = new OpenThermTask(true, 750);
Scheduler.start(tOt);
tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL);
@@ -69,10 +159,17 @@ void setup() {
tRegulator = new RegulatorTask(true, 10000);
Scheduler.start(tRegulator);
tMain = new MainTask(true);
tPortal = new PortalTask(true, 0);
Scheduler.start(tPortal);
tMain = new MainTask(true, 100);
Scheduler.start(tMain);
Scheduler.begin();
}
void loop() {}
void loop() {
#if defined(ARDUINO_ARCH_ESP32)
vTaskDelete(NULL);
#endif
}

24
src/strings.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#ifndef PROGMEM
#define PROGMEM
#endif
const char L_SETTINGS[] PROGMEM = "SETTINGS";
const char L_NETWORK[] PROGMEM = "NETWORK";
const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS";
const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER";
const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER";
const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE";
const char L_PORTAL_OTA[] PROGMEM = "PORTAL.OTA";
const char L_MAIN[] PROGMEM = "MAIN";
const char L_MQTT[] PROGMEM = "MQTT";
const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG";
const char L_OT[] PROGMEM = "OT";
const char L_OT_DHW[] PROGMEM = "OT.DHW";
const char L_OT_HEATING[] PROGMEM = "OT.HEATING";
const char L_SENSORS_OUTDOOR[] PROGMEM = "SENSORS.OUTDOOR";
const char L_SENSORS_INDOOR[] PROGMEM = "SENSORS.INDOOR";
const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
const char L_REGULATOR[] PROGMEM = "REGULATOR";
const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID";
const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM";

1472
src/utils.h Normal file

File diff suppressed because it is too large Load Diff

415
src_data/dashboard.html Normal file
View File

@@ -0,0 +1,415 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dashboard - 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>
<hgroup>
<h2>Dashboard</h2>
<p></p>
</hgroup>
<div id="dashboard-busy" aria-busy="true"></div>
<div id="dashboard-container" class="hidden">
<details open>
<summary><b>Control</b></summary>
<div class="grid">
<div class="thermostat" id="thermostat-heating">
<div class="thermostat-header">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">Current: <span id="thermostat-heating-current"></span> <span class="temp-unit"></span></div>
</div>
<div class="thermostat-minus"><button id="thermostat-heating-minus" class="outline"><b>-</b></button></div>
<div class="thermostat-plus"><button id="thermostat-heating-plus" class="outline"><b>+</b></button></div>
<div class="thermostat-control">
<input type="checkbox" role="switch" id="thermostat-heating-enabled" value="true">
<label htmlFor="thermostat-heating-enabled">Enable</label>
<input type="checkbox" role="switch" id="thermostat-heating-turbo" value="true">
<label htmlFor="thermostat-heating-turbo">Turbo mode</label>
</div>
</div>
<div class="thermostat" id="thermostat-dhw">
<div class="thermostat-header">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">Current: <span id="thermostat-dhw-current"></span> <span class="temp-unit"></span></div>
</div>
<div class="thermostat-minus"><button class="outline" id="thermostat-dhw-minus"><b>-</b></button></div>
<div class="thermostat-plus"><button class="outline" id="thermostat-dhw-plus"><b>+</b></button></div>
<div class="thermostat-control">
<input type="checkbox" role="switch" id="thermostat-dhw-enabled" value="true">
<label htmlFor="thermostat-dhw-enabled">Enable</label>
</div>
</div>
</div>
</details>
<hr />
<details>
<summary><b>States and sensors</b></summary>
<table>
<tbody>
<tr>
<th scope="row">OpenTherm connected:</th>
<td><input type="radio" id="ot-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">MQTT connected:</th>
<td><input type="radio" id="mqtt-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Emergency:</th>
<td><input type="radio" id="ot-emergency" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Heating:</th>
<td><input type="radio" id="ot-heating" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">DHW:</th>
<td><input type="radio" id="ot-dhw" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Flame:</th>
<td><input type="radio" id="ot-flame" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Fault:</th>
<td><input type="radio" id="ot-fault" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Diagnostic:</th>
<td><input type="radio" id="ot-diagnostic" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">External pump:</th>
<td><input type="radio" id="ot-external-pump" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">Modulation:</th>
<td><b id="ot-modulation"></b> %</td>
</tr>
<tr>
<th scope="row">Pressure:</th>
<td><b id="ot-pressure"></b> <span class="pressure-unit"></span></td>
</tr>
<tr>
<th scope="row">DHW flow rate:</th>
<td><b id="ot-dhw-flow-rate"></b> <span class="volume-unit"></span>/min</td>
</tr>
<tr>
<th scope="row">Fault code:</th>
<td><b id="ot-fault-code"></b></td>
</tr>
<tr>
<th scope="row">Indoor temp:</th>
<td><b id="indoor-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">Outdoor temp:</th>
<td><b id="outdoor-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">Heating temp:</th>
<td><b id="heating-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">Heating setpoint temp:</th>
<td><b id="heating-setpoint-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">Heating return temp:</th>
<td><b id="heating-return-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">DHW temp:</th>
<td><b id="dhw-temp"></b> <span class="temp-unit"></span></td>
</tr>
<tr>
<th scope="row">Exhaust temp:</th>
<td><b id="exhaust-temp"></b> <span class="temp-unit"></span></td>
</tr>
</tbody>
</table>
</details>
<hr />
<details>
<summary><b>OpenTherm diagnostic</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">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/issues" 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>
let modifiedTime = null;
let noRegulators;
let prevSettings;
let newSettings = {
heating: {
enable: false,
turbo: false,
target: 0
},
dhw: {
enable: false,
target: 0
}
};
window.onload = async function () {
document.querySelector('#thermostat-heating-minus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.heating.target -= 0.5;
modifiedTime = Date.now();
let minTemp;
if (noRegulators) {
minTemp = prevSettings.heating.minTemp;
} else {
minTemp = prevSettings.system.unitSystem == 0 ? 5 : 41;
}
if (prevSettings && newSettings.heating.target < minTemp) {
newSettings.heating.target = minTemp;
}
setValue('#thermostat-heating-target', newSettings.heating.target);
});
document.querySelector('#thermostat-heating-plus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.heating.target += 0.5;
modifiedTime = Date.now();
let maxTemp;
if (noRegulators) {
maxTemp = prevSettings.heating.maxTemp;
} else {
maxTemp = prevSettings.system.unitSystem == 0 ? 30 : 86;
}
if (prevSettings && newSettings.heating.target > maxTemp) {
newSettings.heating.target = maxTemp;
}
setValue('#thermostat-heating-target', newSettings.heating.target);
});
document.querySelector('#thermostat-dhw-minus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target -= 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target < prevSettings.dhw.minTemp) {
newSettings.dhw.target = prevSettings.dhw.minTemp;
}
setValue('#thermostat-dhw-target', newSettings.dhw.target);
});
document.querySelector('#thermostat-dhw-plus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target += 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target > prevSettings.dhw.maxTemp) {
newSettings.dhw.target = prevSettings.dhw.maxTemp;
}
setValue('#thermostat-dhw-target', newSettings.dhw.target);
});
document.querySelector('#thermostat-heating-enabled').addEventListener('change', (event) => {
modifiedTime = Date.now();
newSettings.heating.enable = event.currentTarget.checked;
});
document.querySelector('#thermostat-heating-turbo').addEventListener('change', (event) => {
modifiedTime = Date.now();
newSettings.heating.turbo = event.currentTarget.checked;
});
document.querySelector('#thermostat-dhw-enabled').addEventListener('change', (event) => {
modifiedTime = Date.now();
newSettings.dhw.enable = event.currentTarget.checked;
});
setTimeout(async function onLoadPage() {
if (modifiedTime) {
if ((Date.now() - modifiedTime) < 5000) {
setTimeout(onLoadPage, 1000);
return;
}
modifiedTime = null;
}
// settings
try {
let modified = prevSettings && (
(prevSettings.heating.enable != newSettings.heating.enable)
|| (prevSettings.heating.turbo != newSettings.heating.turbo)
|| (prevSettings.heating.target != newSettings.heating.target)
|| (prevSettings.opentherm.dhwPresent && prevSettings.dhw.enable != newSettings.dhw.enable)
|| (prevSettings.opentherm.dhwPresent && prevSettings.dhw.target != newSettings.dhw.target)
);
if (modified) {
console.log(newSettings);
}
let parameters = {cache: 'no-cache'};
if (modified) {
parameters.method = "POST";
parameters.body = JSON.stringify(newSettings);
}
const response = await fetch('/api/settings', parameters);
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
noRegulators = !result.opentherm.nativeHeatingControl && !result.equitherm.enable && !result.pid.enable;
prevSettings = result;
newSettings.heating.enable = result.heating.enable;
newSettings.heating.turbo = result.heating.turbo;
newSettings.heating.target = result.heating.target;
newSettings.dhw.enable = result.dhw.enable;
newSettings.dhw.target = result.dhw.target;
if (result.opentherm.dhwPresent) {
show('#thermostat-dhw');
} else {
hide('#thermostat-dhw');
}
setCheckboxValue('#thermostat-heating-enabled', result.heating.enable);
setCheckboxValue('#thermostat-heating-turbo', result.heating.turbo);
setValue('#thermostat-heating-target', result.heating.target);
setCheckboxValue('#thermostat-dhw-enabled', result.dhw.enable);
setValue('#thermostat-dhw-target', result.dhw.target);
setValue('.temp-unit', temperatureUnit(result.system.unitSystem));
setValue('.pressure-unit', pressureUnit(result.system.unitSystem));
setValue('.volume-unit', volumeUnit(result.system.unitSystem));
} catch (error) {
console.log(error);
}
// vars
try {
const response = await fetch('/api/vars', { cache: 'no-cache' });
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
setValue('#thermostat-heating-current', noRegulators ? result.temperatures.heating : result.temperatures.indoor);
setValue('#thermostat-dhw-current', result.temperatures.dhw);
setState('#ot-connected', result.states.otStatus);
setState('#ot-emergency', result.states.emergency);
setState('#ot-heating', result.states.heating);
setState('#ot-dhw', result.states.dhw);
setState('#ot-flame', result.states.flame);
setState('#ot-fault', result.states.fault);
setState('#ot-diagnostic', result.states.diagnostic);
setState('#ot-external-pump', result.states.externalPump);
setState('#mqtt-connected', result.states.mqtt);
setValue('#ot-modulation', result.sensors.modulation);
setValue('#ot-pressure', result.sensors.pressure);
setValue('#ot-dhw-flow-rate', result.sensors.dhwFlowRate);
setValue('#ot-fault-code', result.sensors.faultCode ? ("E" + result.sensors.faultCode) : "-");
setValue('#indoor-temp', result.temperatures.indoor);
setValue('#outdoor-temp', result.temperatures.outdoor);
setValue('#heating-temp', result.temperatures.heating);
setValue('#heating-return-temp', result.temperatures.heatingReturn);
setValue('#dhw-temp', result.temperatures.dhw);
setValue('#exhaust-temp', result.temperatures.exhaust);
setValue('#heating-min-temp', result.parameters.heatingMinTemp);
setValue('#heating-max-temp', result.parameters.heatingMaxTemp);
setValue('#heating-setpoint-temp', result.parameters.heatingSetpoint);
setValue('#dhw-min-temp', result.parameters.dhwMinTemp);
setValue('#dhw-max-temp', result.parameters.dhwMaxTemp);
setValue('#slave-member-id', result.parameters.slaveMemberId);
setValue('#slave-vendor', memberIdToVendor(result.parameters.slaveMemberId));
setValue('#slave-flags', result.parameters.slaveFlags);
setValue('#slave-type', result.parameters.slaveType);
setValue('#slave-version', result.parameters.slaveVersion);
setValue('#slave-ot-version', result.parameters.slaveOtVersion);
setBusy('#dashboard-busy', '#dashboard-container', false);
} catch (error) {
console.log(error);
}
setTimeout(onLoadPage, 10000);
}, 1000);
};
</script>
</body>
</html>

178
src_data/index.html Normal file
View File

@@ -0,0 +1,178 @@
<!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 id="main-busy" aria-busy="true"></div>
<table id="main-table" class="hidden">
<tbody>
<tr>
<th scope="row">Hostname:</th>
<td><b id="network-hostname"></b></td>
</tr>
<tr>
<th scope="row">MAC:</th>
<td><b id="network-mac"></b></td>
</tr>
<tr>
<th scope="row">Connected:</th>
<td><input type="radio" id="network-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row">SSID:</th>
<td><b id="network-ssid"></b></td>
</tr>
<tr>
<th scope="row">Signal:</th>
<td><b id="network-signal"></b> %</td>
</tr>
<tr>
<th scope="row">IP:</th>
<td><b id="network-ip"></b></td>
</tr>
<tr>
<th scope="row">Subnet:</th>
<td><b id="network-subnet"></b></td>
</tr>
<tr>
<th scope="row">Gateway:</th>
<td><b id="network-gateway"></b></td>
</tr>
<tr>
<th scope="row">DNS:</th>
<td><b id="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 id="system-busy" aria-busy="true"></div>
<table id="system-table" class="hidden">
<tbody>
<tr>
<th scope="row">Version:</th>
<td><b id="version"></b></td>
</tr>
<tr>
<th scope="row">Build date:</th>
<td><b id="build-date"></b></td>
</tr>
<tr>
<th scope="row">Uptime:</th>
<td><b id="uptime-days"></b> days, <b id="uptime-hours"></b> hours, <b id="uptime-min"></b> min., <b id="uptime-sec"></b> sec.</td>
</tr>
<tr>
<th scope="row">Free memory:</th>
<td><b id="free-heap"></b> of <b id="total-heap"></b> bytes (min: <b id="min-free-heap"></b> bytes)<br />max free block: <b id="max-free-block-heap"></b> bytes (min: <b id="min-max-free-block-heap"></b> bytes)</td>
</tr>
<tr>
<th scope="row">Last reset reason:</th>
<td><b id="reset-reason"></b></td>
</tr>
</tbody>
</table>
<div class="grid">
<a href="/dashboard.html" role="button">Dashboard</a>
<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>
</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/issues" 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() {
try {
const response = await fetch('/api/info', { cache: 'no-cache' });
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
setValue('#network-hostname', result.network.hostname);
setValue('#network-mac', result.network.mac);
setState('#network-connected', result.network.connected);
setValue('#network-ssid', result.network.ssid);
setValue('#network-signal', result.network.signalQuality);
setValue('#network-ip', result.network.ip);
setValue('#network-subnet', result.network.subnet);
setValue('#network-gateway', result.network.gateway);
setValue('#network-dns', result.network.dns);
setBusy('#main-busy', '#main-table', false);
setValue('#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);
setBusy('#system-busy', '#system-table', false);
} catch (error) {
console.log(error);
}
setTimeout(onLoadPage, 10000);
}, 1000);
};
</script>
</body>
</html>

207
src_data/network.html Normal file
View File

@@ -0,0 +1,207 @@
<!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="network-hostname">
Hostname
<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">
Use DHCP
</label>
<br />
<hr />
<label for="network-static-ip">
Static IP:
<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">
Static gateway:
<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">
Static subnet:
<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">
Static DNS:
<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">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h3>Available networks</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">#</th>
<th scope="col">SSID</th>
<th scope="col">Signal</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<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" id="sta-ssid" name="sta[ssid]" maxlength="32" required>
</label>
<label for="sta-password">
Password:
<input type="password" id="sta-password" name="sta[password]" maxlength="64" required>
</label>
<label for="sta-channel">
Channel:
<input type="number" inputmode="numeric" id="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" id="ap-ssid" name="ap[ssid]" maxlength="32" required>
</label>
<label for="ap-password">
Password:
<input type="text" id="ap-password" name="ap[password]" maxlength="64" required>
</label>
<label for="ap-channel">
Channel:
<input type="number" inputmode="numeric" id="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/issues" 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 () {
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();
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);
} catch (error) {
console.log(error);
}
setupForm('#network-settings');
setupNetworkScanForm('#network-scan', '#networks');
setupForm('#sta-settings');
setupForm('#ap-settings');
};
</script>
</body>
</html>

816
src_data/settings.html Normal file
View File

@@ -0,0 +1,816 @@
<!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>
<hgroup>
<h2>Settings</h2>
<p></p>
</hgroup>
<details>
<summary><b>Portal settings</b></summary>
<div>
<div id="portal-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="portal-settings" class="hidden">
<div class="grid">
<label for="portal-login">
Login
<input type="text" id="portal-login" name="portal[login]" maxlength="12" required>
</label>
<label for="portal-password">
Password
<input type="password" id="portal-password" name="portal[password]" maxlength="32" required>
</label>
</div>
<label for="portal-auth">
<input type="checkbox" id="portal-auth" name="portal[auth]" value="true">
Require authentication
</label>
<br />
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>System settings</b></summary>
<div>
<div id="system-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="system-settings" class="hidden">
<fieldset>
<legend>Unit system</legend>
<label>
<input type="radio" class="system-unit-system" name="system[unitSystem]" value="0" />
Metric (celsius, liters, bar)
</label>
<label>
<input type="radio" class="system-unit-system" name="system[unitSystem]" value="1" />
Imperial (fahrenheit, gallons, psi)
</label>
</fieldset>
<fieldset>
<label for="system-status-led-gpio">
Status LED GPIO
<input type="number" inputmode="numeric" id="system-status-led-gpio" name="system[statusLedGpio]" min="0" max="254" step="1">
<small>blank - not use</small>
</label>
</fieldset>
<fieldset>
<legend>Diagnostic</legend>
<label for="system-debug">
<input type="checkbox" id="system-debug" name="system[debug]" value="true">
Debug mode
</label>
<label for="system-serial-enable">
<input type="checkbox" id="system-serial-enable" name="system[serial][enable]" value="true">
Enable Serial port
</label>
<label for="system-telnet-enable">
<input type="checkbox" id="system-telnet-enable" name="system[telnet][enable]" value="true">
Enable Telnet
</label>
<div class="grid">
<label for="system-serial-baudrate">
Serial port baud rate
<input type="number" inputmode="numeric" id="system-serial-baudrate" name="system[serial][baudrate]" min="9600" max="115200" step="1" required>
<small>Available: 9600, 19200, 38400, 57600, 74880, 115200</small>
</label>
<label for="system-telnet-port">
Telnet port
<input type="number" inputmode="numeric" id="system-telnet-port" name="system[telnet][port]" min="1" max="65535" step="1" required>
<small>Default: 23</small>
</label>
</div>
<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>
</details>
<hr />
<details>
<summary><b>Heating settings</b></summary>
<div>
<div id="heating-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="heating-settings" class="hidden">
<div class="grid">
<label for="heating-min-temp">
Minimum temperature
<input type="number" inputmode="numeric" id="heating-min-temp" name="heating[minTemp]" min="0" max="0" step="1" required>
</label>
<label for="heating-max-temp">
Maximum temperature
<input type="number" inputmode="numeric" id="heating-max-temp" name="heating[maxTemp]" min="0" max="0" step="1" required>
</label>
</div>
<div class="grid">
<label for="heating-hysteresis">
Hysteresis
<input type="number" inputmode="numeric" id="heating-hysteresis" name="heating[hysteresis]" min="0" max="5" step="0.05" required>
</label>
<label for="heating-max-modulation">
Max modulation level
<input type="number" inputmode="numeric" id="heating-max-modulation" name="heating[maxModulation]" min="1" max="100" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>DHW settings</b></summary>
<div>
<div id="dhw-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="dhw-settings" class="hidden">
<div class="grid">
<label for="dhw-min-temp">
Minimum temperature
<input type="number" inputmode="numeric" id="dhw-min-temp" name="dhw[minTemp]" min="0" max="0" step="1" required>
</label>
<label for="dhw-max-temp">
Maximum temperature
<input type="number" inputmode="numeric" id="dhw-max-temp" name="dhw[maxTemp]" min="0" max="0" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>Emergency mode settings</b></summary>
<div>
<div id="emergency-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="emergency-settings" class="hidden">
<fieldset>
<label for="emergency-enable">
<input type="checkbox" id="emergency-enable" name="emergency[enable]" value="true">
Enable
</label>
<small>
<b>!</b> Emergency mode can be useful <u>only</u> when using Equitherm and/or PID (when normal work) and when reporting indoor/outdoor temperature via MQTT or API. In this mode, sensor values that are reported via MQTT/API are not used.
</small>
</fieldset>
<div class="grid">
<label for="emergency-target">
Target temperature
<input type="number" inputmode="numeric" id="emergency-target" name="emergency[target]" min="0" max="0" step="1" required>
<small>
<u>Indoor temperature</u> if Equitherm or PID is <b>enabled</b><br />
<u>Heat carrier temperature</u> if Equitherm and PID <b>is disabled</b>
</small>
</label>
<label for="emergency-treshold-time">
Treshold time <small>(sec)</small>
<input type="number" inputmode="numeric" id="emergency-treshold-time" name="emergency[tresholdTime]" min="60" max="1800" step="1" required>
</label>
</div>
<fieldset>
<legend>Events</legend>
<label for="emergency-on-network-fault">
<input type="checkbox" id="emergency-on-network-fault" name="emergency[onNetworkFault]" value="true">
On network fault
</label>
<label for="emergency-on-mqtt-fault">
<input type="checkbox" id="emergency-on-mqtt-fault" name="emergency[onMqttFault]" value="true">
On MQTT fault
</label>
</fieldset>
<fieldset>
<legend>Using regulators</legend>
<label for="emergency-use-equitherm">
<input type="checkbox" id="emergency-use-equitherm" name="emergency[useEquitherm]" value="true">
<span>
Equitherm <small>(requires at least an external/boiler <u>outdoor</u> sensor)</small>
</span>
</label>
<label for="emergency-use-pid">
<input type="checkbox" id="emergency-use-pid" name="emergency[usePid]" value="true">
<span>
PID <small>(requires at least an external/BLE <u>indoor</u> sensor)</small>
</span>
</label>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>Equitherm settings</b></summary>
<div>
<div id="equitherm-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="equitherm-settings" class="hidden">
<fieldset>
<label for="equitherm-enable">
<input type="checkbox" id="equitherm-enable" name="equitherm[enable]" value="true">
Enable
</label>
</fieldset>
<div class="grid">
<label for="equitherm-n-factor">
N factor
<input type="number" inputmode="numeric" id="equitherm-n-factor" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required>
</label>
<label for="equitherm-k-factor">
K factor
<input type="number" inputmode="numeric" id="equitherm-k-factor" name="equitherm[k_factor]" min="0" max="10" step="0.01" required>
</label>
<label for="equitherm-t-factor">
T factor
<input type="number" inputmode="numeric" id="equitherm-t-factor" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
<small>Not used if PID is enabled</small>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>PID settings</b></summary>
<div>
<div id="pid-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="pid-settings" class="hidden">
<fieldset>
<label for="pid-enable">
<input type="checkbox" id="pid-enable" name="pid[enable]" value="true">
Enable
</label>
</fieldset>
<div class="grid">
<label for="pid-p-factor">
P factor
<input type="number" inputmode="numeric" id="pid-p-factor" name="pid[p_factor]" min="0.1" max="1000" step="0.1" required>
</label>
<label for="pid-i-factor">
I factor
<input type="number" inputmode="numeric" id="pid-i-factor" name="pid[i_factor]" min="0" max="100" step="0.0001" required>
</label>
<label for="pid-d-factor">
D factor
<input type="number" inputmode="numeric" id="pid-d-factor" name="pid[d_factor]" min="0" max="100000" step="1" required>
</label>
</div>
<label for="pid-dt">
DT <small>in seconds</small>
<input type="number" inputmode="numeric" id="pid-dt" name="pid[dt]" min="30" max="600" step="1" required>
</label>
<hr />
<div class="grid">
<label for="pid-min-temp">
Minimum temperature
<input type="number" inputmode="numeric" id="pid-min-temp" name="pid[minTemp]" min="0" max="0" step="1" required>
</label>
<label for="pid-max-temp">
Maximum temperature
<input type="number" inputmode="numeric" id="pid-max-temp" name="pid[maxTemp]" min="0" max="0" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>OpenTherm settings</b></summary>
<div>
<div id="opentherm-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="opentherm-settings" class="hidden">
<fieldset>
<legend>Unit system</legend>
<label>
<input type="radio" class="opentherm-unit-system" name="opentherm[unitSystem]" value="0" />
Metric (celsius)
</label>
<label>
<input type="radio" class="opentherm-unit-system" name="opentherm[unitSystem]" value="1" />
Imperial (fahrenheit)
</label>
</fieldset>
<div class="grid">
<label for="opentherm-in-gpio">
In GPIO
<input type="number" inputmode="numeric" id="opentherm-in-gpio" name="opentherm[inGpio]" min="0" max="254" step="1">
</label>
<label for="opentherm-in-gpio">
Out GPIO
<input type="number" inputmode="numeric" id="opentherm-out-gpio" name="opentherm[outGpio]" min="0" max="254" step="1">
</label>
</div>
<div class="grid">
<label for="opentherm-rx-led-gpio">
RX LED GPIO
<input type="number" inputmode="numeric" id="opentherm-rx-led-gpio" name="opentherm[rxLedGpio]" min="0" max="254" step="1">
<small>blank - not use</small>
</label>
<label for="opentherm-member-id-code">
Master MemberID code
<input type="number" inputmode="numeric" id="opentherm-member-id-code" name="opentherm[memberIdCode]" min="0" max="65535" step="1" required>
</label>
</div>
<fieldset>
<legend>Options</legend>
<label for="opentherm-dhw-present">
<input type="checkbox" id="opentherm-dhw-present" name="opentherm[dhwPresent]" value="true">
DHW present
</label>
<label for="opentherm-sw-mode">
<input type="checkbox" id="opentherm-sw-mode" name="opentherm[summerWinterMode]" value="true">
Summer/winter mode
</label>
<label for="opentherm-heating-ch2-enabled">
<input type="checkbox" id="opentherm-heating-ch2-enabled" name="opentherm[heatingCh2Enabled]" value="true">
Heating CH2 always enabled
</label>
<label for="opentherm-heating-ch1-to-ch2">
<input type="checkbox" id="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" id="opentherm-dhw-to-ch2" name="opentherm[dhwToCh2]" value="true">
Duplicate DHW to CH2
</label>
<label for="opentherm-dhw-blocking">
<input type="checkbox" id="opentherm-dhw-blocking" name="opentherm[dhwBlocking]" value="true">
DHW blocking
</label>
<label for="opentherm-sync-modulation-with-heating">
<input type="checkbox" id="opentherm-sync-modulation-with-heating" name="opentherm[modulationSyncWithHeating]" value="true">
Sync modulation with heating
</label>
<label for="opentherm-get-min-max-temp">
<input type="checkbox" id="opentherm-get-min-max-temp" name="opentherm[getMinMaxTemp]" value="true">
Get min/max temp from boiler
</label>
<hr />
<label for="opentherm-native-heating-control">
<input type="checkbox" id="opentherm-native-heating-control" name="opentherm[nativeHeatingControl]" value="true">
Native heating control (boiler)<br />
<small>Works <u>ONLY</u> if the boiler requires the desired room temperature and regulates the temperature of the coolant itself. Not compatible with PID and Equitherm regulators and hysteresis in firmware.</small>
</label>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>MQTT settings</b></summary>
<div>
<div id="mqtt-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="mqtt-settings" class="hidden">
<fieldset>
<label for="mqtt-enable">
<input type="checkbox" id="mqtt-enable" name="mqtt[enable]" value="true">
Enable
</label>
<label for="mqtt-ha-discovery">
<input type="checkbox" id="mqtt-ha-discovery" name="mqtt[homeAssistantDiscovery]" value="true">
Home Assistant Discovery
</label>
</fieldset>
<div class="grid">
<label for="mqtt-server">
Server
<input type="text" id="mqtt-server" name="mqtt[server]" maxlength="80" required>
</label>
<label for="mqtt-port">
Port
<input type="number" inputmode="numeric" id="mqtt-port" name="mqtt[port]" min="1" max="65535" step="1" required>
</label>
</div>
<div class="grid">
<label for="mqtt-user">
User
<input type="text" id="mqtt-user" name="mqtt[user]" maxlength="32" required>
</label>
<label for="mqtt-password">
Password
<input type="password" id="mqtt-password" name="mqtt[password]" maxlength="32">
</label>
</div>
<div class="grid">
<label for="mqtt-prefix">
Prefix
<input type="text" id="mqtt-prefix" name="mqtt[prefix]" maxlength="32" required>
</label>
<label for="mqtt-interval">
Publish interval <small>(sec)</small>
<input type="number" inputmode="numeric" id="mqtt-interval" name="mqtt[interval]" min="3" max="60" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>Outdoor sensor settings</b></summary>
<div>
<div id="outdoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="outdoor-sensor-settings" class="hidden">
<fieldset>
<legend>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-gpio">
GPIO
<input type="number" inputmode="numeric" id="outdoor-sensor-gpio" name="sensors[outdoor][gpio]" min="0" max="254" step="1">
</label>
<label for="outdoor-sensor-offset">
Temp offset (calibration)
<input type="number" inputmode="numeric" id="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-10" max="10" step="0.01" required>
</label>
<button type="submit">Save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b>Indoor sensor settings</b></summary>
<div>
<div id="indoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="indoor-sensor-settings" class="hidden">
<fieldset>
<legend>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-gpio">
GPIO
<input type="number" inputmode="numeric" id="indoor-sensor-gpio" name="sensors[indoor][gpio]" min="0" max="254" step="1">
</label>
<div class="grid">
<label for="indoor-sensor-offset">
Temp offset (calibration)
<input type="number" inputmode="numeric" id="indoor-sensor-offset" name="sensors[indoor][offset]" min="-10" max="10" step="0.01" required>
</label>
<label for="indoor-sensor-ble-addresss">
BLE addresss
<input type="text" id="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>
</details>
<hr />
<details>
<summary><b>External pump settings</b></summary>
<div>
<div id="extpump-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="extpump-settings" class="hidden">
<fieldset>
<label for="extpump-use">
<input type="checkbox" id="extpump-use" name="externalPump[use]" value="true">
Use external pump
</label>
</fieldset>
<div class="grid">
<label for="extpump-gpio">
Relay GPIO
<input type="number" inputmode="numeric" id="extpump-gpio" name="externalPump[gpio]" min="0" max="254" step="1">
</label>
<label for="extpump-pc-time">
Post circulation time <small>(min)</small>
<input type="number" inputmode="numeric" id="extpump-pc-time" name="externalPump[postCirculationTime]" min="1" max="120" step="1" required>
</label>
</div>
<div class="grid">
<label for="extpump-as-interval">
Anti stuck interval <small>(days)</small>
<input type="number" inputmode="numeric" id="extpump-as-interval" name="externalPump[antiStuckInterval]" min="1" max="366" step="1" required>
</label>
<label for="extpump-as-time">
Anti stuck time <small>(min)</small>
<input type="number" inputmode="numeric" id="extpump-as-time" name="externalPump[antiStuckTime]" min="1" max="20" step="1" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</details>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">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/issues" 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 () {
const fillData = (data) => {
// System
setCheckboxValue('#system-debug', data.system.debug);
setCheckboxValue('#system-serial-enable', data.system.serial.enable);
setInputValue('#system-serial-baudrate', data.system.serial.baudrate);
setCheckboxValue('#system-telnet-enable', data.system.telnet.enable);
setInputValue('#system-telnet-port', data.system.telnet.port);
setRadioValue('.system-unit-system', data.system.unitSystem);
setInputValue('#system-status-led-gpio', data.system.statusLedGpio < 255 ? data.system.statusLedGpio : '');
setBusy('#system-settings-busy', '#system-settings', false);
// Portal
setCheckboxValue('#portal-auth', data.portal.auth);
setInputValue('#portal-login', data.portal.login);
setInputValue('#portal-password', data.portal.password);
setBusy('#portal-settings-busy', '#portal-settings', false);
// Opentherm
setRadioValue('.opentherm-unit-system', data.opentherm.unitSystem);
setInputValue('#opentherm-in-gpio', data.opentherm.inGpio < 255 ? data.opentherm.inGpio : '');
setInputValue('#opentherm-out-gpio', data.opentherm.outGpio < 255 ? data.opentherm.outGpio : '');
setInputValue('#opentherm-rx-led-gpio', data.opentherm.rxLedGpio < 255 ? data.opentherm.rxLedGpio : '');
setInputValue('#opentherm-member-id-code', data.opentherm.memberIdCode);
setCheckboxValue('#opentherm-dhw-present', data.opentherm.dhwPresent);
setCheckboxValue('#opentherm-sw-mode', data.opentherm.summerWinterMode);
setCheckboxValue('#opentherm-heating-ch2-enabled', data.opentherm.heatingCh2Enabled);
setCheckboxValue('#opentherm-heating-ch1-to-ch2', data.opentherm.heatingCh1ToCh2);
setCheckboxValue('#opentherm-dhw-to-ch2', data.opentherm.dhwToCh2);
setCheckboxValue('#opentherm-dhw-blocking', data.opentherm.dhwBlocking);
setCheckboxValue('#opentherm-sync-modulation-with-heating', data.opentherm.modulationSyncWithHeating);
setCheckboxValue('#opentherm-get-min-max-temp', data.opentherm.getMinMaxTemp);
setCheckboxValue('#opentherm-native-heating-control', data.opentherm.nativeHeatingControl);
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
// MQTT
setCheckboxValue('#mqtt-enable', data.mqtt.enable);
setCheckboxValue('#mqtt-ha-discovery', data.mqtt.homeAssistantDiscovery);
setInputValue('#mqtt-server', data.mqtt.server);
setInputValue('#mqtt-port', data.mqtt.port);
setInputValue('#mqtt-user', data.mqtt.user);
setInputValue('#mqtt-password', data.mqtt.password);
setInputValue('#mqtt-prefix', data.mqtt.prefix);
setInputValue('#mqtt-interval', data.mqtt.interval);
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
// Outdoor sensor
setRadioValue('.outdoor-sensor-type', data.sensors.outdoor.type);
setInputValue('#outdoor-sensor-gpio', data.sensors.outdoor.gpio < 255 ? data.sensors.outdoor.gpio : '');
setInputValue('#outdoor-sensor-offset', data.sensors.outdoor.offset);
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
// Indoor sensor
setRadioValue('.indoor-sensor-type', data.sensors.indoor.type);
setInputValue('#indoor-sensor-gpio', data.sensors.indoor.gpio < 255 ? data.sensors.indoor.gpio : '');
setInputValue('#indoor-sensor-offset', data.sensors.indoor.offset);
setInputValue('#indoor-sensor-ble-addresss', data.sensors.indoor.bleAddresss);
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
// Extpump
setCheckboxValue('#extpump-use', data.externalPump.use);
setInputValue('#extpump-gpio', data.externalPump.gpio < 255 ? data.externalPump.gpio : '');
setInputValue('#extpump-pc-time', data.externalPump.postCirculationTime);
setInputValue('#extpump-as-interval', data.externalPump.antiStuckInterval);
setInputValue('#extpump-as-time', data.externalPump.antiStuckTime);
setBusy('#extpump-settings-busy', '#extpump-settings', false);
// Heating
setInputValue('#heating-min-temp', data.heating.minTemp, {
"min": data.system.unitSystem == 0 ? 0 : 32,
"max": data.system.unitSystem == 0 ? 99 : 211
});
setInputValue('#heating-max-temp', data.heating.maxTemp, {
"min": data.system.unitSystem == 0 ? 1 : 33,
"max": data.system.unitSystem == 0 ? 100 : 212
});
setInputValue('#heating-hysteresis', data.heating.hysteresis);
setInputValue('#heating-max-modulation', data.heating.maxModulation);
setBusy('#heating-settings-busy', '#heating-settings', false);
// DHW
setInputValue('#dhw-min-temp', data.dhw.minTemp, {
"min": data.system.unitSystem == 0 ? 0 : 32,
"max": data.system.unitSystem == 0 ? 99 : 211
});
setInputValue('#dhw-max-temp', data.dhw.maxTemp, {
"min": data.system.unitSystem == 0 ? 1 : 33,
"max": data.system.unitSystem == 0 ? 100 : 212
});
setBusy('#dhw-settings-busy', '#dhw-settings', false);
// Emergency mode
setCheckboxValue('#emergency-enable', data.emergency.enable);
setInputValue('#emergency-treshold-time', data.emergency.tresholdTime);
setCheckboxValue('#emergency-use-equitherm', data.emergency.useEquitherm);
setCheckboxValue('#emergency-use-pid', data.emergency.usePid);
setCheckboxValue('#emergency-on-network-fault', data.emergency.onNetworkFault);
setCheckboxValue('#emergency-on-mqtt-fault', data.emergency.onMqttFault);
setInputValue('#emergency-target', data.emergency.target, {
"min": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.minTemp : 10,
"max": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.maxTemp : 30,
});
setBusy('#emergency-settings-busy', '#emergency-settings', false);
// Equitherm
setCheckboxValue('#equitherm-enable', data.equitherm.enable);
setInputValue('#equitherm-n-factor', data.equitherm.n_factor);
setInputValue('#equitherm-k-factor', data.equitherm.k_factor);
setInputValue('#equitherm-t-factor', data.equitherm.t_factor);
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
// PID
setCheckboxValue('#pid-enable', data.pid.enable);
setInputValue('#pid-p-factor', data.pid.p_factor);
setInputValue('#pid-i-factor', data.pid.i_factor);
setInputValue('#pid-d-factor', data.pid.d_factor);
setInputValue('#pid-dt', data.pid.dt);
setInputValue('#pid-min-temp', data.pid.minTemp, {
"min": 0,
"max": data.system.unitSystem == 0 ? 99 : 211
});
setInputValue('#pid-max-temp', data.pid.maxTemp, {
"min": 1,
"max": data.system.unitSystem == 0 ? 100 : 212
});
setBusy('#pid-settings-busy', '#pid-settings', false);
};
try {
const response = await fetch('/api/settings', { cache: 'no-cache' });
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
fillData(result);
setupForm('#portal-settings', fillData);
setupForm('#system-settings', fillData);
setupForm('#heating-settings', fillData);
setupForm('#dhw-settings', fillData);
setupForm('#emergency-settings', fillData);
setupForm('#equitherm-settings', fillData);
setupForm('#pid-settings', fillData);
setupForm('#opentherm-settings', fillData);
setupForm('#mqtt-settings', fillData);
setupForm('#outdoor-sensor-settings', fillData);
setupForm('#indoor-sensor-settings', fillData);
setupForm('#extpump-settings', fillData);
} catch (error) {
console.log(error);
}
};
</script>
</body>
</html>

196
src_data/static/app.css Normal file
View File

@@ -0,0 +1,196 @@
@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;
}
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;
}
.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);
}
.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;
}

639
src_data/static/app.js Normal file
View File

@@ -0,0 +1,639 @@
function setupForm(formSelector, onResultCallback = null) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
form.querySelectorAll('input').forEach(item => {
item.addEventListener('change', (e) => {
e.target.setAttribute('aria-invalid', !e.target.checkValidity());
})
});
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (result) => {
if (button) {
button.textContent = 'Saved';
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = () => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
try {
let fd = new FormData(form);
let checkboxes = form.querySelectorAll('input[type="checkbox"]');
for (let checkbox of checkboxes) {
fd.append(checkbox.getAttribute('name'), checkbox.checked);
}
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: form2json(fd)
});
if (!response.ok) {
throw new Error('Response not valid');
}
const result = response.status != 204 ? (await response.json()) : null;
onSuccess(result);
if (onResultCallback instanceof Function) {
onResultCallback(result);
}
} catch (err) {
console.log(err);
onFailed();
}
});
}
function setupNetworkScanForm(formSelector, tableSelector) {
const form = document.querySelector(formSelector);
if (!form) {
console.error("form not found");
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.innerHTML;
}
const onSubmitFn = async (event) => {
if (event) {
event.preventDefault();
}
if (button) {
button.innerHTML = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
let table = document.querySelector(tableSelector);
if (!table) {
console.error("table not found");
return;
}
const onSuccess = async (response) => {
let result = await response.json();
console.log('networks: ', result);
let tbody = table.querySelector('tbody');
if (!tbody) {
tbody = table.createTBody();
}
while (tbody.rows.length > 0) {
tbody.rows[0].remove();
}
for (let i = 0; i < result.length; i++) {
let row = tbody.insertRow(-1);
row.classList.add("network");
row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid);
row.onclick = function () {
const input = document.querySelector('input.sta-ssid');
const ssid = this.getAttribute('data-ssid');
if (!input || !ssid) {
return;
}
input.value = ssid;
input.focus();
};
row.insertCell().textContent = "#" + (i + 1);
row.insertCell().innerHTML = result[i].hidden ? '<i>Hidden</i>' : result[i].ssid;
const signalCell = row.insertCell();
const signalElement = document.createElement("kbd");
signalElement.textContent = result[i].signalQuality + "%";
if (result[i].signalQuality > 60) {
signalElement.classList.add('greatSignal');
} else if (result[i].signalQuality > 40) {
signalElement.classList.add('normalSignal');
} else {
signalElement.classList.add('badSignal');
}
signalCell.appendChild(signalElement);
}
if (button) {
button.innerHTML = defaultText;
button.removeAttribute('disabled');
button.removeAttribute('aria-busy');
}
};
const onFailed = async (response) => {
table.classList.remove('hidden');
if (button) {
button.innerHTML = defaultText;
button.removeAttribute('disabled');
button.removeAttribute('aria-busy');
}
};
let attempts = 6;
let attemptFn = async () => {
attempts--;
try {
let response = await fetch(url, { cache: 'no-cache' });
if (response.status == 200) {
await onSuccess(response);
} else if (attempts <= 0) {
await onFailed(response);
} else {
setTimeout(attemptFn, 5000);
}
} catch (err) {
if (attempts <= 0) {
onFailed(err);
} else {
setTimeout(attemptFn, 10000);
}
}
};
attemptFn();
};
form.addEventListener('submit', onSubmitFn);
onSubmitFn();
}
function setupRestoreBackupForm(formSelector) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (response) => {
if (button) {
button.textContent = 'Restored';
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const files = form.querySelector('#restore-file').files;
if (files.length <= 0) {
onFailed(false);
return;
}
let reader = new FileReader();
reader.readAsText(files[0]);
reader.onload = async function () {
try {
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: reader.result
});
if (response.ok) {
onSuccess(response);
} else {
onFailed(response);
}
} catch (err) {
onFailed(false);
}
};
reader.onerror = function () {
console.log(reader.error);
};
});
}
function setupUpgradeForm(formSelector) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
const statusToText = (status) => {
switch (status) {
case 0:
return "None";
case 1:
return "No file";
case 2:
return "Success";
case 3:
return "Prohibited";
case 4:
return "Aborted";
case 5:
return "Error on start";
case 6:
return "Error on write";
case 7:
return "Error on finish";
default:
return "Unknown";
}
};
const onResult = async (response) => {
if (!response) {
return;
}
const result = await response.json();
let resItem = form.querySelector('.upgrade-firmware-result');
if (resItem && result.firmware.status > 1) {
resItem.textContent = statusToText(result.firmware.status);
resItem.classList.remove('hidden');
if (result.firmware.status == 2) {
resItem.classList.remove('failed');
resItem.classList.add('success');
} else {
resItem.classList.remove('success');
resItem.classList.add('failed');
if (result.firmware.error != "") {
resItem.textContent += ": " + result.firmware.error;
}
}
}
resItem = form.querySelector('.upgrade-filesystem-result');
if (resItem && result.filesystem.status > 1) {
resItem.textContent = statusToText(result.filesystem.status);
resItem.classList.remove('hidden');
if (result.filesystem.status == 2) {
resItem.classList.remove('failed');
resItem.classList.add('success');
} else {
resItem.classList.remove('success');
resItem.classList.add('failed');
if (result.filesystem.error != "") {
resItem.textContent += ": " + result.filesystem.error;
}
}
}
};
const onSuccess = (response) => {
onResult(response);
if (button) {
button.textContent = defaultText;
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
form.addEventListener('submit', async (event) => {
event.preventDefault();
hide('.upgrade-firmware-result');
hide('.upgrade-filesystem-result');
if (button) {
button.textContent = 'Uploading...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
try {
let fd = new FormData(form);
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
body: fd
});
if (response.status >= 200 && response.status < 500) {
onSuccess(response);
} else {
onFailed(response);
}
} catch (err) {
onFailed(false);
}
});
}
function setBusy(busySelector, contentSelector, value) {
if (!value) {
hide(busySelector);
show(contentSelector);
} else {
show(busySelector);
hide(contentSelector);
}
}
function setState(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.setAttribute('aria-invalid', !value);
}
function setValue(selector, value) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.innerHTML = value;
}
}
function setCheckboxValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.checked = value;
}
function setRadioValue(selector, value) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.checked = item.value == value;
}
}
function setInputValue(selector, value, attrs = {}) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.value = value;
if (attrs instanceof Object) {
for (let attrKey of Object.keys(attrs)) {
item.setAttribute(attrKey, attrs[attrKey]);
}
}
}
}
function show(selector) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
if (item.classList.contains('hidden')) {
item.classList.remove('hidden');
}
}
}
function hide(selector) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
if (!item.classList.contains('hidden')) {
item.classList.add('hidden');
}
}
}
function unit2str(unitSystem, units = {}, defaultValue = '?') {
return (unitSystem in units)
? units[unitSystem]
: defaultValue;
}
function temperatureUnit(unitSystem) {
return unit2str(unitSystem, {
0: "°C",
1: "°F"
});
}
function pressureUnit(unitSystem) {
return unit2str(unitSystem, {
0: "bar",
1: "psi"
});
}
function volumeUnit(unitSystem) {
return unit2str(unitSystem, {
0: "L",
1: "gal"
});
}
function memberIdToVendor(memberId) {
// https://github.com/Jeroen88/EasyOpenTherm/blob/main/src/EasyOpenTherm.h
// https://github.com/Evgen2/SmartTherm/blob/v0.7/src/Web.cpp
const vendorList = {
1: "Baxi Fourtech/Luna 3",
2: "AWB/Brink",
4: "ATAG/Brötje/ELCO/GEMINOX",
5: "Itho Daalderop",
6: "IDEAL",
8: "Buderus/Bosch/Hoval",
9: "Ferroli",
11: "Remeha/De Dietrich",
16: "Unical",
24: "Vaillant/Bulex",
27: "Baxi",
29: "Itho Daalderop",
33: "Viessmann",
41: "Italtherm",
56: "Baxi Luna Duo-Tec",
131: "Nefit",
148: "Navien",
173: "Intergas",
247: "Baxi Ampera",
248: "Zota Lux-X"
};
return (memberId in vendorList)
? vendorList[memberId]
: "unknown vendor";
}
function form2json(data) {
let method = function (object, pair) {
let keys = pair[0].replace(/\]/g, '').split('[');
let key = keys[0];
let value = pair[1];
if (value === 'true' || value === 'false') {
value = value === 'true';
} else if (typeof (value) === 'string' && value.trim() !== '' && !isNaN(value)) {
value = parseFloat(value);
}
if (keys.length > 1) {
let i, x, segment;
let last = value;
let type = isNaN(keys[1]) ? {} : [];
value = segment = object[key] || type;
for (i = 1; i < keys.length; i++) {
x = keys[i];
if (i == keys.length - 1) {
if (Array.isArray(segment)) {
segment.push(last);
} else {
segment[x] = last;
}
} else if (segment[x] == undefined) {
segment[x] = isNaN(keys[i + 1]) ? {} : [];
}
segment = segment[x];
}
}
object[key] = value;
return object;
}
let object = Array.from(data).reduce(method, {});
return JSON.stringify(object);
}

BIN
src_data/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

4
src_data/static/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

108
src_data/upgrade.html Normal file
View File

@@ -0,0 +1,108 @@
<!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/issues" 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>

54
tools/build.py Normal file
View File

@@ -0,0 +1,54 @@
import shutil
import gzip
import os
Import("env")
def post_build(source, target, env):
copy_to_build_dir({
source[0].get_abspath(): "firmware_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
env.subst("$BUILD_DIR/${PROGNAME}.factory.bin"): "firmware_%s_%s.factory.bin" % (env["PIOENV"], env.GetProjectOption("version")),
}, os.path.join(env["PROJECT_DIR"], "build"));
env.Execute("pio run --target buildfs --environment %s" % env["PIOENV"]);
def before_buildfs(source, target, env):
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)
with gzip.open(dst_path, 'wb', 9) as f_out:
shutil.copyfileobj(f_in, f_out)
print("Compressed '%s' to '%s'" % (src_path, dst_path))
def after_buildfs(source, target, env):
copy_to_build_dir({
source[0].get_abspath(): "filesystem_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
}, os.path.join(env["PROJECT_DIR"], "build"));
def copy_to_build_dir(files, build_dir):
if os.path.exists(build_dir) == False:
return
for src in files:
if os.path.exists(src):
dst = os.path.join(build_dir, files[src])
print("Copying '%s' to '%s'" % (src, dst))
shutil.copy(src, dst)
env.AddPostAction("buildprog", post_build)
env.AddPreAction("$BUILD_DIR/spiffs.bin", before_buildfs)
env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs)
env.AddPostAction("buildfs", after_buildfs)

79
tools/esp32.py Normal file
View File

@@ -0,0 +1,79 @@
# Source: https://raw.githubusercontent.com/letscontrolit/ESPEasy/mega/tools/pio/post_esp32.py
# Part of ESPEasy build toolchain.
#
# Combines separate bin files with their respective offsets into a single file
# This single file must then be flashed to an ESP32 node with 0 offset.
#
# Original implementation: Bartłomiej Zimoń (@uzi18)
# Maintainer: Gijs Noorlander (@TD-er)
#
# Special thanks to @Jason2866 (Tasmota) for helping debug flashing to >4MB flash
# Thanks @jesserockz (esphome) for adapting to use esptool.py with merge_bin
#
# Typical layout of the generated file:
# Offset | File
# - 0x1000 | ~\.platformio\packages\framework-arduinoespressif32\tools\sdk\esp32\bin\bootloader_dout_40m.bin
# - 0x8000 | ~\ESPEasy\.pio\build\<env name>\partitions.bin
# - 0xe000 | ~\.platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin
# - 0x10000 | ~\ESPEasy\.pio\build\<env name>/<built binary>.bin
Import("env")
platform = env.PioPlatform()
import sys
from os.path import join
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool
def esp32_create_combined_bin(source, target, env):
print("Generating combined binary for serial flashing")
# The offset from begin of the file where the app0 partition starts
# This is defined in the partition .csv file
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
flash_size = env.BoardConfig().get("upload.flash_size")
flash_freq = env.BoardConfig().get("build.f_flash", '40m')
flash_freq = flash_freq.replace('000000L', 'm')
flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
]
print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
print('Using esptool.py arguments: %s' % ' '.join(cmd))
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)

View File

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

View File

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