mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-11 18:54:28 +05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b9ebeaa40 | ||
|
|
76eaec10ea | ||
|
|
21de692888 | ||
|
|
ce318b8cde | ||
|
|
77b0859cc8 | ||
|
|
3fcb17f2c3 | ||
|
|
6204b46c17 | ||
|
|
b5760eb314 | ||
|
|
eedbd7b80a | ||
|
|
e9bf4c4bd5 | ||
|
|
a255dda8dd | ||
|
|
6de6f7d138 | ||
|
|
5e8916b254 | ||
|
|
7db49350a2 | ||
|
|
8e2a70ec04 | ||
|
|
b2ff71c59a | ||
|
|
3660b89f7c | ||
|
|
5c0dfc544e | ||
|
|
5fba94312b | ||
|
|
62bea87f8a | ||
|
|
f52aa8e889 | ||
|
|
df8354866f | ||
|
|
6242db7a29 | ||
|
|
dc00fdcdb6 | ||
|
|
2615e9106e | ||
|
|
0f60a07a71 | ||
|
|
f8750373d4 | ||
|
|
d5a92c47c7 | ||
|
|
bc91168bbf | ||
|
|
96c1a187cd | ||
|
|
6d3172b73b | ||
|
|
fca6dc9393 | ||
|
|
b54ea9b745 | ||
|
|
5de3238f6f | ||
|
|
2270b12b36 | ||
|
|
fd4fd119da | ||
|
|
47849eab01 | ||
|
|
ef99d2af96 | ||
|
|
826581562a | ||
|
|
d10d44bd13 | ||
|
|
229628fdc5 | ||
|
|
b0e01afecb | ||
|
|
f544baee0a | ||
|
|
3ff68f544f |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
.vscode
|
||||
build/*
|
||||
129
README.md
129
README.md
@@ -7,20 +7,20 @@
|
||||
- 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](#compatible-temperature-sensors))
|
||||
- Emergency mode. If the Wi-Fi connection is lost or the gateway cannot connect to the MQTT server, the mode will turn on. This mode will automatically maintain the set temperature and prevent your home from freezing. In this mode it is also possible to use equithermal curves (weather-compensated control).
|
||||
- Automatic error reset (not with all boilers)
|
||||
- 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!
|
||||
@@ -28,52 +28,70 @@
|
||||

|
||||
|
||||
## Tested on
|
||||
| Boiler | Master Member ID |
|
||||
| --- | --- |
|
||||
| BAXI ECO Nova | default or 4 |
|
||||
| BAXI Ampera | 1028 |
|
||||
| Boiler | Master Member ID | Notes |
|
||||
| --- | --- | --- |
|
||||
| BAXI ECO Nova | default | Pressure sensor not supported, modulation level not stable |
|
||||
| BAXI Ampera | 1028 | Pressure sensor not supported, only heating (DHW not tested) |
|
||||
| [Remeha Calenta Ace 40C](https://github.com/Laxilef/OTGateway/issues/1#issuecomment-1726081554) | default | - |
|
||||
| [Baxi Nuvola DUO-TEC HT 16](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1751061488) | default | - |
|
||||
| [AEG GBA124](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1765857609) | default | Pressure sensor not supported |
|
||||
| [Ferroli DOMIcompact C 24](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1765310058)<br><sub>Board: MF08FA</sub> | 211 | Pressure sensor not supported |
|
||||
| [Thermet Ecocondens Silver 35kW](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1767026384) | default | Pressure sensor not supported |
|
||||
| [BAXI LUNA-3](https://github.com/Laxilef/OTGateway/issues/3#issuecomment-1794187178) | default | - |
|
||||
|
||||
|
||||
## PCB
|
||||
<img src="/assets/pcb.svg" width="25%" /> <img src="/assets/pcb_3d.png" width="30%" /> <img src="/assets/after_assembly.png" width="37%" />
|
||||
<img src="/assets/pcb.svg" width="27%" /> <img src="/assets/pcb_3d.png" width="30%" /> <img src="/assets/after_assembly.png" width="40%" />
|
||||
|
||||
Housing for installation on DIN rail - D2MG. Occupies only 2 DIN modules.<br>
|
||||
The 220V > 5V power supply is already on the board, so additional power supplies are not needed.<br>
|
||||
To save money, 2 levels are ordered as one board. After manufacturing, the boards need to be divided into 2 parts - upper and lower.<br>
|
||||
**Important!** On this board opentherm IN pin = 5, OUT pin = 4
|
||||
To save money, 2 levels are ordered as one board. After manufacturing, the boards need to be divided into 2 parts - upper and lower. The boards are inexpensively (5pcs for $2) manufactured at JLCPCB (Remove Order Number = Specify a location).<br><br>
|
||||
Some components can be replaced with similar ones (for example use a fuse and led with legs). Some SMD components (for example optocouplers) can be replaced with similar SOT components.<br>Most of the components can be purchased inexpensively on Aliexpress, the rest in your local stores.
|
||||
|
||||
- [Sheet](/assets/sheet.pdf)
|
||||
#### Connection
|
||||
The outdoor temperature sensor must be connected to the **TEMP1** connector, the indoor temperature sensor must be connected to the **TEMP2** connector. The power supply for the sensors must be connected to the **3.3V** connector **(NOT 5V!)**, GND to **GND**.<br>
|
||||
**The opentherm connection polarity does not matter.**
|
||||
<!-- **Important!** On this board opentherm IN pin = 5, OUT pin = 4 -->
|
||||
|
||||
#### Leds
|
||||
| LED | States |
|
||||
| --- | --- |
|
||||
| OT RX | Flashes when a response to the request is received from the boiler |
|
||||
| Status | Controller status.<br>On, not blinking - no errors;<br>2 flashes - no connection to Wifi;<br>3 flashes - no connection to boiler;<br>4 flashes - boiler is fault;<br>5 flashes - emergency mode (no connection to Wifi or to the MQTT server)<br>10 fast flashes - end of the list of errors |
|
||||
| Power | Always on when power is on |
|
||||
|
||||
#### Files for production
|
||||
- [Schematic](/assets/Schematic.pdf)
|
||||
- [BOM](/assets/BOM.xlsx)
|
||||
- [Gerber](/assets/gerber.zip)
|
||||
|
||||
## Another compatible Open Therm Adapters
|
||||
## Another compatible OpenTherm Adapters
|
||||
- [Ihor Melnyk OpenTherm Adapter](http://ihormelnyk.com/opentherm_adapter)
|
||||
- [DIYLESS Master OpenTherm Shield](https://diyless.com/product/master-opentherm-shield)
|
||||
- [OpenTherm master shield for Wemos/Lolin](https://www.tindie.com/products/thehognl/opentherm-master-shield-for-wemoslolin/)
|
||||
- And others. It's just that the adapter must implement [the schema](http://ihormelnyk.com/Content/Pages/opentherm_adapter/opentherm_adapter_schematic_o.png)
|
||||
|
||||
## Compatible Temperature Sensors
|
||||
* DS18B20
|
||||
* DS1822
|
||||
* DS1820
|
||||
* MAX31820
|
||||
* MAX31850
|
||||
|
||||
[See more](https://github.com/milesburton/Arduino-Temperature-Control-Library#usage)
|
||||
|
||||
# Quick Start
|
||||
## Dependencies
|
||||
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler)
|
||||
- [NTPClient](https://github.com/arduino-libraries/NTPClient)
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
|
||||
- [PubSubClient](https://github.com/knolleary/pubsubclient)
|
||||
- [TelnetStream](https://github.com/jandrassy/TelnetStream)
|
||||
- [EEManager](https://github.com/GyverLibs/EEManager)
|
||||
- [GyverPID](https://github.com/GyverLibs/GyverPID)
|
||||
- [microDS18B20](https://github.com/GyverLibs/microDS18B20)
|
||||
- [WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
1. Download the latest firmware from the [releases page](https://github.com/Laxilef/OTGateway/releases) (or compile yourself) and flash your ESP8266 board using the [ESP Flash Download Tool](https://www.espressif.com/en/support/download/other-tools) or other software.
|
||||
2. Connect to *OpenTherm Gateway* hotspot, password: otgateway123456
|
||||
3. Open configuration page in browser: 192.168.4.1
|
||||
4. Set up a connection to your wifi network
|
||||
5. Set up a connection to your MQTT server: ip, port, user, password
|
||||
6. Set up a **Opentherm pin IN** & **Opentherm pin OUT**. No change for my board. Typically used **IN** = 4, **OUT** = 5
|
||||
7. Set up a **Outdoor sensor pin** & **Indoor sensor pin**. No change for my board.
|
||||
8. if necessary, set up a the master member ID ([see more](#tested-on))
|
||||
9. Restart module (required after changing OT pins and/or sensors pins!)
|
||||
|
||||
|
||||
## 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.
|
||||
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
|
||||
@@ -109,7 +127,7 @@ The temperature inside the house can be set using simple automation:
|
||||
```
|
||||
</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.
|
||||
If your boiler does not support the installation of an outdoor temperature sensor or does not provide this value via the opentherm protocol, then you can use an external sensor or use automation.
|
||||
<details>
|
||||
<summary>Simple automation</summary>
|
||||
|
||||
@@ -153,7 +171,7 @@ Weather-compensated temperature control maintains a comfortable set temperature
|
||||
|
||||
#### 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
|
||||
Range: 0.001...10, default: 0.7, step 0.001
|
||||
|
||||
|
||||
***K*** - сorrection for desired room temperature.<br>
|
||||
@@ -166,13 +184,16 @@ 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>
|
||||
1. Set the ***K*** and ***T*** coefficients to 0.
|
||||
2. The first thing you need to do is to fit the curve (***N*** coefficient). If your home has low heat loss, then start with 0.5. Otherwise start at 0.7. When the temperature inside the house stops changing, increase or decrease the coefficient value in increments of 0.1 to select the optimal curve.<br>
|
||||
Please note that passive heating (sun) will affect the house temperature during curve fitting. This process is not fast and will take you 1-2 days.
|
||||
Important. During curve fitting, the temperature must be kept stable as the outside temperature changes. 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.
|
||||
Important. During curve fitting, the temperature must be kept stable as the outside temperature changes.<br>
|
||||
At this stage, it is important for you to stabilize the indoor temperature at exactly 20 (+- 0.5) degrees.<br>
|
||||
For example. You fit curve 0.67; set temperature 20; the temperature in the house is 20.1 degrees while the outside temperature is -10 degrees and -5 degrees. This is good.
|
||||
3. After fitting the curve, you must select the ***K*** coefficient. It influences the boiler temperature correction to maintain the set temperature.
|
||||
For example. Set temperature: 23 degrees; temperature in the house: 20 degrees. Try setting it to 2 and see how the temperature in the house changes after stabilization. Select the value so that the temperature in the house is close to the set.
|
||||
4. Now you can choose the ***T*** coefficient. Simply put, it affects the sharpness of the temperature change. If you want fast heating, then set a high value (6-10), but then the room may overheat. If you want smooth heating, set 1-5. Choose the optimal value for yourself.
|
||||
5. Check to see if it works correctly at different set temperatures over several days.
|
||||
|
||||
Read more about the algorithm [here](https://wdn.su/blog/1154).
|
||||
|
||||
@@ -182,9 +203,23 @@ See [Wikipedia](https://en.wikipedia.org/wiki/PID_controller).
|
||||
|
||||
In Google you can find instructions for tuning the PID controller.
|
||||
|
||||
### Use Equitherm mode + PID mode
|
||||
@todo
|
||||
<!--### Use Equitherm mode + PID mode
|
||||
@todo-->
|
||||
|
||||
## Dependencies
|
||||
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler) (for ESP8266)
|
||||
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
|
||||
- [NTPClient](https://github.com/arduino-libraries/NTPClient)
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
|
||||
- [PubSubClient](https://github.com/knolleary/pubsubclient)
|
||||
- [TelnetStream](https://github.com/jandrassy/TelnetStream)
|
||||
- [EEManager](https://github.com/GyverLibs/EEManager)
|
||||
- [GyverPID](https://github.com/GyverLibs/GyverPID)
|
||||
- [GyverBlinker](https://github.com/GyverLibs/GyverBlinker)
|
||||
- [DallasTemperature](https://github.com/milesburton/Arduino-Temperature-Control-Library)
|
||||
- [WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
|
||||
## 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
1
assets/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/*.priv.*
|
||||
BIN
assets/BOM.xlsx
BIN
assets/BOM.xlsx
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 689 KiB After Width: | Height: | Size: 716 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 236 KiB |
@@ -1,12 +1,12 @@
|
||||
#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);
|
||||
void(*yieldCallback)(void*);
|
||||
void* yieldArg;
|
||||
|
||||
public:
|
||||
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
|
||||
@@ -14,10 +14,25 @@ public:
|
||||
this->handleSendRequestCallback = handleSendRequestCallback;
|
||||
}
|
||||
|
||||
void setYieldCallback(void(*yieldCallback)(void*)) {
|
||||
this->yieldCallback = yieldCallback;
|
||||
this->yieldArg = nullptr;
|
||||
}
|
||||
|
||||
void setYieldCallback(void(*yieldCallback)(void*), void* arg) {
|
||||
this->yieldCallback = yieldCallback;
|
||||
this->yieldArg = arg;
|
||||
}
|
||||
|
||||
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
||||
_attempt++;
|
||||
while (send_ts > 0 && millis() - send_ts < 200) {
|
||||
Scheduler.yield();
|
||||
if (yieldCallback != NULL) {
|
||||
yieldCallback(yieldArg);
|
||||
|
||||
} else {
|
||||
::yield();
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long _response;
|
||||
@@ -25,19 +40,26 @@ public:
|
||||
_response = 0;
|
||||
} else {
|
||||
while (!isReady()) {
|
||||
Scheduler.yield();
|
||||
if (yieldCallback != NULL) {
|
||||
yieldCallback(yieldArg);
|
||||
|
||||
} else {
|
||||
::yield();
|
||||
}
|
||||
|
||||
process();
|
||||
}
|
||||
|
||||
_response = getLastResponse();
|
||||
}
|
||||
|
||||
OpenThermResponseStatus _responseStatus = getLastResponseStatus();
|
||||
if (handleSendRequestCallback != NULL) {
|
||||
handleSendRequestCallback(request, _response, getLastResponseStatus(), _attempt);
|
||||
handleSendRequestCallback(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);
|
||||
@@ -92,7 +114,7 @@ public:
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
89
lib/HomeAssistantHelper/HomeAssistantHelper.h
Normal file
89
lib/HomeAssistantHelper/HomeAssistantHelper.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
class HomeAssistantHelper {
|
||||
public:
|
||||
HomeAssistantHelper(PubSubClient& client) :
|
||||
client(&client)
|
||||
{
|
||||
}
|
||||
|
||||
void setDevicePrefix(String value) {
|
||||
devicePrefix = value;
|
||||
}
|
||||
|
||||
void setDeviceVersion(String value) {
|
||||
deviceVersion = value;
|
||||
}
|
||||
|
||||
void setDeviceManufacturer(String value) {
|
||||
deviceManufacturer = value;
|
||||
}
|
||||
|
||||
void setDeviceModel(String value) {
|
||||
deviceModel = value;
|
||||
}
|
||||
|
||||
void setDeviceName(String value) {
|
||||
deviceName = value;
|
||||
}
|
||||
|
||||
void setDeviceConfigUrl(String value) {
|
||||
deviceConfigUrl = value;
|
||||
}
|
||||
|
||||
bool publish(const char* topic, JsonDocument& doc) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_IDENTIFIERS)][0] = devicePrefix;
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_SW_VERSION)] = deviceVersion;
|
||||
|
||||
if (deviceManufacturer) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MANUFACTURER)] = deviceManufacturer;
|
||||
}
|
||||
|
||||
if (deviceModel) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_MODEL)] = deviceModel;
|
||||
}
|
||||
|
||||
if (deviceName) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_NAME)] = deviceName;
|
||||
}
|
||||
|
||||
if (deviceConfigUrl) {
|
||||
doc[FPSTR(HA_DEVICE)][FPSTR(HA_CONF_URL)] = deviceConfigUrl;
|
||||
}
|
||||
|
||||
// Feeding the watchdog
|
||||
yield();
|
||||
|
||||
client->beginPublish(topic, measureJson(doc), true);
|
||||
serializeJson(doc, *client);
|
||||
return client->endPublish();
|
||||
}
|
||||
|
||||
bool publish(const char* topic) {
|
||||
return client->publish(topic, NULL, true);
|
||||
}
|
||||
|
||||
String getTopic(const char* category, const char* name, const char* nameSeparator = "/") {
|
||||
String topic = "";
|
||||
topic.concat(prefix);
|
||||
topic.concat("/");
|
||||
topic.concat(category);
|
||||
topic.concat("/");
|
||||
topic.concat(devicePrefix);
|
||||
topic.concat(nameSeparator);
|
||||
topic.concat(name);
|
||||
topic.concat("/config");
|
||||
return topic;
|
||||
}
|
||||
|
||||
protected:
|
||||
PubSubClient* client;
|
||||
String prefix = "homeassistant";
|
||||
String devicePrefix = "";
|
||||
String deviceVersion = "1.0";
|
||||
String deviceManufacturer = "Community";
|
||||
String deviceModel = "";
|
||||
String deviceName = "";
|
||||
String deviceConfigUrl = "";
|
||||
};
|
||||
37
lib/HomeAssistantHelper/strings.h
Normal file
37
lib/HomeAssistantHelper/strings.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
const char HA_DEVICE[] PROGMEM = "device";
|
||||
const char HA_IDENTIFIERS[] PROGMEM = "identifiers";
|
||||
const char HA_SW_VERSION[] PROGMEM = "sw_version";
|
||||
const char HA_MANUFACTURER[] PROGMEM = "manufacturer";
|
||||
const char HA_MODEL[] PROGMEM = "model";
|
||||
const char HA_NAME[] PROGMEM = "name";
|
||||
const char HA_CONF_URL[] PROGMEM = "configuration_url";
|
||||
const char HA_COMMAND_TOPIC[] PROGMEM = "command_topic";
|
||||
const char HA_COMMAND_TEMPLATE[] PROGMEM = "command_template";
|
||||
const char HA_ENABLED_BY_DEFAULT[] PROGMEM = "enabled_by_default";
|
||||
const char HA_UNIQUE_ID[] PROGMEM = "unique_id";
|
||||
const char HA_OBJECT_ID[] PROGMEM = "object_id";
|
||||
const char HA_ENTITY_CATEGORY[] PROGMEM = "entity_category";
|
||||
const char HA_STATE_TOPIC[] PROGMEM = "state_topic";
|
||||
const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template";
|
||||
const char HA_OPTIONS[] PROGMEM = "options";
|
||||
const char HA_AVAILABILITY[] PROGMEM = "availability";
|
||||
const char HA_AVAILABILITY_MODE[] PROGMEM = "availability_mode";
|
||||
const char HA_TOPIC[] PROGMEM = "topic";
|
||||
const char HA_DEVICE_CLASS[] PROGMEM = "device_class";
|
||||
const char HA_UNIT_OF_MEASUREMENT[] PROGMEM = "unit_of_measurement";
|
||||
const char HA_ICON[] PROGMEM = "icon";
|
||||
const char HA_MIN[] PROGMEM = "min";
|
||||
const char HA_MAX[] PROGMEM = "max";
|
||||
const char HA_STEP[] PROGMEM = "step";
|
||||
const char HA_MODE[] PROGMEM = "mode";
|
||||
const char HA_STATE_ON[] PROGMEM = "state_on";
|
||||
const char HA_STATE_OFF[] PROGMEM = "state_off";
|
||||
const char HA_PAYLOAD_ON[] PROGMEM = "payload_on";
|
||||
const char HA_PAYLOAD_OFF[] PROGMEM = "payload_off";
|
||||
const char HA_STATE_CLASS[] PROGMEM = "state_class";
|
||||
const char HA_EXPIRE_AFTER[] PROGMEM = "expire_after";
|
||||
49
lib/WiFiManagerParameters/WiFiManagerParameters.h
Normal file
49
lib/WiFiManagerParameters/WiFiManagerParameters.h
Normal file
@@ -0,0 +1,49 @@
|
||||
class IntParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
IntParameter(const char* id, const char* label, int value, const uint8_t length = 10) : WiFiManagerParameter("") {
|
||||
init(id, label, String(value).c_str(), length, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
int getValue() {
|
||||
return atoi(WiFiManagerParameter::getValue());
|
||||
}
|
||||
};
|
||||
|
||||
class UnsignedIntParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
UnsignedIntParameter(const char* id, const char* label, unsigned int value, const uint8_t length = 10) : WiFiManagerParameter("") {
|
||||
init(id, label, String(value).c_str(), length, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
unsigned int getValue() {
|
||||
return (unsigned int) atoi(WiFiManagerParameter::getValue());
|
||||
}
|
||||
};
|
||||
|
||||
class CheckboxParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
const char* checked = "type=\"checkbox\" checked";
|
||||
const char* noChecked = "type=\"checkbox\"";
|
||||
const char* trueVal = "T";
|
||||
|
||||
CheckboxParameter(const char* id, const char* label, bool value) : WiFiManagerParameter("") {
|
||||
init(id, label, value ? trueVal : "0", 1, "", WFM_LABEL_AFTER);
|
||||
}
|
||||
|
||||
const char* getValue() const override {
|
||||
return trueVal;
|
||||
}
|
||||
|
||||
const char* getCustomHTML() const override {
|
||||
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? checked : noChecked;
|
||||
}
|
||||
|
||||
bool getCheckboxValue() {
|
||||
return strcmp(WiFiManagerParameter::getValue(), trueVal) == 0 ? true : false;
|
||||
}
|
||||
};
|
||||
|
||||
class SeparatorParameter : public WiFiManagerParameter {
|
||||
public:
|
||||
SeparatorParameter() : WiFiManagerParameter("<hr>") {}
|
||||
};
|
||||
4
otgateway.ino
Normal file
4
otgateway.ino
Normal 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
|
||||
*/
|
||||
143
platformio.ini
143
platformio.ini
@@ -8,12 +8,9 @@
|
||||
; 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
|
||||
[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
|
||||
@@ -21,8 +18,140 @@ lib_deps =
|
||||
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
|
||||
gyverlibs/GyverBlinker@^1.0
|
||||
milesburton/DallasTemperature@^3.11.0
|
||||
https://github.com/Laxilef/WiFiManager/archive/refs/heads/patch-1.zip
|
||||
;https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
|
||||
build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH -mtext-section-literals
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
version = 1.3.3
|
||||
|
||||
; Defaults
|
||||
[esp8266_defaults]
|
||||
platform = espressif8266
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
nrwiersma/ESP8266Scheduler@^1.0
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/build.py
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[esp32_defaults]
|
||||
platform = espressif32
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
laxilef/ESP32Scheduler@^1.0.0
|
||||
lib_ignore =
|
||||
extra_scripts =
|
||||
post:tools/esp32.py
|
||||
post:tools/build.py
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
; 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}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
|
||||
[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}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
|
||||
[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}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D OT_IN_PIN_DEFAULT=4
|
||||
-D OT_OUT_PIN_DEFAULT=5
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=14
|
||||
-D LED_STATUS_PIN=13
|
||||
-D LED_OT_RX_PIN=15
|
||||
|
||||
[env:s2_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
board = lolin_s2_mini
|
||||
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 OT_IN_PIN_DEFAULT=33
|
||||
-D OT_OUT_PIN_DEFAULT=35
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=9
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=7
|
||||
-D LED_STATUS_PIN=11
|
||||
-D LED_OT_RX_PIN=12
|
||||
|
||||
[env:s3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
board = lolin_s3_mini
|
||||
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 OT_IN_PIN_DEFAULT=35
|
||||
-D OT_OUT_PIN_DEFAULT=36
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=13
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=12
|
||||
-D LED_STATUS_PIN=11
|
||||
-D LED_OT_RX_PIN=10
|
||||
|
||||
[env:c3_mini]
|
||||
platform = ${esp32_defaults.platform}
|
||||
board = lolin_c3_mini
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_flags =
|
||||
-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
|
||||
-D OT_IN_PIN_DEFAULT=8
|
||||
-D OT_OUT_PIN_DEFAULT=10
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=0
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=1
|
||||
-D LED_STATUS_PIN=4
|
||||
-D LED_OT_RX_PIN=5
|
||||
|
||||
[env:nodemcu_32s]
|
||||
platform = ${esp32_defaults.platform}
|
||||
board = nodemcu-32s
|
||||
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 OT_IN_PIN_DEFAULT=21
|
||||
-D OT_OUT_PIN_DEFAULT=22
|
||||
-D SENSOR_OUTDOOR_PIN_DEFAULT=12
|
||||
-D SENSOR_INDOOR_PIN_DEFAULT=13
|
||||
-D LED_STATUS_PIN=2 ; 18
|
||||
-D LED_OT_RX_PIN=19
|
||||
1154
src/HaHelper.h
Normal file
1154
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
148
src/MainTask.h
148
src/MainTask.h
@@ -1,19 +1,42 @@
|
||||
#include <Blinker.h>
|
||||
|
||||
extern MqttTask* tMqtt;
|
||||
extern SensorsTask* tSensors;
|
||||
extern OpenThermTask* tOt;
|
||||
|
||||
class MainTask: public Task {
|
||||
class MainTask : public Task {
|
||||
public:
|
||||
MainTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
|
||||
MainTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
|
||||
|
||||
protected:
|
||||
Blinker* blinker = nullptr;
|
||||
unsigned long lastHeapInfo = 0;
|
||||
unsigned long firstFailConnect = 0;
|
||||
unsigned short minFreeHeapSize = 65535;
|
||||
unsigned int heapSize = 0;
|
||||
unsigned int minFreeHeapSize = 0;
|
||||
|
||||
const char* getTaskName() {
|
||||
return "Main";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifdef LED_STATUS_PIN
|
||||
pinMode(LED_STATUS_PIN, OUTPUT);
|
||||
//pinMode(LED_OT_RX_PIN, OUTPUT);
|
||||
digitalWrite(LED_STATUS_PIN, false);
|
||||
#endif
|
||||
|
||||
#if defined(ESP32)
|
||||
heapSize = ESP.getHeapSize();
|
||||
#elif defined(ESP8266)
|
||||
heapSize = 81920;
|
||||
#elif
|
||||
heapSize = 99999;
|
||||
#endif
|
||||
minFreeHeapSize = heapSize;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
@@ -21,12 +44,23 @@ protected:
|
||||
INFO("Settings updated (EEPROM)");
|
||||
}
|
||||
|
||||
if (vars.parameters.restartAfterTime > 0 && millis() - vars.parameters.restartSignalTime > vars.parameters.restartAfterTime) {
|
||||
vars.parameters.restartAfterTime = 0;
|
||||
|
||||
INFO("Received restart message...");
|
||||
eeSettings.updateNow();
|
||||
INFO("Restart...");
|
||||
delay(1000);
|
||||
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!tMqtt->isEnabled()) {
|
||||
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
|
||||
tMqtt->enable();
|
||||
}
|
||||
|
||||
if ( firstFailConnect != 0 ) {
|
||||
if (firstFailConnect != 0) {
|
||||
firstFailConnect = 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +75,7 @@ protected:
|
||||
if (firstFailConnect == 0) {
|
||||
firstFailConnect = millis();
|
||||
}
|
||||
|
||||
|
||||
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
|
||||
vars.states.emergency = true;
|
||||
INFO("Emergency mode enabled");
|
||||
@@ -49,17 +83,13 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
if (!tSensors->isEnabled() && settings.outdoorTempSource == 2) {
|
||||
tSensors->enable();
|
||||
} else if (tSensors->isEnabled() && settings.outdoorTempSource != 2) {
|
||||
tSensors->disable();
|
||||
}
|
||||
|
||||
if (!tOt->isEnabled() && settings.opentherm.inPin > 0 && settings.opentherm.outPin > 0 && settings.opentherm.inPin != settings.opentherm.outPin) {
|
||||
tOt->enable();
|
||||
}
|
||||
|
||||
ledStatus();
|
||||
#ifdef LED_STATUS_PIN
|
||||
ledStatus(LED_STATUS_PIN);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TELNET
|
||||
yield();
|
||||
@@ -72,70 +102,80 @@ protected:
|
||||
#endif
|
||||
|
||||
if (settings.debug) {
|
||||
unsigned short freeHeapSize = ESP.getFreeHeap();
|
||||
unsigned short minFreeHeapSizeDiff = 0;
|
||||
unsigned int freeHeapSize = ESP.getFreeHeap();
|
||||
unsigned int minFreeHeapSizeDiff = 0;
|
||||
|
||||
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);
|
||||
DEBUG_F("Free heap size: %u of %u bytes, min: %u bytes (diff: %u bytes)\n", freeHeapSize, heapSize, minFreeHeapSize, minFreeHeapSizeDiff);
|
||||
lastHeapInfo = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ledStatus() {
|
||||
static byte blinkLeft = 0;
|
||||
void ledStatus(uint8_t ledPin) {
|
||||
uint8_t errors[4];
|
||||
uint8_t errCount = 0;
|
||||
static uint8_t errPos = 0;
|
||||
static unsigned long endBlinkTime = 0;
|
||||
static bool ledOn = false;
|
||||
static unsigned long changeTime = 0;
|
||||
|
||||
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->blinker == nullptr) {
|
||||
this->blinker = new Blinker(ledPin);
|
||||
}
|
||||
|
||||
if (errNo == 0) {
|
||||
if (!ledOn) {
|
||||
digitalWrite(LED_STATUS_PIN, true);
|
||||
ledOn = true;
|
||||
}
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
errors[errCount++] = 2;
|
||||
}
|
||||
|
||||
if (blinkLeft > 0) {
|
||||
blinkLeft = 0;
|
||||
}
|
||||
if (!vars.states.otStatus) {
|
||||
errors[errCount++] = 3;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (blinkLeft == 0) {
|
||||
if (ledOn) {
|
||||
digitalWrite(LED_STATUS_PIN, false);
|
||||
ledOn = false;
|
||||
changeTime = millis();
|
||||
}
|
||||
if (vars.states.fault) {
|
||||
errors[errCount++] = 4;
|
||||
}
|
||||
|
||||
if (millis() - changeTime >= 3000) {
|
||||
blinkLeft = errNo;
|
||||
}
|
||||
}
|
||||
if (vars.states.emergency) {
|
||||
errors[errCount++] = 5;
|
||||
}
|
||||
|
||||
if (blinkLeft > 0 && millis() - changeTime >= 500) {
|
||||
if (ledOn) {
|
||||
digitalWrite(LED_STATUS_PIN, false);
|
||||
ledOn = false;
|
||||
blinkLeft--;
|
||||
|
||||
} else {
|
||||
digitalWrite(LED_STATUS_PIN, true);
|
||||
if (this->blinker->ready()) {
|
||||
endBlinkTime = millis();
|
||||
}
|
||||
|
||||
if (!this->blinker->running() && millis() - endBlinkTime >= 5000) {
|
||||
if (errCount == 0) {
|
||||
if (!ledOn) {
|
||||
digitalWrite(ledPin, true);
|
||||
ledOn = true;
|
||||
}
|
||||
|
||||
changeTime = millis();
|
||||
return;
|
||||
|
||||
} else if (ledOn) {
|
||||
digitalWrite(ledPin, false);
|
||||
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();
|
||||
}
|
||||
};
|
||||
325
src/MqttTask.h
325
src/MqttTask.h
@@ -1,28 +1,36 @@
|
||||
#include <WiFiClient.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <netif/etharp.h>
|
||||
#include "HomeAssistantHelper.h"
|
||||
#include "HaHelper.h"
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
HomeAssistantHelper haHelper;
|
||||
HaHelper haHelper(client);
|
||||
|
||||
|
||||
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) {}
|
||||
|
||||
protected:
|
||||
unsigned long lastReconnectAttempt = 0;
|
||||
unsigned long firstFailConnect = 0;
|
||||
|
||||
const char* getTaskName() {
|
||||
return "Mqtt";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
DEBUG("[MQTT] Started");
|
||||
|
||||
client.setServer(settings.mqtt.server, settings.mqtt.port);
|
||||
client.setCallback(__callback);
|
||||
haHelper.setPrefix(settings.mqtt.prefix);
|
||||
haHelper.setDevicePrefix(settings.mqtt.prefix);
|
||||
haHelper.setDeviceVersion(OT_GATEWAY_VERSION);
|
||||
haHelper.setDeviceModel("Opentherm Gateway");
|
||||
haHelper.setDeviceName("Opentherm Gateway");
|
||||
|
||||
sprintf(buffer, CONFIG_URL, WiFi.localIP().toString().c_str());
|
||||
haHelper.setDeviceConfigUrl(buffer);
|
||||
@@ -32,6 +40,7 @@ protected:
|
||||
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);
|
||||
|
||||
client.setServer(settings.mqtt.server, settings.mqtt.port);
|
||||
if (client.connect(settings.hostname, settings.mqtt.user, settings.mqtt.password)) {
|
||||
INFO("Connected to MQTT server");
|
||||
|
||||
@@ -50,14 +59,13 @@ protected:
|
||||
if (firstFailConnect == 0) {
|
||||
firstFailConnect = millis();
|
||||
}
|
||||
|
||||
|
||||
if (millis() - firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
|
||||
vars.states.emergency = true;
|
||||
INFO("Emergency mode enabled");
|
||||
}
|
||||
}
|
||||
|
||||
forceARP();
|
||||
lastReconnectAttempt = millis();
|
||||
}
|
||||
}
|
||||
@@ -77,14 +85,6 @@ protected:
|
||||
}
|
||||
|
||||
|
||||
static void forceARP() {
|
||||
struct netif* netif = netif_list;
|
||||
while (netif) {
|
||||
etharp_gratuitous(netif);
|
||||
netif = netif->next;
|
||||
}
|
||||
}
|
||||
|
||||
static bool updateSettings(JsonDocument& doc) {
|
||||
bool flag = false;
|
||||
|
||||
@@ -93,15 +93,6 @@ protected:
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (!doc["outdoorTempSource"].isNull() && doc["outdoorTempSource"].is<int>() && doc["outdoorTempSource"] >= 0 && doc["outdoorTempSource"] <= 2) {
|
||||
settings.outdoorTempSource = doc["outdoorTempSource"];
|
||||
flag = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// emergency
|
||||
if (!doc["emergency"]["enable"].isNull() && doc["emergency"]["enable"].is<bool>()) {
|
||||
@@ -109,9 +100,11 @@ protected:
|
||||
flag = true;
|
||||
}
|
||||
|
||||
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"]["target"].isNull() && doc["emergency"]["target"].is<float>()) {
|
||||
if (doc["emergency"]["target"].as<float>() > 0 && doc["emergency"]["target"].as<float>() < 100) {
|
||||
settings.emergency.target = round(doc["emergency"]["target"].as<float>() * 10) / 10;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is<bool>()) {
|
||||
@@ -119,6 +112,7 @@ protected:
|
||||
flag = true;
|
||||
}
|
||||
|
||||
|
||||
// heating
|
||||
if (!doc["heating"]["enable"].isNull() && doc["heating"]["enable"].is<bool>()) {
|
||||
settings.heating.enable = doc["heating"]["enable"].as<bool>();
|
||||
@@ -130,46 +124,106 @@ protected:
|
||||
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"]["target"].isNull() && doc["heating"]["target"].is<float>()) {
|
||||
if (doc["heating"]["target"].as<float>() > 0 && doc["heating"]["target"].as<float>() < 100) {
|
||||
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;
|
||||
if (!doc["heating"]["hysteresis"].isNull() && doc["heating"]["hysteresis"].is<float>()) {
|
||||
if (doc["heating"]["hysteresis"].as<float>() >= 0 && doc["heating"]["hysteresis"].as<float>() <= 5) {
|
||||
settings.heating.hysteresis = round(doc["heating"]["hysteresis"].as<float>() * 10) / 10;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["heating"]["maxTemp"].isNull() && doc["heating"]["maxTemp"].is<unsigned char>()) {
|
||||
if (doc["heating"]["maxTemp"].as<unsigned char>() > 0 && doc["heating"]["maxTemp"].as<unsigned char>() <= 100 && doc["heating"]["maxTemp"].as<unsigned char>() > settings.heating.minTemp) {
|
||||
settings.heating.maxTemp = doc["heating"]["maxTemp"].as<unsigned char>();
|
||||
vars.parameters.heatingMaxTemp = settings.heating.maxTemp;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["heating"]["minTemp"].isNull() && doc["heating"]["minTemp"].is<unsigned char>()) {
|
||||
if (doc["heating"]["minTemp"].as<unsigned char>() >= 0 && doc["heating"]["minTemp"].as<unsigned char>() < 100 && doc["heating"]["minTemp"].as<unsigned char>() < settings.heating.maxTemp) {
|
||||
settings.heating.minTemp = doc["heating"]["minTemp"].as<unsigned char>();
|
||||
vars.parameters.heatingMinTemp = settings.heating.minTemp;
|
||||
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;
|
||||
if (!doc["dhw"]["target"].isNull() && doc["dhw"]["target"].is<unsigned char>()) {
|
||||
if (doc["dhw"]["target"].as<unsigned char>() >= 0 && doc["dhw"]["target"].as<unsigned char>() < 100) {
|
||||
settings.dhw.target = doc["dhw"]["target"].as<unsigned char>();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["dhw"]["maxTemp"].isNull() && doc["dhw"]["maxTemp"].is<unsigned char>()) {
|
||||
if (doc["dhw"]["maxTemp"].as<unsigned char>() > 0 && doc["dhw"]["maxTemp"].as<unsigned char>() <= 100 && doc["dhw"]["maxTemp"].as<unsigned char>() > settings.dhw.minTemp) {
|
||||
settings.dhw.maxTemp = doc["dhw"]["maxTemp"].as<unsigned char>();
|
||||
vars.parameters.dhwMaxTemp = settings.dhw.maxTemp;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["dhw"]["minTemp"].isNull() && doc["dhw"]["minTemp"].is<unsigned char>()) {
|
||||
if (doc["dhw"]["minTemp"].as<unsigned char>() >= 0 && doc["dhw"]["minTemp"].as<unsigned char>() < 100 && doc["dhw"]["minTemp"].as<unsigned char>() < settings.dhw.maxTemp) {
|
||||
settings.dhw.minTemp = doc["dhw"]["minTemp"].as<unsigned char>();
|
||||
vars.parameters.dhwMinTemp = settings.dhw.minTemp;
|
||||
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"]["p_factor"].isNull() && doc["pid"]["p_factor"].is<float>()) {
|
||||
if (doc["pid"]["p_factor"].as<float>() > 0 && doc["pid"]["p_factor"].as<float>() <= 10) {
|
||||
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"]["i_factor"].isNull() && doc["pid"]["i_factor"].is<float>()) {
|
||||
if (doc["pid"]["i_factor"].as<float>() >= 0 && doc["pid"]["i_factor"].as<float>() <= 10) {
|
||||
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;
|
||||
if (!doc["pid"]["d_factor"].isNull() && doc["pid"]["d_factor"].is<float>()) {
|
||||
if (doc["pid"]["d_factor"].as<float>() >= 0 && doc["pid"]["d_factor"].as<float>() <= 10) {
|
||||
settings.pid.d_factor = round(doc["pid"]["d_factor"].as<float>() * 1000) / 1000;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["pid"]["maxTemp"].isNull() && doc["pid"]["maxTemp"].is<unsigned char>()) {
|
||||
if (doc["pid"]["maxTemp"].as<unsigned char>() > 0 && doc["pid"]["maxTemp"].as<unsigned char>() <= 100 && doc["pid"]["maxTemp"].as<unsigned char>() > settings.pid.minTemp) {
|
||||
settings.pid.maxTemp = doc["pid"]["maxTemp"].as<unsigned char>();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["pid"]["minTemp"].isNull() && doc["pid"]["minTemp"].is<unsigned char>()) {
|
||||
if (doc["pid"]["minTemp"].as<unsigned char>() >= 0 && doc["pid"]["minTemp"].as<unsigned char>() < 100 && doc["pid"]["minTemp"].as<unsigned char>() < settings.pid.maxTemp) {
|
||||
settings.pid.minTemp = doc["pid"]["minTemp"].as<unsigned char>();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// equitherm
|
||||
@@ -178,21 +232,58 @@ protected:
|
||||
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"]["n_factor"].isNull() && doc["equitherm"]["n_factor"].is<float>()) {
|
||||
if (doc["equitherm"]["n_factor"].as<float>() > 0 && doc["equitherm"]["n_factor"].as<float>() <= 10) {
|
||||
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"]["k_factor"].isNull() && doc["equitherm"]["k_factor"].is<float>()) {
|
||||
if (doc["equitherm"]["k_factor"].as<float>() >= 0 && doc["equitherm"]["k_factor"].as<float>() <= 10) {
|
||||
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 (!doc["equitherm"]["t_factor"].isNull() && doc["equitherm"]["t_factor"].is<float>()) {
|
||||
if (doc["equitherm"]["t_factor"].as<float>() >= 0 && doc["equitherm"]["t_factor"].as<float>() <= 10) {
|
||||
settings.equitherm.t_factor = round(doc["equitherm"]["t_factor"].as<float>() * 1000) / 1000;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sensors
|
||||
if (!doc["sensors"]["outdoor"]["type"].isNull() && doc["sensors"]["outdoor"]["type"].is<unsigned char>()) {
|
||||
if (doc["sensors"]["outdoor"]["type"].as<unsigned char>() >= 0 && doc["sensors"]["outdoor"]["type"].as<unsigned char>() <= 2) {
|
||||
settings.sensors.outdoor.type = doc["sensors"]["outdoor"]["type"].as<unsigned char>();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["sensors"]["outdoor"]["offset"].isNull() && doc["sensors"]["outdoor"]["offset"].is<float>()) {
|
||||
if (doc["sensors"]["outdoor"]["offset"].as<float>() >= -10 && doc["sensors"]["outdoor"]["offset"].as<float>() <= 10) {
|
||||
settings.sensors.outdoor.offset = round(doc["sensors"]["outdoor"]["offset"].as<float>() * 1000) / 1000;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["sensors"]["indoor"]["type"].isNull() && doc["sensors"]["indoor"]["type"].is<unsigned char>()) {
|
||||
if (doc["sensors"]["indoor"]["type"].as<unsigned char>() >= 1 && doc["sensors"]["indoor"]["type"].as<unsigned char>() <= 2) {
|
||||
settings.sensors.indoor.type = doc["sensors"]["indoor"]["type"].as<unsigned char>();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc["sensors"]["indoor"]["offset"].isNull() && doc["sensors"]["indoor"]["offset"].is<float>()) {
|
||||
if (doc["sensors"]["indoor"]["offset"].as<float>() >= -10 && doc["sensors"]["indoor"]["offset"].as<float>() <= 10) {
|
||||
settings.sensors.indoor.offset = round(doc["sensors"]["indoor"]["offset"].as<float>() * 1000) / 1000;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (flag) {
|
||||
eeSettings.update();
|
||||
publish(true);
|
||||
@@ -215,28 +306,30 @@ protected:
|
||||
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["tuning"]["regulator"].isNull() && doc["tuning"]["regulator"].is<unsigned char>()) {
|
||||
if (doc["tuning"]["regulator"].as<unsigned char>() >= 0 && doc["tuning"]["regulator"].as<unsigned char>() <= 1) {
|
||||
vars.tuning.regulator = doc["tuning"]["regulator"].as<unsigned char>();
|
||||
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"]["indoor"].isNull() && doc["temperatures"]["indoor"].is<float>()) {
|
||||
if (settings.sensors.indoor.type == 1 && doc["temperatures"]["indoor"].as<float>() > -100 && doc["temperatures"]["indoor"].as<float>() < 100) {
|
||||
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["temperatures"]["outdoor"].isNull() && doc["temperatures"]["outdoor"].is<float>()) {
|
||||
if (settings.sensors.outdoor.type == 1 && doc["temperatures"]["outdoor"].as<float>() > -100 && doc["temperatures"]["outdoor"].as<float>() < 100) {
|
||||
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 (!doc["restart"].isNull() && doc["restart"].is<bool>() && doc["restart"].as<bool>()) {
|
||||
vars.parameters.restartAfterTime = 5000;
|
||||
vars.parameters.restartSignalTime = millis();
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
@@ -261,8 +354,7 @@ protected:
|
||||
} else {
|
||||
client.publish(getTopicPath("status").c_str(), vars.states.otStatus ? "online" : "offline");
|
||||
}
|
||||
|
||||
forceARP();
|
||||
|
||||
prevPubVars = millis();
|
||||
}
|
||||
|
||||
@@ -275,7 +367,10 @@ protected:
|
||||
|
||||
static void publishHaEntities() {
|
||||
// main
|
||||
haHelper.publishSelectOutdoorTempSource();
|
||||
haHelper.publishSelectOutdoorSensorType();
|
||||
haHelper.publishSelectIndoorSensorType();
|
||||
haHelper.publishNumberOutdoorSensorOffset(false);
|
||||
haHelper.publishNumberIndoorSensorOffset(false);
|
||||
haHelper.publishSwitchDebug(false);
|
||||
|
||||
// emergency
|
||||
@@ -289,16 +384,18 @@ protected:
|
||||
//haHelper.publishNumberHeatingTarget(false);
|
||||
haHelper.publishNumberHeatingHysteresis();
|
||||
haHelper.publishSensorHeatingSetpoint(false);
|
||||
|
||||
// dhw
|
||||
haHelper.publishSwitchDHW(false);
|
||||
//haHelper.publishNumberDHWTarget(false);
|
||||
haHelper.publishSensorCurrentHeatingMinTemp(false);
|
||||
haHelper.publishSensorCurrentHeatingMaxTemp(false);
|
||||
haHelper.publishNumberHeatingMinTemp(false);
|
||||
haHelper.publishNumberHeatingMaxTemp(false);
|
||||
|
||||
// pid
|
||||
haHelper.publishSwitchPID();
|
||||
haHelper.publishNumberPIDFactorP();
|
||||
haHelper.publishNumberPIDFactorI();
|
||||
haHelper.publishNumberPIDFactorD();
|
||||
haHelper.publishNumberPIDMinTemp(false);
|
||||
haHelper.publishNumberPIDMaxTemp(false);
|
||||
|
||||
// equitherm
|
||||
haHelper.publishSwitchEquitherm();
|
||||
@@ -314,12 +411,11 @@ protected:
|
||||
haHelper.publishBinSensorStatus();
|
||||
haHelper.publishBinSensorOtStatus();
|
||||
haHelper.publishBinSensorHeating();
|
||||
haHelper.publishBinSensorDHW();
|
||||
haHelper.publishBinSensorFlame();
|
||||
haHelper.publishBinSensorFault();
|
||||
haHelper.publishBinSensorDiagnostic();
|
||||
haHelper.publishSensorFaultCode();
|
||||
haHelper.publishSensorRssi();
|
||||
haHelper.publishSensorRssi(false);
|
||||
|
||||
// sensors
|
||||
haHelper.publishSensorModulation(false);
|
||||
@@ -329,22 +425,45 @@ protected:
|
||||
haHelper.publishNumberIndoorTemp();
|
||||
//haHelper.publishNumberOutdoorTemp();
|
||||
haHelper.publishSensorHeatingTemp();
|
||||
haHelper.publishSensorDHWTemp();
|
||||
}
|
||||
|
||||
static bool publishNonStaticHaEntities(bool force = false) {
|
||||
static byte _heatingMinTemp;
|
||||
static byte _heatingMaxTemp;
|
||||
static byte _dhwMinTemp;
|
||||
static byte _dhwMaxTemp;
|
||||
static bool _editableOutdoorTemp;
|
||||
static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp;
|
||||
static bool _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent;
|
||||
|
||||
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;
|
||||
bool editableOutdoorTemp = settings.sensors.outdoor.type == 1;
|
||||
bool editableIndoorTemp = settings.sensors.indoor.type == 1;
|
||||
|
||||
if (force || _dhwPresent != settings.opentherm.dhwPresent) {
|
||||
_dhwPresent = settings.opentherm.dhwPresent;
|
||||
|
||||
if (_dhwPresent) {
|
||||
haHelper.publishSwitchDHW(false);
|
||||
haHelper.publishSensorCurrentDHWMinTemp(false);
|
||||
haHelper.publishSensorCurrentDHWMaxTemp(false);
|
||||
haHelper.publishNumberDHWMinTemp(false);
|
||||
haHelper.publishNumberDHWMaxTemp(false);
|
||||
haHelper.publishBinSensorDHW();
|
||||
haHelper.publishSensorDHWTemp();
|
||||
|
||||
} else {
|
||||
haHelper.deleteSwitchDHW();
|
||||
haHelper.deleteSensorCurrentDHWMinTemp();
|
||||
haHelper.deleteSensorCurrentDHWMaxTemp();
|
||||
haHelper.deleteNumberDHWMinTemp();
|
||||
haHelper.deleteNumberDHWMaxTemp();
|
||||
haHelper.deleteBinSensorDHW();
|
||||
haHelper.deleteSensorDHWTemp();
|
||||
haHelper.deleteNumberDHWTarget();
|
||||
haHelper.deleteClimateDHW();
|
||||
}
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
|
||||
if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) {
|
||||
@@ -360,7 +479,7 @@ protected:
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _dhwMinTemp != vars.parameters.dhwMinTemp || _dhwMaxTemp != vars.parameters.dhwMaxTemp) {
|
||||
if (_dhwPresent && (force || _dhwMinTemp != vars.parameters.dhwMinTemp || _dhwMaxTemp != vars.parameters.dhwMaxTemp)) {
|
||||
_dhwMinTemp = vars.parameters.dhwMinTemp;
|
||||
_dhwMaxTemp = vars.parameters.dhwMaxTemp;
|
||||
|
||||
@@ -384,6 +503,20 @@ protected:
|
||||
published = true;
|
||||
}
|
||||
|
||||
if (force || _editableIndoorTemp != editableIndoorTemp) {
|
||||
_editableIndoorTemp = editableIndoorTemp;
|
||||
|
||||
if (editableIndoorTemp) {
|
||||
haHelper.deleteSensorIndoorTemp();
|
||||
haHelper.publishNumberIndoorTemp();
|
||||
} else {
|
||||
haHelper.deleteNumberIndoorTemp();
|
||||
haHelper.publishSensorIndoorTemp();
|
||||
}
|
||||
|
||||
published = true;
|
||||
}
|
||||
|
||||
return published;
|
||||
}
|
||||
|
||||
@@ -391,7 +524,6 @@ protected:
|
||||
StaticJsonDocument<2048> doc;
|
||||
|
||||
doc["debug"] = settings.debug;
|
||||
doc["outdoorTempSource"] = settings.outdoorTempSource;
|
||||
|
||||
doc["emergency"]["enable"] = settings.emergency.enable;
|
||||
doc["emergency"]["target"] = settings.emergency.target;
|
||||
@@ -401,20 +533,32 @@ protected:
|
||||
doc["heating"]["turbo"] = settings.heating.turbo;
|
||||
doc["heating"]["target"] = settings.heating.target;
|
||||
doc["heating"]["hysteresis"] = settings.heating.hysteresis;
|
||||
doc["heating"]["minTemp"] = settings.heating.minTemp;
|
||||
doc["heating"]["maxTemp"] = settings.heating.maxTemp;
|
||||
|
||||
doc["dhw"]["enable"] = settings.dhw.enable;
|
||||
doc["dhw"]["target"] = settings.dhw.target;
|
||||
doc["dhw"]["minTemp"] = settings.dhw.minTemp;
|
||||
doc["dhw"]["maxTemp"] = settings.dhw.maxTemp;
|
||||
|
||||
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["pid"]["minTemp"] = settings.pid.minTemp;
|
||||
doc["pid"]["maxTemp"] = settings.pid.maxTemp;
|
||||
|
||||
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;
|
||||
|
||||
doc["sensors"]["outdoor"]["type"] = settings.sensors.outdoor.type;
|
||||
doc["sensors"]["outdoor"]["offset"] = settings.sensors.outdoor.offset;
|
||||
|
||||
doc["sensors"]["indoor"]["type"] = settings.sensors.indoor.type;
|
||||
doc["sensors"]["indoor"]["offset"] = settings.sensors.indoor.offset;
|
||||
|
||||
client.beginPublish(topic, measureJson(doc), false);
|
||||
//BufferingPrint bufferedClient(client, 32);
|
||||
//serializeJson(doc, bufferedClient);
|
||||
@@ -446,6 +590,7 @@ protected:
|
||||
doc["temperatures"]["heating"] = vars.temperatures.heating;
|
||||
doc["temperatures"]["dhw"] = vars.temperatures.dhw;
|
||||
|
||||
doc["parameters"]["heatingEnabled"] = vars.parameters.heatingEnabled;
|
||||
doc["parameters"]["heatingMinTemp"] = vars.parameters.heatingMinTemp;
|
||||
doc["parameters"]["heatingMaxTemp"] = vars.parameters.heatingMaxTemp;
|
||||
doc["parameters"]["heatingSetpoint"] = vars.parameters.heatingSetpoint;
|
||||
|
||||
@@ -3,16 +3,37 @@
|
||||
|
||||
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) {}
|
||||
|
||||
void static IRAM_ATTR handleInterrupt() {
|
||||
ot->handleInterrupt();
|
||||
}
|
||||
|
||||
protected:
|
||||
const char* getTaskName() {
|
||||
return "OpenTherm";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
vars.parameters.heatingMinTemp = settings.heating.minTemp;
|
||||
vars.parameters.heatingMaxTemp = settings.heating.maxTemp;
|
||||
vars.parameters.dhwMinTemp = settings.dhw.minTemp;
|
||||
vars.parameters.dhwMaxTemp = settings.dhw.maxTemp;
|
||||
|
||||
ot = new CustomOpenTherm(settings.opentherm.inPin, settings.opentherm.outPin);
|
||||
|
||||
ot->begin(handleInterrupt, responseCallback);
|
||||
ot->setHandleSendRequestCallback(sendRequestCallback);
|
||||
ot->setHandleSendRequestCallback(this->sendRequestCallback);
|
||||
ot->begin(OpenThermTask::handleInterrupt, this->responseCallback);
|
||||
|
||||
ot->setYieldCallback([](void* self) {
|
||||
static_cast<OpenThermTask*>(self)->delay(10);
|
||||
}, this);
|
||||
|
||||
#ifdef LED_OT_RX_PIN
|
||||
pinMode(LED_OT_RX_PIN, OUTPUT);
|
||||
@@ -23,7 +44,7 @@ protected:
|
||||
static byte currentHeatingTemp, currentDHWTemp = 0;
|
||||
unsigned long localResponse;
|
||||
|
||||
if ( setMasterMemberIdCode() ) {
|
||||
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);
|
||||
|
||||
@@ -31,25 +52,30 @@ protected:
|
||||
WARN("Slave member id failed");
|
||||
}
|
||||
|
||||
bool heatingEnable = (vars.states.emergency || settings.heating.enable) && pump && isReady();
|
||||
bool heatingEnabled = (vars.states.emergency || settings.heating.enable) && pump && isReady();
|
||||
localResponse = ot->setBoilerStatus(
|
||||
heatingEnable,
|
||||
settings.dhw.enable,
|
||||
heatingEnabled,
|
||||
settings.opentherm.dhwPresent && 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()));
|
||||
return;
|
||||
}
|
||||
|
||||
INFO_F("Heating enabled: %d\r\n", heatingEnable);
|
||||
setMaxModulationLevel(heatingEnable ? 100 : 0);
|
||||
if (vars.parameters.heatingEnabled != heatingEnabled) {
|
||||
vars.parameters.heatingEnabled = heatingEnabled;
|
||||
INFO_F("Heating enabled: %s\r\n", heatingEnabled ? "on\0" : "off\0");
|
||||
}
|
||||
|
||||
vars.states.heating = ot->isCentralHeatingActive(localResponse);
|
||||
vars.states.dhw = ot->isHotWaterActive(localResponse);
|
||||
vars.states.dhw = settings.opentherm.dhwPresent ? ot->isHotWaterActive(localResponse) : false;
|
||||
vars.states.flame = ot->isFlameOn(localResponse);
|
||||
vars.states.fault = ot->isFault(localResponse);
|
||||
vars.states.diagnostic = ot->isDiagnostic(localResponse);
|
||||
|
||||
setMaxModulationLevel(heatingEnabled ? 100 : 0);
|
||||
yield();
|
||||
|
||||
// Команды чтения данных котла
|
||||
@@ -60,10 +86,12 @@ protected:
|
||||
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);
|
||||
|
||||
updateMinMaxDhwTemp();
|
||||
if (settings.opentherm.dhwPresent) {
|
||||
updateMinMaxDhwTemp();
|
||||
}
|
||||
updateMinMaxHeatingTemp();
|
||||
|
||||
if (settings.outdoorTempSource == 0) {
|
||||
if (settings.sensors.outdoor.type == 0) {
|
||||
updateOutsideTemp();
|
||||
}
|
||||
if (vars.states.fault) {
|
||||
@@ -71,7 +99,7 @@ protected:
|
||||
ot->sendBoilerReset();
|
||||
}
|
||||
|
||||
if ( vars.states.diagnostic ) {
|
||||
if (vars.states.diagnostic) {
|
||||
ot->sendServiceReset();
|
||||
}
|
||||
|
||||
@@ -80,28 +108,27 @@ protected:
|
||||
}
|
||||
|
||||
updatePressure();
|
||||
if ( settings.dhw.enable || settings.heating.enable || heatingEnable ) {
|
||||
if ((settings.opentherm.dhwPresent && settings.dhw.enable) || settings.heating.enable || heatingEnabled) {
|
||||
updateModulationLevel();
|
||||
}
|
||||
|
||||
if ( settings.dhw.enable ) {
|
||||
} else {
|
||||
vars.sensors.modulation = 0;
|
||||
}
|
||||
yield();
|
||||
|
||||
if (settings.opentherm.dhwPresent) {
|
||||
updateDHWTemp();
|
||||
} else {
|
||||
vars.temperatures.dhw = 0;
|
||||
}
|
||||
|
||||
if ( settings.heating.enable || heatingEnable ) {
|
||||
updateHeatingTemp();
|
||||
} else {
|
||||
vars.temperatures.heating = 0;
|
||||
}
|
||||
|
||||
updateHeatingTemp();
|
||||
yield();
|
||||
|
||||
//
|
||||
// Температура ГВС
|
||||
byte newDHWTemp = settings.dhw.target;
|
||||
if (settings.dhw.enable && newDHWTemp != currentDHWTemp) {
|
||||
if (settings.opentherm.dhwPresent && settings.dhw.enable && (needSetDhwTemp() || newDHWTemp != currentDHWTemp)) {
|
||||
if (newDHWTemp < vars.parameters.dhwMinTemp || newDHWTemp > vars.parameters.dhwMaxTemp) {
|
||||
newDHWTemp = constrain(newDHWTemp, vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp);
|
||||
}
|
||||
@@ -111,6 +138,7 @@ protected:
|
||||
// Записываем заданную температуру ГВС
|
||||
if (ot->setDHWSetpoint(newDHWTemp)) {
|
||||
currentDHWTemp = newDHWTemp;
|
||||
dhwSetTempTime = millis();
|
||||
|
||||
} else {
|
||||
WARN("Failed set DHW temp");
|
||||
@@ -119,12 +147,13 @@ protected:
|
||||
|
||||
//
|
||||
// Температура отопления
|
||||
if (heatingEnable && fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001) {
|
||||
if (heatingEnabled && (needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001)) {
|
||||
INFO_F("Setting heating temp = %u \n", vars.parameters.heatingSetpoint);
|
||||
|
||||
// Записываем заданную температуру
|
||||
if (ot->setBoilerTemperature(vars.parameters.heatingSetpoint)) {
|
||||
currentHeatingTemp = vars.parameters.heatingSetpoint;
|
||||
heatingSetTempTime = millis();
|
||||
|
||||
} else {
|
||||
WARN("Failed set heating temp");
|
||||
@@ -141,16 +170,12 @@ protected:
|
||||
} else if (!pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) {
|
||||
pump = true;
|
||||
}
|
||||
|
||||
|
||||
} else if (!pump) {
|
||||
pump = true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -188,13 +213,27 @@ protected:
|
||||
}
|
||||
|
||||
protected:
|
||||
unsigned short readyTime = 60000;
|
||||
unsigned short dhwSetTempInterval = 60000;
|
||||
unsigned short heatingSetTempInterval = 60000;
|
||||
|
||||
bool pump = true;
|
||||
unsigned long prevUpdateNonEssentialVars = 0;
|
||||
unsigned long startupTime = millis();
|
||||
unsigned long dhwSetTempTime = 0;
|
||||
unsigned long heatingSetTempTime = 0;
|
||||
|
||||
|
||||
bool isReady() {
|
||||
return millis() - startupTime > 60000;
|
||||
return millis() - startupTime > readyTime;
|
||||
}
|
||||
|
||||
bool needSetDhwTemp() {
|
||||
return millis() - dhwSetTempTime > dhwSetTempInterval;
|
||||
}
|
||||
|
||||
bool needSetHeatingTemp() {
|
||||
return millis() - heatingSetTempTime > heatingSetTempInterval;
|
||||
}
|
||||
|
||||
void static printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
|
||||
@@ -219,33 +258,33 @@ protected:
|
||||
//=======================================================================================
|
||||
|
||||
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 ) {
|
||||
/*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
|
||||
);*/
|
||||
|
||||
} 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
|
||||
));
|
||||
|
||||
|
||||
return ot->isValidResponse(response);
|
||||
}
|
||||
|
||||
@@ -306,8 +345,8 @@ protected:
|
||||
byte maxTemp = (response & 0xFFFF) >> 8;
|
||||
|
||||
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||||
vars.parameters.dhwMinTemp = minTemp;
|
||||
vars.parameters.dhwMaxTemp = maxTemp;
|
||||
vars.parameters.dhwMinTemp = minTemp < settings.dhw.minTemp ? settings.dhw.minTemp : minTemp;
|
||||
vars.parameters.dhwMaxTemp = maxTemp > settings.dhw.maxTemp ? settings.dhw.maxTemp : maxTemp;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -325,8 +364,8 @@ protected:
|
||||
byte maxTemp = (response & 0xFFFF) >> 8;
|
||||
|
||||
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||||
vars.parameters.heatingMinTemp = minTemp;
|
||||
vars.parameters.heatingMaxTemp = maxTemp;
|
||||
vars.parameters.heatingMinTemp = minTemp < settings.heating.minTemp ? settings.heating.minTemp : minTemp;
|
||||
vars.parameters.heatingMaxTemp = maxTemp > settings.heating.maxTemp ? settings.heating.maxTemp : maxTemp;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -340,7 +379,7 @@ protected:
|
||||
return false;
|
||||
}
|
||||
|
||||
vars.temperatures.outdoor = ot->getFloat(response);
|
||||
vars.temperatures.outdoor = ot->getFloat(response) + settings.sensors.outdoor.offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#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 +18,14 @@ protected:
|
||||
float prevEtResult = 0;
|
||||
float prevPidResult = 0;
|
||||
|
||||
|
||||
void setup() {}
|
||||
const char* getTaskName() {
|
||||
return "Regulator";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
byte newTemp = vars.parameters.heatingSetpoint;
|
||||
|
||||
@@ -73,7 +79,7 @@ protected:
|
||||
float newTemp = 0;
|
||||
|
||||
// if use equitherm
|
||||
if (settings.emergency.useEquitherm && settings.outdoorTempSource != 1) {
|
||||
if (settings.emergency.useEquitherm && settings.sensors.outdoor.type != 1) {
|
||||
float etResult = getEquithermTemp(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||
|
||||
if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) {
|
||||
@@ -123,21 +129,24 @@ protected:
|
||||
}
|
||||
|
||||
// if use pid
|
||||
if (settings.pid.enable) {
|
||||
if (settings.pid.enable && vars.parameters.heatingEnabled) {
|
||||
float pidResult = getPidTemp(
|
||||
settings.equitherm.enable ? -30 : vars.parameters.heatingMinTemp,
|
||||
settings.equitherm.enable ? 30 : vars.parameters.heatingMaxTemp
|
||||
settings.equitherm.enable ? (settings.pid.maxTemp * -1) : settings.pid.minTemp,
|
||||
settings.equitherm.enable ? settings.pid.maxTemp : settings.pid.maxTemp
|
||||
);
|
||||
|
||||
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);
|
||||
INFO_F("[REGULATOR][PID] New result: %d (%f) \n", (int)round(pidResult), pidResult);
|
||||
|
||||
} else {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
} else if (settings.pid.enable && !vars.parameters.heatingEnabled && prevPidResult != 0) {
|
||||
newTemp += prevPidResult;
|
||||
}
|
||||
|
||||
// default temp, manual mode
|
||||
@@ -145,7 +154,9 @@ protected:
|
||||
newTemp = settings.heating.target;
|
||||
}
|
||||
|
||||
return round(newTemp);
|
||||
newTemp = round(newTemp);
|
||||
newTemp = constrain(newTemp, 0, 100);
|
||||
return newTemp;
|
||||
}
|
||||
|
||||
byte getTuningModeTemp() {
|
||||
@@ -269,7 +280,7 @@ protected:
|
||||
pidRegulator.input = vars.temperatures.indoor;
|
||||
pidRegulator.setpoint = settings.heating.target;
|
||||
|
||||
return pidRegulator.getResultTimer();
|
||||
return pidRegulator.getResultNow();
|
||||
}
|
||||
|
||||
float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) {
|
||||
|
||||
@@ -1,45 +1,152 @@
|
||||
#include <microDS18B20.h>
|
||||
#include <OneWire.h>
|
||||
#include <DallasTemperature.h>
|
||||
|
||||
MicroDS18B20<DS18B20_PIN> outdoorSensor;
|
||||
|
||||
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) {}
|
||||
|
||||
protected:
|
||||
OneWire* oneWireOutdoorSensor;
|
||||
OneWire* oneWireIndoorSensor;
|
||||
|
||||
DallasTemperature* outdoorSensor;
|
||||
DallasTemperature* indoorSensor;
|
||||
|
||||
bool initOutdoorSensor = false;
|
||||
unsigned long startConversionTime = 0;
|
||||
float filteredOutdoorTemp = 0;
|
||||
bool emptyOutdoorTemp = true;
|
||||
void setup() {}
|
||||
|
||||
bool initIndoorSensor = false;
|
||||
float filteredIndoorTemp = 0;
|
||||
bool emptyIndoorTemp = true;
|
||||
|
||||
|
||||
const char* getTaskName() {
|
||||
return "Sensors";
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// DS18B20 sensor
|
||||
if (outdoorSensor.online()) {
|
||||
if (outdoorSensor.readTemp()) {
|
||||
float rawTemp = outdoorSensor.getTemp();
|
||||
DEBUG_F("[SENSORS][DS18B20] Raw temp: %f \n", rawTemp);
|
||||
if (settings.sensors.outdoor.type == 2) {
|
||||
outdoorTemperatureSensor();
|
||||
}
|
||||
|
||||
if (emptyOutdoorTemp) {
|
||||
filteredOutdoorTemp = rawTemp;
|
||||
emptyOutdoorTemp = false;
|
||||
|
||||
} else {
|
||||
filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * OUTDOOR_SENSOR_FILTER_K;
|
||||
}
|
||||
|
||||
filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100;
|
||||
|
||||
if (fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099) {
|
||||
vars.temperatures.outdoor = filteredOutdoorTemp;
|
||||
INFO_F("[SENSORS][DS18B20] New temp: %f \n", filteredOutdoorTemp);
|
||||
}
|
||||
|
||||
} else {
|
||||
ERROR("[SENSORS][DS18B20] Invalid data from sensor");
|
||||
}
|
||||
|
||||
outdoorSensor.requestTemp();
|
||||
} else {
|
||||
ERROR("[SENSORS][DS18B20] Failed to connect to sensor");
|
||||
if (settings.sensors.indoor.type == 2) {
|
||||
indoorTemperatureSensor();
|
||||
}
|
||||
}
|
||||
|
||||
void outdoorTemperatureSensor() {
|
||||
if (!initOutdoorSensor) {
|
||||
oneWireOutdoorSensor = new OneWire(settings.sensors.outdoor.pin);
|
||||
outdoorSensor = new DallasTemperature(oneWireOutdoorSensor);
|
||||
outdoorSensor->begin();
|
||||
outdoorSensor->setResolution(12);
|
||||
outdoorSensor->setWaitForConversion(false);
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
initOutdoorSensor = true;
|
||||
}
|
||||
|
||||
unsigned long estimateConversionTime = millis() - startConversionTime;
|
||||
if (estimateConversionTime < outdoorSensor->millisToWaitForConversion()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = outdoorSensor->isConversionComplete();
|
||||
if (!completed && estimateConversionTime >= 1000) {
|
||||
// fail, retry
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
|
||||
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (no response)");
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = outdoorSensor->getTempCByIndex(0);
|
||||
if (rawTemp == DEVICE_DISCONNECTED_C) {
|
||||
ERROR("[SENSORS][OUTDOOR] Could not read temperature data (not connected)");
|
||||
|
||||
} else {
|
||||
DEBUG_F("[SENSORS][OUTDOOR] Raw temp: %f \n", rawTemp);
|
||||
|
||||
if (emptyOutdoorTemp) {
|
||||
filteredOutdoorTemp = rawTemp;
|
||||
emptyOutdoorTemp = false;
|
||||
|
||||
} else {
|
||||
filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100;
|
||||
|
||||
if (fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099) {
|
||||
vars.temperatures.outdoor = filteredOutdoorTemp + settings.sensors.outdoor.offset;
|
||||
INFO_F("[SENSORS][OUTDOOR] New temp: %f \n", filteredOutdoorTemp);
|
||||
}
|
||||
}
|
||||
|
||||
outdoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
}
|
||||
|
||||
void indoorTemperatureSensor() {
|
||||
if (!initIndoorSensor) {
|
||||
oneWireIndoorSensor = new OneWire(settings.sensors.indoor.pin);
|
||||
indoorSensor = new DallasTemperature(oneWireIndoorSensor);
|
||||
indoorSensor->begin();
|
||||
indoorSensor->setResolution(12);
|
||||
indoorSensor->setWaitForConversion(false);
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
initIndoorSensor = true;
|
||||
}
|
||||
|
||||
unsigned long estimateConversionTime = millis() - startConversionTime;
|
||||
if (estimateConversionTime < indoorSensor->millisToWaitForConversion()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = indoorSensor->isConversionComplete();
|
||||
if (!completed && estimateConversionTime >= 1000) {
|
||||
// fail, retry
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
|
||||
ERROR("[SENSORS][INDOOR] Could not read temperature data (no response)");
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
float rawTemp = indoorSensor->getTempCByIndex(0);
|
||||
if (rawTemp == DEVICE_DISCONNECTED_C) {
|
||||
ERROR("[SENSORS][INDOOR] Could not read temperature data (not connected)");
|
||||
|
||||
} else {
|
||||
DEBUG_F("[SENSORS][INDOOR] Raw temp: %f \n", rawTemp);
|
||||
|
||||
if (emptyIndoorTemp) {
|
||||
filteredIndoorTemp = rawTemp;
|
||||
emptyIndoorTemp = false;
|
||||
|
||||
} else {
|
||||
filteredIndoorTemp += (rawTemp - filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
|
||||
}
|
||||
|
||||
filteredIndoorTemp = floor(filteredIndoorTemp * 100) / 100;
|
||||
|
||||
if (fabs(vars.temperatures.indoor - filteredIndoorTemp) > 0.099) {
|
||||
vars.temperatures.indoor = filteredIndoorTemp + settings.sensors.indoor.offset;
|
||||
INFO_F("[SENSORS][INDOOR] New temp: %f \n", filteredIndoorTemp);
|
||||
}
|
||||
}
|
||||
|
||||
indoorSensor->requestTemperatures();
|
||||
startConversionTime = millis();
|
||||
}
|
||||
};
|
||||
@@ -1,18 +1,17 @@
|
||||
struct Settings {
|
||||
bool debug = false;
|
||||
// 0 - boiler, 1 - manual, 2 - ds18b20
|
||||
byte outdoorTempSource = 0;
|
||||
char hostname[80] = "opentherm";
|
||||
|
||||
struct {
|
||||
byte inPin = 5;
|
||||
byte outPin = 4;
|
||||
byte inPin = OT_IN_PIN_DEFAULT;
|
||||
byte outPin = OT_OUT_PIN_DEFAULT;
|
||||
unsigned int memberIdCode = 0;
|
||||
bool dhwPresent = true;
|
||||
} opentherm;
|
||||
|
||||
struct {
|
||||
char server[80];
|
||||
int port = 1883;
|
||||
unsigned int port = 1883;
|
||||
char user[32];
|
||||
char password[32];
|
||||
char prefix[80] = "opentherm";
|
||||
@@ -30,11 +29,15 @@ struct Settings {
|
||||
bool turbo = false;
|
||||
float target = 40.0f;
|
||||
float hysteresis = 0.5f;
|
||||
byte minTemp = 20.0f;
|
||||
byte maxTemp = 90.0f;
|
||||
} heating;
|
||||
|
||||
struct {
|
||||
bool enable = true;
|
||||
byte target = 40;
|
||||
byte minTemp = 30.0f;
|
||||
byte maxTemp = 60.0f;
|
||||
} dhw;
|
||||
|
||||
struct {
|
||||
@@ -42,6 +45,8 @@ struct Settings {
|
||||
float p_factor = 3;
|
||||
float i_factor = 0.2f;
|
||||
float d_factor = 0;
|
||||
byte minTemp = 0.0f;
|
||||
byte maxTemp = 90.0f;
|
||||
} pid;
|
||||
|
||||
struct {
|
||||
@@ -51,6 +56,23 @@ struct Settings {
|
||||
float t_factor = 2.0f;
|
||||
} equitherm;
|
||||
|
||||
struct {
|
||||
struct {
|
||||
// 0 - boiler, 1 - manual, 2 - ds18b20
|
||||
byte type = 0;
|
||||
byte pin = SENSOR_OUTDOOR_PIN_DEFAULT;
|
||||
float offset = 0.0f;
|
||||
} outdoor;
|
||||
|
||||
struct {
|
||||
// 1 - manual, 2 - ds18b20
|
||||
byte type = 1;
|
||||
byte pin = SENSOR_INDOOR_PIN_DEFAULT;
|
||||
float offset = 0.0f;
|
||||
} indoor;
|
||||
} sensors;
|
||||
|
||||
char validationValue[8] = SETTINGS_VALID_VALUE;
|
||||
} settings;
|
||||
|
||||
struct Variables {
|
||||
@@ -84,6 +106,9 @@ struct Variables {
|
||||
} temperatures;
|
||||
|
||||
struct {
|
||||
unsigned long restartSignalTime = 0;
|
||||
unsigned int restartAfterTime = 0;
|
||||
bool heatingEnabled = false;
|
||||
byte heatingMinTemp = 20;
|
||||
byte heatingMaxTemp = 90;
|
||||
byte heatingSetpoint = 0.0f;
|
||||
|
||||
@@ -1,57 +1,105 @@
|
||||
#define WM_MDNS
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameters.h>
|
||||
#include <netif/etharp.h>
|
||||
|
||||
|
||||
// Wifimanager
|
||||
WiFiManager wm;
|
||||
WiFiManagerParameter* wmHostname;
|
||||
WiFiManagerParameter* wmOtInPin;
|
||||
WiFiManagerParameter* wmOtOutPin;
|
||||
WiFiManagerParameter* wmOtMemberIdCode;
|
||||
WiFiManagerParameter* wmMqttServer;
|
||||
WiFiManagerParameter* wmMqttPort;
|
||||
UnsignedIntParameter* wmMqttPort;
|
||||
WiFiManagerParameter* wmMqttUser;
|
||||
WiFiManagerParameter* wmMqttPassword;
|
||||
WiFiManagerParameter* wmMqttPrefix;
|
||||
UnsignedIntParameter* wmMqttPublishInterval;
|
||||
UnsignedIntParameter* wmOtInPin;
|
||||
UnsignedIntParameter* wmOtOutPin;
|
||||
UnsignedIntParameter* wmOtMemberIdCode;
|
||||
CheckboxParameter* wmOtDHWPresent;
|
||||
UnsignedIntParameter* wmOutdoorSensorPin;
|
||||
UnsignedIntParameter* wmIndoorSensorPin;
|
||||
|
||||
class WifiManagerTask: public Task {
|
||||
SeparatorParameter* wmSep1;
|
||||
SeparatorParameter* wmSep2;
|
||||
|
||||
|
||||
class WifiManagerTask : public Task {
|
||||
public:
|
||||
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0): Task(_enabled, _interval) {}
|
||||
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
|
||||
|
||||
protected:
|
||||
bool connected = false;
|
||||
unsigned long lastArpGratuitous = 0;
|
||||
|
||||
const char* getTaskName() {
|
||||
return "WifiManager";
|
||||
}
|
||||
|
||||
int getTaskCore() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
//WiFi.mode(WIFI_STA);
|
||||
wm.setDebugOutput(settings.debug);
|
||||
//wm.setDebugOutput(settings.debug, WM_DEBUG_VERBOSE);
|
||||
|
||||
wm.setTitle("OpenTherm Gateway");
|
||||
wm.setCustomMenuHTML(PSTR(
|
||||
"<style>.wrap h1 {display: none;} .wrap h3 {display: none;} .nh {margin: 0 0 1em 0;} .nh .logo {font-size: 1.8em; margin: 0.5em; text-align: center;} .nh .links {text-align: center;}</style>"
|
||||
"<div class=\"nh\">"
|
||||
"<div class=\"logo\">OpenTherm Gateway</div>"
|
||||
"<div class=\"links\"><a href=\"" OT_GATEWAY_REPO "\" target=\"_blank\">Repo</a> | <a href=\"" OT_GATEWAY_REPO "/issues\" target=\"_blank\">Issues</a> | <a href=\"" OT_GATEWAY_REPO "/releases\" target=\"_blank\">Releases</a> | <small>v" OT_GATEWAY_VERSION " (" __DATE__ ")</small></div>"
|
||||
"</div>"
|
||||
));
|
||||
|
||||
std::vector<const char *> menu = {"custom", "wifi", "param", "sep", "info", "update", "restart"};
|
||||
wm.setMenu(menu);
|
||||
|
||||
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
|
||||
wm.addParameter(wmHostname);
|
||||
|
||||
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);
|
||||
wmMqttPort = new UnsignedIntParameter("mqtt_port", "MQTT port", settings.mqtt.port, 6);
|
||||
wm.addParameter(wmMqttPort);
|
||||
|
||||
wmMqttUser = new WiFiManagerParameter("mqtt_user", "MQTT username", settings.mqtt.user, 32);
|
||||
wm.addParameter(wmMqttUser);
|
||||
|
||||
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32);
|
||||
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32, "type=\"password\"");
|
||||
wm.addParameter(wmMqttPassword);
|
||||
|
||||
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32);
|
||||
wm.addParameter(wmMqttPrefix);
|
||||
|
||||
wmMqttPublishInterval = new UnsignedIntParameter("mqtt_publish_interval", "MQTT publish interval", settings.mqtt.interval, 5);
|
||||
wm.addParameter(wmMqttPublishInterval);
|
||||
|
||||
wmSep1 = new SeparatorParameter();
|
||||
wm.addParameter(wmSep1);
|
||||
|
||||
wmOtInPin = new UnsignedIntParameter("ot_in_pin", "Opentherm pin IN", settings.opentherm.inPin, 2);
|
||||
wm.addParameter(wmOtInPin);
|
||||
|
||||
wmOtOutPin = new UnsignedIntParameter("ot_out_pin", "Opentherm pin OUT", settings.opentherm.outPin, 2);
|
||||
wm.addParameter(wmOtOutPin);
|
||||
|
||||
wmOtMemberIdCode = new UnsignedIntParameter("ot_member_id_code", "Opentherm member id", settings.opentherm.memberIdCode, 5);
|
||||
wm.addParameter(wmOtMemberIdCode);
|
||||
|
||||
wmOtDHWPresent = new CheckboxParameter("ot_dhw_present", "Opentherm DHW present", settings.opentherm.dhwPresent);
|
||||
wm.addParameter(wmOtDHWPresent);
|
||||
|
||||
wmSep2 = new SeparatorParameter();
|
||||
wm.addParameter(wmSep2);
|
||||
|
||||
wmOutdoorSensorPin = new UnsignedIntParameter("outdoor_sensor_pin", "Outdoor sensor pin", settings.sensors.outdoor.pin, 2);
|
||||
wm.addParameter(wmOutdoorSensorPin);
|
||||
|
||||
wmIndoorSensorPin = new UnsignedIntParameter("indoor_sensor_pin", "Indoor sensor pin", settings.sensors.indoor.pin, 2);
|
||||
wm.addParameter(wmIndoorSensorPin);
|
||||
|
||||
//wm.setCleanConnect(true);
|
||||
wm.setRestorePersistent(false);
|
||||
|
||||
@@ -60,70 +108,189 @@ protected:
|
||||
wm.setAPClientCheck(true);
|
||||
wm.setConfigPortalBlocking(false);
|
||||
wm.setSaveParamsCallback(saveParamsCallback);
|
||||
wm.setConfigPortalTimeout(180);
|
||||
//wm.setDisableConfigPortal(false);
|
||||
wm.setConfigPortalTimeout(wm.getWiFiIsSaved() ? 180 : 0);
|
||||
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;
|
||||
|
||||
if (wm.getWebPortalActive()) {
|
||||
wm.stopWebPortal();
|
||||
}
|
||||
|
||||
#ifdef USE_TELNET
|
||||
TelnetStream.stop();
|
||||
#endif
|
||||
|
||||
INFO("[wifi] Disconnected");
|
||||
|
||||
} else if (!connected && WiFi.status() == WL_CONNECTED) {
|
||||
connected = true;
|
||||
|
||||
wm.setConfigPortalTimeout(180);
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
if (!wm.getWebPortalActive()) {
|
||||
wm.startWebPortal();
|
||||
}
|
||||
|
||||
#ifdef USE_TELNET
|
||||
TelnetStream.begin();
|
||||
#endif
|
||||
|
||||
INFO_F("[wifi] Connected. IP address: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED && !wm.getWebPortalActive() && !wm.getConfigPortalActive()) {
|
||||
wm.startWebPortal();
|
||||
#if defined(ESP8266)
|
||||
if (connected && millis() - lastArpGratuitous > 60000) {
|
||||
arpGratuitous();
|
||||
lastArpGratuitous = millis();
|
||||
}
|
||||
#endif
|
||||
|
||||
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());
|
||||
static void saveParamsCallback() {
|
||||
bool changed = false;
|
||||
bool needRestart = false;
|
||||
|
||||
if (strcmp(wmHostname->getValue(), settings.hostname) != 0) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
strcpy(settings.hostname, wmHostname->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttServer->getValue(), settings.mqtt.server) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.server, wmMqttServer->getValue());
|
||||
}
|
||||
|
||||
if (wmMqttPort->getValue() != settings.mqtt.port) {
|
||||
changed = true;
|
||||
|
||||
settings.mqtt.port = wmMqttPort->getValue();
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttUser->getValue(), settings.mqtt.user) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.user, wmMqttUser->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttPassword->getValue(), settings.mqtt.password) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
|
||||
}
|
||||
|
||||
if (strcmp(wmMqttPrefix->getValue(), settings.mqtt.prefix) != 0) {
|
||||
changed = true;
|
||||
|
||||
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
|
||||
}
|
||||
|
||||
if (wmMqttPublishInterval->getValue() != settings.mqtt.interval) {
|
||||
changed = true;
|
||||
|
||||
settings.mqtt.interval = wmMqttPublishInterval->getValue();
|
||||
}
|
||||
|
||||
if (wmOtInPin->getValue() != settings.opentherm.inPin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.opentherm.inPin = wmOtInPin->getValue();
|
||||
}
|
||||
|
||||
if (wmOtOutPin->getValue() != settings.opentherm.outPin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.opentherm.outPin = wmOtOutPin->getValue();
|
||||
}
|
||||
|
||||
if (wmOtMemberIdCode->getValue() != settings.opentherm.memberIdCode) {
|
||||
changed = true;
|
||||
|
||||
settings.opentherm.memberIdCode = wmOtMemberIdCode->getValue();
|
||||
}
|
||||
|
||||
if (wmOtDHWPresent->getCheckboxValue() != settings.opentherm.dhwPresent) {
|
||||
changed = true;
|
||||
|
||||
settings.opentherm.dhwPresent = wmOtDHWPresent->getCheckboxValue();
|
||||
}
|
||||
|
||||
if (wmOutdoorSensorPin->getValue() != settings.sensors.outdoor.pin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.sensors.outdoor.pin = wmOutdoorSensorPin->getValue();
|
||||
}
|
||||
|
||||
if (wmIndoorSensorPin->getValue() != settings.sensors.indoor.pin) {
|
||||
changed = true;
|
||||
needRestart = true;
|
||||
|
||||
settings.sensors.indoor.pin = wmIndoorSensorPin->getValue();
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (needRestart) {
|
||||
vars.parameters.restartAfterTime = 5000;
|
||||
vars.parameters.restartSignalTime = millis();
|
||||
}
|
||||
|
||||
INFO_F(
|
||||
"New settings:\r\n"
|
||||
" Hostname: %s\r\n"
|
||||
" 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",
|
||||
" Mqtt pass: %s\r\n"
|
||||
" Mqtt prefix: %s\r\n"
|
||||
" Mqtt publish interval: %d\r\n"
|
||||
" OT in pin: %d\r\n"
|
||||
" OT out pin: %d\r\n"
|
||||
" OT member id code: %d\r\n"
|
||||
" OT DHW present: %d\r\n"
|
||||
" Outdoor sensor pin: %d\r\n"
|
||||
" Indoor sensor pin: %d\r\n",
|
||||
settings.hostname,
|
||||
settings.opentherm.inPin,
|
||||
settings.opentherm.outPin,
|
||||
settings.opentherm.memberIdCode,
|
||||
settings.mqtt.server,
|
||||
settings.mqtt.port,
|
||||
settings.mqtt.user,
|
||||
settings.mqtt.password
|
||||
settings.mqtt.password,
|
||||
settings.mqtt.prefix,
|
||||
settings.mqtt.interval,
|
||||
settings.opentherm.inPin,
|
||||
settings.opentherm.outPin,
|
||||
settings.opentherm.memberIdCode,
|
||||
settings.opentherm.dhwPresent,
|
||||
settings.sensors.outdoor.pin,
|
||||
settings.sensors.indoor.pin
|
||||
);
|
||||
|
||||
eeSettings.updateNow();
|
||||
INFO(F("Settings saved"));
|
||||
}
|
||||
|
||||
bool connected = false;
|
||||
static void arpGratuitous() {
|
||||
struct netif* netif = netif_list;
|
||||
while (netif) {
|
||||
etharp_gratuitous(netif);
|
||||
netif = netif->next;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
#define OT_GATEWAY_VERSION "1.2.1"
|
||||
#define OT_GATEWAY_VERSION "1.3.3"
|
||||
#define OT_GATEWAY_REPO "https://github.com/Laxilef/OTGateway"
|
||||
#define AP_SSID "OpenTherm Gateway"
|
||||
#define AP_PASSWORD "otgateway123456"
|
||||
#define USE_TELNET
|
||||
@@ -9,18 +10,30 @@
|
||||
|
||||
#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!
|
||||
|
||||
|
||||
|
||||
#ifndef OT_IN_PIN_DEFAULT
|
||||
#define OT_IN_PIN_DEFAULT 0
|
||||
#endif
|
||||
|
||||
#ifndef OT_OUT_PIN_DEFAULT
|
||||
#define OT_OUT_PIN_DEFAULT 0
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_OUTDOOR_PIN_DEFAULT
|
||||
#define SENSOR_OUTDOOR_PIN_DEFAULT 0
|
||||
#endif
|
||||
|
||||
#ifndef SENSOR_INDOOR_PIN_DEFAULT
|
||||
#define SENSOR_INDOOR_PIN_DEFAULT 0
|
||||
#endif
|
||||
|
||||
#ifdef USE_TELNET
|
||||
#define INFO_STREAM TelnetStream
|
||||
#define WARN_STREAM TelnetStream
|
||||
@@ -43,4 +56,4 @@
|
||||
#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__);
|
||||
|
||||
char buffer[120];
|
||||
char buffer[255];
|
||||
43
src/main.cpp
43
src/main.cpp
@@ -3,13 +3,22 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include <TelnetStream.h>
|
||||
#include <EEManager.h>
|
||||
#include <Scheduler.h>
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#include "Settings.h"
|
||||
|
||||
EEManager eeSettings(settings, 30000);
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <ESP32Scheduler.h>
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <Scheduler.h>
|
||||
#include <Task.h>
|
||||
#include <LeanTask.h>
|
||||
#elif
|
||||
#error Wrong board. Supported boards: esp8266, esp32
|
||||
#endif
|
||||
|
||||
#include "WifiManagerTask.h"
|
||||
#include "MqttTask.h"
|
||||
#include "OpenThermTask.h"
|
||||
@@ -27,19 +36,23 @@ MainTask* tMain;
|
||||
|
||||
|
||||
void setup() {
|
||||
#ifdef USE_TELNET
|
||||
TelnetStream.begin();
|
||||
delay(5000);
|
||||
#else
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n\n");
|
||||
#endif
|
||||
#ifndef USE_TELNET
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n\n");
|
||||
#endif
|
||||
|
||||
EEPROM.begin(eeSettings.blockSize());
|
||||
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
|
||||
if (eeSettingsResult == 0) {
|
||||
INFO("Settings loaded");
|
||||
|
||||
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
|
||||
INFO("Settings not valid, reset and restart...");
|
||||
eeSettings.reset();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
} else if (eeSettingsResult == 1) {
|
||||
INFO("Settings NOT loaded, first start");
|
||||
|
||||
@@ -56,16 +69,20 @@ void setup() {
|
||||
tOt = new OpenThermTask(false);
|
||||
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);
|
||||
tMain = new MainTask(true, 50);
|
||||
Scheduler.start(tMain);
|
||||
|
||||
Scheduler.begin();
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
void loop() {
|
||||
#if defined(ESP32)
|
||||
vTaskDelete(NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
21
tools/build.py
Normal file
21
tools/build.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import shutil
|
||||
import os
|
||||
Import("env")
|
||||
|
||||
def post_build(source, target, env):
|
||||
if os.path.exists(os.path.join(env["PROJECT_DIR"], "build")) == False:
|
||||
return
|
||||
|
||||
files = {
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.bin"): "firmware_%s_%s.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
env.subst("$BUILD_DIR/${PROGNAME}.factory.bin"): "firmware_%s_%s.factory.bin" % (env["PIOENV"], env.GetProjectOption("version")),
|
||||
}
|
||||
|
||||
for src in files:
|
||||
if os.path.exists(src):
|
||||
dest = os.path.join(env["PROJECT_DIR"], "build", files[src])
|
||||
|
||||
print("Copying '%s' to '%s'" % (src, dest))
|
||||
shutil.copy(src, dest)
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", post_build)
|
||||
79
tools/esp32.py
Normal file
79
tools/esp32.py
Normal 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)
|
||||
Reference in New Issue
Block a user