243 Commits

Author SHA1 Message Date
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
Yurii
6d3172b73b fix pid 2023-10-11 19:04:58 +03:00
Yurii
fca6dc9393 fix set hysteresis 2023-10-11 18:46:37 +03:00
Yurii
b54ea9b745 dependency update (temporary) 2023-10-09 06:59:41 +03:00
Yurii
5de3238f6f Added DHW present switch 2023-10-09 06:20:55 +03:00
Yurii
2270b12b36 upd readme 2023-09-22 00:20:45 +03:00
Yurii
fd4fd119da upd readme 2023-09-21 23:47:43 +03:00
Yurii
47849eab01 upd readme & pcb 2023-09-21 23:09:47 +03:00
Yurii
ef99d2af96 upd readme 2023-09-21 23:02:04 +03:00
Yurii
826581562a upd readme, small fix 2023-09-21 22:51:14 +03:00
Yurii
d10d44bd13 upd pcb 2023-09-21 06:23:26 +03:00
Yurii
229628fdc5 Many changes.
1. Migrate from microDS18B20 to DallasTemperature
2. Refactoring of sensors: added an external temperature sensor inside the house, added an "offset" parameter for sensors
3. Fixed PID
4. New parameters added:
- settings.heating.minTemp
- settings.heating.maxTemp
- settings.dhw.minTemp
- settings.dhw.maxTemp
- settings.pid.minTemp
- settings.pid.maxTemp
- settings.sensors.outdoor.type
- settings.sensors.outdoor.pin
- settings.sensors.outdoor.offset
- settings.sensors.indoor.type
- settings.sensors.indoor.pin
- settings.sensors.indoor.offset
5. Fixed and updated HomeAssistantHelper
7. Added check for validity of settings. After some updates, the settings may be reset to default, but this will prevent the settings from being distorted.
2023-09-21 05:18:05 +03:00
Laxilef
b0e01afecb Update README.md 2023-09-19 22:01:40 +03:00
Yurii
f544baee0a Merge branch 'master' of https://github.com/Laxilef/OTGateway 2023-09-19 21:59:46 +03:00
Yurii
3ff68f544f upd readme
upd gitignore
add ino file for arduino ide
2023-09-19 21:59:29 +03:00
63 changed files with 15631 additions and 8822 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"

8
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
.vscode
build/*.bin
secrets.ini
!.gitkeep

207
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)](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,184 +16,70 @@
- 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 sensor to monitor outdoor temperature (DS18B20)
- 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 water for hot water: working / not working
- 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 |
| --- | --- |
| BAXI ECO Nova | default or 4 |
| BAXI Ampera | 1028 |
## Documentation
All available information and instructions can be found in the wiki:
## PCB
<img src="/assets/pcb.svg" width="25%" /> <img src="/assets/pcb_3d.png" width="30%" /> <img src="/assets/after_assembly.png" width="37%" />
* [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)
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.<br>
**Important!** On this board opentherm IN pin = 5, OUT pin = 4
- [Sheet](/assets/sheet.pdf)
- [BOM](/assets/BOM.xlsx)
- [Gerber](/assets/gerber.zip)
## Another compatible Open Therm Adapters
- [Ihor Melnyk OpenTherm Adapter](http://ihormelnyk.com/opentherm_adapter)
- [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)
# Quick Start
## 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)
- [microDS18B20](https://github.com/GyverLibs/microDS18B20)
- [WiFiManager](https://github.com/tzapu/WiFiManager)
## Settings
1. Connect to *OpenTherm Gateway* hotspot, password: otgateway123456
2. Open configuration page in browser: 192.168.4.1
3. Set up a connection to your wifi network
4. Set up a connection to your MQTT server
5. Set up a **Opentherm pin IN** & **Opentherm pin OUT**. Typically used **IN** = 4, **OUT** = 5
6. if necessary, set the master member ID.
7. Restart module (required after changing OT 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 DS18B20 sensor or use automation.
<details>
<summary>Simple automation</summary>
**weather.home** - [weather entity](https://www.home-assistant.io/integrations/weather/). It is important that the address of your home is entered correctly in the Home Assistant settings.<br>
**number.opentherm_outdoor_temp** - an entity that stores the temperature value outside the house. The default does not need to be changed.
```yaml
alias: Set boiler outdoor temp
description: ""
trigger:
- platform: state
entity_id:
- weather.home
attribute: temperature
for:
hours: 0
minutes: 1
seconds: 0
- platform: time_pattern
seconds: /30
condition: []
action:
- if:
- condition: template
value_template: "{{ has_value('weather.home') and (state_attr('weather.home', 'temperature')|float(0) - states('number.opentherm_outdoor_temp')|float(0)) | abs | round(2) >= 0.1 }}"
then:
- service: number.set_value
data:
value: "{{ state_attr('weather.home', 'temperature')|float(0)|round(2) }}"
target:
entity_id: number.opentherm_outdoor_temp
mode: single
```
</details>
After these settings, you can enable the "Equitherm" and/or "PID" modes and configure them as described below.
## About modes
### Equitherm
Weather-compensated temperature control maintains a comfortable set temperature in the house. The algorithm requires temperature sensors in the house and outside.<br> Instead of an outdoor sensor, you can use the weather forecast and automation for HA.
#### Ratios:
***N*** - heating curve coefficient. The coefficient is selected individually, depending on the insulation of the room, the heated area, etc.<br>
Range: 0.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. 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. The temperature does not have to be equal to the set one.<br>
For example. You fit curve 0.67; set temperature 23; the temperature in the house is 20 degrees while the outside temperature is -10 degrees and -5 degrees. This is good.
2. 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 5 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.
3. 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.
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
- [GyverBlinker](https://github.com/GyverLibs/GyverBlinker)
- [DallasTemperature](https://github.com/milesburton/Arduino-Temperature-Control-Library)
- [TinyLogger](https://github.com/laxilef/TinyLogger)
## Debug
To display DEBUG messages you must enable debug in settings (switch is disabled by default).
You can connect via Telnet to read messages. IP: esp8266 ip, port: 23
You can connect via Telnet to read messages. IP: ESP8266 ip, port: 23

1
assets/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/*.priv.*

Binary file not shown.

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

6480
assets/Schematic.pdf Normal file

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: 689 KiB

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because it is too large Load Diff

0
build/.gitkeep Normal file
View File

230
data/index.html Normal file
View File

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

166
data/network.html Normal file
View File

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

353
data/settings.html Normal file
View File

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

BIN
data/static/app.css.gz Normal file

Binary file not shown.

BIN
data/static/app.js.gz Normal file

Binary file not shown.

BIN
data/static/favicon.ico.gz Normal file

Binary file not shown.

BIN
data/static/pico.min.css.gz Normal file

Binary file not shown.

108
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>

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,190 @@
#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 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) {
float fromF88(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
float value = (int8_t) valueHB;
float value = (int8_t)valueHB;
return value + (float)valueLB / 256.0;
}
int16_t s16(unsigned long response) {
template <class T> unsigned int toF88(T val) {
return (unsigned int)(val * 256);
}
int16_t fromS16(unsigned long response) {
const byte valueLB = response & 0xFF;
const byte valueHB = (response >> 8) & 0xFF;
int16_t value = valueHB;
return ((value << 8) + valueLB);
}
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,67 @@
#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_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_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,438 @@
#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
return WiFi.mode(WIFI_OFF);
}
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,98 @@
#include <FS.h>
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
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;
const char* path = nullptr;
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};
};

4
otgateway.ino Normal file
View File

@@ -0,0 +1,4 @@
/*
This file is needed by the Arduino IDE because the ino file needs to be named as the directory name.
Don't worry, the Arduino compiler will "merge" all files, including src/main.cpp
*/

View File

@@ -8,21 +8,219 @@
; 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
gyverlibs/microDS18B20@^3.10
https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
bblanchon/ArduinoJson@^7.0.3
;ihormelnyk/OpenTherm Library@^1.1.5
https://github.com/ihormelnyk/opentherm_library.git
arduino-libraries/ArduinoMqttClient@^0.1.8
lennarthennigs/ESP Telnet@^2.2
gyverlibs/FileData@^1.0.2
gyverlibs/GyverPID@^3.3.2
gyverlibs/GyverBlinker@^1.0
milesburton/DallasTemperature@^3.11.0
laxilef/TinyLogger@^1.1.0
build_flags =
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
-D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305
-mtext-section-literals
-D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_PORT=Serial
-D USE_SERIAL=${secrets.use_serial}
-D USE_TELNET=${secrets.use_telnet}
-D DEBUG_BY_DEFAULT=${secrets.debug}
-D 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.17
; Defaults
[esp8266_defaults]
platform = espressif8266
lib_deps =
${env.lib_deps}
nrwiersma/ESP8266Scheduler@^1.1
lib_ignore =
extra_scripts =
post:tools/build.py
build_flags = ${env.build_flags}
board_build.ldscript = eagle.flash.1m256.ld
;board_build.ldscript = eagle.flash.4m1m.ld
[esp32_defaults]
platform = espressif32@^6.5
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 LED_STATUS_GPIO=13
-D LED_OT_RX_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 LED_STATUS_GPIO=13
-D LED_OT_RX_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 LED_STATUS_GPIO=13
-D LED_OT_RX_GPIO=15
[env:s2_mini]
platform = ${esp32_defaults.platform}
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_flags =
${esp32_defaults.build_flags}
-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 LED_STATUS_GPIO=11
-D LED_OT_RX_GPIO=12
[env:s3_mini]
platform = ${esp32_defaults.platform}
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_flags =
${esp32_defaults.build_flags}
-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 LED_STATUS_GPIO=11
-D LED_OT_RX_GPIO=10
[env:c3_mini]
platform = ${esp32_defaults.platform}
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 LED_STATUS_GPIO=4
-D LED_OT_RX_GPIO=5
[env:nodemcu_32s]
platform = ${esp32_defaults.platform}
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 LED_STATUS_GPIO=2 ; 18
-D LED_OT_RX_GPIO=19
;-D WOKWI=1
[env:d1_mini32]
platform = ${esp32_defaults.platform}
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 LED_STATUS_GPIO=2
-D LED_OT_RX_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

1362
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,141 +1,337 @@
extern MqttTask* tMqtt;
extern SensorsTask* tSensors;
extern OpenThermTask* tOt;
#include <Blinker.h>
class MainTask: public Task {
extern Network::Manager* network;
extern MqttTask* tMqtt;
extern OpenThermTask* tOt;
extern FileData fsSettings, fsNetworkSettings;
extern ESPTelnetStream* telnetStream;
class MainTask : public Task {
public:
MainTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
this->blinker = new Blinker();
network->setDelayCallback([this](unsigned int time) {
this->delay(time);
})->setYieldCallback([this]() {
this->yield();
});
}
~MainTask() {
delete this->blinker;
}
protected:
unsigned long lastHeapInfo = 0;
enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
Blinker* blinker = nullptr;
bool blinkerInitialized = false;
unsigned long firstFailConnect = 0;
unsigned short minFreeHeapSize = 65535;
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() {
pinMode(LED_STATUS_PIN, OUTPUT);
//pinMode(LED_OT_RX_PIN, OUTPUT);
#ifdef LED_STATUS_GPIO
pinMode(LED_STATUS_GPIO, OUTPUT);
digitalWrite(LED_STATUS_GPIO, LOW);
#endif
if (GPIO_IS_VALID(settings.externalPump.gpio)) {
pinMode(settings.externalPump.gpio, OUTPUT);
digitalWrite(settings.externalPump.gpio, LOW);
}
}
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."));
}
if (network->isConnected()) {
vars.sensors.rssi = WiFi.RSSI();
if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false);
this->telnetStarted = true;
}
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
tMqtt->enable();
}
if ( firstFailConnect != 0 ) {
firstFailConnect = 0;
if (this->firstFailConnect != 0) {
this->firstFailConnect = 0;
}
vars.states.rssi = WiFi.RSSI();
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 (this->firstFailConnect == 0) {
this->firstFailConnect = millis();
}
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
if (millis() - this->firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
INFO("Emergency mode enabled");
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
}
}
this->yield();
if (!tSensors->isEnabled() && settings.outdoorTempSource == 2) {
tSensors->enable();
} else if (tSensors->isEnabled() && settings.outdoorTempSource != 2) {
tSensors->disable();
#ifdef LED_STATUS_GPIO
this->ledStatus(LED_STATUS_GPIO);
#endif
this->externalPump();
this->yield();
// telnet
if (this->telnetStarted) {
telnetStream->loop();
this->yield();
}
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
tOt->enable();
}
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 ledStatus() {
static byte blinkLeft = 0;
static bool ledOn = false;
static unsigned long changeTime = 0;
void heap() {
unsigned int freeHeap = getFreeHeap();
unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap();
byte errNo = 0;
if (!vars.states.otStatus) {
errNo = 1;
} else if (vars.states.fault) {
errNo = 2;
} else if (vars.states.emergency) {
errNo = 3;
if (!this->restartSignalTime && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
this->restartSignalTime = millis();
}
if (errNo == 0) {
if (!ledOn) {
digitalWrite(LED_STATUS_PIN, true);
ledOn = true;
}
if (!settings.system.debug) {
return;
}
if (blinkLeft > 0) {
blinkLeft = 0;
}
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;
}
} else {
if (blinkLeft == 0) {
if (ledOn) {
digitalWrite(LED_STATUS_PIN, false);
ledOn = false;
changeTime = millis();
}
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();
}
}
if (millis() - changeTime >= 3000) {
blinkLeft = errNo;
}
}
void ledStatus(uint8_t gpio) {
uint8_t errors[4];
uint8_t errCount = 0;
static uint8_t errPos = 0;
static unsigned long endBlinkTime = 0;
static bool ledOn = false;
if (blinkLeft > 0 && millis() - changeTime >= 500) {
if (ledOn) {
digitalWrite(LED_STATUS_PIN, false);
ledOn = false;
blinkLeft--;
if (!this->blinkerInitialized) {
this->blinker->init(gpio);
this->blinkerInitialized = true;
}
} else {
digitalWrite(LED_STATUS_PIN, true);
if (!network->isConnected()) {
errors[errCount++] = 2;
}
if (!vars.states.otStatus) {
errors[errCount++] = 3;
}
if (vars.states.fault) {
errors[errCount++] = 4;
}
if (vars.states.emergency) {
errors[errCount++] = 5;
}
if (this->blinker->ready()) {
endBlinkTime = millis();
}
if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
if (errCount == 0) {
if (!ledOn) {
digitalWrite(gpio, HIGH);
ledOn = true;
}
changeTime = millis();
return;
} else if (ledOn) {
digitalWrite(gpio, 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() {
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 || !GPIO_IS_VALID(settings.externalPump.gpio)) {
if (vars.states.externalPump) {
if (GPIO_IS_VALID(settings.externalPump.gpio)) {
digitalWrite(settings.externalPump.gpio, 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(settings.externalPump.gpio, 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(settings.externalPump.gpio, 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(settings.externalPump.gpio, 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(settings.externalPump.gpio, HIGH);
Log.sinfoln("EXTPUMP", F("Enabled: anti stuck"));
}
}
};

View File

@@ -1,350 +1,421 @@
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <netif/etharp.h>
#include "HomeAssistantHelper.h"
#include <MqttClient.h>
#include <MqttWiFiClient.h>
#include <MqttWriter.h>
#include "HaHelper.h"
WiFiClient espClient;
PubSubClient client(espClient);
HomeAssistantHelper haHelper;
extern FileData fsSettings;
class MqttTask: public Task {
class MqttTask : public Task {
public:
MqttTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
MqttTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
this->wifiClient = new MqttWiFiClient();
this->client = new MqttClient(this->wifiClient);
this->writer = new MqttWriter(this->client, 256);
this->haHelper = new HaHelper();
}
~MqttTask() {
delete this->haHelper;
if (this->client != nullptr) {
if (this->client->connected()) {
this->client->stop();
}
delete this->client;
}
delete this->writer;
delete this->wifiClient;
}
void disable() {
this->client->stop();
Task::disable();
Log.sinfoln(FPSTR(L_MQTT), F("Disabled"));
}
void enable() {
Task::enable();
Log.sinfoln(FPSTR(L_MQTT), F("Enabled"));
}
bool isConnected() {
return this->connected;
}
protected:
unsigned long lastReconnectAttempt = 0;
unsigned long firstFailConnect = 0;
MqttWiFiClient* wifiClient = nullptr;
MqttClient* client = nullptr;
HaHelper* haHelper = nullptr;
MqttWriter* writer = nullptr;
unsigned short readyForSendTime = 15000;
unsigned long lastReconnectTime = 0;
unsigned long connectedTime = 0;
unsigned long disconnectedTime = 0;
unsigned long prevPubVarsTime = 0;
unsigned long prevPubSettingsTime = 0;
bool connected = false;
bool newConnection = false;
const char* getTaskName() {
return "Mqtt";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 2;
}
bool isReadyForSend() {
return millis() - this->connectedTime > this->readyForSendTime;
}
void setup() {
DEBUG("[MQTT] Started");
Log.sinfoln(FPSTR(L_MQTT), F("Started"));
client.setServer(settings.mqtt.server, settings.mqtt.port);
client.setCallback(__callback);
haHelper.setPrefix(settings.mqtt.prefix);
haHelper.setDeviceVersion(OT_GATEWAY_VERSION);
// wificlient settings
#ifdef ARDUINO_ARCH_ESP8266
this->wifiClient->setSync(true);
this->wifiClient->setNoDelay(true);
#endif
// client settings
this->client->setKeepAliveInterval(15000);
this->client->setTxPayloadSize(256);
#ifdef ARDUINO_ARCH_ESP8266
this->client->setConnectionTimeout(1000);
#else
this->client->setConnectionTimeout(3000);
#endif
this->client->onMessage([this] (void*, size_t length) {
String topic = this->client->messageTopic();
if (!length || length > 2048 || !topic.length()) {
return;
}
uint8_t payload[length];
for (size_t i = 0; i < length && this->client->available(); i++) {
payload[i] = this->client->read();
}
this->onMessage(topic.c_str(), payload, length);
});
// writer settings
#ifdef ARDUINO_ARCH_ESP32
this->writer->setYieldCallback([this] {
this->delay(10);
});
#endif
this->writer->setPublishEventCallback([this] (const char* topic, size_t written, size_t length, bool result) {
Log.straceln(FPSTR(L_MQTT), F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
//this->client->poll();
this->delay(250);
});
#ifdef ARDUINO_ARCH_ESP8266
this->writer->setFlushEventCallback([this] (size_t, size_t) {
::delay(0);
if (this->wifiClient->connected()) {
this->wifiClient->flush();
}
::delay(0);
});
#endif
// ha helper settings
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
this->haHelper->setDeviceVersion(PROJECT_VERSION);
this->haHelper->setDeviceModel(PROJECT_NAME);
this->haHelper->setDeviceName(PROJECT_NAME);
this->haHelper->setWriter(this->writer);
sprintf(buffer, CONFIG_URL, WiFi.localIP().toString().c_str());
haHelper.setDeviceConfigUrl(buffer);
this->haHelper->setDeviceConfigUrl(buffer);
}
void loop() {
if (!client.connected() && millis() - lastReconnectAttempt >= MQTT_RECONNECT_INTERVAL) {
INFO_F("Mqtt not connected, state: %i, connecting to server %s...\n", client.state(), settings.mqtt.server);
if (client.connect(settings.hostname, settings.mqtt.user, settings.mqtt.password)) {
INFO("Connected to MQTT server");
client.subscribe(getTopicPath("settings/set").c_str());
client.subscribe(getTopicPath("state/set").c_str());
publishHaEntities();
publishNonStaticHaEntities(true);
firstFailConnect = 0;
lastReconnectAttempt = 0;
} else {
INFO("Failed to connect to MQTT server\n");
if (settings.emergency.enable && !vars.states.emergency) {
if (firstFailConnect == 0) {
firstFailConnect = millis();
}
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
INFO("Emergency mode enabled");
}
}
forceARP();
lastReconnectAttempt = millis();
}
if (settings.mqtt.interval > 120) {
settings.mqtt.interval = 5;
fsSettings.update();
}
if (this->connected && !this->client->connected()) {
this->connected = false;
this->onDisconnect();
if (client.connected()) {
if (vars.states.emergency) {
vars.states.emergency = false;
} else if (!this->connected && millis() - this->lastReconnectTime >= MQTT_RECONNECT_INTERVAL) {
Log.sinfoln(FPSTR(L_MQTT), F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
INFO("Emergency mode disabled");
}
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
this->client->stop();
this->client->setId(networkSettings.hostname);
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
this->client->connect(settings.mqtt.server, settings.mqtt.port);
this->lastReconnectTime = millis();
this->yield();
client.loop();
bool published = publishNonStaticHaEntities();
publish(published);
}
}
static void forceARP() {
struct netif* netif = netif_list;
while (netif) {
etharp_gratuitous(netif);
netif = netif->next;
}
}
static bool updateSettings(JsonDocument& doc) {
bool flag = false;
if (!doc["debug"].isNull() && doc["debug"].is<bool>()) {
settings.debug = doc["debug"].as<bool>();
flag = true;
} else if (!this->connected && this->client->connected()) {
this->connected = true;
this->onConnect();
}
if (!doc["outdoorTempSource"].isNull() && doc["outdoorTempSource"].is<int>() && doc["outdoorTempSource"] >= 0 && doc["outdoorTempSource"] <= 2) {
settings.outdoorTempSource = doc["outdoorTempSource"];
flag = true;
if (!this->connected && settings.emergency.enable && !vars.states.emergency && millis() - this->disconnectedTime > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
} else if (this->connected && vars.states.emergency && millis() - this->connectedTime > 10000) {
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
}
if (!doc["mqtt"]["interval"].isNull() && doc["mqtt"]["interval"].is<int>() && doc["mqtt"]["interval"] >= 1000 && doc["mqtt"]["interval"] <= 120000) {
settings.mqtt.interval = doc["mqtt"]["interval"].as<unsigned int>();
flag = true;
if (!this->connected) {
return;
}
// emergency
if (!doc["emergency"]["enable"].isNull() && doc["emergency"]["enable"].is<bool>()) {
settings.emergency.enable = doc["emergency"]["enable"].as<bool>();
flag = true;
this->client->poll();
// delay for publish data
if (!this->isReadyForSend()) {
return;
}
if (!doc["emergency"]["target"].isNull() && (doc["emergency"]["target"].is<float>() || doc["emergency"]["target"].is<int>())) {
settings.emergency.target = round(doc["emergency"]["target"].as<float>() * 10) / 10;
flag = true;
}
if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is<bool>()) {
settings.emergency.useEquitherm = doc["emergency"]["useEquitherm"].as<bool>();
flag = true;
}
// heating
if (!doc["heating"]["enable"].isNull() && doc["heating"]["enable"].is<bool>()) {
settings.heating.enable = doc["heating"]["enable"].as<bool>();
flag = true;
}
if (!doc["heating"]["turbo"].isNull() && doc["heating"]["turbo"].is<bool>()) {
settings.heating.turbo = doc["heating"]["turbo"].as<bool>();
flag = true;
}
if (!doc["heating"]["target"].isNull() && (doc["heating"]["target"].is<float>() || doc["heating"]["target"].is<int>())) {
settings.heating.target = round(doc["heating"]["target"].as<float>() * 10) / 10;
flag = true;
}
if (!doc["heating"]["hysteresis"].isNull() && (doc["heating"]["hysteresis"].is<float>() || doc["heating"]["hysteresis"].is<int>())) {
settings.heating.hysteresis = round(doc["heating"]["hysteresis"].as<float>() * 10) / 10;
flag = true;
}
// dhw
if (!doc["dhw"]["enable"].isNull() && doc["dhw"]["enable"].is<bool>()) {
settings.dhw.enable = doc["dhw"]["enable"].as<bool>();
flag = true;
}
if (!doc["dhw"]["target"].isNull() && doc["dhw"]["target"].is<int>()) {
settings.dhw.target = doc["dhw"]["target"].as<int>();
flag = true;
}
// pid
if (!doc["pid"]["enable"].isNull() && doc["pid"]["enable"].is<bool>()) {
settings.pid.enable = doc["pid"]["enable"].as<bool>();
flag = true;
}
if (!doc["pid"]["p_factor"].isNull() && (doc["pid"]["p_factor"].is<float>() || doc["pid"]["p_factor"].is<int>())) {
settings.pid.p_factor = round(doc["pid"]["p_factor"].as<float>() * 1000) / 1000;
flag = true;
}
if (!doc["pid"]["i_factor"].isNull() && (doc["pid"]["i_factor"].is<float>() || doc["pid"]["i_factor"].is<int>())) {
settings.pid.i_factor = round(doc["pid"]["i_factor"].as<float>() * 1000) / 1000;
flag = true;
}
if (!doc["pid"]["d_factor"].isNull() && (doc["pid"]["d_factor"].is<float>() || doc["pid"]["d_factor"].is<int>())) {
settings.pid.d_factor = round(doc["pid"]["d_factor"].as<float>() * 1000) / 1000;
flag = true;
}
// equitherm
if (!doc["equitherm"]["enable"].isNull() && doc["equitherm"]["enable"].is<bool>()) {
settings.equitherm.enable = doc["equitherm"]["enable"].as<bool>();
flag = true;
}
if (!doc["equitherm"]["n_factor"].isNull() && (doc["equitherm"]["n_factor"].is<float>() || doc["equitherm"]["n_factor"].is<int>())) {
settings.equitherm.n_factor = round(doc["equitherm"]["n_factor"].as<float>() * 1000) / 1000;
flag = true;
}
if (!doc["equitherm"]["k_factor"].isNull() && (doc["equitherm"]["k_factor"].is<float>() || doc["equitherm"]["k_factor"].is<int>())) {
settings.equitherm.k_factor = round(doc["equitherm"]["k_factor"].as<float>() * 1000) / 1000;
flag = true;
}
if (!doc["equitherm"]["t_factor"].isNull() && (doc["equitherm"]["t_factor"].is<float>() || doc["equitherm"]["t_factor"].is<int>())) {
settings.equitherm.t_factor = round(doc["equitherm"]["t_factor"].as<float>() * 1000) / 1000;
flag = true;
}
if (flag) {
eeSettings.update();
publish(true);
return true;
}
return false;
}
static bool updateVariables(const JsonDocument& doc) {
bool flag = false;
if (!doc["ping"].isNull() && doc["ping"]) {
flag = true;
}
if (!doc["tuning"]["enable"].isNull() && doc["tuning"]["enable"].is<bool>()) {
vars.tuning.enable = doc["tuning"]["enable"].as<bool>();
flag = true;
}
if (!doc["tuning"]["regulator"].isNull() && doc["tuning"]["regulator"].is<int>() && doc["tuning"]["regulator"] >= 0 && doc["tuning"]["regulator"] <= 1) {
vars.tuning.regulator = doc["tuning"]["regulator"];
flag = true;
}
if (!doc["temperatures"]["indoor"].isNull() && (doc["temperatures"]["indoor"].is<float>() || doc["temperatures"]["indoor"].is<int>())) {
vars.temperatures.indoor = round(doc["temperatures"]["indoor"].as<float>() * 100) / 100;
flag = true;
}
if (!doc["temperatures"]["outdoor"].isNull() && (doc["temperatures"]["outdoor"].is<float>() || doc["temperatures"]["outdoor"].is<int>()) && settings.outdoorTempSource == 1) {
vars.temperatures.outdoor = round(doc["temperatures"]["outdoor"].as<float>() * 100) / 100;
flag = true;
}
if (!doc["restart"].isNull() && doc["restart"].is<bool>() && doc["restart"]) {
DEBUG("Received restart message...");
Scheduler.delay(10000);
DEBUG("Restart...");
eeSettings.updateNow();
ESP.restart();
}
if (flag) {
publish(true);
return true;
}
return false;
}
static void publish(bool force = false) {
static unsigned int prevPubVars = 0;
static unsigned int prevPubSettings = 0;
#ifdef ARDUINO_ARCH_ESP8266
::delay(0);
#endif
// publish variables and status
if (force || millis() - prevPubVars > settings.mqtt.interval) {
publishVariables(getTopicPath("state").c_str());
if (vars.states.fault) {
client.publish(getTopicPath("status").c_str(), "fault");
} else {
client.publish(getTopicPath("status").c_str(), vars.states.otStatus ? "online" : "offline");
}
forceARP();
prevPubVars = millis();
if (this->newConnection || millis() - this->prevPubVarsTime > (settings.mqtt.interval * 1000u)) {
this->writer->publish(this->haHelper->getDeviceTopic("status").c_str(), "online", false);
this->publishVariables(this->haHelper->getDeviceTopic("state").c_str());
this->prevPubVarsTime = millis();
}
// publish settings
if (force || millis() - prevPubSettings > settings.mqtt.interval * 10) {
publishSettings(getTopicPath("settings").c_str());
prevPubSettings = millis();
if (this->newConnection || millis() - this->prevPubSettingsTime > (settings.mqtt.interval * 10000u)) {
this->publishSettings(this->haHelper->getDeviceTopic("settings").c_str());
this->prevPubSettingsTime = millis();
}
// publish ha entities if not published
if (this->newConnection) {
this->publishHaEntities();
this->publishNonStaticHaEntities(true);
this->newConnection = false;
} else {
// publish non static ha entities
this->publishNonStaticHaEntities();
}
}
static void publishHaEntities() {
// main
haHelper.publishSelectOutdoorTempSource();
haHelper.publishSwitchDebug(false);
void onConnect() {
this->connectedTime = millis();
this->newConnection = true;
unsigned long downtime = (millis() - this->disconnectedTime) / 1000;
Log.sinfoln(FPSTR(L_MQTT), F("Connected (downtime: %u s.)"), downtime);
// emergency
haHelper.publishSwitchEmergency();
haHelper.publishNumberEmergencyTarget();
haHelper.publishSwitchEmergencyUseEquitherm();
// heating
haHelper.publishSwitchHeating(false);
haHelper.publishSwitchHeatingTurbo();
//haHelper.publishNumberHeatingTarget(false);
haHelper.publishNumberHeatingHysteresis();
haHelper.publishSensorHeatingSetpoint(false);
// dhw
haHelper.publishSwitchDHW(false);
//haHelper.publishNumberDHWTarget(false);
// pid
haHelper.publishSwitchPID();
haHelper.publishNumberPIDFactorP();
haHelper.publishNumberPIDFactorI();
haHelper.publishNumberPIDFactorD();
// equitherm
haHelper.publishSwitchEquitherm();
haHelper.publishNumberEquithermFactorN();
haHelper.publishNumberEquithermFactorK();
haHelper.publishNumberEquithermFactorT();
// tuning
haHelper.publishSwitchTuning();
haHelper.publishSelectTuningRegulator();
// states
haHelper.publishBinSensorStatus();
haHelper.publishBinSensorOtStatus();
haHelper.publishBinSensorHeating();
haHelper.publishBinSensorDHW();
haHelper.publishBinSensorFlame();
haHelper.publishBinSensorFault();
haHelper.publishBinSensorDiagnostic();
haHelper.publishSensorFaultCode();
haHelper.publishSensorRssi();
// sensors
haHelper.publishSensorModulation(false);
haHelper.publishSensorPressure(false);
// temperatures
haHelper.publishNumberIndoorTemp();
//haHelper.publishNumberOutdoorTemp();
haHelper.publishSensorHeatingTemp();
haHelper.publishSensorDHWTemp();
this->client->subscribe(this->haHelper->getDeviceTopic("settings/set").c_str());
this->client->subscribe(this->haHelper->getDeviceTopic("state/set").c_str());
}
static bool publishNonStaticHaEntities(bool force = false) {
static byte _heatingMinTemp;
static byte _heatingMaxTemp;
static byte _dhwMinTemp;
static byte _dhwMaxTemp;
static bool _editableOutdoorTemp;
void onDisconnect() {
this->disconnectedTime = millis();
unsigned long uptime = (millis() - this->connectedTime) / 1000;
Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %u s.)"), this->client->connectError(), uptime);
}
void onMessage(const char* topic, uint8_t* payload, size_t length) {
if (!length) {
return;
}
if (settings.system.debug) {
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
if (Log.lock()) {
for (size_t i = 0; i < length; i++) {
if (payload[i] == 0) {
break;
} else if (payload[i] == 13) {
continue;
} else if (payload[i] == 10) {
Log.print("\r\n> ");
} else {
Log.print((char) payload[i]);
}
}
Log.print("\r\n\n");
Log.flush();
Log.unlock();
}
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, payload, length);
if (dErr != DeserializationError::Ok) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Error on deserialization: %s"), dErr.f_str());
return;
} else if (doc.isNull() || !doc.size()) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Not valid json"));
return;
}
if (this->haHelper->getDeviceTopic("state/set").equals(topic)) {
this->writer->publish(this->haHelper->getDeviceTopic("state/set").c_str(), nullptr, 0, true);
this->updateVariables(doc);
} else if (this->haHelper->getDeviceTopic("settings/set").equals(topic)) {
this->writer->publish(this->haHelper->getDeviceTopic("settings/set").c_str(), nullptr, 0, true);
this->updateSettings(doc);
}
}
bool updateSettings(JsonDocument& doc) {
bool changed = safeJsonToSettings(doc, settings);
doc.clear();
doc.shrinkToFit();
if (changed) {
this->prevPubSettingsTime = 0;
fsSettings.update();
return true;
}
return false;
}
bool updateVariables(JsonDocument& doc) {
bool changed = jsonToVars(doc, vars);
doc.clear();
doc.shrinkToFit();
if (changed) {
this->prevPubVarsTime = 0;
return true;
}
return false;
}
void publishHaEntities() {
// emergency
this->haHelper->publishSwitchEmergency();
this->haHelper->publishNumberEmergencyTarget();
this->haHelper->publishSwitchEmergencyUseEquitherm();
this->haHelper->publishSwitchEmergencyUsePid();
// heating
this->haHelper->publishSwitchHeating(false);
this->haHelper->publishSwitchHeatingTurbo();
this->haHelper->publishNumberHeatingHysteresis();
this->haHelper->publishSensorHeatingSetpoint(false);
this->haHelper->publishSensorBoilerHeatingMinTemp(false);
this->haHelper->publishSensorBoilerHeatingMaxTemp(false);
this->haHelper->publishNumberHeatingMinTemp(false);
this->haHelper->publishNumberHeatingMaxTemp(false);
this->haHelper->publishNumberHeatingMaxModulation(false);
// pid
this->haHelper->publishSwitchPid();
this->haHelper->publishNumberPidFactorP();
this->haHelper->publishNumberPidFactorI();
this->haHelper->publishNumberPidFactorD();
this->haHelper->publishNumberPidDt(false);
this->haHelper->publishNumberPidMinTemp(false);
this->haHelper->publishNumberPidMaxTemp(false);
// equitherm
this->haHelper->publishSwitchEquitherm();
this->haHelper->publishNumberEquithermFactorN();
this->haHelper->publishNumberEquithermFactorK();
this->haHelper->publishNumberEquithermFactorT();
// tuning
this->haHelper->publishSwitchTuning();
this->haHelper->publishSelectTuningRegulator();
// states
this->haHelper->publishBinSensorStatus();
this->haHelper->publishBinSensorOtStatus();
this->haHelper->publishBinSensorHeating();
this->haHelper->publishBinSensorFlame();
this->haHelper->publishBinSensorFault();
this->haHelper->publishBinSensorDiagnostic();
// sensors
this->haHelper->publishSensorModulation(false);
this->haHelper->publishSensorPressure(false);
this->haHelper->publishSensorFaultCode();
this->haHelper->publishSensorRssi(false);
this->haHelper->publishSensorUptime(false);
// temperatures
this->haHelper->publishNumberIndoorTemp();
this->haHelper->publishSensorHeatingTemp();
// buttons
this->haHelper->publishButtonRestart(false);
this->haHelper->publishButtonResetFault();
this->haHelper->publishButtonResetDiagnostic();
}
bool publishNonStaticHaEntities(bool force = false) {
static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp = 0;
static bool _isStupidMode, _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent = false;
bool published = false;
bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable;
byte heatingMinTemp = isStupidMode ? vars.parameters.heatingMinTemp : 10;
byte heatingMaxTemp = isStupidMode ? vars.parameters.heatingMaxTemp : 30;
bool editableOutdoorTemp = settings.outdoorTempSource == 1;
byte heatingMinTemp = isStupidMode ? settings.heating.minTemp : 10;
byte heatingMaxTemp = isStupidMode ? settings.heating.maxTemp : 30;
bool editableOutdoorTemp = settings.sensors.outdoor.type == SensorType::MANUAL;
bool editableIndoorTemp = settings.sensors.indoor.type == SensorType::MANUAL;
if (force || _dhwPresent != settings.opentherm.dhwPresent) {
_dhwPresent = settings.opentherm.dhwPresent;
if (_dhwPresent) {
this->haHelper->publishSwitchDhw(false);
this->haHelper->publishSensorBoilerDhwMinTemp(false);
this->haHelper->publishSensorBoilerDhwMaxTemp(false);
this->haHelper->publishNumberDhwMinTemp(false);
this->haHelper->publishNumberDhwMaxTemp(false);
this->haHelper->publishBinSensorDhw();
this->haHelper->publishSensorDhwTemp();
this->haHelper->publishSensorDhwFlowRate(false);
} else {
this->haHelper->deleteSwitchDhw();
this->haHelper->deleteSensorBoilerDhwMinTemp();
this->haHelper->deleteSensorBoilerDhwMaxTemp();
this->haHelper->deleteNumberDhwMinTemp();
this->haHelper->deleteNumberDhwMaxTemp();
this->haHelper->deleteBinSensorDhw();
this->haHelper->deleteSensorDhwTemp();
this->haHelper->deleteNumberDhwTarget();
this->haHelper->deleteClimateDhw();
this->haHelper->deleteSensorDhwFlowRate();
}
published = true;
}
if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) {
@@ -353,19 +424,34 @@ protected:
_heatingMinTemp = heatingMinTemp;
_heatingMaxTemp = heatingMaxTemp;
_isStupidMode = isStupidMode;
haHelper.publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false);
haHelper.publishClimateHeating(heatingMinTemp, heatingMaxTemp);
this->haHelper->publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false);
this->haHelper->publishClimateHeating(
heatingMinTemp,
heatingMaxTemp,
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
);
published = true;
} else if (_isStupidMode != isStupidMode) {
_isStupidMode = isStupidMode;
this->haHelper->publishClimateHeating(
heatingMinTemp,
heatingMaxTemp,
isStupidMode ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR
);
published = true;
}
if (force || _dhwMinTemp != vars.parameters.dhwMinTemp || _dhwMaxTemp != vars.parameters.dhwMaxTemp) {
_dhwMinTemp = vars.parameters.dhwMinTemp;
_dhwMaxTemp = vars.parameters.dhwMaxTemp;
if (_dhwPresent && (force || _dhwMinTemp != settings.dhw.minTemp || _dhwMaxTemp != settings.dhw.maxTemp)) {
_dhwMinTemp = settings.dhw.minTemp;
_dhwMaxTemp = settings.dhw.maxTemp;
haHelper.publishNumberDHWTarget(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp, false);
haHelper.publishClimateDHW(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp);
this->haHelper->publishNumberDhwTarget(settings.dhw.minTemp, settings.dhw.maxTemp, false);
this->haHelper->publishClimateDhw(settings.dhw.minTemp, settings.dhw.maxTemp);
published = true;
}
@@ -374,11 +460,25 @@ protected:
_editableOutdoorTemp = editableOutdoorTemp;
if (editableOutdoorTemp) {
haHelper.deleteSensorOutdoorTemp();
haHelper.publishNumberOutdoorTemp();
this->haHelper->deleteSensorOutdoorTemp();
this->haHelper->publishNumberOutdoorTemp();
} else {
haHelper.deleteNumberOutdoorTemp();
haHelper.publishSensorOutdoorTemp();
this->haHelper->deleteNumberOutdoorTemp();
this->haHelper->publishSensorOutdoorTemp();
}
published = true;
}
if (force || _editableIndoorTemp != editableIndoorTemp) {
_editableIndoorTemp = editableIndoorTemp;
if (editableIndoorTemp) {
this->haHelper->deleteSensorIndoorTemp();
this->haHelper->publishNumberIndoorTemp();
} else {
this->haHelper->deleteNumberIndoorTemp();
this->haHelper->publishSensorIndoorTemp();
}
published = true;
@@ -387,109 +487,19 @@ protected:
return published;
}
static bool publishSettings(const char* topic) {
StaticJsonDocument<2048> doc;
bool publishSettings(const char* topic) {
JsonDocument doc;
safeSettingsToJson(settings, doc);
doc.shrinkToFit();
doc["debug"] = settings.debug;
doc["outdoorTempSource"] = settings.outdoorTempSource;
doc["emergency"]["enable"] = settings.emergency.enable;
doc["emergency"]["target"] = settings.emergency.target;
doc["emergency"]["useEquitherm"] = settings.emergency.useEquitherm;
doc["heating"]["enable"] = settings.heating.enable;
doc["heating"]["turbo"] = settings.heating.turbo;
doc["heating"]["target"] = settings.heating.target;
doc["heating"]["hysteresis"] = settings.heating.hysteresis;
doc["dhw"]["enable"] = settings.dhw.enable;
doc["dhw"]["target"] = settings.dhw.target;
doc["pid"]["enable"] = settings.pid.enable;
doc["pid"]["p_factor"] = settings.pid.p_factor;
doc["pid"]["i_factor"] = settings.pid.i_factor;
doc["pid"]["d_factor"] = settings.pid.d_factor;
doc["equitherm"]["enable"] = settings.equitherm.enable;
doc["equitherm"]["n_factor"] = settings.equitherm.n_factor;
doc["equitherm"]["k_factor"] = settings.equitherm.k_factor;
doc["equitherm"]["t_factor"] = settings.equitherm.t_factor;
client.beginPublish(topic, measureJson(doc), false);
//BufferingPrint bufferedClient(client, 32);
//serializeJson(doc, bufferedClient);
//bufferedClient.flush();
serializeJson(doc, client);
return client.endPublish();
return this->writer->publish(topic, doc, true);
}
static bool publishVariables(const char* topic) {
StaticJsonDocument<2048> doc;
bool publishVariables(const char* topic) {
JsonDocument doc;
varsToJson(vars, doc);
doc.shrinkToFit();
doc["tuning"]["enable"] = vars.tuning.enable;
doc["tuning"]["regulator"] = vars.tuning.regulator;
doc["states"]["otStatus"] = vars.states.otStatus;
doc["states"]["heating"] = vars.states.heating;
doc["states"]["dhw"] = vars.states.dhw;
doc["states"]["flame"] = vars.states.flame;
doc["states"]["fault"] = vars.states.fault;
doc["states"]["diagnostic"] = vars.states.diagnostic;
doc["states"]["faultCode"] = vars.states.faultCode;
doc["states"]["rssi"] = vars.states.rssi;
doc["sensors"]["modulation"] = vars.sensors.modulation;
doc["sensors"]["pressure"] = vars.sensors.pressure;
doc["temperatures"]["indoor"] = vars.temperatures.indoor;
doc["temperatures"]["outdoor"] = vars.temperatures.outdoor;
doc["temperatures"]["heating"] = vars.temperatures.heating;
doc["temperatures"]["dhw"] = vars.temperatures.dhw;
doc["parameters"]["heatingMinTemp"] = vars.parameters.heatingMinTemp;
doc["parameters"]["heatingMaxTemp"] = vars.parameters.heatingMaxTemp;
doc["parameters"]["heatingSetpoint"] = vars.parameters.heatingSetpoint;
doc["parameters"]["dhwMinTemp"] = vars.parameters.dhwMinTemp;
doc["parameters"]["dhwMaxTemp"] = vars.parameters.dhwMaxTemp;
client.beginPublish(topic, measureJson(doc), false);
//BufferingPrint bufferedClient(client, 32);
//serializeJson(doc, bufferedClient);
//bufferedClient.flush();
serializeJson(doc, client);
return client.endPublish();
}
static std::string getTopicPath(const char* topic) {
return std::string(settings.mqtt.prefix) + "/" + std::string(topic);
}
static void __callback(char* topic, byte* payload, unsigned int length) {
if (!length) {
return;
}
if (settings.debug) {
DEBUG_F("MQTT received message\n\r Topic: %s\n\r Data: ", topic);
for (unsigned int i = 0; i < length; i++) {
DEBUG_STREAM.print((char)payload[i]);
}
DEBUG_STREAM.print("\n");
}
StaticJsonDocument<2048> doc;
DeserializationError dErr = deserializeJson(doc, (const byte*)payload, length);
if (dErr != DeserializationError::Ok || doc.isNull()) {
return;
}
if (getTopicPath("state/set").compare(topic) == 0) {
updateVariables(doc);
client.publish(getTopicPath("state/set").c_str(), NULL, true);
} else if (getTopicPath("settings/set").compare(topic) == 0) {
updateSettings(doc);
client.publish(getTopicPath("settings/set").c_str(), NULL, true);
}
return this->writer->publish(topic, doc, true);
}
};

View File

@@ -1,304 +1,596 @@
#include <new>
#include <CustomOpenTherm.h>
extern FileData fsSettings;
CustomOpenTherm* ot;
class OpenThermTask: public Task {
class OpenThermTask : public Task {
public:
OpenThermTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
OpenThermTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
protected:
void setup() {
ot = new CustomOpenTherm(settings.opentherm.inPin, settings.opentherm.outPin);
ot->begin(handleInterrupt, responseCallback);
ot->setHandleSendRequestCallback(sendRequestCallback);
#ifdef LED_OT_RX_PIN
pinMode(LED_OT_RX_PIN, OUTPUT);
#endif
~OpenThermTask() {
delete this->instance;
}
void loop() {
static byte currentHeatingTemp, currentDHWTemp = 0;
unsigned long localResponse;
protected:
const unsigned short readyTime = 60000;
const unsigned short dhwSetTempInterval = 60000;
const unsigned short heatingSetTempInterval = 60000;
const unsigned int initializingInterval = 3600000;
if ( setMasterMemberIdCode() ) {
DEBUG_F("Slave member id code: %u\r\n", vars.parameters.slaveMemberIdCode);
DEBUG_F("Master member id code: %u\r\n", settings.opentherm.memberIdCode > 0 ? settings.opentherm.memberIdCode : vars.parameters.slaveMemberIdCode);
CustomOpenTherm* instance = nullptr;
unsigned long instanceCreatedTime = 0;
byte instanceInGpio = 0;
byte instanceOutGpio = 0;
bool isInitialized = false;
unsigned long initializedTime = 0;
unsigned int initializedMemberIdCode = 0;
byte dhwFlowRateMultiplier = 1;
byte pressureMultiplier = 1;
bool pump = true;
unsigned long lastSuccessResponse = 0;
unsigned long prevUpdateNonEssentialVars = 0;
unsigned long dhwSetTempTime = 0;
unsigned long heatingSetTempTime = 0;
} else {
WARN("Slave member id failed");
const char* getTaskName() {
return "OpenTherm";
}
int getTaskCore() {
return 1;
}
int getTaskPriority() {
return 5;
}
void setup() {
#ifdef LED_OT_RX_GPIO
pinMode(LED_OT_RX_GPIO, OUTPUT);
digitalWrite(LED_OT_RX_GPIO, LOW);
#endif
// delete instance
if (this->instance != nullptr) {
delete this->instance;
this->instance = nullptr;
Log.sinfoln(FPSTR(L_OT), F("Stopped"));
}
bool heatingEnable = (vars.states.emergency || settings.heating.enable) && pump && isReady();
localResponse = ot->setBoilerStatus(
heatingEnable,
settings.dhw.enable,
false, false, true, false, false
);
if (!ot->isValidResponse(localResponse)) {
WARN_F("Invalid response after setBoilerStatus: %s\r\n", ot->statusToString(ot->getLastResponseStatus()));
if (!GPIO_IS_VALID(settings.opentherm.inGpio) || !GPIO_IS_VALID(settings.opentherm.outGpio)) {
Log.swarningln(FPSTR(L_OT), F("Not started. GPIO IN: %hhu or GPIO OUT: %hhu is not valid"), settings.opentherm.inGpio, settings.opentherm.outGpio);
return;
}
INFO_F("Heating enabled: %d\r\n", heatingEnable);
setMaxModulationLevel(heatingEnable ? 100 : 0);
// create instance
this->instance = new CustomOpenTherm(settings.opentherm.inGpio, settings.opentherm.outGpio);
vars.states.heating = ot->isCentralHeatingActive(localResponse);
vars.states.dhw = ot->isHotWaterActive(localResponse);
vars.states.flame = ot->isFlameOn(localResponse);
vars.states.fault = ot->isFault(localResponse);
vars.states.diagnostic = ot->isDiagnostic(localResponse);
yield();
// flags
this->instanceCreatedTime = millis();
this->instanceInGpio = settings.opentherm.inGpio;
this->instanceOutGpio = settings.opentherm.outGpio;
this->isInitialized = false;
// Команды чтения данных котла
if (millis() - prevUpdateNonEssentialVars > 60000) {
updateSlaveParameters();
updateMasterParameters();
Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inGpio, settings.opentherm.outGpio);
DEBUG_F("Master type: %u, version: %u\r\n", vars.parameters.masterType, vars.parameters.masterVersion);
DEBUG_F("Slave type: %u, version: %u\r\n", vars.parameters.slaveType, vars.parameters.slaveVersion);
this->instance->setAfterSendRequestCallback([this](unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) {
Log.straceln(
FPSTR(L_OT),
F("ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"),
CustomOpenTherm::getDataID(request), request, response, attempt, CustomOpenTherm::statusToString(status)
);
updateMinMaxDhwTemp();
updateMinMaxHeatingTemp();
if (status == OpenThermResponseStatus::SUCCESS) {
this->lastSuccessResponse = millis();
if (settings.outdoorTempSource == 0) {
#ifdef LED_OT_RX_GPIO
{
digitalWrite(LED_OT_RX_GPIO, HIGH);
delayMicroseconds(2000);
digitalWrite(LED_OT_RX_GPIO, LOW);
}
#endif
}
});
this->instance->setYieldCallback([this]() {
this->delay(25);
});
this->instance->begin();
}
void loop() {
static byte currentHeatingTemp, currentDhwTemp = 0;
if (this->instanceInGpio != settings.opentherm.inGpio || this->instanceOutGpio != settings.opentherm.outGpio) {
this->setup();
} else if (this->initializedMemberIdCode != settings.opentherm.memberIdCode || millis() - this->initializedTime > this->initializingInterval) {
this->isInitialized = false;
}
if (this->instance == nullptr) {
this->delay(5000);
return;
}
bool heatingEnabled = (vars.states.emergency || settings.heating.enable) && this->pump && this->isReady();
bool heatingCh2Enabled = settings.opentherm.heatingCh2Enabled;
if (settings.opentherm.heatingCh1ToCh2) {
heatingCh2Enabled = heatingEnabled;
} else if (settings.opentherm.dhwToCh2) {
heatingCh2Enabled = settings.opentherm.dhwPresent && settings.dhw.enable;
}
unsigned long response = this->instance->setBoilerStatus(
heatingEnabled,
settings.opentherm.dhwPresent && settings.dhw.enable,
false,
false,
heatingCh2Enabled,
settings.opentherm.summerWinterMode,
settings.opentherm.dhwBlocking
);
if (!CustomOpenTherm::isValidResponse(response)) {
Log.swarningln(FPSTR(L_OT), F("Invalid response after setBoilerStatus: %s"), CustomOpenTherm::statusToString(this->instance->getLastResponseStatus()));
}
if (!vars.states.otStatus && millis() - this->lastSuccessResponse < 1150) {
Log.sinfoln(FPSTR(L_OT), F("Connected"));
vars.states.otStatus = true;
} else if (vars.states.otStatus && millis() - this->lastSuccessResponse > 1150) {
Log.swarningln(FPSTR(L_OT), F("Disconnected"));
vars.states.otStatus = false;
this->isInitialized = false;
}
// If boiler is disconnected, no need try setting other OT stuff
if (!vars.states.otStatus) {
vars.states.heating = false;
vars.states.dhw = false;
vars.states.flame = false;
vars.states.fault = false;
vars.states.diagnostic = false;
return;
}
if (!this->isInitialized) {
Log.sinfoln(FPSTR(L_OT), F("Initializing..."));
this->isInitialized = true;
this->initializedTime = millis();
this->initializedMemberIdCode = settings.opentherm.memberIdCode;
this->dhwFlowRateMultiplier = 1;
this->pressureMultiplier = 1;
this->initialize();
}
if (vars.parameters.heatingEnabled != heatingEnabled) {
this->prevUpdateNonEssentialVars = 0;
vars.parameters.heatingEnabled = heatingEnabled;
Log.sinfoln(FPSTR(L_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled"));
}
vars.states.heating = CustomOpenTherm::isCentralHeatingActive(response);
vars.states.dhw = settings.opentherm.dhwPresent ? CustomOpenTherm::isHotWaterActive(response) : false;
vars.states.flame = CustomOpenTherm::isFlameOn(response);
vars.states.fault = CustomOpenTherm::isFault(response);
vars.states.diagnostic = CustomOpenTherm::isDiagnostic(response);
// These parameters will be updated every minute
if (millis() - this->prevUpdateNonEssentialVars > 60000) {
if (!heatingEnabled && settings.opentherm.modulationSyncWithHeating) {
if (setMaxModulationLevel(0)) {
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation 0% (off)"));
} else {
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation 0% (off)"));
}
} else {
if (setMaxModulationLevel(settings.heating.maxModulation)) {
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation);
} else {
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation);
}
}
// Get DHW min/max temp (if necessary)
if (settings.opentherm.dhwPresent) {
if (updateMinMaxDhwTemp()) {
if (settings.dhw.minTemp < vars.parameters.dhwMinTemp) {
settings.dhw.minTemp = vars.parameters.dhwMinTemp;
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp);
}
if (settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) {
settings.dhw.maxTemp = vars.parameters.dhwMaxTemp;
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp);
}
} else {
Log.swarningln(FPSTR(L_OT_DHW), F("Failed get min/max temp"));
}
if (settings.dhw.minTemp >= settings.dhw.maxTemp) {
settings.dhw.minTemp = 30;
settings.dhw.maxTemp = 60;
fsSettings.update();
}
}
// Get heating min/max temp
if (updateMinMaxHeatingTemp()) {
if (settings.heating.minTemp < vars.parameters.heatingMinTemp) {
settings.heating.minTemp = vars.parameters.heatingMinTemp;
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp);
}
if (settings.heating.maxTemp > vars.parameters.heatingMaxTemp) {
settings.heating.maxTemp = vars.parameters.heatingMaxTemp;
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp);
}
} else {
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed get min/max temp"));
}
if (settings.heating.minTemp >= settings.heating.maxTemp) {
settings.heating.minTemp = 20;
settings.heating.maxTemp = 90;
fsSettings.update();
}
// Force set max heating temp
setMaxHeatingTemp(settings.heating.maxTemp);
// Get outdoor temp (if necessary)
if (settings.sensors.outdoor.type == SensorType::BOILER) {
updateOutsideTemp();
}
// Get fault code (if necessary)
if (vars.states.fault) {
updateFaultCode();
ot->sendBoilerReset();
}
if ( vars.states.diagnostic ) {
ot->sendServiceReset();
}
updatePressure();
prevUpdateNonEssentialVars = millis();
yield();
this->prevUpdateNonEssentialVars = millis();
}
updatePressure();
if ( settings.dhw.enable || settings.heating.enable || heatingEnable ) {
// Get current modulation level (if necessary)
if ((settings.opentherm.dhwPresent && settings.dhw.enable) || settings.heating.enable || heatingEnabled) {
updateModulationLevel();
}
if ( settings.dhw.enable ) {
updateDHWTemp();
} else {
vars.temperatures.dhw = 0;
vars.sensors.modulation = 0;
}
if ( settings.heating.enable || heatingEnable ) {
updateHeatingTemp();
// Update DHW sensors (if necessary)
if (settings.opentherm.dhwPresent) {
updateDhwTemp();
updateDhwFlowRate();
} else {
vars.temperatures.heating = 0;
vars.temperatures.dhw = 0.0f;
vars.sensors.dhwFlowRate = 0.0f;
}
yield();
//
// Температура ГВС
byte newDHWTemp = settings.dhw.target;
if (settings.dhw.enable && newDHWTemp != currentDHWTemp) {
if (newDHWTemp < vars.parameters.dhwMinTemp || newDHWTemp > vars.parameters.dhwMaxTemp) {
newDHWTemp = constrain(newDHWTemp, vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp);
// Get current heating temp
updateHeatingTemp();
// Fault reset action
if (vars.actions.resetFault) {
if (vars.states.fault) {
if (this->instance->sendBoilerReset()) {
Log.sinfoln(FPSTR(L_OT), F("Boiler fault reset successfully"));
} else {
Log.serrorln(FPSTR(L_OT), F("Boiler fault reset failed"));
}
}
INFO_F("Set DHW temp = %u\r\n", newDHWTemp);
vars.actions.resetFault = false;
}
// Записываем заданную температуру ГВС
if (ot->setDHWSetpoint(newDHWTemp)) {
currentDHWTemp = newDHWTemp;
// Diag reset action
if (vars.actions.resetDiagnostic) {
if (vars.states.diagnostic) {
if (this->instance->sendServiceReset()) {
Log.sinfoln(FPSTR(L_OT), F("Boiler diagnostic reset successfully"));
} else {
Log.serrorln(FPSTR(L_OT), F("Boiler diagnostic reset failed"));
}
}
vars.actions.resetDiagnostic = false;
}
// Update DHW temp
byte newDhwTemp = settings.dhw.target;
if (settings.opentherm.dhwPresent && settings.dhw.enable && (needSetDhwTemp() || newDhwTemp != currentDhwTemp)) {
if (newDhwTemp < settings.dhw.minTemp || newDhwTemp > settings.dhw.maxTemp) {
newDhwTemp = constrain(newDhwTemp, settings.dhw.minTemp, settings.dhw.maxTemp);
}
Log.sinfoln(FPSTR(L_OT_DHW), F("Set temp = %u"), newDhwTemp);
// Set DHW temp
if (this->instance->setDhwTemp(newDhwTemp)) {
currentDhwTemp = newDhwTemp;
this->dhwSetTempTime = millis();
} else {
WARN("Failed set DHW temp");
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp"));
}
// Set DHW temp to CH2
if (settings.opentherm.dhwToCh2) {
if (!this->instance->setHeatingCh2Temp(newDhwTemp)) {
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set ch2 temp"));
}
}
}
//
// Температура отопления
if (heatingEnable && fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001) {
INFO_F("Setting heating temp = %u \n", vars.parameters.heatingSetpoint);
// Записываем заданную температуру
if (ot->setBoilerTemperature(vars.parameters.heatingSetpoint)) {
// Update heating temp
if (heatingEnabled && (needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001)) {
Log.sinfoln(FPSTR(L_OT_HEATING), F("Set temp = %u"), vars.parameters.heatingSetpoint);
// Set heating temp
if (this->instance->setHeatingCh1Temp(vars.parameters.heatingSetpoint)) {
currentHeatingTemp = vars.parameters.heatingSetpoint;
this->heatingSetTempTime = millis();
} else {
WARN("Failed set heating temp");
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set temp"));
}
// Set heating temp to CH2
if (settings.opentherm.heatingCh1ToCh2) {
if (!this->instance->setHeatingCh2Temp(vars.parameters.heatingSetpoint)) {
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set ch2 temp"));
}
}
}
// коммутационная разность (hysteresis)
// только для pid и/или equitherm
if (settings.heating.hysteresis > 0 && !vars.states.emergency && (settings.equitherm.enable || settings.pid.enable)) {
// Hysteresis
// Only if enabled PID or/and Equitherm
if (settings.heating.hysteresis > 0 && (!vars.states.emergency || settings.emergency.usePid) && (settings.equitherm.enable || settings.pid.enable)) {
float halfHyst = settings.heating.hysteresis / 2;
if (pump && vars.temperatures.indoor - settings.heating.target + 0.0001 >= halfHyst) {
pump = false;
if (this->pump && vars.temperatures.indoor - settings.heating.target + 0.0001 >= halfHyst) {
this->pump = false;
} else if (!pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) {
pump = true;
} else if (!this->pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) {
this->pump = true;
}
} else if (!this->pump) {
this->pump = true;
}
}
void initialize() {
// Not all boilers support these, only try once when the boiler becomes connected
if (updateSlaveVersion()) {
Log.straceln(FPSTR(L_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType);
} else {
Log.swarningln(FPSTR(L_OT), F("Get slave version failed"));
}
// 0x013F
if (setMasterVersion(0x3F, 0x01)) {
Log.straceln(FPSTR(L_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType);
} else if (!pump) {
pump = true;
} else {
Log.swarningln(FPSTR(L_OT), F("Set master version failed"));
}
if (updateSlaveConfig()) {
Log.straceln(FPSTR(L_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags);
} else {
Log.swarningln(FPSTR(L_OT), F("Get slave config failed"));
}
if (setMasterConfig(settings.opentherm.memberIdCode & 0xFF, (settings.opentherm.memberIdCode & 0xFFFF) >> 8)) {
Log.straceln(FPSTR(L_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags);
} else {
Log.swarningln(FPSTR(L_OT), F("Set master config failed"));
}
}
void static IRAM_ATTR handleInterrupt() {
ot->handleInterrupt();
}
void static sendRequestCallback(unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) {
printRequestDetail(ot->getDataID(request), status, request, response, attempt);
}
void static responseCallback(unsigned long result, OpenThermResponseStatus status) {
static byte attempt = 0;
switch (status) {
case OpenThermResponseStatus::TIMEOUT:
if (vars.states.otStatus && ++attempt > OPENTHERM_OFFLINE_TRESHOLD) {
vars.states.otStatus = false;
attempt = OPENTHERM_OFFLINE_TRESHOLD;
}
break;
case OpenThermResponseStatus::SUCCESS:
attempt = 0;
if (!vars.states.otStatus) {
vars.states.otStatus = true;
}
#ifdef LED_OT_RX_PIN
{
digitalWrite(LED_OT_RX_PIN, true);
unsigned long ts = millis();
while (millis() - ts < 2) {}
digitalWrite(LED_OT_RX_PIN, false);
}
#endif
break;
default:
break;
}
}
protected:
bool pump = true;
unsigned long prevUpdateNonEssentialVars = 0;
unsigned long startupTime = millis();
bool isReady() {
return millis() - startupTime > 60000;
return millis() - this->instanceCreatedTime > this->readyTime;
}
void static printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
sprintf(buffer, "OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s", id, request, response, attempt, ot->statusToString(status));
if (status != OpenThermResponseStatus::SUCCESS) {
//WARN(buffer);
DEBUG(buffer);
} else {
DEBUG(buffer);
}
bool needSetDhwTemp() {
return millis() - this->dhwSetTempTime > this->dhwSetTempInterval;
}
bool setMasterMemberIdCode() {
//=======================================================================================
// Эта группа элементов данных определяет информацию о конфигурации как на ведомых, так
// и на главных сторонах. Каждый из них имеет группу флагов конфигурации (8 бит)
// и код MemberID (1 байт). Перед передачей информации об управлении и состоянии
// рекомендуется обмен сообщениями о допустимой конфигурации ведомого устройства
// чтения и основной конфигурации записи. Нулевой код MemberID означает клиентское
// неспецифическое устройство. Номер/тип версии продукта следует использовать в сочетании
// с "кодом идентификатора участника", который идентифицирует производителя устройства.
//=======================================================================================
bool needSetHeatingTemp() {
return millis() - this->heatingSetTempTime > this->heatingSetTempInterval;
}
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SConfigSMemberIDcode, 0)); // 0xFFFF
/*uint8_t flags = (response & 0xFFFF) >> 8;
DEBUG_F(
"MasterMemberIdCode:\r\n DHW present: %u\r\n Control type: %u\r\n Cooling configuration: %u\r\n DHW configuration: %u\r\n Pump control: %u\r\n CH2 present: %u\r\n Remote water filling function: %u\r\n Heat/cool mode control: %u\r\n Slave MemberID Code: %u\r\n",
flags & 0x01,
flags & 0x02,
flags & 0x04,
flags & 0x08,
flags & 0x10,
flags & 0x20,
flags & 0x40,
flags & 0x80,
response & 0xFF
);*/
if (ot->isValidResponse(response)) {
vars.parameters.slaveMemberIdCode = response & 0xFF;
} else if ( settings.opentherm.memberIdCode <= 0 ) {
return false;
}
response = ot->sendRequest(ot->buildRequest(
OpenThermRequestType::WRITE,
OpenThermMessageID::MConfigMMemberIDcode,
settings.opentherm.memberIdCode > 0 ? settings.opentherm.memberIdCode : vars.parameters.slaveMemberIdCode
bool updateSlaveConfig() {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::SConfigSMemberIDcode,
0
));
return ot->isValidResponse(response);
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.parameters.slaveMemberId = response & 0xFF;
vars.parameters.slaveFlags = (response & 0xFFFF) >> 8;
/*uint8_t flags = (response & 0xFFFF) >> 8;
Log.straceln(
"OT",
F("MasterMemberIdCode:\r\n DHW present: %u\r\n Control type: %u\r\n Cooling configuration: %u\r\n DHW configuration: %u\r\n Pump control: %u\r\n CH2 present: %u\r\n Remote water filling function: %u\r\n Heat/cool mode control: %u\r\n Slave MemberID Code: %u\r\n Raw: %u"),
(bool) (flags & 0x01),
(bool) (flags & 0x02),
(bool) (flags & 0x04),
(bool) (flags & 0x08),
(bool) (flags & 0x10),
(bool) (flags & 0x20),
(bool) (flags & 0x40),
(bool) (flags & 0x80),
response & 0xFF,
response
);*/
return true;
}
/**
* @brief Set the Master Config
* From slave member id code:
* id: slave.memberIdCode & 0xFF,
* flags: (slave.memberIdCode & 0xFFFF) >> 8
* @param id
* @param flags
* @param force
* @return true
* @return false
*/
bool setMasterConfig(uint8_t id, uint8_t flags, bool force = false) {
//uint8_t configId = settings.opentherm.memberIdCode & 0xFF;
//uint8_t configFlags = (settings.opentherm.memberIdCode & 0xFFFF) >> 8;
vars.parameters.masterMemberId = (force || id || settings.opentherm.memberIdCode > 65535)
? id
: vars.parameters.slaveMemberId;
vars.parameters.masterFlags = (force || flags || settings.opentherm.memberIdCode > 65535)
? flags
: vars.parameters.slaveFlags;
unsigned int request = (unsigned int) vars.parameters.masterMemberId | (unsigned int) vars.parameters.masterFlags << 8;
// if empty request
if (!request) {
return true;
}
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::WRITE_DATA,
OpenThermMessageID::MConfigMMemberIDcode,
request
));
return CustomOpenTherm::isValidResponse(response);
}
bool setMaxModulationLevel(byte value) {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MaxRelModLevelSetting, (unsigned int)(value * 256)));
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::WRITE_DATA,
OpenThermMessageID::MaxRelModLevelSetting,
this->instance->toF88(value)
));
return ot->isValidResponse(response);
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.parameters.maxModulation = this->instance->fromF88(response);
return true;
}
bool setOpenThermVersionMaster() {
unsigned long response;
response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::OpenThermVersionSlave, 0));
if (!ot->isValidResponse(response)) {
return false;
}
// INFO_F("Opentherm version slave: %f\n", ot->getFloat(response));
bool updateSlaveOtVersion() {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::OpenThermVersionSlave,
0
));
response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE_DATA, OpenThermMessageID::OpenThermVersionMaster, response));
if (!ot->isValidResponse(response)) {
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
// INFO_F("Opentherm version master: %f\n", ot->getFloat(response));
vars.parameters.slaveOtVersion = CustomOpenTherm::getFloat(response);
return true;
}
bool setMasterOtVersion(float version) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::WRITE_DATA,
OpenThermMessageID::OpenThermVersionMaster,
this->instance->toF88(version)
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.parameters.masterOtVersion = this->instance->fromF88(response);
return true;
}
bool updateMasterParameters() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MasterVersion, 0x013F));
if (!ot->isValidResponse(response)) {
bool updateSlaveVersion() {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::SlaveVersion,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.parameters.masterType = (response & 0xFFFF) >> 8;
vars.parameters.masterVersion = response & 0xFF;
return true;
}
bool updateSlaveParameters() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SlaveVersion, 0));
if (!ot->isValidResponse(response)) {
return false;
}
vars.parameters.slaveType = (response & 0xFFFF) >> 8;
vars.parameters.slaveVersion = response & 0xFF;
vars.parameters.slaveType = (response & 0xFFFF) >> 8;
return true;
}
bool setMasterVersion(uint8_t version, uint8_t type) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::WRITE_DATA,
OpenThermMessageID::MasterVersion,
(unsigned int) version | (unsigned int) type << 8
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.parameters.masterVersion = response & 0xFF;
vars.parameters.masterType = (response & 0xFFFF) >> 8;
return true;
}
bool updateMinMaxDhwTemp() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0));
if (!ot->isValidResponse(response)) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::TdhwSetUBTdhwSetLB,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
@@ -316,8 +608,13 @@ protected:
}
bool updateMinMaxHeatingTemp() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0));
if (!ot->isValidResponse(response)) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::MaxTSetUBMaxTSetLB,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
@@ -327,63 +624,125 @@ protected:
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
vars.parameters.heatingMinTemp = minTemp;
vars.parameters.heatingMaxTemp = maxTemp;
return true;
}
return false;
}
bool setMaxHeatingTemp(byte value) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermMessageType::WRITE_DATA,
OpenThermMessageID::MaxTSet,
CustomOpenTherm::temperatureToData(value)
));
return CustomOpenTherm::isValidResponse(response);
}
bool updateOutsideTemp() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0));
if (!ot->isValidResponse(response)) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::Toutside,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.temperatures.outdoor = ot->getFloat(response);
vars.temperatures.outdoor = CustomOpenTherm::getFloat(response) + settings.sensors.outdoor.offset;
return true;
}
bool updateHeatingTemp() {
unsigned long response = ot->sendRequest(ot->buildGetBoilerTemperatureRequest());
if (!ot->isValidResponse(response)) {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Tboiler,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.temperatures.heating = ot->getFloat(response);
float value = CustomOpenTherm::getFloat(response);
if (value <= 0) {
return false;
}
vars.temperatures.heating = value;
return true;
}
bool updateDHWTemp() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ, OpenThermMessageID::Tdhw, 0));
if (!ot->isValidResponse(response)) {
bool updateDhwTemp() {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Tdhw,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.temperatures.dhw = ot->getFloat(response);
float value = CustomOpenTherm::getFloat(response);
if (value <= 0) {
return false;
}
vars.temperatures.dhw = value;
return true;
}
bool updateDhwFlowRate() {
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::DHWFlowRate,
0
));
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
float value = CustomOpenTherm::getFloat(response);
if (value > 16 && this->dhwFlowRateMultiplier != 10) {
this->dhwFlowRateMultiplier = 10;
}
vars.sensors.dhwFlowRate = this->dhwFlowRateMultiplier == 1 ? value : value / this->dhwFlowRateMultiplier;
return true;
}
bool updateFaultCode() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0));
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::ASFflags,
0
));
if (!ot->isValidResponse(response)) {
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.states.faultCode = response & 0xFF;
vars.sensors.faultCode = response & 0xFF;
return true;
}
bool updateModulationLevel() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0));
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::RelModLevel,
0
));
if (!ot->isValidResponse(response)) {
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
float modulation = ot->f88(response);
float modulation = this->instance->fromF88(response);
if (!vars.states.flame) {
vars.sensors.modulation = 0;
} else {
@@ -394,13 +753,22 @@ protected:
}
bool updatePressure() {
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0));
unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
OpenThermMessageID::CHPressure,
0
));
if (!ot->isValidResponse(response)) {
if (!CustomOpenTherm::isValidResponse(response)) {
return false;
}
vars.sensors.pressure = ot->getFloat(response);
float value = CustomOpenTherm::getFloat(response);
if (value > 5 && this->pressureMultiplier != 10) {
this->pressureMultiplier = 10;
}
vars.sensors.pressure = this->pressureMultiplier == 1 ? value : value / this->pressureMultiplier;
return true;
}
};

647
src/PortalTask.h Normal file
View File

@@ -0,0 +1,647 @@
#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));
// restart
this->webServer->on("/restart.html", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
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->isNeedAuth() && !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->isNeedAuth() && !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->isNeedAuth() && !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->isNeedAuth() && !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->isNeedAuth()) {
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->isNeedAuth()) {
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->isNeedAuth()) {
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->isNeedAuth()) {
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();
if (changed) {
this->webServer->send(201);
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();
} else {
this->webServer->send(200);
}
});
this->webServer->on("/api/network/status", HTTP_GET, [this]() {
bool isConnected = network->isConnected();
JsonDocument doc;
doc["hostname"] = networkSettings.hostname;
doc["mac"] = network->getStaMac();
doc["isConnected"] = isConnected;
doc["ssid"] = network->getStaSsid();
doc["signalQuality"] = isConnected ? Network::Manager::rssiToSignalQuality(network->getRssi()) : 0;
doc["channel"] = isConnected ? network->getStaChannel() : 0;
doc["ip"] = isConnected ? network->getStaIp().toString() : "";
doc["subnet"] = isConnected ? network->getStaSubnet().toString() : "";
doc["gateway"] = isConnected ? network->getStaGateway().toString() : "";
doc["dns"] = isConnected ? network->getStaDns().toString() : "";
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/network/scan", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->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) {
WiFi.scanNetworks(true, true);
}
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->isNeedAuth()) {
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->isNeedAuth()) {
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();
if (changed) {
fsSettings.update();
this->webServer->send(201);
} else {
this->webServer->send(200);
}
});
// vars
this->webServer->on("/api/vars", HTTP_GET, [this]() {
JsonDocument doc;
varsToJson(vars, doc);
doc["system"]["version"] = PROJECT_VERSION;
doc["system"]["buildDate"] = __DATE__ " " __TIME__;
doc["system"]["uptime"] = millis() / 1000ul;
doc["system"]["totalHeap"] = getTotalHeap();
doc["system"]["freeHeap"] = getFreeHeap();
doc["system"]["minFreeHeap"] = getFreeHeap(true);
doc["system"]["maxFreeBlockHeap"] = getMaxFreeBlockHeap();
doc["system"]["minMaxFreeBlockHeap"] = getMaxFreeBlockHeap(true);
doc["system"]["resetReason"] = getResetReason();
doc["system"]["mqttConnected"] = tMqtt->isConnected();
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/vars", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (!this->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();
if (changed) {
this->webServer->send(201);
} else {
this->webServer->send(200);
}
});
// 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 isNeedAuth() {
return !network->isApEnabled() && settings.portal.useAuth && 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

@@ -3,12 +3,13 @@
#include <PIDtuner.h>
Equitherm etRegulator;
GyverPID pidRegulator(0, 0, 0, 10000);
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;
@@ -18,8 +19,18 @@ protected:
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;
@@ -27,7 +38,7 @@ protected:
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();
@@ -37,7 +48,7 @@ protected:
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 = getTuningModeTemp();
@@ -48,10 +59,10 @@ protected:
}
if (!vars.tuning.enable) {
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || (settings.equitherm.enable && settings.pid.enable))) {
if (settings.heating.turbo && (fabs(settings.heating.target - vars.temperatures.indoor) < 1 || !settings.heating.enable || (settings.equitherm.enable && settings.pid.enable))) {
settings.heating.turbo = false;
INFO("[REGULATOR] Turbo mode auto disabled");
Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled"));
}
newTemp = getNormalModeTemp();
@@ -59,8 +70,8 @@ protected:
}
// Ограничиваем, если до этого не ограничило
if (newTemp < vars.parameters.heatingMinTemp || newTemp > vars.parameters.heatingMaxTemp) {
newTemp = constrain(newTemp, vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
if (newTemp < settings.heating.minTemp || newTemp > settings.heating.maxTemp) {
newTemp = constrain(newTemp, settings.heating.minTemp, settings.heating.maxTemp);
}
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
@@ -73,19 +84,40 @@ protected:
float newTemp = 0;
// if use equitherm
if (settings.emergency.useEquitherm && settings.outdoorTempSource != 1) {
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != SensorType::MANUAL) {
float etResult = getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp);
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
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: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
} else {
newTemp += prevEtResult;
}
} else if(settings.emergency.usePid && settings.sensors.indoor.type != SensorType::MANUAL) {
if (vars.parameters.heatingEnabled) {
float pidResult = getPidTemp(
settings.heating.minTemp,
settings.heating.maxTemp
);
if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) {
prevPidResult = pidResult;
newTemp += pidResult;
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New emergency result: %hhu (%.2f)"), (uint8_t) round(pidResult), pidResult);
} else {
newTemp += prevPidResult;
}
} else if (!vars.parameters.heatingEnabled && prevPidResult != 0) {
newTemp += prevPidResult;
}
} else {
// default temp, manual mode
newTemp = settings.emergency.target;
@@ -99,23 +131,23 @@ protected:
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
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) {
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) {
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: %hhu (%.2f)"), (uint8_t) round(etResult), etResult);
} else {
newTemp += prevEtResult;
@@ -124,17 +156,21 @@ protected:
// if use pid
if (settings.pid.enable) {
float pidResult = getPidTemp(
settings.equitherm.enable ? -30 : vars.parameters.heatingMinTemp,
settings.equitherm.enable ? 30 : vars.parameters.heatingMaxTemp
);
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.0001 >= 0.5) {
prevPidResult = pidResult;
newTemp += pidResult;
INFO_F("[REGULATOR][PID] New result: %u (%f) \n", (int)round(pidResult), pidResult);
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("New result: %hhd (%.2f)"), (int8_t) round(pidResult), pidResult);
} else {
newTemp += prevPidResult;
}
} else {
newTemp += prevPidResult;
}
@@ -145,7 +181,9 @@ protected:
newTemp = settings.heating.target;
}
return round(newTemp);
newTemp = round(newTemp);
newTemp = constrain(newTemp, 0, 100);
return newTemp;
}
byte getTuningModeTemp() {
@@ -157,7 +195,7 @@ protected:
tunerInit = false;
tunerRegulator = 0;
tunerState = 0;
INFO(F("[REGULATOR][TUNING] Stopped"));
Log.sinfoln("REGULATOR.TUNING", F("Stopped"));
}
if (!vars.tuning.enable) {
@@ -167,18 +205,20 @@ protected:
if (vars.tuning.regulator == 0) {
// @TODO дописать
INFO(F("[REGULATOR][TUNING][EQUITHERM] Not implemented"));
Log.sinfoln("REGULATOR.TUNING.EQUITHERM", F("Not implemented"));
return 0;
} else if (vars.tuning.regulator == 1) {
// PID tuner
float defaultTemp = settings.equitherm.enable
? getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp)
? getEquithermTemp(settings.heating.minTemp, settings.heating.maxTemp)
: settings.heating.target;
if (tunerInit && pidTuner.getState() == 3) {
INFO(F("[REGULATOR][TUNING][PID] Finished"));
pidTuner.debugText(&INFO_STREAM);
Log.sinfoln("REGULATOR.TUNING.PID", F("Finished"));
for (Stream* stream : Log.getStreams()) {
pidTuner.debugText(stream);
}
pidTuner.reset();
tunerInit = false;
@@ -186,7 +226,7 @@ protected:
tunerState = 0;
if (pidTuner.getAccuracy() < 90) {
WARN(F("[REGULATOR][TUNING][PID] Bad result, try again..."));
Log.swarningln("REGULATOR.TUNING.PID", F("Bad result, try again..."));
} else {
settings.pid.p_factor = pidTuner.getPID_p();
@@ -198,7 +238,7 @@ protected:
}
if (!tunerInit) {
INFO(F("[REGULATOR][TUNING][PID] Start..."));
Log.sinfoln("REGULATOR.TUNING.PID", F("Start..."));
float step;
if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) {
@@ -208,7 +248,7 @@ protected:
}
float startTemp = step;
INFO_F("[REGULATOR][TUNING][PID] Started. Start value: %f, step: %f \n", startTemp, step);
Log.sinfoln("REGULATOR.TUNING.PID", F("Started. Start value: %f, step: %f"), startTemp, step);
pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000);
tunerInit = true;
tunerRegulator = 1;
@@ -218,8 +258,11 @@ protected:
pidTuner.compute();
if (tunerState > 0 && pidTuner.getState() != tunerState) {
INFO(F("[REGULATOR][TUNING][PID] Log:"));
pidTuner.debugText(&INFO_STREAM);
Log.sinfoln("REGULATOR.TUNING.PID", F("Log:"));
for (Stream* stream : Log.getStreams()) {
pidTuner.debugText(stream);
}
tunerState = pidTuner.getState();
}
@@ -266,6 +309,7 @@ protected:
pidRegulator.Kd = settings.pid.d_factor;
pidRegulator.setLimits(minTemp, maxTemp);
pidRegulator.setDt(settings.pid.dt * 1000u);
pidRegulator.input = vars.temperatures.indoor;
pidRegulator.setpoint = settings.heating.target;

View File

@@ -1,45 +1,341 @@
#include <microDS18B20.h>
#include <OneWire.h>
#include <DallasTemperature.h>
MicroDS18B20<DS18B20_PIN> outdoorSensor;
#if USE_BLE
#include <NimBLEDevice.h>
#endif
class SensorsTask: public LeanTask {
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 = nullptr;
OneWire* oneWireIndoorSensor = nullptr;
DallasTemperature* outdoorSensor = nullptr;
DallasTemperature* indoorSensor = nullptr;
bool initOutdoorSensor = false;
unsigned long startOutdoorConversionTime = 0;
float filteredOutdoorTemp = 0;
bool emptyOutdoorTemp = true;
void setup() {}
bool initIndoorSensor = false;
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 loop() {
// DS18B20 sensor
if (outdoorSensor.online()) {
if (outdoorSensor.readTemp()) {
float rawTemp = outdoorSensor.getTemp();
DEBUG_F("[SENSORS][DS18B20] Raw temp: %f \n", rawTemp);
bool indoorTempUpdated = false;
bool outdoorTempUpdated = false;
if (emptyOutdoorTemp) {
filteredOutdoorTemp = rawTemp;
emptyOutdoorTemp = false;
if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
outdoorTemperatureSensor();
outdoorTempUpdated = true;
}
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 && fabs(vars.temperatures.outdoor - this->filteredOutdoorTemp) > 0.099) {
vars.temperatures.outdoor = this->filteredOutdoorTemp + settings.sensors.outdoor.offset;
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor);
}
if (indoorTempUpdated && fabs(vars.temperatures.indoor - this->filteredIndoorTemp) > 0.099) {
vars.temperatures.indoor = this->filteredIndoorTemp + settings.sensors.indoor.offset;
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.01);
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
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 {
filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * OUTDOOR_SENSOR_FILTER_K;
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
}
}
}
filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100;
// 0x2A1F - Notify temperature x0.1C (atc1441/pvvx)
if (!this->initBleNotify) {
NimBLEUUID charUUID((uint16_t) 0x2A1F);
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
if (fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099) {
vars.temperatures.outdoor = filteredOutdoorTemp;
INFO_F("[SENSORS][DS18B20] New temp: %f \n", filteredOutdoorTemp);
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) {
if (length != 2) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data"));
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1);
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) {
this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
}
}
}
if (!this->initBleNotify) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics"));
pBleClient->disconnect();
}
}
#endif
void outdoorTemperatureSensor() {
if (!this->initOutdoorSensor) {
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Starting on gpio %hhu..."), settings.sensors.outdoor.gpio);
this->oneWireOutdoorSensor->begin(settings.sensors.outdoor.gpio);
this->outdoorSensor->begin();
Log.straceln(
FPSTR(L_SENSORS_OUTDOOR),
F("Devices on bus: %hhu, DS18* devices: %hhu"),
this->outdoorSensor->getDeviceCount(),
this->outdoorSensor->getDS18Count()
);
if (this->outdoorSensor->getDeviceCount() > 0) {
this->initOutdoorSensor = true;
this->outdoorSensor->setResolution(12);
this->outdoorSensor->requestTemperatures();
this->startOutdoorConversionTime = millis();
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started"));
} else {
ERROR("[SENSORS][DS18B20] Invalid data from sensor");
return;
}
}
unsigned long estimateConversionTime = millis() - this->startOutdoorConversionTime;
if (estimateConversionTime < this->outdoorSensor->millisToWaitForConversion()) {
return;
}
bool completed = this->outdoorSensor->isConversionComplete();
if (!completed && estimateConversionTime >= 1000) {
this->initOutdoorSensor = false;
Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (no response)"));
}
if (!completed) {
return;
}
float rawTemp = 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;
}
outdoorSensor.requestTemp();
this->filteredOutdoorTemp = floor(this->filteredOutdoorTemp * 100) / 100;
this->outdoorSensor->requestTemperatures();
this->startOutdoorConversionTime = millis();
}
}
void indoorTemperatureSensor() {
if (!this->initIndoorSensor) {
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Starting on gpio %hhu..."), settings.sensors.indoor.gpio);
this->oneWireIndoorSensor->begin(settings.sensors.indoor.gpio);
this->indoorSensor->begin();
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Devices on bus: %hhu, DS18* devices: %hhu"),
this->indoorSensor->getDeviceCount(),
this->indoorSensor->getDS18Count()
);
if (this->indoorSensor->getDeviceCount() > 0) {
this->initIndoorSensor = true;
this->indoorSensor->setResolution(12);
this->indoorSensor->requestTemperatures();
this->startIndoorConversionTime = millis();
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started"));
} else {
return;
}
}
unsigned long estimateConversionTime = millis() - 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 {
ERROR("[SENSORS][DS18B20] Failed to connect to sensor");
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,28 +1,67 @@
struct Settings {
bool debug = false;
// 0 - boiler, 1 - manual, 2 - ds18b20
byte outdoorTempSource = 0;
char hostname[80] = "opentherm";
struct NetworkSettings {
char hostname[25] = DEFAULT_HOSTNAME;
bool useDhcp = true;
struct {
byte inPin = 5;
byte outPin = 4;
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;
bool useSerial = USE_SERIAL;
bool useTelnet = USE_TELNET;
} system;
struct {
bool useAuth = false;
char login[13] = DEFAULT_PORTAL_LOGIN;
char password[33] = DEFAULT_PORTAL_PASSWORD;
} portal;
struct {
byte inGpio = DEFAULT_OT_IN_GPIO;
byte outGpio = DEFAULT_OT_OUT_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;
} opentherm;
struct {
char server[80];
int port = 1883;
char user[32];
char password[32];
char prefix[80] = "opentherm";
unsigned int interval = 5000;
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;
} mqtt;
struct {
bool enable = true;
float target = 40.0f;
bool useEquitherm = false;
bool usePid = false;
} emergency;
struct {
@@ -30,18 +69,26 @@ struct Settings {
bool turbo = false;
float target = 40.0f;
float hysteresis = 0.5f;
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 = 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 d_factor = 0;
float p_factor = 50;
float i_factor = 0.006f;
float d_factor = 10000;
unsigned short dt = 180;
byte minTemp = 0;
byte maxTemp = DEFAULT_HEATING_MAX_TEMP;
} pid;
struct {
@@ -51,6 +98,30 @@ struct Settings {
float t_factor = 2.0f;
} equitherm;
struct {
struct {
SensorType type = SensorType::BOILER;
byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO;
float offset = 0.0f;
} outdoor;
struct {
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 {
@@ -67,13 +138,15 @@ struct Variables {
bool flame = false;
bool fault = false;
bool diagnostic = false;
byte faultCode = 0;
int8_t rssi = 0;
bool externalPump = false;
} states;
struct {
float modulation = 0.0f;
float pressure = 0.0f;
float dhwFlowRate = 0.0f;
byte faultCode = 0;
int8_t rssi = 0;
} sensors;
struct {
@@ -84,15 +157,29 @@ struct Variables {
} temperatures;
struct {
byte heatingMinTemp = 20;
byte heatingMaxTemp = 90;
byte heatingSetpoint = 0.0f;
byte dhwMinTemp = 30;
byte dhwMaxTemp = 60;
uint8_t slaveMemberIdCode;
uint8_t slaveType;
uint8_t slaveVersion;
uint8_t masterType;
uint8_t masterVersion;
bool heatingEnabled = false;
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
byte heatingSetpoint = 0;
unsigned long extPumpLastEnableTime = 0;
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
byte maxModulation = 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,129 +0,0 @@
#include <WiFiManager.h>
// Wifimanager
WiFiManager wm;
WiFiManagerParameter* wmHostname;
WiFiManagerParameter* wmOtInPin;
WiFiManagerParameter* wmOtOutPin;
WiFiManagerParameter* wmOtMemberIdCode;
WiFiManagerParameter* wmMqttServer;
WiFiManagerParameter* wmMqttPort;
WiFiManagerParameter* wmMqttUser;
WiFiManagerParameter* wmMqttPassword;
WiFiManagerParameter* wmMqttPrefix;
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);
sprintf(buffer, "%d", settings.opentherm.inPin);
wmOtInPin = new WiFiManagerParameter("ot_in_pin", "Opentherm pin IN", buffer, 1);
wm.addParameter(wmOtInPin);
sprintf(buffer, "%d", settings.opentherm.outPin);
wmOtOutPin = new WiFiManagerParameter("ot_out_pin", "Opentherm pin OUT", buffer, 1);
wm.addParameter(wmOtOutPin);
sprintf(buffer, "%d", settings.opentherm.memberIdCode);
wmOtMemberIdCode = new WiFiManagerParameter("ot_member_id_code", "Opentherm member id code", buffer, 5);
wm.addParameter(wmOtMemberIdCode);
wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80);
wm.addParameter(wmMqttServer);
sprintf(buffer, "%d", settings.mqtt.port);
wmMqttPort = new WiFiManagerParameter("mqtt_port", "MQTT port", buffer, 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);
wm.addParameter(wmMqttPassword);
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32);
wm.addParameter(wmMqttPrefix);
//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());
settings.opentherm.inPin = atoi(wmOtInPin->getValue());
settings.opentherm.outPin = atoi(wmOtOutPin->getValue());
settings.opentherm.memberIdCode = atoi(wmOtMemberIdCode->getValue());
strcpy(settings.mqtt.server, wmMqttServer->getValue());
settings.mqtt.port = atoi(wmMqttPort->getValue());
strcpy(settings.mqtt.user, wmMqttUser->getValue());
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
INFO_F(
"New settings:\r\n"
" Hostname: %s\r\n"
" OT in pin: %d"
" OT out pin: %d"
" OT member id code: %d"
" Mqtt server: %s:%d\r\n"
" Mqtt user: %s\r\n"
" Mqtt pass: %s\r\n",
settings.hostname,
settings.opentherm.inPin,
settings.opentherm.outPin,
settings.opentherm.memberIdCode,
settings.mqtt.server,
settings.mqtt.port,
settings.mqtt.user,
settings.mqtt.password
);
eeSettings.updateNow();
INFO(F("Settings saved"));
}
bool connected = false;
};

View File

@@ -1,46 +1,121 @@
#define OT_GATEWAY_VERSION "1.2.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.17"
#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 DS18B20_PIN 2
#define DS18B20_INTERVAL 5000
#define OUTDOOR_SENSOR_FILTER_K 0.15
#define DS_CHECK_CRC true
#define DS_CRC_USE_TABLE true
#define LED_STATUS_PIN 13
#define LED_OT_RX_PIN 15
#define EXT_SENSORS_INTERVAL 5000
#define EXT_SENSORS_FILTER_K 0.15
#define CONFIG_URL "http://%s/"
#define SETTINGS_VALID_VALUE "stvalid" // only 8 chars!
#define GPIO_IS_NOT_CONFIGURED 0xff
#define DEFAULT_HEATING_MIN_TEMP 20
#define DEFAULT_HEATING_MAX_TEMP 90
#define DEFAULT_DHW_MIN_TEMP 30
#define DEFAULT_DHW_MAX_TEMP 60
#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_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_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
};
char buffer[255];

View File

@@ -1,71 +1,168 @@
#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(5000);
#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");
} 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.useSerial) {
Log.clearStreams();
Serial.end();
}
if (settings.system.useTelnet) {
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(false, DS18B20_INTERVAL);
tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL);
Scheduler.start(tSensors);
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";

1063
src/utils.h Normal file

File diff suppressed because it is too large Load Diff

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

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

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

@@ -0,0 +1,680 @@
function setupForm(formSelector) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
form.querySelectorAll('input').forEach(item => {
item.addEventListener('change', (e) => {
e.target.setAttribute('aria-invalid', !e.target.checkValidity());
})
});
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (response) => {
if (button) {
button.textContent = 'Saved';
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
try {
let fd = new FormData(form);
let checkboxes = form.querySelectorAll('input[type="checkbox"]');
for (let checkbox of checkboxes) {
fd.append(checkbox.getAttribute('name'), checkbox.checked);
}
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: form2json(fd)
});
if (response.ok) {
onSuccess(response);
} else {
onFailed(response);
}
} catch (err) {
onFailed(false);
}
});
}
function setupNetworkScanForm(formSelector, tableSelector) {
const form = document.querySelector(formSelector);
if (!form) {
console.error("form not found");
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.innerHTML;
}
const onSubmitFn = async (event) => {
if (event) {
event.preventDefault();
}
if (button) {
button.innerHTML = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
let table = document.querySelector(tableSelector);
if (!table) {
console.error("table not found");
return;
}
const onSuccess = async (response) => {
let result = await response.json();
console.log('networks: ', result);
let tbody = table.querySelector('tbody');
if (!tbody) {
tbody = table.createTBody();
}
while (tbody.rows.length > 0) {
tbody.rows[0].remove();
}
for (let i = 0; i < result.length; i++) {
let row = tbody.insertRow(-1);
row.classList.add("network");
row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid);
row.onclick = function () {
const input = document.querySelector('input.sta-ssid');
const ssid = this.getAttribute('data-ssid');
if (!input || !ssid) {
return;
}
input.value = ssid;
input.focus();
};
row.insertCell().textContent = "#" + (i + 1);
row.insertCell().innerHTML = result[i].hidden ? '<i>Hidden</i>' : result[i].ssid;
const signalCell = row.insertCell();
const signalElement = document.createElement("kbd");
signalElement.textContent = result[i].signalQuality + "%";
if (result[i].signalQuality > 60) {
signalElement.classList.add('greatSignal');
} else if (result[i].signalQuality > 40) {
signalElement.classList.add('normalSignal');
} else {
signalElement.classList.add('badSignal');
}
signalCell.appendChild(signalElement);
}
if (button) {
button.innerHTML = defaultText;
button.removeAttribute('disabled');
button.removeAttribute('aria-busy');
}
};
const onFailed = async (response) => {
table.classList.remove('hidden');
if (button) {
button.innerHTML = defaultText;
button.removeAttribute('disabled');
button.removeAttribute('aria-busy');
}
};
let attempts = 6;
let attemptFn = async () => {
attempts--;
try {
let response = await fetch(url, { cache: 'no-cache' });
if (response.status == 200) {
await onSuccess(response);
} else if (attempts <= 0) {
await onFailed(response);
} else {
setTimeout(attemptFn, 5000);
}
} catch (err) {
if (attempts <= 0) {
onFailed(err);
} else {
setTimeout(attemptFn, 10000);
}
}
};
attemptFn();
};
form.addEventListener('submit', onSubmitFn);
onSubmitFn();
}
function setupRestoreBackupForm(formSelector) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Please wait...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
const onSuccess = (response) => {
if (button) {
button.textContent = 'Restored';
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const files = form.querySelector('#restore-file').files;
if (files.length <= 0) {
onFailed(false);
return;
}
let reader = new FileReader();
reader.readAsText(files[0]);
reader.onload = async function () {
try {
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: reader.result
});
if (response.ok) {
onSuccess(response);
} else {
onFailed(response);
}
} catch (err) {
onFailed(false);
}
};
reader.onerror = function () {
console.log(reader.error);
};
});
}
function setupUpgradeForm(formSelector) {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
if (button) {
defaultText = button.textContent;
}
const statusToText = (status) => {
switch (status) {
case 0:
return "None";
case 1:
return "No file";
case 2:
return "Success";
case 3:
return "Prohibited";
case 4:
return "Aborted";
case 5:
return "Error on start";
case 6:
return "Error on write";
case 7:
return "Error on finish";
default:
return "Unknown";
}
};
const onResult = async (response) => {
if (!response) {
return;
}
const result = await response.json();
let resItem = form.querySelector('.upgrade-firmware-result');
if (resItem && result.firmware.status > 1) {
resItem.textContent = statusToText(result.firmware.status);
resItem.classList.remove('hidden');
if (result.firmware.status == 2) {
resItem.classList.remove('failed');
resItem.classList.add('success');
} else {
resItem.classList.remove('success');
resItem.classList.add('failed');
if (result.firmware.error != "") {
resItem.textContent += ": " + result.firmware.error;
}
}
}
resItem = form.querySelector('.upgrade-filesystem-result');
if (resItem && result.filesystem.status > 1) {
resItem.textContent = statusToText(result.filesystem.status);
resItem.classList.remove('hidden');
if (result.filesystem.status == 2) {
resItem.classList.remove('failed');
resItem.classList.add('success');
} else {
resItem.classList.remove('success');
resItem.classList.add('failed');
if (result.filesystem.error != "") {
resItem.textContent += ": " + result.filesystem.error;
}
}
}
};
const onSuccess = (response) => {
onResult(response);
if (button) {
button.textContent = defaultText;
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const onFailed = (response) => {
if (button) {
button.textContent = 'Error';
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
button.textContent = 'Uploading...';
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
try {
let fd = new FormData(form);
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
body: fd
});
if (response.status >= 200 && response.status < 500) {
onSuccess(response);
} else {
onFailed(response);
}
} catch (err) {
onFailed(false);
}
});
}
async function loadNetworkStatus() {
let response = await fetch('/api/network/status', { cache: 'no-cache' });
let result = await response.json();
setValue('.network-hostname', result.hostname);
setValue('.network-mac', result.mac);
setState('.network-connected', result.isConnected);
setValue('.network-ssid', result.ssid);
setValue('.network-signal', result.signalQuality);
setValue('.network-ip', result.ip);
setValue('.network-subnet', result.subnet);
setValue('.network-gateway', result.gateway);
setValue('.network-dns', result.dns);
setBusy('.main-busy', '.main-table', false);
}
async function loadNetworkSettings() {
let response = await fetch('/api/network/settings', { cache: 'no-cache' });
let result = await response.json();
setInputValue('.network-hostname', result.hostname);
setCheckboxValue('.network-use-dhcp', result.useDhcp);
setInputValue('.network-static-ip', result.staticConfig.ip);
setInputValue('.network-static-gateway', result.staticConfig.gateway);
setInputValue('.network-static-subnet', result.staticConfig.subnet);
setInputValue('.network-static-dns', result.staticConfig.dns);
setBusy('#network-settings-busy', '#network-settings', false);
setInputValue('.sta-ssid', result.sta.ssid);
setInputValue('.sta-password', result.sta.password);
setInputValue('.sta-channel', result.sta.channel);
setBusy('#sta-settings-busy', '#sta-settings', false);
setInputValue('.ap-ssid', result.ap.ssid);
setInputValue('.ap-password', result.ap.password);
setInputValue('.ap-channel', result.ap.channel);
setBusy('#ap-settings-busy', '#ap-settings', false);
}
async function loadSettings() {
let response = await fetch('/api/settings', { cache: 'no-cache' });
let result = await response.json();
setCheckboxValue('.system-debug', result.system.debug);
setCheckboxValue('.system-use-serial', result.system.useSerial);
setCheckboxValue('.system-use-telnet', result.system.useTelnet);
setBusy('#system-settings-busy', '#system-settings', false);
setCheckboxValue('.portal-use-auth', result.portal.useAuth);
setInputValue('.portal-login', result.portal.login);
setInputValue('.portal-password', result.portal.password);
setBusy('#portal-settings-busy', '#portal-settings', false);
setInputValue('.opentherm-in-gpio', result.opentherm.inGpio < 255 ? result.opentherm.inGpio : '');
setInputValue('.opentherm-out-gpio', result.opentherm.outGpio < 255 ? result.opentherm.outGpio : '');
setInputValue('.opentherm-member-id-code', result.opentherm.memberIdCode);
setCheckboxValue('.opentherm-dhw-present', result.opentherm.dhwPresent);
setCheckboxValue('.opentherm-sw-mode', result.opentherm.summerWinterMode);
setCheckboxValue('.opentherm-heating-ch2-enabled', result.opentherm.heatingCh2Enabled);
setCheckboxValue('.opentherm-heating-ch1-to-ch2', result.opentherm.heatingCh1ToCh2);
setCheckboxValue('.opentherm-dhw-to-ch2', result.opentherm.dhwToCh2);
setCheckboxValue('.opentherm-dhw-blocking', result.opentherm.dhwBlocking);
setCheckboxValue('.opentherm-sync-modulation-with-heating', result.opentherm.modulationSyncWithHeating);
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
setInputValue('.mqtt-server', result.mqtt.server);
setInputValue('.mqtt-port', result.mqtt.port);
setInputValue('.mqtt-user', result.mqtt.user);
setInputValue('.mqtt-password', result.mqtt.password);
setInputValue('.mqtt-prefix', result.mqtt.prefix);
setInputValue('.mqtt-interval', result.mqtt.interval);
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
setRadioValue('.outdoor-sensor-type', result.sensors.outdoor.type);
setInputValue('.outdoor-sensor-gpio', result.sensors.outdoor.gpio < 255 ? result.sensors.outdoor.gpio : '');
setInputValue('.outdoor-sensor-offset', result.sensors.outdoor.offset);
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
setRadioValue('.indoor-sensor-type', result.sensors.indoor.type);
setInputValue('.indoor-sensor-gpio', result.sensors.indoor.gpio < 255 ? result.sensors.indoor.gpio : '');
setInputValue('.indoor-sensor-offset', result.sensors.indoor.offset);
setInputValue('.indoor-sensor-ble-addresss', result.sensors.indoor.bleAddresss);
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
setCheckboxValue('.extpump-use', result.externalPump.use);
setInputValue('.extpump-gpio', result.externalPump.gpio < 255 ? result.externalPump.gpio : '');
setInputValue('.extpump-pc-time', result.externalPump.postCirculationTime);
setInputValue('.extpump-as-interval', result.externalPump.antiStuckInterval);
setInputValue('.extpump-as-time', result.externalPump.antiStuckTime);
setBusy('#extpump-settings-busy', '#extpump-settings', false);
}
async function loadVars() {
let response = await fetch('/api/vars');
let result = await response.json();
setState('.ot-connected', result.states.otStatus);
setState('.ot-emergency', result.states.emergency);
setState('.ot-heating', result.states.heating);
setState('.ot-dhw', result.states.dhw);
setState('.ot-flame', result.states.flame);
setState('.ot-fault', result.states.fault);
setState('.ot-diagnostic', result.states.diagnostic);
setState('.ot-external-pump', result.states.externalPump);
setValue('.ot-modulation', result.sensors.modulation);
setValue('.ot-pressure', result.sensors.pressure);
setValue('.ot-dhw-flow-rate', result.sensors.dhwFlowRate);
setValue('.ot-fault-code', result.sensors.faultCode ? ("E" + result.sensors.faultCode) : "-");
setValue('.indoor-temp', result.temperatures.indoor);
setValue('.outdoor-temp', result.temperatures.outdoor);
setValue('.heating-temp', result.temperatures.heating);
setValue('.heating-setpoint-temp', result.parameters.heatingSetpoint);
setValue('.dhw-temp', result.temperatures.dhw);
setBusy('.ot-busy', '.ot-table', false);
setValue('.version', result.system.version);
setValue('.build-date', result.system.buildDate);
setValue('.uptime', result.system.uptime);
setValue('.uptime-days', Math.floor(result.system.uptime / 86400));
setValue('.uptime-hours', Math.floor(result.system.uptime % 86400 / 3600));
setValue('.uptime-min', Math.floor(result.system.uptime % 3600 / 60));
setValue('.uptime-sec', Math.floor(result.system.uptime % 60));
setValue('.total-heap', result.system.totalHeap);
setValue('.free-heap', result.system.freeHeap);
setValue('.min-free-heap', result.system.minFreeHeap);
setValue('.max-free-block-heap', result.system.maxFreeBlockHeap);
setValue('.min-max-free-block-heap', result.system.minMaxFreeBlockHeap);
setValue('.reset-reason', result.system.resetReason);
setState('.mqtt-connected', result.system.mqttConnected);
setBusy('.system-busy', '.system-table', false);
}
function setBusy(busySelector, contentSelector, value) {
let busy = document.querySelector(busySelector);
let content = document.querySelector(contentSelector);
if (!busy || !content) {
return;
}
if (!value) {
busy.classList.add('hidden');
content.classList.remove('hidden');
} else {
busy.classList.remove('hidden');
content.classList.add('hidden');
}
}
function setState(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.setAttribute('aria-invalid', !value);
}
function setValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.innerHTML = value;
}
function setCheckboxValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.checked = value;
}
function setRadioValue(selector, value) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.checked = item.value == value;
}
}
function setInputValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {
return;
}
item.value = value;
}
function form2json(data) {
let method = function (object, pair) {
let keys = pair[0].replace(/\]/g, '').split('[');
let key = keys[0];
let value = pair[1];
if (value === 'true' || value === 'false') {
value = value === 'true';
} else if (typeof (value) === 'string' && value.trim() !== '' && !isNaN(value)) {
value = parseFloat(value);
}
if (keys.length > 1) {
let i, x, segment;
let last = value;
let type = isNaN(keys[1]) ? {} : [];
value = segment = object[key] || type;
for (i = 1; i < keys.length; i++) {
x = keys[i];
if (i == keys.length - 1) {
if (Array.isArray(segment)) {
segment.push(last);
} else {
segment[x] = last;
}
} else if (segment[x] == undefined) {
segment[x] = isNaN(keys[i + 1]) ? {} : [];
}
segment = segment[x];
}
}
object[key] = value;
return object;
}
let object = Array.from(data).reduce(method, {});
return JSON.stringify(object);
}

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

34
tools/build.py Normal file
View File

@@ -0,0 +1,34 @@
import shutil
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 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.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"