mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-10 18:24:27 +05:00
* 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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.pio
|
||||
.vscode
|
||||
build/*
|
||||
build/*
|
||||
secrets.ini
|
||||
230
data/index.html
Normal file
230
data/index.html
Normal 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
166
data/network.html
Normal 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
275
data/settings.html
Normal 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
BIN
data/static/app.css.gz
Normal file
Binary file not shown.
BIN
data/static/app.js.gz
Normal file
BIN
data/static/app.js.gz
Normal file
Binary file not shown.
BIN
data/static/favicon.ico.gz
Normal file
BIN
data/static/favicon.ico.gz
Normal file
Binary file not shown.
BIN
data/static/pico.min.css.gz
Normal file
BIN
data/static/pico.min.css.gz
Normal file
Binary file not shown.
108
data/upgrade.html
Normal file
108
data/upgrade.html
Normal 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>
|
||||
82
lib/BufferedWebServer/BufferedWebServer.h
Normal file
82
lib/BufferedWebServer/BufferedWebServer.h
Normal 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;
|
||||
};
|
||||
129
lib/Connection/Connection.cpp
Normal file
129
lib/Connection/Connection.cpp
Normal 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;
|
||||
43
lib/Connection/Connection.h
Normal file
43
lib/Connection/Connection.h
Normal 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;
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
227
lib/WebServerHandlers/DynamicPage.h
Normal file
227
lib/WebServerHandlers/DynamicPage.h
Normal 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;
|
||||
};
|
||||
96
lib/WebServerHandlers/StaticPage.h
Normal file
96
lib/WebServerHandlers/StaticPage.h
Normal 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;
|
||||
};
|
||||
218
lib/WebServerHandlers/UpgradeHandler.h
Normal file
218
lib/WebServerHandlers/UpgradeHandler.h
Normal 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};
|
||||
};
|
||||
64
noCompressedData/static/app.css
Normal file
64
noCompressedData/static/app.css
Normal 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;
|
||||
}
|
||||
636
noCompressedData/static/app.js
Normal file
636
noCompressedData/static/app.js
Normal 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);
|
||||
}
|
||||
BIN
noCompressedData/static/favicon.ico
Normal file
BIN
noCompressedData/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
4
noCompressedData/static/pico.min.css
vendored
Normal file
4
noCompressedData/static/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
20
secrets.default.ini
Normal 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
|
||||
@@ -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;
|
||||
|
||||
|
||||
475
src/MqttTask.h
475
src/MqttTask.h
@@ -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
406
src/NetworkTask.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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
625
src/PortalTask.h
Normal 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();
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
|
||||
125
src/main.cpp
125
src/main.cpp
@@ -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
23
src/strings.h
Normal 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
939
src/utils.h
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user