* feat: new portal & network manager

* refactor: migrate from PubSubClient to ArduinoMqttClient
* refactor: migrate from EEManager to FileData
* chore: bump ESP Telnet to 2.2
* chore: bump TinyLogger to 1.1.0
This commit is contained in:
Yurii
2024-01-12 18:29:55 +03:00
parent b36e4dca42
commit ab1e9c761f
34 changed files with 4683 additions and 1125 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.pio
.vscode
build/*
build/*
secrets.ini

230
data/index.html Normal file
View File

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

166
data/network.html Normal file
View File

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

275
data/settings.html Normal file
View File

@@ -0,0 +1,275 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Settings - OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css" />
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a href="/"><kbd>OpenTherm Gateway</kbd></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2>Portal settings</h2>
<p></p>
</hgroup>
<div id="portal-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="portal-settings" class="hidden">
<div class="grid">
<label for="portal-login">
Login
<input type="text" class="portal-login" name="portal[login]" maxlength="12" required>
</label>
<label for="portal-password">
Password
<input type="password" class="portal-password" name="portal[password]" maxlength="32" required>
</label>
</div>
<label for="portal-use-auth">
<input type="checkbox" class="portal-use-auth" name="portal[useAuth]" value="true">
Use auth
</label>
<br>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>OpenTherm settings</h2>
<p></p>
</hgroup>
<div id="opentherm-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="opentherm-settings" class="hidden">
<div class="grid">
<label for="opentherm-in-pin">
IN gpio
<input type="number" class="opentherm-in-pin" name="opentherm[inPin]" maxlength="2" required>
</label>
<label for="opentherm-in-pin">
OUT gpio
<input type="number" class="opentherm-out-pin" name="opentherm[outPin]" maxlength="2" required>
</label>
<label for="opentherm-member-id-code">
Master MemberID code
<input type="number" class="opentherm-member-id-code" name="opentherm[memberIdCode]" maxlength="2" required>
</label>
</div>
<fieldset>
<legend>Options</legend>
<label for="opentherm-dhw-present">
<input type="checkbox" class="opentherm-dhw-present" name="opentherm[dhwPresent]" value="true">
DHW present
</label>
<label for="opentherm-sw-mode">
<input type="checkbox" class="opentherm-sw-mode" name="opentherm[summerWinterMode]" value="true">
Summer/winter mode
</label>
<label for="opentherm-heating-ch2-enabled">
<input type="checkbox" class="opentherm-heating-ch2-enabled" name="opentherm[heatingCh2Enabled]" value="true">
Heating CH2 always enabled
</label>
<label for="opentherm-heating-ch1-to-ch2">
<input type="checkbox" class="opentherm-heating-ch1-to-ch2" name="opentherm[heatingCh1ToCh2]" value="true">
Duplicate heating CH1 to CH2
</label>
<label for="opentherm-dhw-to-ch2">
<input type="checkbox" class="opentherm-dhw-to-ch2" name="opentherm[dhwToCh2]" value="true">
Duplicate DHW to CH2
</label>
<label for="opentherm-dhw-blocking">
<input type="checkbox" class="opentherm-dhw-blocking" name="opentherm[dhwBlocking]" value="true">
DHW blocking
</label>
<label for="opentherm-sync-modulation-with-heating">
<input type="checkbox" class="opentherm-sync-modulation-with-heating" name="opentherm[modulationSyncWithHeating]" value="true">
Sync modulation with heating
</label>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>MQTT settings</h2>
<p></p>
</hgroup>
<div id="mqtt-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="mqtt-settings" class="hidden">
<div class="grid">
<label for="mqtt-server">
Server
<input type="text" class="mqtt-server" name="mqtt[server]" maxlength="80" required>
</label>
<label for="mqtt-port">
Port
<input type="number" class="mqtt-port" name="mqtt[port]" maxlength="5" required>
</label>
</div>
<div class="grid">
<label for="mqtt-user">
User
<input type="text" class="mqtt-user" name="mqtt[user]" maxlength="32" required>
</label>
<label for="mqtt-password">
Password
<input type="password" class="mqtt-password" name="mqtt[password]" maxlength="32">
</label>
</div>
<div class="grid">
<label for="mqtt-prefix">
Prefix
<input type="text" class="mqtt-prefix" name="mqtt[prefix]" maxlength="32" required>
</label>
<label for="mqtt-interval">
Publish interval <small>(sec)</small>
<input type="number" class="mqtt-interval" name="mqtt[interval]" maxlength="2" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>Sensor settings</h2>
<p></p>
</hgroup>
<div id="sensors-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="sensors-settings" class="hidden">
<div class="grid">
<label for="sensors-outdoor-pin">
Outdoor GPIO
<input type="number" class="sensors-outdoor-pin" name="sensors[outdoor][pin]" maxlength="2" required>
</label>
<label for="sensors-outdoor-offset">
Outdoor offset
<input type="number" class="sensors-outdoor-offset" name="sensors[outdoor][offset]" maxlength="2" required>
</label>
</div>
<hr><br>
<div class="grid">
<label for="sensors-indoor-pin">
Indoor GPIO
<input type="number" class="sensors-indoor-pin" name="sensors[indoor][pin]" maxlength="2" required>
</label>
<label for="sensors-indoor-offset">
Indoor offset
<input type="number" class="sensors-indoor-offset" name="sensors[indoor][offset]" maxlength="2" required>
</label>
</div>
<label for="sensors-indoor-ble-addresss">
BLE addresss
<input type="text" class="sensors-indoor-ble-addresss" name="sensors[indoor][bleAddresss]" maxlength="17">
<small>ONLY for ESP32</small>
</label>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>External pump settings</h2>
<p></p>
</hgroup>
<div id="extpump-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="extpump-settings" class="hidden">
<label for="extpump-use">
<input type="checkbox" class="extpump-use" name="externalPump[use]" value="true">
Use external pump
</label>
<br>
<div class="grid">
<label for="extpump-pin">
Relay GPIO
<input type="number" class="extpump-pin" name="externalPump[pin]" maxlength="2" required>
</label>
<label for="extpump-pc-time">
Post circulation time <small>(min)</small>
<input type="number" class="extpump-pc-time" name="externalPump[postCirculationTime]" maxlength="2" required>
</label>
</div>
<div class="grid">
<label for="extpump-as-interval">
Anti stuck interval <small>(days)</small>
<input type="number" class="extpump-as-interval" name="externalPump[antiStuckInterval]" maxlength="3" required>
</label>
<label for="extpump-as-time">
Anti stuck time <small>(min)</small>
<input type="number" class="extpump-as-time" name="externalPump[antiStuckTime]" maxlength="2" required>
</label>
</div>
<button type="submit">Save</button>
</form>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary">License</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary">Source code</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary">Help</a>
<a href="https://github.com/Laxilef/OTGateway/issue" target="_blank" class="secondary">Issue & questions</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary">Releases</a>
</small>
</footer>
<script src="/static/app.js"></script>
<script>
window.onload = async function () {
await loadSettings();
setupForm('#portal-settings');
setupForm('#opentherm-settings');
setupForm('#mqtt-settings');
setupForm('#sensors-settings');
setupForm('#extpump-settings');
};
</script>
</body>
</html>

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

108
data/upgrade.html Normal file
View File

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

View File

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

View File

@@ -0,0 +1,129 @@
#include "Connection.h"
void Connection::setup(bool useDhcp) {
setUseDhcp(useDhcp);
#if defined(ARDUINO_ARCH_ESP8266)
wifi_set_event_handler_cb(Connection::onEvent);
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.onEvent(Connection::onEvent);
#endif
}
void Connection::setUseDhcp(bool value) {
useDhcp = value;
}
Connection::Status Connection::getStatus() {
return status;
}
Connection::DisconnectReason Connection::getDisconnectReason() {
return disconnectReason;
}
#if defined(ARDUINO_ARCH_ESP8266)
void Connection::onEvent(System_Event_t *evt) {
switch (evt->event) {
case EVENT_STAMODE_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_GOT_IP:
status = Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case EVENT_STAMODE_DHCP_TIMEOUT:
status = Status::DISCONNECTED;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case EVENT_STAMODE_DISCONNECTED:
status = Status::DISCONNECTED;
disconnectReason = convertDisconnectReason(evt->event_info.disconnected.reason);
break;
default:
break;
}
}
#elif defined(ARDUINO_ARCH_ESP32)
void Connection::onEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
status = useDhcp ? Status::CONNECTING : Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
status = Status::CONNECTED;
disconnectReason = DisconnectReason::NONE;
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
status = Status::DISCONNECTED;
disconnectReason = DisconnectReason::DHCP_TIMEOUT;
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
status = Status::DISCONNECTED;
disconnectReason = convertDisconnectReason(info.wifi_sta_disconnected.reason);
break;
default:
break;
}
//Serial.printf("SYS EVENT: %d, reason: %d\n\r", evt->event, disconnectReason);
}
#endif
Connection::DisconnectReason Connection::convertDisconnectReason(uint8_t reason) {
switch (reason) {
#if defined(ARDUINO_ARCH_ESP8266)
case REASON_BEACON_TIMEOUT:
return DisconnectReason::BEACON_TIMEOUT;
case REASON_NO_AP_FOUND:
return DisconnectReason::NO_AP_FOUND;
case REASON_AUTH_FAIL:
return DisconnectReason::AUTH_FAIL;
case REASON_ASSOC_FAIL:
return DisconnectReason::ASSOC_FAIL;
case REASON_HANDSHAKE_TIMEOUT:
return DisconnectReason::HANDSHAKE_TIMEOUT;
#elif defined(ARDUINO_ARCH_ESP32)
case WIFI_REASON_BEACON_TIMEOUT:
return DisconnectReason::BEACON_TIMEOUT;
case WIFI_REASON_NO_AP_FOUND:
return DisconnectReason::NO_AP_FOUND;
case WIFI_REASON_AUTH_FAIL:
return DisconnectReason::AUTH_FAIL;
case WIFI_REASON_ASSOC_FAIL:
return DisconnectReason::ASSOC_FAIL;
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return DisconnectReason::HANDSHAKE_TIMEOUT;
#endif
default:
return DisconnectReason::OTHER;
}
}
bool Connection::useDhcp = false;
Connection::Status Connection::status = Status::DISCONNECTED;
Connection::DisconnectReason Connection::disconnectReason = DisconnectReason::NONE;

View File

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

View File

@@ -3,6 +3,15 @@
#define PROGMEM
#endif
const char HA_ENTITY_BINARY_SENSOR[] PROGMEM = "binary_sensor";
const char HA_ENTITY_BUTTON[] PROGMEM = "button";
const char HA_ENTITY_FAN[] PROGMEM = "fan";
const char HA_ENTITY_CLIMATE[] PROGMEM = "climate";
const char HA_ENTITY_NUMBER[] PROGMEM = "number";
const char HA_ENTITY_SELECT[] PROGMEM = "select";
const char HA_ENTITY_SENSOR[] PROGMEM = "sensor";
const char HA_ENTITY_SWITCH[] PROGMEM = "switch";
const char HA_DEVICE[] PROGMEM = "device";
const char HA_IDENTIFIERS[] PROGMEM = "identifiers";
const char HA_SW_VERSION[] PROGMEM = "sw_version";

View File

@@ -1,6 +1,6 @@
#pragma once
#include <Arduino.h>
#include <PubSubClient.h>
#include <MqttClient.h>
#ifdef ARDUINO_ARCH_ESP32
#include <mutex>
#endif
@@ -8,7 +8,7 @@
class MqttWriter {
public:
MqttWriter(PubSubClient* client, size_t bufferSize = 64) {
MqttWriter(MqttClient* client, size_t bufferSize = 64) {
this->client = client;
this->bufferSize = bufferSize;
this->buffer = (uint8_t*) malloc(bufferSize * sizeof(*this->buffer));
@@ -94,9 +94,10 @@ public:
this->bufferPos = 0;
size_t docSize = measureJson(doc);
size_t written = 0;
if (this->client->beginPublish(topic, docSize, retained)) {
if (this->client->beginMessage(topic, docSize, retained)) {
serializeJson(doc, *this);
this->flush();
this->client->endMessage();
written = this->writeAfterLock;
}
@@ -110,7 +111,7 @@ public:
}
bool publish(const char* topic, const char* buffer, bool retained = false) {
return this->publish(topic, (uint8_t*) buffer, strlen(buffer), retained);
return this->publish(topic, (const uint8_t*) buffer, strlen(buffer), retained);
}
bool publish(const char* topic, const uint8_t* buffer, size_t length, bool retained = false) {
@@ -128,12 +129,13 @@ public:
this->bufferPos = 0;
size_t written = 0;
bool result = false;
if (length == 0) {
result = this->client->publish(topic, nullptr, 0, retained);
if (!length || buffer == nullptr) {
result = this->client->beginMessage(topic, 0, retained) && this->client->endMessage();
} else if (this->client->beginPublish(topic, length, retained)) {
} else if (this->client->beginMessage(topic, length, retained)) {
this->write(buffer, length);
this->flush();
this->client->endMessage();
written = this->writeAfterLock;
result = written == length;
@@ -204,7 +206,7 @@ public:
}
protected:
PubSubClient* client;
MqttClient* client;
uint8_t* buffer;
size_t bufferSize = 64;
size_t bufferPos = 0;

View File

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

View File

@@ -0,0 +1,96 @@
#include <FS.h>
class StaticPage : public RequestHandler {
public:
typedef std::function<bool(HTTPMethod, const String&)> canHandleFunction;
typedef std::function<bool()> beforeSendFunction;
StaticPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) {
this->uri = uri;
this->fs = fs;
this->path = path;
this->cacheHeader = cacheHeader;
}
StaticPage* setCanHandleFunction(canHandleFunction val = nullptr) {
this->canHandleFn = val;
return this;
}
StaticPage* setBeforeSendFunction(beforeSendFunction val = nullptr) {
this->beforeSendFn = val;
return this;
}
#if defined(ARDUINO_ARCH_ESP32)
bool canHandle(HTTPMethod method, const String uri) override {
#else
bool canHandle(HTTPMethod method, const String& uri) override {
#endif
return method == HTTP_GET && uri.equals(this->uri) && (!this->canHandleFn || this->canHandleFn(method, uri));
}
#if defined(ARDUINO_ARCH_ESP32)
bool handle(WebServer& server, HTTPMethod method, const String uri) override {
#else
bool handle(WebServer& server, HTTPMethod method, const String& uri) override {
#endif
if (!this->canHandle(method, uri)) {
return false;
}
if (this->beforeSendFn && !this->beforeSendFn()) {
return true;
}
#if defined(ARDUINO_ARCH_ESP8266)
if (server._eTagEnabled) {
if (server._eTagFunction) {
this->eTag = (server._eTagFunction)(*this->fs, this->path);
} else if (this->eTag.isEmpty()) {
this->eTag = esp8266webserver::calcETag(*this->fs, this->path);
}
if (server.header("If-None-Match").equals(this->eTag.c_str())) {
server.send(304);
return true;
}
}
#endif
File file = this->fs->open(this->path, "r");
if (!file) {
return false;
} else if (file.isDirectory()) {
file.close();
return false;
}
if (this->cacheHeader != nullptr) {
server.sendHeader("Cache-Control", this->cacheHeader);
}
#if defined(ARDUINO_ARCH_ESP8266)
if (server._eTagEnabled && this->eTag.length() > 0) {
server.sendHeader("ETag", this->eTag);
}
#endif
server.streamFile(file, F("text/html"), method);
return true;
}
protected:
FS* fs = nullptr;
canHandleFunction canHandleFn;
beforeSendFunction beforeSendFn;
String eTag;
const char* uri = nullptr;
const char* path = nullptr;
const char* cacheHeader = nullptr;
};

View File

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

View File

@@ -0,0 +1,64 @@
@media (min-width: 1280px) {
.container {
max-width: 1000px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1000px;
}
}
.hidden {
display: none !important;
}
header,
main,
footer {
padding-top: 1.5em !important;
padding-bottom: 1.5em !important;
}
article {
margin-top: 1em;
margin-bottom: 1em;
}
footer {
text-align: center;
}
button.success {
background-color: var(--pico-form-element-valid-border-color);
border-color: var(--pico-form-element-valid-border-color);
}
button.failed {
background-color: var(--pico-form-element-invalid-border-color);
border-color: var(--pico-form-element-invalid-border-color);
}
tr.network:hover {
--pico-background-color: var(--pico-primary-focus);
cursor: pointer;
}
.greatSignal {
background-color: var(--pico-form-element-valid-border-color);
}
.normalSignal {
background-color: #e48500;
}
.badSignal {
background-color: var(--pico-form-element-invalid-border-color);
}
.primary {
border: 0.25em solid var(--pico-form-element-invalid-border-color);
padding: 1em;
margin-bottom: 1em;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

File diff suppressed because one or more lines are too long

View File

@@ -8,45 +8,65 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
;extra_configs = secrets.ini
extra_configs = secrets.default.ini
[env]
framework = arduino
lib_deps =
;bblanchon/ArduinoJson@^6.21.4
https://github.com/bblanchon/ArduinoJson/archive/refs/heads/7.x.zip
bblanchon/ArduinoJson@^7.0.0
;ihormelnyk/OpenTherm Library@^1.1.4
https://github.com/Laxilef/opentherm_library/archive/refs/heads/dev.zip
knolleary/PubSubClient@^2.8
arduino-libraries/ArduinoMqttClient@^0.1.7
;lennarthennigs/ESP Telnet@^2.1.2
https://github.com/Laxilef/ESPTelnet/archive/refs/heads/alt_fix_freeze.zip
gyverlibs/EEManager@^2.0
https://github.com/LennartHennigs/ESPTelnet/archive/refs/tags/2.2.zip
gyverlibs/FileData@^1.0
;gyverlibs/GyverPID@^3.3
https://github.com/Laxilef/GyverPID/archive/refs/heads/feat_change_dt_type.zip
gyverlibs/GyverBlinker@^1.0
milesburton/DallasTemperature@^3.11.0
laxilef/TinyLogger@^1.0.9
https://github.com/Laxilef/WiFiManager/archive/refs/heads/patch-1.zip
;https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
laxilef/TinyLogger@^1.1.0
build_flags =
-D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
-D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305
-mtext-section-literals
-D USE_SERIAL=0
-D USE_TELNET=1
-D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_PORT=Serial
-D USE_SERIAL=${secrets.use_serial}
-D USE_TELNET=${secrets.use_telnet}
-D DEBUG_BY_DEFAULT=${secrets.debug}
-D HOSTNAME_DEFAULT='"${secrets.hostname}"'
-D AP_SSID_DEFAULT='"${secrets.ap_ssid}"'
-D AP_PASSWORD_DEFAULT='"${secrets.ap_password}"'
-D STA_SSID_DEFAULT='"${secrets.sta_ssid}"'
-D PORTAL_LOGIN_DEFAULT='"${secrets.portal_login}"'
-D PORTAL_PASSWORD_DEFAULT='"${secrets.portal_password}"'
-D MQTT_SERVER_DEFAULT='"${secrets.mqtt_server}"'
-D MQTT_PORT_DEFAULT=${secrets.mqtt_port}
-D MQTT_USER_DEFAULT='"${secrets.mqtt_user}"'
-D MQTT_PASSWORD_DEFAULT='"${secrets.mqtt_password}"'
-D MQTT_PREFIX_DEFAULT='"${secrets.mqtt_prefix}"'
upload_speed = 921600
monitor_speed = 115200
version = 1.4.0-rc.5
board_build.flash_mode = dio
board_build.filesystem = littlefs
version = 1.4.0-rc.9
; Defaults
[esp8266_defaults]
platform = espressif8266
lib_deps =
${env.lib_deps}
;nrwiersma/ESP8266Scheduler@^1.0
https://github.com/Laxilef/ESP8266Scheduler/archive/refs/heads/network_fix.zip
nrwiersma/ESP8266Scheduler@^1.1
lib_ignore =
extra_scripts =
post:tools/build.py
build_flags = ${env.build_flags}
board_build.ldscript = eagle.flash.1m256.ld
;board_build.ldscript = eagle.flash.4m1m.ld
platform_packages =
platformio/framework-espressif8266 @ https://github.com/platformio/platform-espressif8266.git
[esp32_defaults]
platform = espressif32
@@ -60,6 +80,8 @@ extra_scripts =
build_flags =
${env.build_flags}
-D CORE_DEBUG_LEVEL=0
platform_packages =
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git
; Boards
@@ -149,9 +171,11 @@ lib_deps =
h2zero/NimBLE-Arduino@^1.4.1
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags =
-mtext-section-literals
build_flags =
${esp32_defaults.build_flags}
-D USE_BLE=1
-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
-D OT_IN_PIN_DEFAULT=8
-D OT_OUT_PIN_DEFAULT=10
-D SENSOR_OUTDOOR_PIN_DEFAULT=0
@@ -177,8 +201,6 @@ build_flags =
-D LED_STATUS_PIN=2 ; 18
-D LED_OT_RX_PIN=19
;-D WOKWI=1
;-D DEBUG_BY_DEFAULT=1
;-D WM_DEBUG_MODE=3
[env:d1_mini32]
platform = ${esp32_defaults.platform}

20
secrets.default.ini Normal file
View File

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

View File

@@ -1,9 +1,9 @@
#include <Blinker.h>
extern NetworkTask* tNetwork;
extern MqttTask* tMqtt;
extern SensorsTask* tSensors;
extern OpenThermTask* tOt;
extern EEManager eeSettings;
extern FileData fsSettings, fsNetworkSettings;
#if USE_TELNET
extern ESPTelnetStream TelnetStream;
#endif
@@ -29,7 +29,6 @@ protected:
bool blinkerInitialized = false;
unsigned long firstFailConnect = 0;
unsigned long lastHeapInfo = 0;
unsigned int heapSize = 0;
unsigned int minFreeHeapSize = 0;
unsigned int minMaxFreeHeapBlockSize = 0;
unsigned long restartSignalTime = 0;
@@ -37,6 +36,9 @@ protected:
unsigned long heatingDisabledTime = 0;
byte externalPumpStartReason;
unsigned long externalPumpStartTime = 0;
#if USE_TELNET
bool telnetStarted = false;
#endif
const char* getTaskName() {
return "Main";
@@ -61,29 +63,29 @@ protected:
digitalWrite(settings.externalPump.pin, false);
}
#if defined(ARDUINO_ARCH_ESP32)
this->heapSize = ESP.getHeapSize();
#elif defined(ARDUINO_ARCH_ESP8266)
this->heapSize = 81920;
#else
this->heapSize = 99999;
#endif
this->minFreeHeapSize = heapSize;
this->minMaxFreeHeapBlockSize = heapSize;
this->minFreeHeapSize = getTotalHeap();
this->minMaxFreeHeapBlockSize = getTotalHeap();
}
void loop() {
if (eeSettings.tick()) {
Log.sinfoln("MAIN", F("Settings updated (EEPROM)"));
if (fsSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
}
if (fsNetworkSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
}
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.loop();
}
#endif
if (vars.actions.restart) {
Log.sinfoln("MAIN", F("Restart signal received. Restart after 10 sec."));
eeSettings.updateNow();
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
fsSettings.updateNow();
fsNetworkSettings.updateNow();
this->restartSignalTime = millis();
vars.actions.restart = false;
}
@@ -92,7 +94,14 @@ protected:
tOt->enable();
}
if (WiFi.status() == WL_CONNECTED) {
if (tNetwork->isConnected()) {
#if USE_TELNET
if (!this->telnetStarted) {
TelnetStream.begin(23, false);
this->telnetStarted = true;
}
#endif
vars.sensors.rssi = WiFi.RSSI();
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
@@ -111,6 +120,13 @@ protected:
}
} else {
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.stop();
this->telnetStarted = false;
}
#endif
if (tMqtt->isEnabled()) {
tMqtt->disable();
}
@@ -122,7 +138,7 @@ protected:
if (millis() - this->firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln("MAIN", F("Emergency mode enabled"));
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
}
}
@@ -150,7 +166,7 @@ protected:
}
void heap() {
unsigned int freeHeapSize = ESP.getFreeHeap();
unsigned int freeHeapSize = getFreeHeap();
#if defined(ARDUINO_ARCH_ESP32)
unsigned int maxFreeBlockSize = ESP.getMaxAllocHeap();
#else
@@ -189,9 +205,9 @@ protected:
uint8_t heapFrag = 100 - maxFreeBlockSize * 100.0 / freeHeapSize;
if (millis() - this->lastHeapInfo > 20000 || minFreeHeapSizeDiff > 0 || minMaxFreeBlockSizeDiff > 0) {
Log.sverboseln(
"MAIN",
FPSTR(L_MAIN),
F("Free heap size: %u of %u bytes (min: %u, diff: %u), max free block: %u (min: %u, diff: %u, frag: %hhu%%)"),
freeHeapSize, this->heapSize, this->minFreeHeapSize, minFreeHeapSizeDiff, maxFreeBlockSize, this->minMaxFreeHeapBlockSize, minMaxFreeBlockSizeDiff, heapFrag
freeHeapSize, getTotalHeap(), this->minFreeHeapSize, minFreeHeapSizeDiff, maxFreeBlockSize, this->minMaxFreeHeapBlockSize, minMaxFreeBlockSizeDiff, heapFrag
);
this->lastHeapInfo = millis();
}
@@ -209,7 +225,7 @@ protected:
this->blinkerInitialized = true;
}
if (WiFi.status() != WL_CONNECTED) {
if (!tNetwork->isConnected()) {
errors[errCount++] = 2;
}
@@ -270,13 +286,13 @@ protected:
}
if (!settings.externalPump.use || settings.externalPump.pin == 0) {
if (vars.externalPump.enable) {
if (vars.states.externalPump) {
if (settings.externalPump.pin != 0) {
digitalWrite(settings.externalPump.pin, false);
}
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
}
@@ -284,29 +300,29 @@ protected:
return;
}
if (vars.externalPump.enable && !this->heatingEnabled) {
if (vars.states.externalPump && !this->heatingEnabled) {
if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_HEATING && millis() - this->heatingDisabledTime > ((unsigned int) settings.externalPump.postCirculationTime * 1000)) {
digitalWrite(settings.externalPump.pin, false);
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time"));
} else if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK && millis() - this->externalPumpStartTime >= ((unsigned int) settings.externalPump.antiStuckTime * 1000)) {
digitalWrite(settings.externalPump.pin, false);
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time"));
}
} else if (vars.externalPump.enable && this->heatingEnabled && this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK) {
} else if (vars.states.externalPump && this->heatingEnabled && this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK) {
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
} else if (!vars.externalPump.enable && this->heatingEnabled) {
vars.externalPump.enable = true;
} else if (!vars.states.externalPump && this->heatingEnabled) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
@@ -314,8 +330,8 @@ protected:
Log.sinfoln("EXTPUMP", F("Enabled: heating on"));
} else if (!vars.externalPump.enable && (vars.externalPump.lastEnableTime == 0 || millis() - vars.externalPump.lastEnableTime >= ((unsigned long) settings.externalPump.antiStuckInterval * 1000))) {
vars.externalPump.enable = true;
} else if (!vars.states.externalPump && (vars.parameters.extPumpLastEnableTime == 0 || millis() - vars.parameters.extPumpLastEnableTime >= ((unsigned long) settings.externalPump.antiStuckInterval * 1000))) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->externalPumpStartReason = MainTask::REASON_PUMP_START_ANTISTUCK;

View File

@@ -1,16 +1,15 @@
#include <PubSubClient.h>
#include <MqttClient.h>
#include <MqttWiFiClient.h>
#include <MqttWriter.h>
#include "HaHelper.h"
extern EEManager eeSettings;
extern FileData fsSettings;
class MqttTask : public Task {
public:
MqttTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
this->wifiClient = new MqttWiFiClient();
this->client = new PubSubClient();
this->client = new MqttClient(this->wifiClient);
this->writer = new MqttWriter(this->client, 256);
this->haHelper = new HaHelper();
}
@@ -22,7 +21,7 @@ public:
if (this->client != nullptr) {
if (this->client->connected()) {
this->client->disconnect();
this->client->stop();
}
delete this->client;
@@ -37,9 +36,27 @@ public:
}
}
void disable() {
this->client->stop();
this->wifiClient->stop();
Task::disable();
Log.sinfoln(FPSTR(L_MQTT), F("Disabled"));
}
void enable() {
Task::enable();
Log.sinfoln(FPSTR(L_MQTT), F("Enabled"));
}
bool isConnected() {
return this->connected;
}
protected:
MqttWiFiClient* wifiClient = nullptr;
PubSubClient* client = nullptr;
MqttClient* client = nullptr;
HaHelper* haHelper = nullptr;
MqttWriter* writer = nullptr;
unsigned short readyForSendTime = 15000;
@@ -68,7 +85,7 @@ protected:
}
void setup() {
Log.sinfoln("MQTT", F("Started"));
Log.sinfoln(FPSTR(L_MQTT), F("Started"));
// wificlient settings
#ifdef ARDUINO_ARCH_ESP8266
@@ -77,19 +94,27 @@ protected:
#endif
// client settings
this->client->setClient(*this->wifiClient);
this->client->setKeepAlive(15);
//this->client->setClient(*this->wifiClient);
this->client->setKeepAliveInterval(15000);
this->client->setTxPayloadSize(256);
#ifdef ARDUINO_ARCH_ESP8266
this->client->setSocketTimeout(1);
this->client->setBufferSize(768);
this->client->setConnectionTimeout(1000);
#else
this->client->setSocketTimeout(3);
this->client->setBufferSize(1536);
this->client->setConnectionTimeout(3000);
#endif
this->client->setCallback([this] (char* topic, uint8_t* payload, unsigned int length) {
this->onMessage(topic, payload, length);
this->client->onMessage([this] (void*, size_t length) {
String topic = this->client->messageTopic();
if (!length || length > 2048 || !topic.length()) {
return;
}
uint8_t payload[length];
for (size_t i = 0; i < length && this->client->available(); i++) {
payload[i] = this->client->read();
}
this->onMessage(topic.c_str(), payload, length);
});
// writer settings
@@ -100,13 +125,13 @@ protected:
#endif
this->writer->setEventPublishCallback([this] (const char* topic, size_t written, size_t length, bool result) {
Log.straceln("MQTT", F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
Log.straceln(FPSTR(L_MQTT), F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
this->client->loop();
//this->client->poll();
this->delay(250);
});
@@ -132,7 +157,7 @@ protected:
void loop() {
if (settings.mqtt.interval > 120) {
settings.mqtt.interval = 5;
eeSettings.update();
fsSettings.update();
}
if (!this->client->connected() && this->connected) {
@@ -141,10 +166,11 @@ protected:
}
if (this->wifiClient == nullptr || (!this->client->connected() && millis() - this->lastReconnectTime >= MQTT_RECONNECT_INTERVAL)) {
Log.sinfoln("MQTT", F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
Log.sinfoln(FPSTR(L_MQTT), F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
this->client->setServer(settings.mqtt.server, settings.mqtt.port);
this->client->connect(settings.hostname, settings.mqtt.user, settings.mqtt.password);
this->client->setId(networkSettings.hostname);
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
this->client->connect(settings.mqtt.server, settings.mqtt.port);
this->lastReconnectTime = millis();
}
@@ -158,7 +184,7 @@ protected:
if (settings.emergency.enable && !vars.states.emergency) {
if (millis() - this->disconnectedTime > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln("MQTT", F("Emergency mode enabled"));
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
}
}
@@ -168,7 +194,8 @@ protected:
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
this->client->loop();
this->client->poll();
// delay for publish data
if (!this->isReadyForSend()) {
@@ -213,11 +240,11 @@ protected:
this->connectedTime = millis();
this->newConnection = true;
unsigned long downtime = (millis() - this->disconnectedTime) / 1000;
Log.sinfoln("MQTT", F("Connected (downtime: %u s.)"), downtime);
Log.sinfoln(FPSTR(L_MQTT), F("Connected (downtime: %u s.)"), downtime);
if (vars.states.emergency) {
vars.states.emergency = false;
Log.sinfoln("MQTT", F("Emergency mode disabled"));
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
}
this->client->subscribe(this->haHelper->getDeviceTopic("settings/set").c_str());
@@ -228,16 +255,16 @@ protected:
this->disconnectedTime = millis();
unsigned long uptime = (millis() - this->connectedTime) / 1000;
Log.swarningln("MQTT", F("Disconnected (reason: %d uptime: %u s.)"), this->client->state(), uptime);
Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %u s.)"), this->client->connectError(), uptime);
}
void onMessage(char* topic, uint8_t* payload, unsigned int length) {
void onMessage(const char* topic, uint8_t* payload, size_t length) {
if (!length) {
return;
}
if (settings.debug) {
Log.strace("MQTT.MSG", F("Topic: %s\r\n> "), topic);
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
if (Log.lock()) {
for (size_t i = 0; i < length; i++) {
if (payload[i] == 0) {
@@ -258,8 +285,12 @@ protected:
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, payload, length);
if (dErr != DeserializationError::Ok || doc.isNull()) {
Log.swarningln("MQTT.MSG", F("Error on deserialization: %s"), dErr.f_str());
if (dErr != DeserializationError::Ok) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Error on deserialization: %s"), dErr.f_str());
return;
} else if (doc.isNull() || !doc.size()) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Not valid json"));
return;
}
@@ -275,254 +306,13 @@ protected:
bool updateSettings(JsonDocument& doc) {
bool flag = false;
if (!doc["debug"].isNull() && doc["debug"].is<bool>()) {
settings.debug = doc["debug"].as<bool>();
flag = true;
}
// emergency
if (!doc["emergency"]["enable"].isNull() && doc["emergency"]["enable"].is<bool>()) {
settings.emergency.enable = doc["emergency"]["enable"].as<bool>();
flag = true;
}
if (!doc["emergency"]["target"].isNull() && doc["emergency"]["target"].is<double>()) {
if (doc["emergency"]["target"].as<double>() > 0 && doc["emergency"]["target"].as<double>() < 100) {
settings.emergency.target = MqttTask::round(doc["emergency"]["target"].as<double>(), 2);
flag = true;
}
}
if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is<bool>()) {
if (settings.sensors.outdoor.type != 1) {
settings.emergency.useEquitherm = doc["emergency"]["useEquitherm"].as<bool>();
} else {
settings.emergency.useEquitherm = false;
}
if (settings.emergency.useEquitherm && settings.emergency.usePid) {
settings.emergency.usePid = false;
}
flag = true;
}
if (!doc["emergency"]["usePid"].isNull() && doc["emergency"]["usePid"].is<bool>()) {
if (settings.sensors.indoor.type != 1) {
settings.emergency.usePid = doc["emergency"]["usePid"].as<bool>();
} else {
settings.emergency.usePid = false;
}
if (settings.emergency.usePid && settings.emergency.useEquitherm) {
settings.emergency.useEquitherm = false;
}
flag = true;
}
// heating
if (!doc["heating"]["enable"].isNull() && doc["heating"]["enable"].is<bool>()) {
settings.heating.enable = doc["heating"]["enable"].as<bool>();
flag = true;
}
if (!doc["heating"]["turbo"].isNull() && doc["heating"]["turbo"].is<bool>()) {
settings.heating.turbo = doc["heating"]["turbo"].as<bool>();
flag = true;
}
if (!doc["heating"]["target"].isNull() && doc["heating"]["target"].is<double>()) {
if (doc["heating"]["target"].as<double>() > 0 && doc["heating"]["target"].as<double>() < 100) {
settings.heating.target = MqttTask::round(doc["heating"]["target"].as<double>(), 2);
flag = true;
}
}
if (!doc["heating"]["hysteresis"].isNull() && doc["heating"]["hysteresis"].is<double>()) {
if (doc["heating"]["hysteresis"].as<double>() >= 0 && doc["heating"]["hysteresis"].as<double>() <= 5) {
settings.heating.hysteresis = MqttTask::round(doc["heating"]["hysteresis"].as<double>(), 2);
flag = true;
}
}
if (!doc["heating"]["maxModulation"].isNull() && doc["heating"]["maxModulation"].is<unsigned char>()) {
if (doc["heating"]["maxModulation"].as<unsigned char>() > 0 && doc["heating"]["maxModulation"].as<unsigned char>() <= 100) {
settings.heating.maxModulation = doc["heating"]["maxModulation"].as<unsigned char>();
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) {
settings.heating.maxTemp = doc["heating"]["maxTemp"].as<unsigned char>();
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) {
settings.heating.minTemp = doc["heating"]["minTemp"].as<unsigned char>();
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<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) {
settings.dhw.maxTemp = doc["dhw"]["maxTemp"].as<unsigned char>();
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) {
settings.dhw.minTemp = doc["dhw"]["minTemp"].as<unsigned char>();
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<double>()) {
if (doc["pid"]["p_factor"].as<double>() > 0 && doc["pid"]["p_factor"].as<double>() <= 1000) {
settings.pid.p_factor = MqttTask::round(doc["pid"]["p_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["pid"]["i_factor"].isNull() && doc["pid"]["i_factor"].is<double>()) {
if (doc["pid"]["i_factor"].as<double>() >= 0 && doc["pid"]["i_factor"].as<double>() <= 100) {
settings.pid.i_factor = MqttTask::round(doc["pid"]["i_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["pid"]["d_factor"].isNull() && doc["pid"]["d_factor"].is<double>()) {
if (doc["pid"]["d_factor"].as<double>() >= 0 && doc["pid"]["d_factor"].as<double>() <= 100000) {
settings.pid.d_factor = MqttTask::round(doc["pid"]["d_factor"].as<double>(), 1);
flag = true;
}
}
if (!doc["pid"]["dt"].isNull() && doc["pid"]["dt"].is<double>()) {
if (doc["pid"]["dt"].as<unsigned short>() >= 30 && doc["pid"]["dt"].as<unsigned short>() <= 600) {
settings.pid.dt = doc["pid"]["dt"].as<unsigned short>();
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
if (!doc["equitherm"]["enable"].isNull() && doc["equitherm"]["enable"].is<bool>()) {
settings.equitherm.enable = doc["equitherm"]["enable"].as<bool>();
flag = true;
}
if (!doc["equitherm"]["n_factor"].isNull() && doc["equitherm"]["n_factor"].is<double>()) {
if (doc["equitherm"]["n_factor"].as<double>() > 0 && doc["equitherm"]["n_factor"].as<double>() <= 10) {
settings.equitherm.n_factor = MqttTask::round(doc["equitherm"]["n_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["equitherm"]["k_factor"].isNull() && doc["equitherm"]["k_factor"].is<double>()) {
if (doc["equitherm"]["k_factor"].as<double>() >= 0 && doc["equitherm"]["k_factor"].as<double>() <= 10) {
settings.equitherm.k_factor = MqttTask::round(doc["equitherm"]["k_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["equitherm"]["t_factor"].isNull() && doc["equitherm"]["t_factor"].is<double>()) {
if (doc["equitherm"]["t_factor"].as<double>() >= 0 && doc["equitherm"]["t_factor"].as<double>() <= 10) {
settings.equitherm.t_factor = MqttTask::round(doc["equitherm"]["t_factor"].as<double>(), 3);
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>();
if (settings.sensors.outdoor.type == 1) {
settings.emergency.useEquitherm = false;
}
flag = true;
}
}
if (!doc["sensors"]["outdoor"]["offset"].isNull() && doc["sensors"]["outdoor"]["offset"].is<double>()) {
if (doc["sensors"]["outdoor"]["offset"].as<double>() >= -10 && doc["sensors"]["outdoor"]["offset"].as<double>() <= 10) {
settings.sensors.outdoor.offset = MqttTask::round(doc["sensors"]["outdoor"]["offset"].as<double>(), 2);
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>() <= 3) {
settings.sensors.indoor.type = doc["sensors"]["indoor"]["type"].as<unsigned char>();
if (settings.sensors.indoor.type == 1) {
settings.emergency.usePid = false;
}
flag = true;
}
}
if (!doc["sensors"]["indoor"]["offset"].isNull() && doc["sensors"]["indoor"]["offset"].is<double>()) {
if (doc["sensors"]["indoor"]["offset"].as<double>() >= -10 && doc["sensors"]["indoor"]["offset"].as<double>() <= 10) {
settings.sensors.indoor.offset = MqttTask::round(doc["sensors"]["indoor"]["offset"].as<double>(), 2);
flag = true;
}
}
bool changed = safeJsonToSettings(doc, settings);
doc.clear();
doc.shrinkToFit();
if (flag) {
if (changed) {
this->prevPubSettingsTime = 0;
eeSettings.update();
fsSettings.update();
return true;
}
@@ -530,54 +320,11 @@ protected:
}
bool updateVariables(JsonDocument& doc) {
bool flag = false;
if (!doc["ping"].isNull() && doc["ping"]) {
flag = true;
}
if (!doc["tuning"]["enable"].isNull() && doc["tuning"]["enable"].is<bool>()) {
vars.tuning.enable = doc["tuning"]["enable"].as<bool>();
flag = true;
}
if (!doc["tuning"]["regulator"].isNull() && doc["tuning"]["regulator"].is<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<double>()) {
if (settings.sensors.indoor.type == 1 && doc["temperatures"]["indoor"].as<double>() > -100 && doc["temperatures"]["indoor"].as<double>() < 100) {
vars.temperatures.indoor = MqttTask::round(doc["temperatures"]["indoor"].as<double>(), 2);
flag = true;
}
}
if (!doc["temperatures"]["outdoor"].isNull() && doc["temperatures"]["outdoor"].is<double>()) {
if (settings.sensors.outdoor.type == 1 && doc["temperatures"]["outdoor"].as<double>() > -100 && doc["temperatures"]["outdoor"].as<double>() < 100) {
vars.temperatures.outdoor = MqttTask::round(doc["temperatures"]["outdoor"].as<double>(), 2);
flag = true;
}
}
if (!doc["actions"]["restart"].isNull() && doc["actions"]["restart"].is<bool>() && doc["actions"]["restart"].as<bool>()) {
vars.actions.restart = true;
}
if (!doc["actions"]["resetFault"].isNull() && doc["actions"]["resetFault"].is<bool>() && doc["actions"]["resetFault"].as<bool>()) {
vars.actions.resetFault = true;
}
if (!doc["actions"]["resetDiagnostic"].isNull() && doc["actions"]["resetDiagnostic"].is<bool>() && doc["actions"]["resetDiagnostic"].as<bool>()) {
vars.actions.resetDiagnostic = true;
}
bool changed = jsonToVars(doc, vars);
doc.clear();
doc.shrinkToFit();
if (flag) {
if (changed) {
this->prevPubVarsTime = 0;
return true;
}
@@ -766,97 +513,15 @@ protected:
bool publishSettings(const char* topic) {
JsonDocument doc;
doc["debug"] = settings.debug;
doc["emergency"]["enable"] = settings.emergency.enable;
doc["emergency"]["target"] = MqttTask::round(settings.emergency.target, 2);
doc["emergency"]["useEquitherm"] = settings.emergency.useEquitherm;
doc["emergency"]["usePid"] = settings.emergency.usePid;
doc["heating"]["enable"] = settings.heating.enable;
doc["heating"]["turbo"] = settings.heating.turbo;
doc["heating"]["target"] = MqttTask::round(settings.heating.target, 2);
doc["heating"]["hysteresis"] = MqttTask::round(settings.heating.hysteresis, 2);
doc["heating"]["minTemp"] = settings.heating.minTemp;
doc["heating"]["maxTemp"] = settings.heating.maxTemp;
doc["heating"]["maxModulation"] = settings.heating.maxModulation;
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"] = MqttTask::round(settings.pid.p_factor, 3);
doc["pid"]["i_factor"] = MqttTask::round(settings.pid.i_factor, 3);
doc["pid"]["d_factor"] = MqttTask::round(settings.pid.d_factor, 1);
doc["pid"]["dt"] = settings.pid.dt;
doc["pid"]["minTemp"] = settings.pid.minTemp;
doc["pid"]["maxTemp"] = settings.pid.maxTemp;
doc["equitherm"]["enable"] = settings.equitherm.enable;
doc["equitherm"]["n_factor"] = MqttTask::round(settings.equitherm.n_factor, 3);
doc["equitherm"]["k_factor"] = MqttTask::round(settings.equitherm.k_factor, 3);
doc["equitherm"]["t_factor"] = MqttTask::round(settings.equitherm.t_factor, 3);
doc["sensors"]["outdoor"]["type"] = settings.sensors.outdoor.type;
doc["sensors"]["outdoor"]["offset"] = MqttTask::round(settings.sensors.outdoor.offset, 2);
doc["sensors"]["indoor"]["type"] = settings.sensors.indoor.type;
doc["sensors"]["indoor"]["offset"] = MqttTask::round(settings.sensors.indoor.offset, 2);
doc.shrinkToFit();
safeSettingsToJson(settings, doc);
return this->writer->publish(topic, doc, true);
}
bool publishVariables(const char* topic) {
JsonDocument doc;
doc["tuning"]["enable"] = vars.tuning.enable;
doc["tuning"]["regulator"] = vars.tuning.regulator;
doc["states"]["otStatus"] = vars.states.otStatus;
doc["states"]["heating"] = vars.states.heating;
doc["states"]["dhw"] = vars.states.dhw;
doc["states"]["flame"] = vars.states.flame;
doc["states"]["fault"] = vars.states.fault;
doc["states"]["diagnostic"] = vars.states.diagnostic;
doc["sensors"]["modulation"] = MqttTask::round(vars.sensors.modulation, 2);
doc["sensors"]["pressure"] = MqttTask::round(vars.sensors.pressure, 2);
doc["sensors"]["dhwFlowRate"] = vars.sensors.dhwFlowRate;
doc["sensors"]["faultCode"] = vars.sensors.faultCode;
doc["sensors"]["rssi"] = vars.sensors.rssi;
doc["sensors"]["uptime"] = millis() / 1000ul;
doc["temperatures"]["indoor"] = MqttTask::round(vars.temperatures.indoor, 2);
doc["temperatures"]["outdoor"] = MqttTask::round(vars.temperatures.outdoor, 2);
doc["temperatures"]["heating"] = MqttTask::round(vars.temperatures.heating, 2);
doc["temperatures"]["dhw"] = MqttTask::round(vars.temperatures.dhw, 2);
doc["parameters"]["heatingEnabled"] = vars.parameters.heatingEnabled;
doc["parameters"]["heatingMinTemp"] = vars.parameters.heatingMinTemp;
doc["parameters"]["heatingMaxTemp"] = vars.parameters.heatingMaxTemp;
doc["parameters"]["heatingSetpoint"] = vars.parameters.heatingSetpoint;
doc["parameters"]["dhwMinTemp"] = vars.parameters.dhwMinTemp;
doc["parameters"]["dhwMaxTemp"] = vars.parameters.dhwMaxTemp;
doc.shrinkToFit();
varsToJson(vars, doc);
return this->writer->publish(topic, doc, true);
}
static double round(double value, uint8_t decimals = 2) {
if (decimals == 0) {
return (int)(value + 0.001);
} else if (abs(value) < 0.00000001) {
return 0.0;
}
double multiplier = pow10(decimals);
value += 0.5 / multiplier * (value < 0 ? -1 : 1);
return (int)(value * multiplier) / multiplier;
}
};

406
src/NetworkTask.h Normal file
View File

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

View File

@@ -1,13 +1,7 @@
#include <new>
#include <CustomOpenTherm.h>
CustomOpenTherm* ot;
extern EEManager eeSettings;
const char S_OT[] PROGMEM = "OT";
const char S_OT_DHW[] PROGMEM = "OT.DHW";
const char S_OT_HEATING[] PROGMEM = "OT.HEATING";
extern FileData fsSettings;
class OpenThermTask : public Task {
public:
@@ -46,7 +40,7 @@ protected:
}
void setup() {
Log.sinfoln(FPSTR(S_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inPin, settings.opentherm.outPin);
Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inPin, settings.opentherm.outPin);
ot->setHandleSendRequestCallback(OpenThermTask::sendRequestCallback);
ot->setYieldCallback([](void* self) {
@@ -66,32 +60,32 @@ protected:
// Not all boilers support these, only try once when the boiler becomes connected
if (updateSlaveVersion()) {
Log.straceln(FPSTR(S_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType);
Log.straceln(FPSTR(L_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType);
} else {
Log.swarningln(FPSTR(S_OT), F("Get slave version failed"));
Log.swarningln(FPSTR(L_OT), F("Get slave version failed"));
}
// 0x013F
if (setMasterVersion(0x3F, 0x01)) {
Log.straceln(FPSTR(S_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType);
Log.straceln(FPSTR(L_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType);
} else {
Log.swarningln(FPSTR(S_OT), F("Set master version failed"));
Log.swarningln(FPSTR(L_OT), F("Set master version failed"));
}
if (updateSlaveConfig()) {
Log.straceln(FPSTR(S_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags);
Log.straceln(FPSTR(L_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags);
} else {
Log.swarningln(FPSTR(S_OT), F("Get slave config failed"));
Log.swarningln(FPSTR(L_OT), F("Get slave config failed"));
}
if (setMasterConfig(settings.opentherm.memberIdCode & 0xFF, (settings.opentherm.memberIdCode & 0xFFFF) >> 8)) {
Log.straceln(FPSTR(S_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags);
Log.straceln(FPSTR(L_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags);
} else {
Log.swarningln(FPSTR(S_OT), F("Set master config failed"));
Log.swarningln(FPSTR(L_OT), F("Set master config failed"));
}
}
@@ -119,18 +113,18 @@ protected:
);
if (!ot->isValidResponse(localResponse)) {
Log.swarningln(FPSTR(S_OT), F("Invalid response after setBoilerStatus: %s"), ot->statusToString(ot->getLastResponseStatus()));
Log.swarningln(FPSTR(L_OT), F("Invalid response after setBoilerStatus: %s"), ot->statusToString(ot->getLastResponseStatus()));
}
if (vars.states.otStatus && !this->prevOtStatus) {
this->prevOtStatus = vars.states.otStatus;
Log.sinfoln(FPSTR(S_OT), F("Connected. Initializing"));
Log.sinfoln(FPSTR(L_OT), F("Connected. Initializing"));
this->initBoiler();
} else if (!vars.states.otStatus && this->prevOtStatus) {
this->prevOtStatus = vars.states.otStatus;
Log.swarningln(FPSTR(S_OT), F("Disconnected"));
Log.swarningln(FPSTR(L_OT), F("Disconnected"));
}
if (!vars.states.otStatus) {
@@ -141,7 +135,7 @@ protected:
if (vars.parameters.heatingEnabled != heatingEnabled) {
this->prevUpdateNonEssentialVars = 0;
vars.parameters.heatingEnabled = heatingEnabled;
Log.sinfoln(FPSTR(S_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled"));
Log.sinfoln(FPSTR(L_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled"));
}
vars.states.heating = ot->isCentralHeatingActive(localResponse);
@@ -154,18 +148,18 @@ protected:
if (millis() - this->prevUpdateNonEssentialVars > 60000) {
if (!heatingEnabled && settings.opentherm.modulationSyncWithHeating) {
if (setMaxModulationLevel(0)) {
Log.snoticeln(FPSTR(S_OT_HEATING), F("Set max modulation 0% (off)"));
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation 0% (off)"));
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set max modulation 0% (off)"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation 0% (off)"));
}
} else {
if (setMaxModulationLevel(settings.heating.maxModulation)) {
Log.snoticeln(FPSTR(S_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation);
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation);
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation);
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation);
}
}
@@ -174,24 +168,24 @@ protected:
if (updateMinMaxDhwTemp()) {
if (settings.dhw.minTemp < vars.parameters.dhwMinTemp) {
settings.dhw.minTemp = vars.parameters.dhwMinTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp);
}
if (settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) {
settings.dhw.maxTemp = vars.parameters.dhwMaxTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp);
}
} else {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed get min/max temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed get min/max temp"));
}
if (settings.dhw.minTemp >= settings.dhw.maxTemp) {
settings.dhw.minTemp = 30;
settings.dhw.maxTemp = 60;
eeSettings.update();
fsSettings.update();
}
}
@@ -200,24 +194,24 @@ protected:
if (updateMinMaxHeatingTemp()) {
if (settings.heating.minTemp < vars.parameters.heatingMinTemp) {
settings.heating.minTemp = vars.parameters.heatingMinTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp);
}
if (settings.heating.maxTemp > vars.parameters.heatingMaxTemp) {
settings.heating.maxTemp = vars.parameters.heatingMaxTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp);
}
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed get min/max temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed get min/max temp"));
}
if (settings.heating.minTemp >= settings.heating.maxTemp) {
settings.heating.minTemp = 20;
settings.heating.maxTemp = 90;
eeSettings.update();
fsSettings.update();
}
// force set max CH temp
@@ -258,10 +252,10 @@ protected:
if (vars.actions.resetFault) {
if (vars.states.fault) {
if (ot->sendBoilerReset()) {
Log.sinfoln(FPSTR(S_OT), F("Boiler fault reset successfully"));
Log.sinfoln(FPSTR(L_OT), F("Boiler fault reset successfully"));
} else {
Log.serrorln(FPSTR(S_OT), F("Boiler fault reset failed"));
Log.serrorln(FPSTR(L_OT), F("Boiler fault reset failed"));
}
}
@@ -272,10 +266,10 @@ protected:
if (vars.actions.resetDiagnostic) {
if (vars.states.diagnostic) {
if (ot->sendServiceReset()) {
Log.sinfoln(FPSTR(S_OT), F("Boiler diagnostic reset successfully"));
Log.sinfoln(FPSTR(L_OT), F("Boiler diagnostic reset successfully"));
} else {
Log.serrorln(FPSTR(S_OT), F("Boiler diagnostic reset failed"));
Log.serrorln(FPSTR(L_OT), F("Boiler diagnostic reset failed"));
}
}
@@ -290,7 +284,7 @@ protected:
newDhwTemp = constrain(newDhwTemp, settings.dhw.minTemp, settings.dhw.maxTemp);
}
Log.sinfoln(FPSTR(S_OT_DHW), F("Set temp = %u"), newDhwTemp);
Log.sinfoln(FPSTR(L_OT_DHW), F("Set temp = %u"), newDhwTemp);
// Записываем заданную температуру ГВС
if (ot->setDhwTemp(newDhwTemp)) {
@@ -298,12 +292,12 @@ protected:
this->dhwSetTempTime = millis();
} else {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed set temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp"));
}
if (settings.opentherm.dhwToCh2) {
if (!ot->setHeatingCh2Temp(newDhwTemp)) {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed set ch2 temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set ch2 temp"));
}
}
}
@@ -311,7 +305,7 @@ protected:
//
// Температура отопления
if (heatingEnabled && (needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001)) {
Log.sinfoln(FPSTR(S_OT_HEATING), F("Set temp = %u"), vars.parameters.heatingSetpoint);
Log.sinfoln(FPSTR(L_OT_HEATING), F("Set temp = %u"), vars.parameters.heatingSetpoint);
// Записываем заданную температуру
if (ot->setHeatingCh1Temp(vars.parameters.heatingSetpoint)) {
@@ -319,12 +313,12 @@ protected:
this->heatingSetTempTime = millis();
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set temp"));
}
if (settings.opentherm.heatingCh1ToCh2) {
if (!ot->setHeatingCh2Temp(vars.parameters.heatingSetpoint)) {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set ch2 temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set ch2 temp"));
}
}
}
@@ -394,7 +388,7 @@ protected:
}
static void printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
Log.straceln(FPSTR(S_OT), F("OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), id, request, response, attempt, ot->statusToString(status));
Log.straceln(FPSTR(L_OT), F("OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), id, request, response, attempt, ot->statusToString(status));
}
bool updateSlaveConfig() {

625
src/PortalTask.h Normal file
View File

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

View File

@@ -1,6 +1,35 @@
struct NetworkSettings {
char hostname[25] = HOSTNAME_DEFAULT;
bool useDhcp = true;
struct {
char ip[16] = "192.168.0.100";
char gateway[16] = "192.168.0.1";
char subnet[16] = "255.255.255.0";
char dns[16] = "192.168.0.1";
} staticConfig;
struct {
char ssid[33] = AP_SSID_DEFAULT;
char password[65] = AP_PASSWORD_DEFAULT;
byte channel = 1;
} ap;
struct {
char ssid[33] = STA_SSID_DEFAULT;
char password[65] = STA_PASSWORD_DEFAULT;
byte channel = 0;
} sta;
} networkSettings;
struct Settings {
bool debug = DEBUG_BY_DEFAULT;
char hostname[80] = "opentherm";
struct {
bool useAuth = false;
char login[13] = PORTAL_LOGIN_DEFAULT;
char password[33] = PORTAL_PASSWORD_DEFAULT;
} portal;
struct {
byte inPin = OT_IN_PIN_DEFAULT;
@@ -16,11 +45,11 @@ struct Settings {
} opentherm;
struct {
char server[80];
unsigned short port = 1883;
char user[32];
char password[32];
char prefix[80] = "opentherm";
char server[81] = MQTT_SERVER_DEFAULT;
unsigned short port = MQTT_PORT_DEFAULT;
char user[33] = MQTT_USER_DEFAULT;
char password[33] = MQTT_PASSWORD_DEFAULT;
char prefix[33] = MQTT_PREFIX_DEFAULT;
unsigned short interval = 5;
} mqtt;
@@ -107,6 +136,7 @@ struct Variables {
bool flame = false;
bool fault = false;
bool diagnostic = false;
bool externalPump = false;
} states;
struct {
@@ -124,16 +154,12 @@ struct Variables {
float dhw = 0.0f;
} temperatures;
struct {
bool enable = false;
unsigned long lastEnableTime = 0;
} externalPump;
struct {
bool heatingEnabled = false;
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
byte heatingSetpoint = 0;
unsigned long extPumpLastEnableTime = 0;
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
byte maxModulation;

View File

@@ -1,559 +0,0 @@
#define WM_MDNS
#include <WiFiManager.h>
#include <UnsignedIntParameter.h>
#include <UnsignedShortParameter.h>
#include <CheckboxParameter.h>
#include <HeaderParameter.h>
#ifdef ARDUINO_ARCH_ESP8266
extern "C" {
#include "lwip/etharp.h"
}
#endif
WiFiManager wm;
WiFiManagerParameter* wmHostname;
WiFiManagerParameter* wmMqttServer;
UnsignedShortParameter* wmMqttPort;
WiFiManagerParameter* wmMqttUser;
WiFiManagerParameter* wmMqttPassword;
WiFiManagerParameter* wmMqttPrefix;
UnsignedIntParameter* wmMqttPublishInterval;
UnsignedIntParameter* wmOtInPin;
UnsignedIntParameter* wmOtOutPin;
UnsignedIntParameter* wmOtMemberIdCode;
CheckboxParameter* wmOtDhwPresent;
CheckboxParameter* wmOtSummerWinterMode;
CheckboxParameter* wmOtHeatingCh2Enabled;
CheckboxParameter* wmOtHeatingCh1ToCh2;
CheckboxParameter* wmOtDhwToCh2;
CheckboxParameter* wmOtDhwBlocking;
CheckboxParameter* wmOtModSyncWithHeating;
UnsignedIntParameter* wmOutdoorSensorPin;
UnsignedIntParameter* wmIndoorSensorPin;
#if USE_BLE
WiFiManagerParameter* wmOutdoorSensorBleAddress;
#endif
CheckboxParameter* wmExtPumpUse;
UnsignedIntParameter* wmExtPumpPin;
UnsignedShortParameter* wmExtPumpPostCirculationTime;
UnsignedIntParameter* wmExtPumpAntiStuckInterval;
UnsignedShortParameter* wmExtPumpAntiStuckTime;
HeaderParameter* wmMqttHeader;
HeaderParameter* wmOtHeader;
HeaderParameter* wmOtFlagsHeader;
HeaderParameter* wmSensorsHeader;
HeaderParameter* wmExtPumpHeader;
extern EEManager eeSettings;
#if USE_TELNET
extern ESPTelnetStream TelnetStream;
#endif
const char S_WIFI[] PROGMEM = "WIFI";
const char S_WIFI_SETTINGS[] PROGMEM = "WIFI.SETTINGS";
class WifiManagerTask : public LeanTask {
public:
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
wm.addParameter(wmHostname);
wmMqttHeader = new HeaderParameter("MQTT");
wm.addParameter(wmMqttHeader);
wmMqttServer = new WiFiManagerParameter("mqtt_server", "Server", settings.mqtt.server, 80);
wm.addParameter(wmMqttServer);
wmMqttPort = new UnsignedShortParameter("mqtt_port", "Port", settings.mqtt.port, 6);
wm.addParameter(wmMqttPort);
wmMqttUser = new WiFiManagerParameter("mqtt_user", "Username", settings.mqtt.user, 32);
wm.addParameter(wmMqttUser);
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "Password", settings.mqtt.password, 32, "type=\"password\"");
wm.addParameter(wmMqttPassword);
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "Prefix", settings.mqtt.prefix, 32);
wm.addParameter(wmMqttPrefix);
wmMqttPublishInterval = new UnsignedIntParameter("mqtt_publish_interval", "Publish interval (sec)", settings.mqtt.interval, 3);
wm.addParameter(wmMqttPublishInterval);
wmOtHeader = new HeaderParameter("OpenTherm");
wm.addParameter(wmOtHeader);
wmOtInPin = new UnsignedIntParameter("ot_in_pin", "GPIO IN", settings.opentherm.inPin, 2);
wm.addParameter(wmOtInPin);
wmOtOutPin = new UnsignedIntParameter("ot_out_pin", "GPIO OUT", settings.opentherm.outPin, 2);
wm.addParameter(wmOtOutPin);
wmOtMemberIdCode = new UnsignedIntParameter("ot_member_id_code", "Master Member ID", settings.opentherm.memberIdCode, 5);
wm.addParameter(wmOtMemberIdCode);
wmOtFlagsHeader = new HeaderParameter("OpenTherm flags");
wm.addParameter(wmOtFlagsHeader);
wmOtDhwPresent = new CheckboxParameter("ot_dhw_present", "DHW present", settings.opentherm.dhwPresent);
wm.addParameter(wmOtDhwPresent);
wmOtSummerWinterMode = new CheckboxParameter("ot_summer_winter_mode", "Summer/winter mode", settings.opentherm.summerWinterMode);
wm.addParameter(wmOtSummerWinterMode);
wmOtHeatingCh2Enabled = new CheckboxParameter("ot_heating_ch2_enabled", "CH2 enabled", settings.opentherm.heatingCh2Enabled);
wm.addParameter(wmOtHeatingCh2Enabled);
wmOtHeatingCh1ToCh2 = new CheckboxParameter("ot_heating_ch1_to_ch2", "Heating CH1 to CH2", settings.opentherm.heatingCh1ToCh2);
wm.addParameter(wmOtHeatingCh1ToCh2);
wmOtDhwToCh2 = new CheckboxParameter("ot_dhw_to_ch2", "DHW to CH2", settings.opentherm.dhwToCh2);
wm.addParameter(wmOtDhwToCh2);
wmOtDhwBlocking = new CheckboxParameter("ot_dhw_blocking", "DHW blocking", settings.opentherm.dhwBlocking);
wm.addParameter(wmOtDhwBlocking);
wmOtModSyncWithHeating = new CheckboxParameter("ot_mod_sync_with_heating", "Modulation sync with heating", settings.opentherm.modulationSyncWithHeating);
wm.addParameter(wmOtModSyncWithHeating);
wmSensorsHeader = new HeaderParameter("Sensors");
wm.addParameter(wmSensorsHeader);
wmOutdoorSensorPin = new UnsignedIntParameter("outdoor_sensor_pin", "Outdoor sensor GPIO", settings.sensors.outdoor.pin, 2);
wm.addParameter(wmOutdoorSensorPin);
wmIndoorSensorPin = new UnsignedIntParameter("indoor_sensor_pin", "Indoor sensor GPIO", settings.sensors.indoor.pin, 2);
wm.addParameter(wmIndoorSensorPin);
#if USE_BLE
wmOutdoorSensorBleAddress = new WiFiManagerParameter("ble_address", "BLE sensor address", settings.sensors.indoor.bleAddresss, 17);
wm.addParameter(wmOutdoorSensorBleAddress);
#endif
wmExtPumpHeader = new HeaderParameter("External pump");
wm.addParameter(wmExtPumpHeader);
wmExtPumpUse = new CheckboxParameter("ext_pump_use", "Use external pump<br>", settings.externalPump.use);
wm.addParameter(wmExtPumpUse);
wmExtPumpPin = new UnsignedIntParameter("ext_pump_pin", "Relay GPIO", settings.externalPump.pin, 2);
wm.addParameter(wmExtPumpPin);
wmExtPumpPostCirculationTime = new UnsignedShortParameter("ext_pump_ps_time", "Post circulation time (min)", (settings.externalPump.postCirculationTime / 60), 5);
wm.addParameter(wmExtPumpPostCirculationTime);
wmExtPumpAntiStuckInterval = new UnsignedIntParameter("ext_pump_as_interval", "Anti stuck interval (days)", (settings.externalPump.antiStuckInterval / 86400), 7);
wm.addParameter(wmExtPumpAntiStuckInterval);
wmExtPumpAntiStuckTime = new UnsignedShortParameter("ext_pump_as_time", "Anti stuck time (min)", (settings.externalPump.antiStuckTime / 60), 5);
wm.addParameter(wmExtPumpAntiStuckTime);
}
WifiManagerTask* addTaskForDisable(AbstractTask* task) {
this->tasksForDisable.push_back(task);
return this;
}
protected:
bool connected = false;
unsigned long lastArpGratuitous = 0;
unsigned long lastReconnecting = 0;
std::vector<AbstractTask*> tasksForDisable;
const char* getTaskName() {
return "WifiManager";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 0;
}
void setup() {
#ifdef WOKWI
WiFi.begin("Wokwi-GUEST", "", 6);
#endif
wm.setDebugOutput(settings.debug, (wm_debuglevel_t) WM_DEBUG_MODE);
wm.setTitle(PROJECT_NAME);
wm.setCustomHeadElement(PSTR(
"<style>"
".bheader + br {display: none;}"
".bheader {margin: 1.25em 0 0.5em 0;padding: 0;border-bottom: 2px solid #000;font-size: 1.5em;}"
"</style>"
));
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\">" PROJECT_NAME "</div>"
"<div class=\"links\"><a href=\"" PROJECT_REPO "\" target=\"_blank\">Repo</a> | <a href=\"" PROJECT_REPO "/issues\" target=\"_blank\">Issues</a> | <a href=\"" PROJECT_REPO "/releases\" target=\"_blank\">Releases</a> | <small>v" PROJECT_VERSION " (" __DATE__ ")</small></div>"
"</div>"
));
std::vector<const char *> menu = {"custom", "wifi", "param", "sep", "info", "update", "restart"};
wm.setMenu(menu);
//wm.setCleanConnect(true);
wm.setRestorePersistent(false);
wm.setHostname(settings.hostname);
wm.setWiFiAutoReconnect(false);
wm.setAPClientCheck(true);
wm.setConfigPortalBlocking(false);
wm.setSaveParamsCallback(saveParamsCallback);
wm.setPreOtaUpdateCallback([this] {
for (AbstractTask* task : this->tasksForDisable) {
if (task->isEnabled()) {
task->disable();
}
}
});
wm.setConfigPortalTimeout(wm.getWiFiIsSaved() ? 180 : 0);
wm.setDisableConfigPortal(false);
wm.autoConnect(AP_SSID, AP_PASSWORD);
}
void loop() {
if (connected && WiFi.status() != WL_CONNECTED) {
connected = false;
if (wm.getWebPortalActive()) {
wm.stopWebPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
/*wm.setCaptivePortalEnable(true);
if (!wm.getConfigPortalActive()) {
wm.startConfigPortal(AP_SSID, AP_PASSWORD);
}*/
#if USE_TELNET
TelnetStream.stop();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
#endif
Log.sinfoln(FPSTR(S_WIFI), F("Disconnected"));
}
if (WiFi.status() != WL_CONNECTED && !wm.getConfigPortalActive()) {
if (millis() - this->lastReconnecting > 5000) {
Log.sinfoln(FPSTR(S_WIFI), F("Reconnecting..."));
WiFi.reconnect();
this->lastReconnecting = millis();
}
}
if (!connected && WiFi.status() == WL_CONNECTED) {
connected = true;
wm.setConfigPortalTimeout(180);
if (wm.getConfigPortalActive()) {
wm.stopConfigPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
wm.setCaptivePortalEnable(false);
if (!wm.getWebPortalActive()) {
wm.startWebPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
#if USE_TELNET
TelnetStream.begin(23, false);
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
#endif
Log.sinfoln(FPSTR(S_WIFI), F("Connected. IP: %s, RSSI: %hhd"), WiFi.localIP().toString().c_str(), WiFi.RSSI());
}
#ifdef ARDUINO_ARCH_ESP8266
if (connected && millis() - lastArpGratuitous > 60000) {
stationKeepAliveNow();
lastArpGratuitous = millis();
::yield();
}
#endif
wm.process();
}
static void saveParamsCallback() {
bool changed = false;
bool needRestart = false;
if (strcmp(wmHostname->getValue(), settings.hostname) != 0) {
changed = true;
needRestart = true;
strcpy(settings.hostname, wmHostname->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New hostname: %s"), settings.hostname);
}
if (strcmp(wmMqttServer->getValue(), settings.mqtt.server) != 0) {
changed = true;
strcpy(settings.mqtt.server, wmMqttServer->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.server: %s"), settings.mqtt.server);
}
if (wmMqttPort->getValue() != settings.mqtt.port) {
changed = true;
settings.mqtt.port = wmMqttPort->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.port: %hu"), settings.mqtt.port);
}
if (strcmp(wmMqttUser->getValue(), settings.mqtt.user) != 0) {
changed = true;
strcpy(settings.mqtt.user, wmMqttUser->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.user: %s"), settings.mqtt.user);
}
if (strcmp(wmMqttPassword->getValue(), settings.mqtt.password) != 0) {
changed = true;
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.password: %s"), settings.mqtt.password);
}
if (strcmp(wmMqttPrefix->getValue(), settings.mqtt.prefix) != 0) {
changed = true;
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.prefix: %s"), settings.mqtt.prefix);
}
if (wmMqttPublishInterval->getValue() != settings.mqtt.interval) {
changed = true;
settings.mqtt.interval = wmMqttPublishInterval->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.interval: %du"), settings.mqtt.interval);
}
if (wmOtInPin->getValue() != settings.opentherm.inPin) {
changed = true;
needRestart = true;
settings.opentherm.inPin = wmOtInPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.inPin: %hhu"), settings.opentherm.inPin);
}
if (wmOtOutPin->getValue() != settings.opentherm.outPin) {
changed = true;
needRestart = true;
settings.opentherm.outPin = wmOtOutPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.outPin: %hhu"), settings.opentherm.outPin);
}
if (wmOtMemberIdCode->getValue() != settings.opentherm.memberIdCode) {
changed = true;
settings.opentherm.memberIdCode = wmOtMemberIdCode->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.memberIdCode: %du"), settings.opentherm.memberIdCode);
}
if (wmOtDhwPresent->getCheckboxValue() != settings.opentherm.dhwPresent) {
changed = true;
settings.opentherm.dhwPresent = wmOtDhwPresent->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwPresent: %s"), settings.opentherm.dhwPresent ? "on" : "off");
}
if (wmOtSummerWinterMode->getCheckboxValue() != settings.opentherm.summerWinterMode) {
changed = true;
settings.opentherm.summerWinterMode = wmOtSummerWinterMode->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.summerWinterMode: %s"), settings.opentherm.summerWinterMode ? "on" : "off");
}
if (wmOtHeatingCh2Enabled->getCheckboxValue() != settings.opentherm.heatingCh2Enabled) {
changed = true;
settings.opentherm.heatingCh2Enabled = wmOtHeatingCh2Enabled->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
if (settings.opentherm.heatingCh1ToCh2) {
settings.opentherm.heatingCh1ToCh2 = false;
wmOtHeatingCh1ToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
}
if (settings.opentherm.dhwToCh2) {
settings.opentherm.dhwToCh2 = false;
wmOtDhwToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
}
}
if (wmOtHeatingCh1ToCh2->getCheckboxValue() != settings.opentherm.heatingCh1ToCh2) {
changed = true;
settings.opentherm.heatingCh1ToCh2 = wmOtHeatingCh1ToCh2->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
if (settings.opentherm.heatingCh2Enabled) {
settings.opentherm.heatingCh2Enabled = false;
wmOtHeatingCh2Enabled->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
}
if (settings.opentherm.dhwToCh2) {
settings.opentherm.dhwToCh2 = false;
wmOtDhwToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
}
}
if (wmOtDhwToCh2->getCheckboxValue() != settings.opentherm.dhwToCh2) {
changed = true;
settings.opentherm.dhwToCh2 = wmOtDhwToCh2->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
if (settings.opentherm.heatingCh2Enabled) {
settings.opentherm.heatingCh2Enabled = false;
wmOtHeatingCh2Enabled->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
}
if (settings.opentherm.heatingCh1ToCh2) {
settings.opentherm.heatingCh1ToCh2 = false;
wmOtHeatingCh1ToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
}
}
if (wmOtDhwBlocking->getCheckboxValue() != settings.opentherm.dhwBlocking) {
changed = true;
settings.opentherm.dhwBlocking = wmOtDhwBlocking->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwBlocking: %s"), settings.opentherm.dhwBlocking ? "on" : "off");
}
if (wmOtModSyncWithHeating->getCheckboxValue() != settings.opentherm.modulationSyncWithHeating) {
changed = true;
settings.opentherm.modulationSyncWithHeating = wmOtModSyncWithHeating->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.modulationSyncWithHeating: %s"), settings.opentherm.modulationSyncWithHeating ? "on" : "off");
}
if (wmOutdoorSensorPin->getValue() != settings.sensors.outdoor.pin) {
changed = true;
needRestart = true;
settings.sensors.outdoor.pin = wmOutdoorSensorPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.outdoor.pin: %hhu"), settings.sensors.outdoor.pin);
}
if (wmIndoorSensorPin->getValue() != settings.sensors.indoor.pin) {
changed = true;
needRestart = true;
settings.sensors.indoor.pin = wmIndoorSensorPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.indoor.pin: %hhu"), settings.sensors.indoor.pin);
}
#if USE_BLE
if (strcmp(wmOutdoorSensorBleAddress->getValue(), settings.sensors.indoor.bleAddresss) != 0) {
changed = true;
strcpy(settings.sensors.indoor.bleAddresss, wmOutdoorSensorBleAddress->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.indoor.bleAddresss: %s"), settings.sensors.indoor.bleAddresss);
}
#endif
if (wmExtPumpUse->getCheckboxValue() != settings.externalPump.use) {
changed = true;
settings.externalPump.use = wmExtPumpUse->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.use: %s"), settings.externalPump.use ? "on" : "off");
}
if (wmExtPumpPin->getValue() != settings.externalPump.pin) {
changed = true;
needRestart = true;
settings.externalPump.pin = wmExtPumpPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.pin: %hhu"), settings.externalPump.pin);
}
if ((wmExtPumpPostCirculationTime->getValue() * 60) != settings.externalPump.postCirculationTime) {
changed = true;
settings.externalPump.postCirculationTime = wmExtPumpPostCirculationTime->getValue() * 60;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.postCirculationTime: %hu"), settings.externalPump.postCirculationTime);
}
if ((wmExtPumpAntiStuckInterval->getValue() * 86400) != settings.externalPump.antiStuckInterval) {
changed = true;
settings.externalPump.antiStuckInterval = wmExtPumpAntiStuckInterval->getValue() * 86400;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.antiStuckInterval: %du"), settings.externalPump.antiStuckInterval);
}
if ((wmExtPumpAntiStuckTime->getValue() * 60) != settings.externalPump.antiStuckTime) {
changed = true;
settings.externalPump.antiStuckTime = wmExtPumpAntiStuckTime->getValue() * 60;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.antiStuckTime: %hu"), settings.externalPump.antiStuckTime);
}
if (!changed) {
return;
} else if (needRestart) {
vars.actions.restart = true;
}
eeSettings.update();
}
#ifdef ARDUINO_ARCH_ESP8266
/**
* @brief
* https://github.com/arendst/Tasmota/blob/e6515883f0ee5451931b6280ff847b117de5a231/tasmota/tasmota_support/support_wifi.ino#L1196
*/
static void stationKeepAliveNow(void) {
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
if (
(interface->flags & NETIF_FLAG_LINK_UP)
&& (interface->flags & NETIF_FLAG_UP)
&& interface->num == STATION_IF
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
) {
etharp_gratuitous(interface);
break;
}
}
}
#endif
};

View File

@@ -1,8 +1,6 @@
#define PROJECT_NAME "OpenTherm Gateway"
#define PROJECT_VERSION "1.4.0-rc.5"
#define PROJECT_VERSION "1.4.0-rc.9"
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
#define AP_SSID "OpenTherm Gateway"
#define AP_PASSWORD "otgateway123456"
#define EMERGENCY_TIME_TRESHOLD 120000
#define MQTT_RECONNECT_INTERVAL 15000
@@ -38,10 +36,58 @@
#define USE_BLE false
#endif
#ifndef HOSTNAME_DEFAULT
#define HOSTNAME_DEFAULT "opentherm"
#endif
#ifndef AP_SSID_DEFAULT
#define AP_SSID_DEFAULT "OpenTherm Gateway"
#endif
#ifndef AP_PASSWORD_DEFAULT
#define AP_PASSWORD_DEFAULT "otgateway123456"
#endif
#ifndef STA_SSID_DEFAULT
#define STA_SSID_DEFAULT ""
#endif
#ifndef STA_PASSWORD_DEFAULT
#define STA_PASSWORD_DEFAULT ""
#endif
#ifndef DEBUG_BY_DEFAULT
#define DEBUG_BY_DEFAULT false
#endif
#ifndef PORTAL_LOGIN_DEFAULT
#define PORTAL_LOGIN_DEFAULT ""
#endif
#ifndef PORTAL_PASSWORD_DEFAULT
#define PORTAL_PASSWORD_DEFAULT ""
#endif
#ifndef MQTT_SERVER_DEFAULT
#define MQTT_SERVER_DEFAULT ""
#endif
#ifndef MQTT_PORT_DEFAULT
#define MQTT_PORT_DEFAULT 1883
#endif
#ifndef MQTT_USER_DEFAULT
#define MQTT_USER_DEFAULT ""
#endif
#ifndef MQTT_PASSWORD_DEFAULT
#define MQTT_PASSWORD_DEFAULT ""
#endif
#ifndef MQTT_PREFIX_DEFAULT
#define MQTT_PREFIX_DEFAULT "opentherm"
#endif
#ifndef OT_IN_PIN_DEFAULT
#define OT_IN_PIN_DEFAULT 0
#endif

View File

@@ -1,9 +1,12 @@
#include <Arduino.h>
#include "defines.h"
#include "strings.h"
#include <ArduinoJson.h>
#include <EEManager.h>
#include <FileData.h>
#include <LittleFS.h>
#include <TinyLogger.h>
#include "Settings.h"
#include <utils.h>
#if USE_TELNET
#include "ESPTelnetStream.h"
@@ -19,29 +22,34 @@
#include <Task.h>
#include <LeanTask.h>
#include "WifiManagerTask.h"
#include "NetworkTask.h"
#include "MqttTask.h"
#include "OpenThermTask.h"
#include "SensorsTask.h"
#include "RegulatorTask.h"
#include "PortalTask.h"
#include "MainTask.h"
// Vars
EEManager eeSettings(settings, 60000);
FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000);
FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000);
#if USE_TELNET
ESPTelnetStream TelnetStream;
ESPTelnetStream TelnetStream;
#endif
// Tasks
WifiManagerTask* tWm;
NetworkTask* tNetwork;
MqttTask* tMqtt;
OpenThermTask* tOt;
SensorsTask* tSensors;
RegulatorTask* tRegulator;
PortalTask* tPortal;
MainTask* tMain;
void setup() {
LittleFS.begin();
Log.setLevel(TinyLogger::Level::VERBOSE);
Log.setServiceTemplate("\033[1m[%s]\033[22m");
Log.setLevelTemplate("\033[1m[%s]\033[22m");
@@ -52,46 +60,87 @@ void setup() {
int sec = time % 60;
int min = time % 3600 / 60;
int hour = time / 3600;
return tm{sec, min, hour};
});
#if USE_SERIAL
Serial.begin(115200);
Serial.println("\n\n");
Log.addStream(&Serial);
Serial.begin(115200);
Log.addStream(&Serial);
#endif
#if USE_TELNET
TelnetStream.setKeepAliveInterval(500);
Log.addStream(&TelnetStream);
TelnetStream.setKeepAliveInterval(500);
Log.addStream(&TelnetStream);
#endif
EEPROM.begin(eeSettings.blockSize());
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
if (eeSettingsResult == 0) {
Log.sinfoln("MAIN", F("Settings loaded"));
Log.print("\n\n\r");
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
Log.swarningln("MAIN", F("Settings not valid, reset and restart..."));
eeSettings.reset();
delay(5000);
ESP.restart();
}
// network settings
switch (fsNetworkSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Loaded"));
break;
default:
break;
}
} else if (eeSettingsResult == 1) {
Log.sinfoln("MAIN", F("Settings NOT loaded, first start"));
// settings
switch (fsSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_SETTINGS), F("Loaded"));
} else if (eeSettingsResult == 2) {
Log.serrorln("MAIN", F("Settings NOT loaded (error)"));
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
Log.swarningln(FPSTR(L_SETTINGS), F("Not valid, set default and restart..."));
fsSettings.reset();
delay(5000);
ESP.restart();
}
break;
default:
break;
}
Log.setLevel(settings.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
tWm = new WifiManagerTask(true, 0);
Scheduler.start(tWm);
tMqtt = new MqttTask(false, 100);
tNetwork = (new NetworkTask(true, 500))
->setHostname(networkSettings.hostname)
->setStaCredentials(
#ifdef WOKWI
"Wokwi-GUEST", nullptr, 6
#else
strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr,
strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr,
networkSettings.sta.channel
#endif
)->setApCredentials(
strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr,
strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr,
networkSettings.ap.channel
);
Scheduler.start(tNetwork);
tMqtt = new MqttTask(false, 500);
Scheduler.start(tMqtt);
tOt = new OpenThermTask(false, 1000);
@@ -103,21 +152,17 @@ void setup() {
tRegulator = new RegulatorTask(true, 10000);
Scheduler.start(tRegulator);
tPortal = new PortalTask(true, 0);
Scheduler.start(tPortal);
tMain = new MainTask(true, 100);
Scheduler.start(tMain);
tWm
->addTaskForDisable(tMain)
->addTaskForDisable(tMqtt)
->addTaskForDisable(tOt)
->addTaskForDisable(tSensors)
->addTaskForDisable(tRegulator);
Scheduler.begin();
}
void loop() {
#if defined(ARDUINO_ARCH_ESP32)
vTaskDelete(NULL);
#endif
#if defined(ARDUINO_ARCH_ESP32)
vTaskDelete(NULL);
#endif
}

23
src/strings.h Normal file
View File

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

939
src/utils.h Normal file
View File

@@ -0,0 +1,939 @@
#include <Arduino.h>
double roundd(double value, uint8_t decimals = 2) {
if (decimals == 0) {
return (int)(value + 0.5);
} else if (abs(value) < 0.00000001) {
return 0.0;
}
double multiplier = pow10(decimals);
value += 0.5 / multiplier * (value < 0 ? -1 : 1);
return (int)(value * multiplier) / multiplier;
}
size_t getFreeHeap() {
return ESP.getFreeHeap();
}
size_t getTotalHeap() {
#if defined(ARDUINO_ARCH_ESP32)
return ESP.getHeapSize();
#elif defined(ARDUINO_ARCH_ESP8266)
return 81920;
#else
return 99999;
#endif
}
size_t getMaxFreeBlockHeap() {
#if defined(ARDUINO_ARCH_ESP32)
return ESP.getMaxAllocHeap();
#else
return ESP.getMaxFreeBlockSize();
#endif
}
String getResetReason() {
String value;
#if defined(ARDUINO_ARCH_ESP8266)
value = ESP.getResetReason();
#elif defined(ARDUINO_ARCH_ESP32)
switch(esp_reset_reason()) {
case ESP_RST_POWERON:
value = F("Reset due to power-on event");
break;
case ESP_RST_EXT:
value = F("Reset by external pin");
break;
case ESP_RST_SW:
value = F("Software reset via esp_restart");
break;
case ESP_RST_PANIC:
value = F("Software reset due to exception/panic");
break;
case ESP_RST_INT_WDT:
value = F("Reset (software or hardware) due to interrupt watchdog");
break;
case ESP_RST_TASK_WDT:
value = F("Reset due to task watchdog");
break;
case ESP_RST_WDT:
value = F("Reset due to other watchdogs");
break;
case ESP_RST_DEEPSLEEP:
value = F("Reset after exiting deep sleep mode");
break;
case ESP_RST_BROWNOUT:
value = F("Brownout reset (software or hardware)");
break;
case ESP_RST_SDIO:
value = F("Reset over SDIO");
break;
case ESP_RST_UNKNOWN:
default:
value = F("unknown");
break;
}
#else
value = F("unknown");
#endif
return value;
}
void networkSettingsToJson(const NetworkSettings& src, JsonVariant dst) {
dst["hostname"] = src.hostname;
dst["useDhcp"] = src.useDhcp;
dst["staticConfig"]["ip"] = src.staticConfig.ip;
dst["staticConfig"]["gateway"] = src.staticConfig.gateway;
dst["staticConfig"]["subnet"] = src.staticConfig.subnet;
dst["staticConfig"]["dns"] = src.staticConfig.dns;
dst["ap"]["ssid"] = src.ap.ssid;
dst["ap"]["password"] = src.ap.password;
dst["ap"]["channel"] = src.ap.channel;
dst["sta"]["ssid"] = src.sta.ssid;
dst["sta"]["password"] = src.sta.password;
dst["sta"]["channel"] = src.sta.channel;
//dst.shrinkToFit();
}
bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) {
bool changed = false;
// hostname
if (!src["hostname"].isNull()) {
String value = src["hostname"].as<String>();
if (value.length() < sizeof(dst.hostname)) {
strcpy(dst.hostname, value.c_str());
changed = true;
}
}
// use dhcp
if (src["useDhcp"].is<bool>()) {
dst.useDhcp = src["useDhcp"].as<bool>();
changed = true;
}
// static config
if (!src["staticConfig"]["ip"].isNull()) {
String value = src["staticConfig"]["ip"].as<String>();
if (value.length() < sizeof(dst.staticConfig.ip)) {
strcpy(dst.staticConfig.ip, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["gateway"].isNull()) {
String value = src["staticConfig"]["gateway"].as<String>();
if (value.length() < sizeof(dst.staticConfig.gateway)) {
strcpy(dst.staticConfig.gateway, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["subnet"].isNull()) {
String value = src["staticConfig"]["subnet"].as<String>();
if (value.length() < sizeof(dst.staticConfig.subnet)) {
strcpy(dst.staticConfig.subnet, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["dns"].isNull()) {
String value = src["staticConfig"]["dns"].as<String>();
if (value.length() < sizeof(dst.staticConfig.dns)) {
strcpy(dst.staticConfig.dns, value.c_str());
changed = true;
}
}
// ap
if (!src["ap"]["ssid"].isNull()) {
String value = src["ap"]["ssid"].as<String>();
if (value.length() < sizeof(dst.ap.ssid)) {
strcpy(dst.ap.ssid, value.c_str());
changed = true;
}
}
if (!src["ap"]["password"].isNull()) {
String value = src["ap"]["password"].as<String>();
if (value.length() < sizeof(dst.ap.password)) {
strcpy(dst.ap.password, value.c_str());
changed = true;
}
}
if (!src["ap"]["channel"].isNull()) {
unsigned char value = src["ap"]["channel"].as<unsigned char>();
if (value >= 0 && value < 12) {
dst.ap.channel = value;
changed = true;
}
}
// ap
if (!src["sta"]["ssid"].isNull()) {
String value = src["sta"]["ssid"].as<String>();
if (value.length() < sizeof(dst.sta.ssid)) {
strcpy(dst.sta.ssid, value.c_str());
changed = true;
}
}
if (!src["sta"]["password"].isNull()) {
String value = src["sta"]["password"].as<String>();
if (value.length() < sizeof(dst.sta.password)) {
strcpy(dst.sta.password, value.c_str());
changed = true;
}
}
if (!src["sta"]["channel"].isNull()) {
unsigned char value = src["sta"]["channel"].as<unsigned char>();
if (value >= 0 && value < 12) {
dst.sta.channel = value;
changed = true;
}
}
return changed;
}
void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
dst["debug"] = src.debug;
if (!safe) {
dst["portal"]["useAuth"] = src.portal.useAuth;
dst["portal"]["login"] = src.portal.login;
dst["portal"]["password"] = src.portal.password;
dst["opentherm"]["inPin"] = src.opentherm.inPin;
dst["opentherm"]["outPin"] = src.opentherm.outPin;
dst["opentherm"]["memberIdCode"] = src.opentherm.memberIdCode;
dst["opentherm"]["dhwPresent"] = src.opentherm.dhwPresent;
dst["opentherm"]["summerWinterMode"] = src.opentherm.summerWinterMode;
dst["opentherm"]["heatingCh2Enabled"] = src.opentherm.heatingCh2Enabled;
dst["opentherm"]["heatingCh1ToCh2"] = src.opentherm.heatingCh1ToCh2;
dst["opentherm"]["dhwToCh2"] = src.opentherm.dhwToCh2;
dst["opentherm"]["dhwBlocking"] = src.opentherm.dhwBlocking;
dst["opentherm"]["modulationSyncWithHeating"] = src.opentherm.modulationSyncWithHeating;
dst["mqtt"]["server"] = src.mqtt.server;
dst["mqtt"]["port"] = src.mqtt.port;
dst["mqtt"]["user"] = src.mqtt.user;
dst["mqtt"]["password"] = src.mqtt.password;
dst["mqtt"]["prefix"] = src.mqtt.prefix;
dst["mqtt"]["interval"] = src.mqtt.interval;
}
dst["emergency"]["enable"] = src.emergency.enable;
dst["emergency"]["target"] = roundd(src.emergency.target, 2);
dst["emergency"]["useEquitherm"] = src.emergency.useEquitherm;
dst["emergency"]["usePid"] = src.emergency.usePid;
dst["heating"]["enable"] = src.heating.enable;
dst["heating"]["turbo"] = src.heating.turbo;
dst["heating"]["target"] = roundd(src.heating.target, 2);
dst["heating"]["hysteresis"] = roundd(src.heating.hysteresis, 2);
dst["heating"]["minTemp"] = src.heating.minTemp;
dst["heating"]["maxTemp"] = src.heating.maxTemp;
dst["heating"]["maxModulation"] = src.heating.maxModulation;
dst["dhw"]["enable"] = src.dhw.enable;
dst["dhw"]["target"] = src.dhw.target;
dst["dhw"]["minTemp"] = src.dhw.minTemp;
dst["dhw"]["maxTemp"] = src.dhw.maxTemp;
dst["pid"]["enable"] = src.pid.enable;
dst["pid"]["p_factor"] = roundd(src.pid.p_factor, 3);
dst["pid"]["i_factor"] = roundd(src.pid.i_factor, 3);
dst["pid"]["d_factor"] = roundd(src.pid.d_factor, 1);
dst["pid"]["dt"] = src.pid.dt;
dst["pid"]["minTemp"] = src.pid.minTemp;
dst["pid"]["maxTemp"] = src.pid.maxTemp;
dst["equitherm"]["enable"] = src.equitherm.enable;
dst["equitherm"]["n_factor"] = roundd(src.equitherm.n_factor, 3);
dst["equitherm"]["k_factor"] = roundd(src.equitherm.k_factor, 3);
dst["equitherm"]["t_factor"] = roundd(src.equitherm.t_factor, 3);
dst["sensors"]["outdoor"]["type"] = src.sensors.outdoor.type;
dst["sensors"]["outdoor"]["pin"] = src.sensors.outdoor.pin;
dst["sensors"]["outdoor"]["offset"] = roundd(src.sensors.outdoor.offset, 2);
dst["sensors"]["indoor"]["type"] = src.sensors.indoor.type;
dst["sensors"]["indoor"]["pin"] = src.sensors.indoor.pin;
dst["sensors"]["indoor"]["bleAddresss"] = src.sensors.indoor.bleAddresss;
dst["sensors"]["indoor"]["offset"] = roundd(src.sensors.indoor.offset, 2);
if (!safe) {
dst["externalPump"]["use"] = src.externalPump.use;
dst["externalPump"]["pin"] = src.externalPump.pin;
dst["externalPump"]["postCirculationTime"] = roundd(src.externalPump.postCirculationTime / 60, 0);
dst["externalPump"]["antiStuckInterval"] = roundd(src.externalPump.antiStuckInterval / 86400, 0);
dst["externalPump"]["antiStuckTime"] = roundd(src.externalPump.antiStuckTime / 60, 0);
}
//dst.shrinkToFit();
}
void safeSettingsToJson(const Settings& src, JsonVariant dst) {
settingsToJson(src, dst, true);
}
bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false) {
bool changed = false;
if (src["debug"].is<bool>()) {
dst.debug = src["debug"].as<bool>();
changed = true;
}
if (!safe) {
// portal
if (src["portal"]["useAuth"].is<bool>()) {
dst.portal.useAuth = src["portal"]["useAuth"].as<bool>();
changed = true;
}
if (!src["portal"]["login"].isNull()) {
String value = src["portal"]["login"].as<String>();
if (value.length() < sizeof(dst.portal.login)) {
strcpy(dst.portal.login, value.c_str());
changed = true;
}
}
if (!src["portal"]["password"].isNull()) {
String value = src["portal"]["password"].as<String>();
if (value.length() < sizeof(dst.portal.password)) {
strcpy(dst.portal.password, value.c_str());
changed = true;
}
}
// opentherm
if (!src["opentherm"]["inPin"].isNull()) {
unsigned char value = src["opentherm"]["inPin"].as<unsigned char>();
if (value >= 0 && value < 50) {
dst.opentherm.inPin = value;
changed = true;
}
}
if (!src["opentherm"]["outPin"].isNull()) {
unsigned char value = src["opentherm"]["outPin"].as<unsigned char>();
if (value >= 0 && value < 50) {
dst.opentherm.outPin = value;
changed = true;
}
}
if (!src["opentherm"]["memberIdCode"].isNull()) {
unsigned int value = src["opentherm"]["memberIdCode"].as<unsigned int>();
if (value >= 0 && value < 65536) {
dst.opentherm.memberIdCode = value;
changed = true;
}
}
if (src["opentherm"]["dhwPresent"].is<bool>()) {
dst.opentherm.dhwPresent = src["opentherm"]["dhwPresent"].as<bool>();
changed = true;
}
if (src["opentherm"]["summerWinterMode"].is<bool>()) {
dst.opentherm.summerWinterMode = src["opentherm"]["summerWinterMode"].as<bool>();
changed = true;
}
if (src["opentherm"]["heatingCh2Enabled"].is<bool>()) {
dst.opentherm.heatingCh2Enabled = src["opentherm"]["heatingCh2Enabled"].as<bool>();
if (dst.opentherm.heatingCh2Enabled) {
dst.opentherm.heatingCh1ToCh2 = false;
dst.opentherm.dhwToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["heatingCh1ToCh2"].is<bool>()) {
dst.opentherm.heatingCh1ToCh2 = src["opentherm"]["heatingCh1ToCh2"].as<bool>();
if (dst.opentherm.heatingCh1ToCh2) {
dst.opentherm.heatingCh2Enabled = false;
dst.opentherm.dhwToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["dhwToCh2"].is<bool>()) {
dst.opentherm.dhwToCh2 = src["opentherm"]["dhwToCh2"].as<bool>();
if (dst.opentherm.dhwToCh2) {
dst.opentherm.heatingCh2Enabled = false;
dst.opentherm.heatingCh1ToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["dhwBlocking"].is<bool>()) {
dst.opentherm.dhwBlocking = src["opentherm"]["dhwBlocking"].as<bool>();
changed = true;
}
if (src["opentherm"]["modulationSyncWithHeating"].is<bool>()) {
dst.opentherm.modulationSyncWithHeating = src["opentherm"]["modulationSyncWithHeating"].as<bool>();
changed = true;
}
// mqtt
if (!src["mqtt"]["server"].isNull()) {
String value = src["mqtt"]["server"].as<String>();
if (value.length() < sizeof(dst.mqtt.server)) {
strcpy(dst.mqtt.server, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["port"].isNull()) {
unsigned short value = src["mqtt"]["port"].as<unsigned short>();
if (value >= 0 && value <= 65536) {
dst.mqtt.port = value;
changed = true;
}
}
if (!src["mqtt"]["user"].isNull()) {
String value = src["mqtt"]["user"].as<String>();
if (value.length() < sizeof(dst.mqtt.user)) {
strcpy(dst.mqtt.user, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["password"].isNull()) {
String value = src["mqtt"]["password"].as<String>();
if (value.length() < sizeof(dst.mqtt.password)) {
strcpy(dst.mqtt.password, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["prefix"].isNull()) {
String value = src["mqtt"]["prefix"].as<String>();
if (value.length() < sizeof(dst.mqtt.prefix)) {
strcpy(dst.mqtt.prefix, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["interval"].isNull()) {
unsigned short value = src["mqtt"]["interval"].as<unsigned short>();
if (value >= 3 && value <= 60) {
dst.mqtt.interval = value;
changed = true;
}
}
}
// emergency
if (src["emergency"]["enable"].is<bool>()) {
dst.emergency.enable = src["emergency"]["enable"].as<bool>();
changed = true;
}
if (!src["emergency"]["target"].isNull()) {
double value = src["emergency"]["target"].as<double>();
if (value > 0 && value < 100) {
dst.emergency.target = roundd(value, 2);
changed = true;
}
}
if (src["emergency"]["useEquitherm"].is<bool>()) {
if (dst.sensors.outdoor.type != 1) {
dst.emergency.useEquitherm = src["emergency"]["useEquitherm"].as<bool>();
} else {
dst.emergency.useEquitherm = false;
}
if (dst.emergency.useEquitherm && dst.emergency.usePid) {
dst.emergency.usePid = false;
}
changed = true;
}
if (src["emergency"]["usePid"].is<bool>()) {
if (dst.sensors.indoor.type != 1) {
dst.emergency.usePid = src["emergency"]["usePid"].as<bool>();
} else {
dst.emergency.usePid = false;
}
if (dst.emergency.usePid && dst.emergency.useEquitherm) {
dst.emergency.useEquitherm = false;
}
changed = true;
}
// heating
if (src["heating"]["enable"].is<bool>()) {
dst.heating.enable = src["heating"]["enable"].as<bool>();
changed = true;
}
if (src["heating"]["turbo"].is<bool>()) {
dst.heating.turbo = src["heating"]["turbo"].as<bool>();
changed = true;
}
if (!src["heating"]["target"].isNull()) {
double value = src["heating"]["target"].as<double>();
if (value > 0 && value < 100) {
dst.heating.target = roundd(value, 2);
changed = true;
}
}
if (!src["heating"]["hysteresis"].isNull()) {
double value = src["heating"]["hysteresis"].as<double>();
if (value >= 0 && value <= 5) {
dst.heating.hysteresis = roundd(value, 2);
changed = true;
}
}
if (!src["heating"]["minTemp"].isNull()) {
unsigned char value = src["heating"]["minTemp"].as<unsigned char>();
if (value >= vars.parameters.heatingMinTemp && value <= vars.parameters.heatingMaxTemp) {
dst.heating.minTemp = value;
changed = true;
}
}
if (!src["heating"]["maxTemp"].isNull()) {
unsigned char value = src["heating"]["maxTemp"].as<unsigned char>();
if (value >= vars.parameters.heatingMinTemp && value <= vars.parameters.heatingMaxTemp) {
dst.heating.maxTemp = value;
changed = true;
}
}
if (!src["heating"]["maxModulation"].isNull()) {
unsigned char value = src["heating"]["maxModulation"].as<unsigned char>();
if (value > 0 && value <= 100) {
dst.heating.maxModulation = value;
changed = true;
}
}
// dhw
if (src["dhw"]["enable"].is<bool>()) {
dst.dhw.enable = src["dhw"]["enable"].as<bool>();
changed = true;
}
if (!src["dhw"]["target"].isNull()) {
unsigned char value = src["dhw"]["target"].as<unsigned char>();
if (value >= 0 && value < 100) {
dst.dhw.target = value;
changed = true;
}
}
if (!src["dhw"]["minTemp"].isNull()) {
unsigned char value = src["dhw"]["minTemp"].as<unsigned char>();
if (value >= vars.parameters.dhwMinTemp && value <= vars.parameters.dhwMaxTemp) {
dst.dhw.minTemp = value;
changed = true;
}
}
if (!src["dhw"]["maxTemp"].isNull()) {
unsigned char value = src["dhw"]["maxTemp"].as<unsigned char>();
if (value >= vars.parameters.dhwMinTemp && value <= vars.parameters.dhwMaxTemp) {
dst.dhw.maxTemp = value;
changed = true;
}
}
// pid
if (src["pid"]["enable"].is<bool>()) {
dst.pid.enable = src["pid"]["enable"].as<bool>();
changed = true;
}
if (!src["pid"]["p_factor"].isNull()) {
double value = src["pid"]["p_factor"].as<double>();
if (value > 0 && value <= 1000) {
dst.pid.p_factor = roundd(value, 3);
changed = true;
}
}
if (!src["pid"]["i_factor"].isNull()) {
double value = src["pid"]["i_factor"].as<double>();
if (value >= 0 && value <= 100) {
dst.pid.i_factor = roundd(value, 3);
changed = true;
}
}
if (!src["pid"]["d_factor"].isNull()) {
double value = src["pid"]["d_factor"].as<double>();
if (value >= 0 && value <= 100000) {
dst.pid.d_factor = roundd(value, 1);
changed = true;
}
}
if (!src["pid"]["dt"].isNull()) {
unsigned short value = src["pid"]["dt"].as<unsigned short>();
if (value >= 30 && value <= 600) {
dst.pid.dt = value;
changed = true;
}
}
if (!src["pid"]["maxTemp"].isNull()) {
unsigned char value = src["pid"]["maxTemp"].as<unsigned char>();
if (value > 0 && value <= 100 && value > dst.pid.minTemp) {
dst.pid.maxTemp = value;
changed = true;
}
}
if (!src["pid"]["minTemp"].isNull()) {
unsigned char value = src["pid"]["minTemp"].as<unsigned char>();
if (value >= 0 && value < 100 && value < dst.pid.maxTemp) {
dst.pid.minTemp = value;
changed = true;
}
}
// equitherm
if (src["equitherm"]["enable"].is<bool>()) {
dst.equitherm.enable = src["equitherm"]["enable"].as<bool>();
changed = true;
}
if (!src["equitherm"]["n_factor"].isNull()) {
double value = src["equitherm"]["n_factor"].as<double>();
if (value > 0 && value <= 10) {
dst.equitherm.n_factor = roundd(value, 3);
changed = true;
}
}
if (!src["equitherm"]["k_factor"].isNull()) {
double value = src["equitherm"]["k_factor"].as<double>();
if (value >= 0 && value <= 10) {
dst.equitherm.k_factor = roundd(value, 3);
changed = true;
}
}
if (!src["equitherm"]["t_factor"].isNull()) {
double value = src["equitherm"]["t_factor"].as<double>();
if (value >= 0 && value <= 10) {
dst.equitherm.t_factor = roundd(value, 3);
changed = true;
}
}
// sensors
if (!src["sensors"]["outdoor"]["type"].isNull()) {
unsigned char value = src["sensors"]["outdoor"]["type"].as<unsigned char>();
if (value >= 0 && value <= 2) {
dst.sensors.outdoor.type = value;
if (dst.sensors.outdoor.type == 1) {
dst.emergency.useEquitherm = false;
}
changed = true;
}
}
if (!src["sensors"]["outdoor"]["pin"].isNull()) {
unsigned char value = src["sensors"]["outdoor"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.sensors.outdoor.pin = value;
changed = true;
}
}
if (!src["sensors"]["outdoor"]["offset"].isNull()) {
double value = src["sensors"]["outdoor"]["offset"].as<double>();
if (value >= -10 && value <= 10) {
dst.sensors.outdoor.offset = roundd(value, 2);
changed = true;
}
}
if (!src["sensors"]["indoor"]["type"].isNull()) {
unsigned char value = src["sensors"]["indoor"]["type"].as<unsigned char>();
if (value >= 1 && value <= 3) {
dst.sensors.indoor.type = value;
if (dst.sensors.indoor.type == 1) {
dst.emergency.usePid = false;
}
changed = true;
}
}
if (!src["sensors"]["indoor"]["pin"].isNull()) {
unsigned char value = src["sensors"]["indoor"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.sensors.indoor.pin = value;
changed = true;
}
}
#if USE_BLE
if (!src["sensors"]["indoor"]["bleAddresss"].isNull()) {
String value = src["sensors"]["indoor"]["bleAddresss"].as<String>();
if (value.length() < sizeof(dst.sensors.indoor.bleAddresss)) {
strcpy(dst.sensors.indoor.bleAddresss, value.c_str());
changed = true;
}
}
#endif
if (!src["sensors"]["indoor"]["offset"].isNull()) {
double value = src["sensors"]["indoor"]["offset"].as<double>();
if (value >= -10 && value <= 10) {
dst.sensors.indoor.offset = roundd(value, 2);
changed = true;
}
}
if (!safe) {
// external pump
if (src["externalPump"]["use"].is<bool>()) {
dst.externalPump.use = src["externalPump"]["use"].as<bool>();
changed = true;
}
if (!src["externalPump"]["pin"].isNull()) {
unsigned char value = src["externalPump"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.externalPump.pin = value;
changed = true;
}
}
if (!src["externalPump"]["postCirculationTime"].isNull()) {
unsigned short value = src["externalPump"]["postCirculationTime"].as<unsigned short>();
if (value >= 0 && value <= 120) {
dst.externalPump.postCirculationTime = value * 60;
changed = true;
}
}
if (!src["externalPump"]["antiStuckInterval"].isNull()) {
unsigned int value = src["externalPump"]["antiStuckInterval"].as<unsigned int>();
if (value >= 0 && value <= 366) {
dst.externalPump.antiStuckInterval = value * 86400;
changed = true;
}
}
if (!src["externalPump"]["antiStuckTime"].isNull()) {
unsigned short value = src["externalPump"]["antiStuckTime"].as<unsigned short>();
if (value >= 0 && value <= 20) {
dst.externalPump.antiStuckTime = value * 60;
changed = true;
}
}
}
return changed;
}
bool safeJsonToSettings(const JsonVariantConst src, Settings& dst) {
return jsonToSettings(src, dst, true);
}
void varsToJson(const Variables& src, JsonVariant dst) {
dst["tuning"]["enable"] = src.tuning.enable;
dst["tuning"]["regulator"] = src.tuning.regulator;
dst["states"]["otStatus"] = src.states.otStatus;
dst["states"]["emergency"] = src.states.emergency;
dst["states"]["heating"] = src.states.heating;
dst["states"]["dhw"] = src.states.dhw;
dst["states"]["flame"] = src.states.flame;
dst["states"]["fault"] = src.states.fault;
dst["states"]["diagnostic"] = src.states.diagnostic;
dst["states"]["externalPump"] = src.states.externalPump;
dst["sensors"]["modulation"] = roundd(src.sensors.modulation, 2);
dst["sensors"]["pressure"] = roundd(src.sensors.pressure, 2);
dst["sensors"]["dhwFlowRate"] = src.sensors.dhwFlowRate;
dst["sensors"]["faultCode"] = src.sensors.faultCode;
dst["sensors"]["rssi"] = src.sensors.rssi;
dst["sensors"]["uptime"] = millis() / 1000ul;
dst["temperatures"]["indoor"] = roundd(src.temperatures.indoor, 2);
dst["temperatures"]["outdoor"] = roundd(src.temperatures.outdoor, 2);
dst["temperatures"]["heating"] = roundd(src.temperatures.heating, 2);
dst["temperatures"]["dhw"] = roundd(src.temperatures.dhw, 2);
dst["parameters"]["heatingEnabled"] = src.parameters.heatingEnabled;
dst["parameters"]["heatingMinTemp"] = src.parameters.heatingMinTemp;
dst["parameters"]["heatingMaxTemp"] = src.parameters.heatingMaxTemp;
dst["parameters"]["heatingSetpoint"] = src.parameters.heatingSetpoint;
dst["parameters"]["dhwMinTemp"] = src.parameters.dhwMinTemp;
dst["parameters"]["dhwMaxTemp"] = src.parameters.dhwMaxTemp;
//dst.shrinkToFit();
}
bool jsonToVars(const JsonVariantConst src, Variables& dst) {
bool changed = false;
// tuning
if (src["tuning"]["enable"].is<bool>()) {
dst.tuning.enable = src["tuning"]["enable"].as<bool>();
changed = true;
}
if (!src["tuning"]["regulator"].isNull()) {
unsigned char value = src["tuning"]["regulator"].as<unsigned char>();
if (value >= 0 && value <= 1) {
dst.tuning.regulator = value;
changed = true;
}
}
// temperatures
if (!src["temperatures"]["indoor"].isNull()) {
double value = src["temperatures"]["indoor"].as<double>();
if (settings.sensors.indoor.type == 1 && value > -100 && value < 100) {
dst.temperatures.indoor = roundd(value, 2);
changed = true;
}
}
if (!src["temperatures"]["outdoor"].isNull()) {
double value = src["temperatures"]["outdoor"].as<double>();
if (settings.sensors.outdoor.type == 1 && value > -100 && value < 100) {
dst.temperatures.outdoor = roundd(value, 2);
changed = true;
}
}
// actions
if (src["actions"]["restart"].is<bool>() && src["actions"]["restart"].as<bool>()) {
dst.actions.restart = true;
}
if (src["actions"]["resetFault"].is<bool>() && src["actions"]["resetFault"].as<bool>()) {
dst.actions.resetFault = true;
}
if (src["actions"]["resetDiagnostic"].is<bool>() && src["actions"]["resetDiagnostic"].as<bool>()) {
dst.actions.resetDiagnostic = true;
}
return changed;
}