Compare commits

...

32 Commits

Author SHA1 Message Date
de21cff76c smaller SPIFFS (eagle.flash.1m64.ld) + platformio.ini cleanup 2025-09-08 10:00:51 +02:00
c390829f35 platformio.ini cleanup 2025-09-08 08:59:27 +02:00
9bebffc743 width=device-width, initial-scale=1 2025-09-04 14:27:25 +02:00
5ca6425918 gridPowerDeltaAlarmSince reset FIX 2025-09-03 22:59:28 +02:00
9b7b08e9cb GosundSP111 relay0LED FIX 2025-09-03 22:59:14 +02:00
556f277016 httpUpload WDT -> yield 2025-09-03 17:23:06 +02:00
2652de5cb8 Relay publish after any change 2025-09-03 14:31:15 +02:00
a3076b9b2f Relay code clean 2025-09-03 14:27:58 +02:00
b8d79dbc67 mqtt buffer size FIX 2025-09-03 14:27:43 +02:00
d4282d3fba gridPowerDelta Relay logging 2025-09-03 13:52:16 +02:00
8024c2ddcd gridPowerDeltaValue in UI 2025-09-03 12:58:29 +02:00
ccc9eea312 gridPowerDelta Relay FIX 2025-09-03 12:49:03 +02:00
465d4e0624 gridPowerDelta UI 2025-09-03 12:39:40 +02:00
1336d3103f replaced config-overloads by explicitly named functions + password & log cleanup 2025-09-03 12:00:35 +02:00
46fcc4b872 more http configuration 2025-09-03 11:25:12 +02:00
f72c04b9b3 gridPowerDelta 2025-09-03 11:25:12 +02:00
951d15ac81 logging 2025-09-03 10:05:48 +02:00
7700e9bd86 platformio cleanup 2025-09-03 10:05:48 +02:00
0215f2ab94 removed tries to reduce size 2025-09-03 10:05:48 +02:00
0eb51d8ad6 gridPowerDelta 2025-09-03 10:05:48 +02:00
c4d30371cf gosund relay0LED 2025-09-03 10:05:48 +02:00
7aa39483de SP111 2025-09-02 14:35:20 +02:00
1b1f295b23 moved "Zyklus" button to the right 2025-09-01 14:24:06 +02:00
177cb68987 configWrite isPassword FIX 2025-09-01 14:22:45 +02:00
b6ea584a4b mqtt + admin ui + cycle FIX 2025-09-01 12:46:14 +02:00
801b99a3a7 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	platformio.ini
#	src/io.cpp
2025-09-01 09:51:42 +02:00
4e5fff2498 WiFi logging 2025-09-01 09:49:14 +02:00
b88df843ad Output.cycle FIX 2025-09-01 09:47:01 +02:00
ae1c6f5cda GosundSP111 2025-09-01 09:45:58 +02:00
71490427f2 GosundSP111 2025-09-01 09:18:24 +02:00
e604f90a14 upload to prod 2025-08-29 15:16:28 +02:00
194da1a78b ui look 2025-08-29 15:16:22 +02:00
14 changed files with 885 additions and 269 deletions

View File

@ -1,8 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<meta charset="UTF-8">
<head> <head>
<title id="title"></title> <title id="title"></title>
<link rel="icon" type="image/svg" href="icon.svg"> <link rel="icon" type="image/svg" href="icon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
body { body {
font-family: sans-serif; font-family: sans-serif;
@ -27,7 +29,7 @@
padding: 0.25em; padding: 0.25em;
} }
input, select { input[type=text], input[type=number], select {
all: unset; all: unset;
width: 100%; width: 100%;
} }
@ -55,29 +57,59 @@
.state { .state {
} }
.countdown { .topic {
flex: 1;
padding: 0;
}
label {
white-space: nowrap;
} }
.stateOn { .stateOn {
background-color: palegreen; background-color: #7aca7a;
} }
.stateOff { .stateOff {
background-color: indianred; background-color: #ae4d4d;
} }
.switchOn { .switchOn {
border-radius: 0.5em;
filter: brightness(50%);
background-color: palegreen; background-color: palegreen;
} }
.switchOn_Active {
z-index: 9999;
filter: brightness(120%);
box-shadow: 0 0 5px #006400, 0 0 10px #228B22, 0 0 20px #32CD32;
}
.switchOff { .switchOff {
border-radius: 0.5em;
filter: brightness(50%);
background-color: indianred; background-color: indianred;
} }
.switchOff_Active {
z-index: 9999;
filter: brightness(120%);
box-shadow: 0 0 5px #8B0000, 0 0 10px #B22222, 0 0 20px #FF4500;
}
.switchCycle { .switchCycle {
border-radius: 0.5em;
filter: brightness(50%);
background-color: lightskyblue; background-color: lightskyblue;
} }
.switchCycle_Active {
z-index: 9999;
filter: brightness(120%);
box-shadow: 0 0 5px #00008B, 0 0 10px #1E90FF, 0 0 20px #00BFFF;
}
.config { .config {
flex: 1; flex: 1;
} }
@ -87,10 +119,19 @@
flex: 1; flex: 1;
} }
.initial {
text-align: right;
}
.adminHidden {
display: none;
}
@media (min-width: 1000px) { @media (min-width: 1000px) {
body { body {
font-size: 16px; font-size: 16px;
} }
.relayBox { .relayBox {
width: 400px; width: 400px;
} }
@ -102,18 +143,26 @@
<div id="relayList"></div> <div id="relayList"></div>
<button id="admin" onclick="toggleAdmin()">Admin</button>
<div id="gridPowerDelta"></div>
<script> <script>
let admin = false;
function toggleAdmin() {
admin = !admin;
update();
}
const title = document.getElementById("title"); const title = document.getElementById("title");
const gridPowerDelta = document.getElementById("gridPowerDelta");
const relayList = document.getElementById("relayList"); const relayList = document.getElementById("relayList");
function getUrl(path) { function getUrl(path) {
return `http://10.42.0.204/${path}`; return `http://10.42.0.204/${path}`;
} }
function setState(index, value) {
set("state", index, value ? 'true' : 'false');
}
function set(key, index, value) { function set(key, index, value) {
request(`${key}${index}=${encodeURIComponent(value)}`); request(`${key}${index}=${encodeURIComponent(value)}`);
} }
@ -122,21 +171,20 @@
function updateValue(tag, clazz, innerTag, value) { function updateValue(tag, clazz, innerTag, value) {
const input = tag.getElementsByClassName(clazz)[0].getElementsByTagName(innerTag)[0]; const input = tag.getElementsByClassName(clazz)[0].getElementsByTagName(innerTag)[0];
if (document.activeElement !== input) { if (input.type === "checkbox") {
input.checked = value;
} else if (document.activeElement !== input) {
input.value = value; input.value = value;
} }
} }
function updateState(relayTag, state) { function updateState(relayTag, state) {
const tag = relayTag.getElementsByClassName("state")[0];
if (state) { if (state) {
tag.innerText = "Ein"; relayTag.classList.add("stateOn");
tag.classList.add("stateOn"); relayTag.classList.remove("stateOff");
tag.classList.remove("stateOff");
} else { } else {
tag.innerText = "Aus"; relayTag.classList.add("stateOff");
tag.classList.add("stateOff"); relayTag.classList.remove("stateOn");
tag.classList.remove("stateOn");
} }
} }
@ -146,10 +194,12 @@
const DAY = (24 * HOUR); const DAY = (24 * HOUR);
function countdownString(relayData, millis) { function countdownString(relayData, millis) {
const rest = Math.ceil((millis - relayData.stateMillis - (Date.now() - dataAge)) / SECOND) * SECOND; const rest = Math.ceil((millis - relayData.stateAgeMillis - (Date.now() - dataAge)) / SECOND) * SECOND;
if (millis <= 0 || (relayData.onCount === 0 && !relayData.state)) { const cycle = relayData.onCount !== 0 && relayData.onMillis > 0 && relayData.offMillis > 0;
if (millis <= 0 || (!cycle && !relayData.state)) {
return ""; return "";
} }
const days = rest / DAY; const days = rest / DAY;
const hours = rest / HOUR; const hours = rest / HOUR;
const minutes = rest / MINUTE; const minutes = rest / MINUTE;
@ -189,30 +239,61 @@
timeout = setTimeout(() => request(), 2000); timeout = setTimeout(() => request(), 2000);
const r = new XMLHttpRequest(); const r = new XMLHttpRequest();
r.open("GET", getUrl(`set?${query}`)); r.open("GET", getUrl(`status?${query}`));
r.onreadystatechange = () => { r.onreadystatechange = () => {
if (r.readyState === 4 && r.status === 200) { if (r.readyState === 4 && r.status === 200) {
data = JSON.parse(r.response); data = JSON.parse(r.response);
dataAge = Date.now(); dataAge = Date.now();
title.innerText = data.hostname; title.innerText = data.wifi.hostname;
gridPowerDelta.innerText = "gridPowerDelta=" + data.gridPowerDeltaValue + "W";
for (let index = 0; index < data.relays.length; index++) { for (let index = 0; index < data.relays.length; index++) {
const relayData = data.relays[index]; const relayData = data.relays[index];
const relay = document.getElementById("relay" + index) || create(index); const relayTag = document.getElementById("relay" + index) || create(index);
updateValue(relay, "name", "input", relayData.name); updateValue(relayTag, "name", "input", relayData.name);
updateState(relay, relayData.state); updateValue(relayTag, "topic", "input", relayData.topic);
updateCountdown(relay, relayData); updateState(relayTag, relayData.state);
updateValue(relay, "onMillis", "input", relayData.onMillis); updateCountdown(relayTag, relayData);
updateValue(relay, "offMillis", "input", relayData.offMillis); updateValue(relayTag, "onMillis", "input", relayData.onMillis);
updateValue(relay, "initial", "select", relayData.initial); updateValue(relayTag, "offMillis", "input", relayData.offMillis);
updateValue(relayTag, "initial", "select", relayData.initial);
updateValue(relayTag, "gridPowerDeltaOnEnabled", "input", relayData.gridPowerDeltaOnEnabled);
updateValue(relayTag, "gridPowerDeltaOnThreshold", "input", relayData.gridPowerDeltaOnThreshold);
updateValue(relayTag, "gridPowerDeltaOnDelay", "input", relayData.gridPowerDeltaOnDelay);
updateValue(relayTag, "gridPowerDeltaOffEnabled", "input", relayData.gridPowerDeltaOffEnabled);
updateValue(relayTag, "gridPowerDeltaOffThreshold", "input", relayData.gridPowerDeltaOffThreshold);
updateValue(relayTag, "gridPowerDeltaOffDelay", "input", relayData.gridPowerDeltaOffDelay);
const cycle = relayData.onCount !== 0 && relayData.onMillis > 0 && relayData.offMillis > 0;
const switchOn = relayTag.getElementsByClassName("switchOn")[0];
if (!cycle && relayData.state) {
switchOn.classList.add("switchOn_Active");
} else {
switchOn.classList.remove("switchOn_Active");
}
const switchOff = relayTag.getElementsByClassName("switchOff")[0];
if (!cycle && !relayData.state) {
switchOff.classList.add("switchOff_Active");
} else {
switchOff.classList.remove("switchOff_Active");
}
const switchCycle = relayTag.getElementsByClassName("switchCycle")[0];
if (cycle) {
switchCycle.classList.add("switchCycle_Active");
} else {
switchCycle.classList.remove("switchCycle_Active");
}
} }
} }
} }
r.send(); r.send();
} }
function newDiv(parent, name) { function newDiv(parent, clazz) {
const div = document.createElement("div") const div = document.createElement("div")
div.className = name; div.className = clazz;
parent.append(div); parent.append(div);
return div; return div;
} }
@ -228,6 +309,22 @@
return input; return input;
} }
function newCheckbox(relayIndex, parent, clazz, name, text) {
const div = newDiv(parent, clazz);
const label = document.createElement("label")
label.onchange = () => set(name, relayIndex, input.checked);
div.append(label);
const input = document.createElement("input")
input.type = "checkbox";
input.onchange = () => set(name, relayIndex, input.value);
label.append(input);
label.append(text);
return input;
}
function newButton(relayIndex, parent, clazz, key, value, text) { function newButton(relayIndex, parent, clazz, key, value, text) {
const div = newDiv(parent, clazz); const div = newDiv(parent, clazz);
@ -269,17 +366,29 @@
const header = newDiv(relay, "flex"); const header = newDiv(relay, "flex");
newInput(relayIndex, header, "name", "name", "text"); newInput(relayIndex, header, "name", "name", "text");
newDiv(header, "countdown"); newDiv(header, "countdown");
newDiv(header, "state");
const config = newDiv(relay, "flex"); const topicDiv = newDiv(relay, "flex admin adminHidden");
newInput(relayIndex, topicDiv, "topic", "topic", "text");
const config = newDiv(relay, "flex admin adminHidden");
newInput(relayIndex, config, "config onMillis", "onMillis", "number"); newInput(relayIndex, config, "config onMillis", "onMillis", "number");
newInput(relayIndex, config, "config offMillis", "offMillis", "number"); newInput(relayIndex, config, "config offMillis", "offMillis", "number");
newSelect(relayIndex, config, "config initial", "initial", [["OFF", "Init: Aus"], ["ON", "Init: Ein"], ["CYCLE", "Init: Zyklus"]]); newSelect(relayIndex, config, "config initial", "initial", [["OFF", "Init: Aus"], ["ON", "Init: Ein"], ["CYCLE", "Init: Zyklus"]]);
const gridPowerDeltaOn = newDiv(relay, "flex admin adminHidden");
newCheckbox(relayIndex, gridPowerDeltaOn, "config gridPowerDeltaOnEnabled", "gridPowerDeltaOnEnabled", "Überschuss EIN");
newInput(relayIndex, gridPowerDeltaOn, "config gridPowerDeltaOnThreshold", "gridPowerDeltaOnThreshold", "number");
newInput(relayIndex, gridPowerDeltaOn, "config gridPowerDeltaOnDelay", "gridPowerDeltaOnDelay", "number");
const gridPowerDeltaOff = newDiv(relay, "flex admin adminHidden");
newCheckbox(relayIndex, gridPowerDeltaOff, "config gridPowerDeltaOffEnabled", "gridPowerDeltaOffEnabled", "Defizit AUS");
newInput(relayIndex, gridPowerDeltaOff, "config gridPowerDeltaOffThreshold", "gridPowerDeltaOffThreshold", "number");
newInput(relayIndex, gridPowerDeltaOff, "config gridPowerDeltaOffDelay", "gridPowerDeltaOffDelay", "number");
const switches = newDiv(relay, "flex"); const switches = newDiv(relay, "flex");
newButton(relayIndex, switches, "switch switchCycle", "onCount", -1, "Zyklus");
newButton(relayIndex, switches, "switch switchOn", "state", "true", "Ein"); newButton(relayIndex, switches, "switch switchOn", "state", "true", "Ein");
newButton(relayIndex, switches, "switch switchOff", "state", "false", "Aus"); newButton(relayIndex, switches, "switch switchOff", "state", "false", "Aus");
newButton(relayIndex, switches, "switch switchCycle", "onCount", -1, "Zyklus");
return relay; return relay;
} }
@ -293,6 +402,13 @@
const relayData = data.relays[index]; const relayData = data.relays[index];
const relayTag = document.getElementById("relay" + index) || create(index); const relayTag = document.getElementById("relay" + index) || create(index);
updateCountdown(relayTag, relayData); updateCountdown(relayTag, relayData);
for (let element of document.getElementsByClassName("admin")) {
if (admin) {
element.classList.remove("adminHidden");
} else {
element.classList.add("adminHidden");
}
}
} }
} }

View File

@ -1,19 +1,104 @@
[env:Sonoff4ChPro] [common]
framework = arduino
upload_speed = 921600
monitor_speed = 115200
build.filesystem = littlefs
lib_deps = bblanchon/ArduinoJson @ 7.4.2
knolleary/PubSubClient
[ESP8285]
platform = espressif8266 platform = espressif8266
board = esp8285 board = esp8285
framework = arduino framework = ${common.framework}
upload_speed = 921600 upload_speed = ${common.upload_speed}
upload_port = 10.0.0.178 monitor_speed = ${common.monitor_speed}
monitor_speed = 115200 build.filesystem = ${common.build.filesystem}
build.filesystem = littlefs lib_deps = ${common.lib_deps}
lib_deps = bblanchon/ArduinoJson @ 7.4.2 board_build.ldscript = eagle.flash.1m64.ld
[env:ESP32Test] [Ch4Pro]
platform = ${ESP8285.platform}
board = ${ESP8285.board}
framework = ${ESP8285.framework}
upload_speed = ${ESP8285.upload_speed}
monitor_speed = ${ESP8285.monitor_speed}
build.filesystem = ${ESP8285.build.filesystem}
lib_deps = ${ESP8285.lib_deps}
board_build.ldscript = ${ESP8285.board_build.ldscript}
build_flags = -D Ch4Pro
[SP111]
platform = ${ESP8285.platform}
board = ${ESP8285.board}
framework = ${ESP8285.framework}
upload_speed = ${ESP8285.upload_speed}
monitor_speed = ${ESP8285.monitor_speed}
build.filesystem = ${ESP8285.build.filesystem}
lib_deps = ${ESP8285.lib_deps}
board_build.ldscript = ${ESP8285.board_build.ldscript}
build_flags = -D SP111
[ESP32_TEST]
platform = espressif32 platform = espressif32
board = esp32dev board = esp32dev
framework = arduino framework = ${common.framework}
upload_speed = 921600 upload_speed = ${common.upload_speed}
monitor_speed = 115200 monitor_speed = ${common.monitor_speed}
build.filesystem = littlefs build_type = debug
lib_deps = bblanchon/ArduinoJson @ 7.4.2 debug_tool = esp-prog
build_flags = -D STATUS_PIN=2 -D STATUS_INVERT=false -D CORE_DEBUG_LEVEL=0 monitor_filters = esp32_exception_decoder
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
build_flags = -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0
[env:ESP32_Ch4Pro]
platform = ${ESP32_TEST.platform}
board = ${ESP32_TEST.board}
framework = ${ESP32_TEST.framework}
upload_speed = ${ESP32_TEST.upload_speed}
monitor_speed = ${ESP32_TEST.monitor_speed}
build_type = ${ESP32_TEST.build_type}
debug_tool = ${ESP32_TEST.debug_tool}
monitor_filters = ${ESP32_TEST.monitor_filters}
build.filesystem = ${ESP32_TEST.build.filesystem}
lib_deps = ${ESP32_TEST.lib_deps}
build_flags = ${Ch4Pro.build_flags} -D WIFI_HOSTNAME_FALLBACK=\"ESP32_Test_Ch4Pro\" ${ESP32_TEST.build_flags}
[env:ESP32_SP111]
platform = ${ESP32_TEST.platform}
board = ${ESP32_TEST.board}
framework = ${ESP32_TEST.framework}
upload_speed = ${ESP32_TEST.upload_speed}
monitor_speed = ${ESP32_TEST.monitor_speed}
build_type = ${ESP32_TEST.build_type}
debug_tool = ${ESP32_TEST.debug_tool}
monitor_filters = ${ESP32_TEST.monitor_filters}
build.filesystem = ${ESP32_TEST.build.filesystem}
lib_deps = ${ESP32_TEST.lib_deps}
build_flags = ${SP111.build_flags} -D WIFI_HOSTNAME_FALLBACK=\"ESP32_Test_SP111\" ${ESP32_TEST.build_flags}
[env:Gewaechshaus]
platform = ${Ch4Pro.platform}
board = ${Ch4Pro.board}
framework = ${Ch4Pro.framework}
upload_speed = ${Ch4Pro.upload_speed}
monitor_speed = ${Ch4Pro.monitor_speed}
build.filesystem = ${Ch4Pro.build.filesystem}
lib_deps = ${Ch4Pro.lib_deps}
board_build.ldscript = ${Ch4Pro.board_build.ldscript}
build_flags = ${Ch4Pro.build_flags} -D WIFI_HOSTNAME_FALLBACK=\"Gewaechshaus\"
upload_protocol = espota
upload_port = 10.0.0.178
[env:Infrarotheizung]
platform = ${SP111.platform}
board = ${SP111.board}
framework = ${SP111.framework}
upload_speed = ${SP111.upload_speed}
monitor_speed = ${SP111.monitor_speed}
build.filesystem = ${SP111.build.filesystem}
lib_deps = ${SP111.lib_deps}
board_build.ldscript = ${SP111.board_build.ldscript}
build_flags = ${SP111.build_flags} -D WIFI_HOSTNAME_FALLBACK=\"Infrarotheizung\"
upload_protocol = espota
upload_port = 10.0.0.179

View File

@ -27,14 +27,20 @@ protected:
unsigned long stateMillis = 0; unsigned long stateMillis = 0;
void _write(const bool state) { virtual void publish() {
if (state != get()) { // nothing
}
void _write(const bool wanted) {
const auto current = get();
if (wanted != current) {
digitalWrite(pin, wanted ^ inverted ? HIGH : LOW);
publish();
if (logState) { if (logState) {
Serial.printf("[RELAY] \"%s\" = %s\n", name.c_str(), state ? "ON" : "OFF"); Serial.printf("[RELAY] \"%s\": %s\n", name.c_str(), wanted ? "ON" : "OFF");
} }
stateMillis = millis(); stateMillis = millis();
} }
digitalWrite(pin, state ^ inverted ? HIGH : LOW);
} }
void _applyInitial() { void _applyInitial() {
@ -63,9 +69,9 @@ public:
return (digitalRead(pin) == HIGH) ^ inverted; return (digitalRead(pin) == HIGH) ^ inverted;
} }
void set(const bool state) { virtual void set(const bool state) {
_write(state);
onCount = 0; onCount = 0;
_write(state);
} }
void toggle() { void toggle() {
@ -73,19 +79,21 @@ public:
} }
void cycle(const unsigned long onMillis_, const unsigned long offMillis_, const unsigned long onCount_ = -1) { void cycle(const unsigned long onMillis_, const unsigned long offMillis_, const unsigned long onCount_ = -1) {
set(false); // this sets onCount=0, so do this first
this->onMillis = onMillis_; this->onMillis = onMillis_;
this->offMillis = offMillis_; this->offMillis = offMillis_;
this->onCount = onCount_; this->onCount = onCount_;
set(false);
} }
void loop() { virtual void loop() {
if (get()) { const auto status = get();
if (onMillis > 0 && millis() - stateMillis > onMillis) { const auto ageMillis = millis() - stateMillis;
if (status) {
if (onMillis > 0 && ageMillis >= onMillis) {
_write(false); _write(false);
} }
} else { } else {
if (offMillis > 0 && millis() - stateMillis > offMillis && onCount != 0) { if (offMillis > 0 && ageMillis >= offMillis && onCount != 0) {
_write(true); _write(true);
if (onCount > 0) { if (onCount > 0) {
onCount--; onCount--;
@ -114,30 +122,6 @@ public:
offMillis = value; offMillis = value;
} }
Initial getInitial() const {
return initial;
}
long getOnCount() const {
return onCount;
}
unsigned long getOnMillis() const {
return onMillis;
}
unsigned long getOffMillis() const {
return offMillis;
}
unsigned long getStateMillis() const {
return millis() - stateMillis;
}
String getName() const {
return name;
}
}; };
#endif #endif

View File

@ -3,46 +3,164 @@
#include "config.h" #include "config.h"
#include "Output.h" #include "Output.h"
#include "mqtt.h"
class Relay final : public Output { class Relay final : public Output {
String nameFallback; String nameFallback;
String topicFallback;
const uint8_t index; const uint8_t index;
String topic;
bool gridPowerDeltaOffEnabled = false;
long gridPowerDeltaOnThreshold = 0;
long gridPowerDeltaOnDelay = 0;
bool gridPowerDeltaOnEnabled = false;
long gridPowerDeltaOffThreshold = 0;
long gridPowerDeltaOffDelay = 0;
unsigned long gridPowerDeltaAlarmSince = 0;
public: public:
Relay(const uint8_t index, const char *name, const uint8_t pin, const bool inverted, const bool logState) : Output(name, pin, inverted, logState), nameFallback(name), index(index) { Relay(const uint8_t index, const String &topic, const char *name, const uint8_t pin, const bool inverted, const bool logState) : Output(name, pin, inverted, logState), nameFallback(name), topicFallback(topic), index(index), topic(topic) {
// //
} }
void setup() override { void setup() override {
Output::setup(); Output::setup();
Output::setName(configRead(path("name"), nameFallback));
Output::setInitial(configRead(path("initial"), INITIAL_OFF)); Output::setName(loadString(path("name"), nameFallback));
Output::setOnMillis(configRead(path("onMillis"), 0L)); Output::setInitial(loadInitial(path("initial"), INITIAL_OFF));
Output::setOffMillis(configRead(path("offMillis"), 0L)); Output::setOnMillis(loadLong(path("onMillis"), 0L));
Output::setOffMillis(loadLong(path("offMillis"), 0L));
topic = loadString(path("topic"), topicFallback);
gridPowerDeltaOnEnabled = loadBool(path("gridPowerDeltaOnEnabled"), false);
gridPowerDeltaOnThreshold = loadLong(path("gridPowerDeltaOnThreshold"), 0L);
gridPowerDeltaOnDelay = loadLong(path("gridPowerDeltaOnDelay"), 0L);
gridPowerDeltaOffEnabled = loadBool(path("gridPowerDeltaOffEnabled"), false);
gridPowerDeltaOffThreshold = loadLong(path("gridPowerDeltaOffThreshold"), 0L);
gridPowerDeltaOffDelay = loadLong(path("gridPowerDeltaOffDelay"), 0L);
_applyInitial(); _applyInitial();
} }
void loop() override {
Output::loop();
doGridPowerDelta();
}
void setName(const String &value) override { void setName(const String &value) override {
Output::setName(value); Output::setName(value);
configWrite(path("name"), nameFallback, value, false); storeString(path("name"), nameFallback, value);
publish();
} }
void setInitial(const Initial value) override { void setInitial(const Initial value) override {
Output::setInitial(value); Output::setInitial(value);
configWrite(path("initial"), INITIAL_OFF, value); storeInitial(path("initial"), INITIAL_OFF, value);
publish();
} }
void setOnMillis(const unsigned long value) override { void setOnMillis(const unsigned long value) override {
Output::setOnMillis(value); Output::setOnMillis(value);
configWrite(path("onMillis"), 0L, value); storeLong(path("onMillis"), 0L, value);
publish();
} }
void setOffMillis(const unsigned long value) override { void setOffMillis(const unsigned long value) override {
Output::setOffMillis(value); Output::setOffMillis(value);
configWrite(path("offMillis"), 0L, value); storeLong(path("offMillis"), 0L, value);
publish();
}
void setTopic(const String &value) {
topic = value;
storeString(path("topic"), topicFallback, value);
publish();
}
void setGridPowerDeltaOnEnabled(const bool value) {
gridPowerDeltaOnEnabled = value;
storeBool(path("gridPowerDeltaOnEnabled"), false, value);
publish();
}
void setGridPowerDeltaOnThreshold(const long value) {
gridPowerDeltaOnThreshold = value;
storeLong(path("gridPowerDeltaOnThreshold"), 0L, value);
publish();
}
void setGridPowerDeltaOnDelay(const long value) {
gridPowerDeltaOnDelay = value;
storeLong(path("gridPowerDeltaOnDelay"), 0L, value);
publish();
}
void setGridPowerDeltaOffEnabled(const bool value) {
gridPowerDeltaOffEnabled = value;
storeBool(path("gridPowerDeltaOffEnabled"), false, value);
publish();
}
void setGridPowerDeltaOffThreshold(const long value) {
gridPowerDeltaOffThreshold = value;
storeLong(path("gridPowerDeltaOffThreshold"), 0L, value);
publish();
}
void setGridPowerDeltaOffDelay(const long value) {
gridPowerDeltaOffDelay = value;
storeLong(path("gridPowerDeltaOffDelay"), 0L, value);
publish();
}
void json(const JsonObject json) const {
json["name"] = name;
json["initial"] = initialToString(initial);
json["onCount"] = onCount;
json["onMillis"] = onMillis;
json["offMillis"] = offMillis;
json["topic"] = topic;
json["gridPowerDeltaAlarmSince"] = gridPowerDeltaAlarmSince;
json["gridPowerDeltaOffEnabled"] = gridPowerDeltaOffEnabled;
json["gridPowerDeltaOnThreshold"] = gridPowerDeltaOnThreshold;
json["gridPowerDeltaOnDelay"] = gridPowerDeltaOnDelay;
json["gridPowerDeltaOnEnabled"] = gridPowerDeltaOnEnabled;
json["gridPowerDeltaOffThreshold"] = gridPowerDeltaOffThreshold;
json["gridPowerDeltaOffDelay"] = gridPowerDeltaOffDelay;
json["state"] = get();
json["stateAgeMillis"] = millis() - stateMillis;
}
void set(const bool state) override {
gridPowerDeltaAlarmSince = 0;
Output::set(state);
}
protected:
void publish() override {
JsonDocument doc;
json(doc.to<JsonObject>());
mqttPublish(topic, doc);
} }
private: private:
@ -53,6 +171,59 @@ private:
return String(path); return String(path);
} }
void doGridPowerDelta() {
if (get()) {
gridPowerDeltaOff();
} else {
gridPowerDeltaOn();
}
}
bool gridPowerDeltaConfigInvalid() {
return gridPowerDeltaOffThreshold < gridPowerDeltaOnThreshold;
}
void gridPowerDeltaOff() {
if (!gridPowerDeltaOffEnabled) {
return;
}
const auto now = max(1UL, millis());
const auto invalid = isnan(gridPowerDeltaValue) || now - gridPowerDeltaMillis > 10000 || gridPowerDeltaConfigInvalid();
if (gridPowerDeltaValue > gridPowerDeltaOffThreshold || invalid) {
if (gridPowerDeltaAlarmSince == 0 && gridPowerDeltaOffDelay > 0) {
Serial.printf("[RELAY] \"%s\": CONSUMING TOO MUCH: Powering OFF in %ld ms...\n", name.c_str(), gridPowerDeltaOffDelay);
gridPowerDeltaAlarmSince = now;
}
if (now - gridPowerDeltaAlarmSince >= gridPowerDeltaOffDelay) {
Serial.printf("[RELAY] \"%s\": CONSUMING TOO MUCH: Powering OFF!\n", name.c_str());
set(false);
}
} else if (gridPowerDeltaAlarmSince > 0) {
Serial.printf("[RELAY] \"%s\": Powering off CANCELED!\n", name.c_str());
gridPowerDeltaAlarmSince = 0;
}
}
void gridPowerDeltaOn() {
if (!gridPowerDeltaOnEnabled) {
return;
}
const auto now = max(1UL, millis());
if (gridPowerDeltaValue < gridPowerDeltaOnThreshold && !gridPowerDeltaConfigInvalid()) {
if (gridPowerDeltaAlarmSince == 0 && gridPowerDeltaOnDelay > 0) {
Serial.printf("[RELAY] \"%s\": PRODUCING TOO MUCH: Powering ON in %ld ms...\n", name.c_str(), gridPowerDeltaOnDelay);
gridPowerDeltaAlarmSince = now;
}
if (now - gridPowerDeltaAlarmSince >= gridPowerDeltaOnDelay) {
Serial.printf("[RELAY] \"%s\": PRODUCING TOO MUCH: Powering ON!\n", name.c_str());
set(true);
}
} else if (gridPowerDeltaAlarmSince > 0) {
Serial.printf("[RELAY] \"%s\": Powering on CANCELED!\n", name.c_str());
gridPowerDeltaAlarmSince = 0;
}
}
}; };
#endif #endif

View File

@ -46,7 +46,7 @@ void listDir(const String &path, const String &indent) {
void configSetup() { void configSetup() {
#ifdef ESP32 #ifdef ESP32
LittleFS.begin(true) LittleFS.begin(true);
#endif #endif
#ifdef ESP8266 #ifdef ESP8266
if (!LittleFS.begin()) { if (!LittleFS.begin()) {
@ -55,7 +55,7 @@ void configSetup() {
} }
#endif #endif
Serial.println("Filesystem-content:"); Serial.println("Filesystem-content:");
listDir("/", ""); listDir("/", " ");
} }
File configOpen(const String &path, const bool write) { File configOpen(const String &path, const bool write) {
@ -75,14 +75,14 @@ void doLog(const String &path, const String &value, const bool isPassword, const
return; return;
} }
Serial.printf( Serial.printf(
"[CONFIG] %-20s = %-30s [%s]\n", "[CONFIG] %-40s = %-30s [%s]\n",
path.c_str(), path.c_str(),
isPassword ? "*" : value.c_str(), isPassword ? "*" : value.c_str(),
type == CONFIG_LOG_FALLBACK ? "fallback" : type == CONFIG_LOG_READ ? "READ" : type == CONFIG_LOG_UNCHANGED ? "UNCHANGED" : type == CONFIG_LOG_WRITE ? "WRITE" : "" type == CONFIG_LOG_FALLBACK ? "fallback" : type == CONFIG_LOG_READ ? "READ" : type == CONFIG_LOG_UNCHANGED ? "UNCHANGED" : type == CONFIG_LOG_WRITE ? "WRITE" : ""
); );
} }
long configRead(const String &path, const long fallback, const bool log) { long loadLong(const String &path, const long fallback, const bool log) {
if (auto file = configOpen(path, false)) { if (auto file = configOpen(path, false)) {
const auto content = file.readString(); const auto content = file.readString();
file.close(); file.close();
@ -94,8 +94,8 @@ long configRead(const String &path, const long fallback, const bool log) {
return fallback; return fallback;
} }
bool configWrite(const String &path, const long fallback, const long value) { bool storeLong(const String &path, const long fallback, const long value) {
if (configRead(path, fallback, false) == value) { if (loadLong(path, fallback, false) == value) {
doLog(path, String(value), false, CONFIG_LOG_UNCHANGED, true); doLog(path, String(value), false, CONFIG_LOG_UNCHANGED, true);
return false; return false;
} }
@ -109,51 +109,67 @@ bool configWrite(const String &path, const long fallback, const long value) {
return false; return false;
} }
bool configRead(const String &path, const bool fallback) { bool loadBool(const String &path, const bool fallback, const bool log) {
return configRead(path, fallback ? 1L : 0L) > 0; const auto value = loadString(path, fallback ? "true" : "false", log);
} if (value == "true") {
return true;
bool configWrite(const String &path, const bool fallback, const bool value) {
return configWrite(path, fallback ? 1L : 0L, value ? 1L : 0L);
}
String configRead(const String &path, const char *fallback) {
return configRead(path, String(fallback));
}
bool configWrite(const String &path, const char *fallback, const char *value) {
return configWrite(path, String(fallback), String(value), false);
}
String configRead(const String &path, const String &fallback, const bool log, const bool isPassword) {
if (auto file = configOpen(path, false)) {
const auto value = file.readString();
file.close();
doLog(path, value.c_str(), isPassword, CONFIG_LOG_READ, log);
return value;
} }
doLog(path, fallback.c_str(), isPassword, CONFIG_LOG_FALLBACK, log); if (value == "false") {
return false;
}
Serial.printf("[CONFIG] Not a boolean: path=%s, value=%s\n", path.c_str(), value.c_str());
return fallback; return fallback;
} }
bool configWrite(const String &path, const String &fallback, const String &value, const bool isPassword) { bool storeBool(const String &path, const bool fallback, const bool value) {
if (configRead(path, fallback, false) == value) { return storeString(path, fallback ? "true" : "false", value ? "true" : "false");
doLog(path, value.c_str(), isPassword, CONFIG_LOG_UNCHANGED, true); }
String _loadString(const String &path, const String &fallback, const bool password, const bool log) {
if (auto file = configOpen(path, false)) {
const auto value = file.readString();
file.close();
doLog(path, value.c_str(), password, CONFIG_LOG_READ, log);
return value;
}
doLog(path, fallback.c_str(), password, CONFIG_LOG_FALLBACK, log);
return fallback;
}
bool _storeString(const String &path, const String &fallback, const bool password, const String &value) {
if (_loadString(path, fallback, password, false) == value) {
doLog(path, value.c_str(), password, CONFIG_LOG_UNCHANGED, true);
return false; return false;
} }
if (auto file = configOpen(path, true)) { if (auto file = configOpen(path, true)) {
file.write(reinterpret_cast<const uint8_t *>(value.c_str()), value.length()); file.write(reinterpret_cast<const uint8_t *>(value.c_str()), value.length());
file.close(); file.close();
doLog(path, value.c_str(), isPassword, CONFIG_LOG_WRITE, true); doLog(path, value.c_str(), password, CONFIG_LOG_WRITE, true);
return true; return true;
} }
return false; return false;
} }
Initial configRead(const String &path, const Initial &fallback) { String loadString(const String &path, const String &fallback, const bool log) {
return stringToInitial(configRead(path, initialToString(fallback))); return _loadString(path, fallback, false, log);
} }
bool configWrite(const String &path, const Initial &fallback, const Initial &value) { bool storeString(const String &path, const String &fallback, const String &value) {
return configWrite(path, initialToString(fallback), initialToString(value)); return _storeString(path, fallback, false, value);
}
String loadPassword(const String &path, const String &fallback, const bool log) {
return _loadString(path, fallback, true, log);
}
bool storePassword(const String &path, const String &fallback, const String &value) {
return _storeString(path, fallback, true, value);
}
Initial loadInitial(const String &path, const Initial &fallback, const bool log) {
return stringToInitial(loadString(path, initialToString(fallback), log));
}
bool storeInitial(const String &path, const Initial &fallback, const Initial &value) {
return storeString(path, initialToString(fallback), initialToString(value));
} }

View File

@ -7,24 +7,24 @@
void configSetup(); void configSetup();
long configRead(const String &path, long fallback, bool log = true); long loadLong(const String &path, long fallback, bool log = true);
bool configWrite(const String &path, long fallback, long value); bool storeLong(const String &path, long fallback, long value);
bool configRead(const String &path, bool fallback); bool loadBool(const String &path, bool fallback, bool log = true);
bool configWrite(const String &path, bool fallback, bool value); bool storeBool(const String &path, bool fallback, bool value);
String configRead(const String &path, const char *fallback); String loadString(const String &path, const String &fallback, bool log = true);
bool configWrite(const String &path, const char *fallback, const char *value); bool storeString(const String &path, const String &fallback, const String &value);
String configRead(const String &path, const String &fallback, bool log = true, bool isPassword = false); String loadPassword(const String &path, const String &fallback, bool log = true);
bool configWrite(const String &path, const String &fallback, const String &value, bool isPassword); bool storePassword(const String &path, const String &fallback, const String &value);
Initial configRead(const String &path, const Initial &fallback); Initial loadInitial(const String &path, const Initial &fallback, bool log = true);
bool configWrite(const String &path, const Initial &fallback, const Initial &value); bool storeInitial(const String &path, const Initial &fallback, const Initial &value);
#endif #endif

View File

@ -4,6 +4,8 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include "wifi.h"
#ifdef ESP8266 #ifdef ESP8266
#include <ESP8266WebServer.h> #include <ESP8266WebServer.h>
@ -18,73 +20,86 @@ WebServer server(80);
bool httpRunning = false; bool httpRunning = false;
void httpRelay(const int index, Output &relay) { File httpUploadFile;
const auto nameKey = String("name") + index;
if (server.hasArg(nameKey)) {
const auto name = server.arg(nameKey);
relay.setName(name);
}
const auto stateKey = String("state") + index; void httpString(const String &key, const std::function<void(const String &)> &modifier) {
if (server.hasArg(stateKey)) { if (server.hasArg(key)) {
const auto state = server.arg(stateKey); const auto name = server.arg(key);
if (state == "true") { modifier(name);
relay.set(true);
} else if (state == "false") {
relay.set(false);
}
}
const auto initialKey = String("initial") + index;
if (server.hasArg(initialKey)) {
const auto initial = server.arg(initialKey);
if (initial == "OFF") {
relay.setInitial(INITIAL_OFF);
} else if (initial == "ON") {
relay.setInitial(INITIAL_ON);
} else if (initial == "CYCLE") {
relay.setInitial(INITIAL_CYCLE);
}
}
const auto onCountKey = String("onCount") + index;
if (server.hasArg(onCountKey)) {
const auto value = server.arg(onCountKey).toInt();
relay.setOnCount(value);
}
const auto onMillisKey = String("onMillis") + index;
if (server.hasArg(onMillisKey)) {
const auto value = server.arg(onMillisKey).toInt();
relay.setOnMillis(value);
}
const auto offMillisKey = String("offMillis") + index;
if (server.hasArg(offMillisKey)) {
const auto value = server.arg(offMillisKey).toInt();
relay.setOffMillis(value);
} }
} }
void httpRelayJson(const Output &relay, const JsonObject json) { void httpBool(const String &key, const std::function<void(const bool &)> &modifier) {
json["name"] = relay.getName(); if (server.hasArg(key)) {
json["state"] = relay.get(); const auto state = server.arg(key);
json["stateMillis"] = relay.getStateMillis(); if (state == "true") {
json["initial"] = initialToString(relay.getInitial()); modifier(true);
json["onCount"] = relay.getOnCount(); } else if (state == "false") {
json["onMillis"] = relay.getOnMillis(); modifier(false);
json["offMillis"] = relay.getOffMillis(); }
}
}
void httpLong(const String &key, const std::function<void(const long &)> &modifier) {
if (server.hasArg(key)) {
modifier(server.arg(key).toInt());
}
}
void httpInitial(const String &key, const std::function<void(const Initial &)> &modifier) {
httpString(key, [&modifier](const String &value) {
if (value == "OFF") {
modifier(INITIAL_OFF);
} else if (value == "ON") {
modifier(INITIAL_ON);
} else if (value == "CYCLE") {
modifier(INITIAL_CYCLE);
}
});
}
void httpRelay(const int index, Relay &relay) {
const String suffix(index);
httpString("name" + suffix, [&relay](const String &value) { relay.setName(value); });
httpInitial("initial" + suffix, [&relay](const Initial &value) { relay.setInitial(value); });
httpLong("onCount" + suffix, [&relay](const long &value) { relay.setOnCount(value); });
httpLong("onMillis" + suffix, [&relay](const long &value) { relay.setOnMillis(value); });
httpLong("offMillis" + suffix, [&relay](const long &value) { relay.setOffMillis(value); });
httpString("topic" + suffix, [&relay](const String &value) { relay.setTopic(value); });
httpBool("gridPowerDeltaOnEnabled" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOnEnabled(value); });
httpLong("gridPowerDeltaOnThreshold" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOnThreshold(value); });
httpLong("gridPowerDeltaOnDelay" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOnDelay(value); });
httpBool("gridPowerDeltaOffEnabled" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOffEnabled(value); });
httpLong("gridPowerDeltaOffThreshold" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOffThreshold(value); });
httpLong("gridPowerDeltaOffDelay" + suffix, [&relay](const long &value) { relay.setGridPowerDeltaOffDelay(value); });
httpBool("state" + suffix, [&relay](const bool &value) { relay.set(value); });
} }
void httpStatus() { void httpStatus() {
JsonDocument json; JsonDocument json;
json["hostname"] = WiFi.getHostname();
const auto wifi = json["wifi"].to<JsonObject>();
wifi["hostname"] = WiFi.getHostname();
wifi["ssid"] = WiFi.SSID();
const auto mqtt = json["mqtt"].to<JsonObject>();
mqtt["user"] = getMqttUser();
mqtt["host"] = getMqttHost();
mqtt["port"] = getMqttPort();
json["gridPowerDeltaValue"] = gridPowerDeltaValue;
json["gridPowerDeltaAge"] = millis() - gridPowerDeltaMillis;
const auto relays = json["relays"].to<JsonArray>(); const auto relays = json["relays"].to<JsonArray>();
httpRelayJson(relay0, relays.add<JsonObject>()); relay0.json(relays.add<JsonObject>());
httpRelayJson(relay1, relays.add<JsonObject>()); #ifdef Ch4Pro
httpRelayJson(relay2, relays.add<JsonObject>()); relay1.json(relays.add<JsonObject>());
httpRelayJson(relay3, relays.add<JsonObject>()); relay2.json(relays.add<JsonObject>());
relay3.json(relays.add<JsonObject>());
#endif
String response; String response;
serializeJson(json, response); serializeJson(json, response);
@ -92,24 +107,27 @@ void httpStatus() {
} }
void httpSet() { void httpSet() {
httpString("wifiHostname", wifiSetHostname);
httpString("wifiSSID", wifiSetSSID);
httpString("wifiPassword", wifiSetPassword);
httpString("mqttHost", mqttSetHost);
httpLong("mqttPort", mqttSetPort);
httpString("mqttUser", mqttSetUser);
httpString("mqttPassword", mqttSetPassword);
httpRelay(0, relay0); httpRelay(0, relay0);
#ifdef Ch4Pro
httpRelay(1, relay1); httpRelay(1, relay1);
httpRelay(2, relay2); httpRelay(2, relay2);
httpRelay(3, relay3); httpRelay(3, relay3);
#endif
httpStatus(); httpStatus();
} }
void httpOff() {
relay0.set(false);
relay1.set(false);
relay2.set(false);
relay3.set(false);
httpStatus();
}
File httpUploadFile;
void httpUpload(const char *name) { void httpUpload(const char *name) {
yield();
const auto upload = server.upload(); const auto upload = server.upload();
if (upload.status == UPLOAD_FILE_START) { if (upload.status == UPLOAD_FILE_START) {
char path[64]; char path[64];
@ -136,17 +154,15 @@ void httpSetup() {
server.serveStatic("/", LittleFS, "/index.html"); server.serveStatic("/", LittleFS, "/index.html");
server.serveStatic("/icon.svg", LittleFS, "/icon.svg"); server.serveStatic("/icon.svg", LittleFS, "/icon.svg");
server.on("/set", httpSet); server.on("/status", httpSet);
server.on("/set/", httpSet); server.on("/status/", httpSet);
server.on("/off", httpOff);
server.on("/off/", httpOff);
server.on("/upload/index", HTTP_POST, [] { server.send(200); }, [] { httpUpload("index.html"); }); server.on("/upload/index", HTTP_POST, [] { server.send(200); }, [] { httpUpload("index.html"); });
server.on("/upload/icon", HTTP_POST, [] { server.send(200); }, [] { httpUpload("icon.svg"); }); server.on("/upload/icon", HTTP_POST, [] { server.send(200); }, [] { httpUpload("icon.svg"); });
server.begin(); server.begin();
Serial.println("HTTP server started"); Serial.println("[HTTP] Server started");
httpRunning = true; httpRunning = true;
} }
@ -158,6 +174,6 @@ void httpStop() {
if (httpRunning) { if (httpRunning) {
httpRunning = false; httpRunning = false;
server.stop(); server.stop();
Serial.println("HTTP server stopped"); Serial.println("[HTTP] Server stopped");
} }
} }

View File

@ -1,22 +1,28 @@
#include "io.h" #include "io.h"
#ifndef STATUS_PIN #ifdef SP111
#define STATUS_PIN 13
#ifndef ESP32_TESTBOARD
#define STATUS_PIN 0
#define STATUS_INVERT true
#endif #endif
#ifndef STATUS_INVERT Button button0(13, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); });
#define STATUS_INVERT true
Relay relay0(0, "fallback/relay0", "RELAY #0", 15, false, true);
#ifdef SP111
Output relay0Led("relay0Led", 2, true, false);
#endif #endif
Relay relay0(0, "RELAY #0", 12, false, true); #endif
Relay relay1(1, "RELAY #1", 5, false, true); #ifdef Ch4Pro
Relay relay2(2, "RELAY #2", 4, false, true); #ifndef ESP32_TESTBOARD
#define STATUS_PIN 13
Relay relay3(3, "RELAY #3", 15, false, true); #define STATUS_INVERT true
#endif
Output status("Status", STATUS_PIN, STATUS_INVERT, false);
Button button0(0, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); }); Button button0(0, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); });
@ -26,6 +32,23 @@ Button button2(10, true, true, [](const ButtonEvent event) { buttonCallback(rela
Button button3(14, true, true, [](const ButtonEvent event) { buttonCallback(relay3, event); }); Button button3(14, true, true, [](const ButtonEvent event) { buttonCallback(relay3, event); });
Relay relay0(0, "fallback/relay0", "RELAY #0", 12, false, true);
Relay relay1(1, "fallback/relay1", "RELAY #1", 5, false, true);
Relay relay2(2, "fallback/relay2", "RELAY #2", 4, false, true);
Relay relay3(3, "fallback/relay3", "RELAY #3", 15, false, true);
#endif
#ifdef ESP32_TESTBOARD
#define STATUS_PIN 2
#define STATUS_INVERT false
#endif
Output status("Status", STATUS_PIN, STATUS_INVERT, false);
void buttonCallback(Output &output, const ButtonEvent event) { void buttonCallback(Output &output, const ButtonEvent event) {
if (event == BUTTON_PRESSED) { if (event == BUTTON_PRESSED) {
output.toggle(); output.toggle();

View File

@ -6,50 +6,65 @@
void buttonCallback(Output &output, ButtonEvent event); void buttonCallback(Output &output, ButtonEvent event);
extern Relay relay0;
extern Relay relay1;
extern Relay relay2;
extern Relay relay3;
extern Output status; extern Output status;
extern Button button0; extern Button button0;
extern Relay relay0;
#ifdef SP111
extern Output relay0Led;
#endif
#ifdef Ch4Pro
extern Button button1; extern Button button1;
extern Button button2; extern Button button2;
extern Button button3; extern Button button3;
extern Relay relay1;
extern Relay relay2;
extern Relay relay3;
#endif
inline void ioSetup() { inline void ioSetup() {
status.setup();
button0.setup(); button0.setup();
relay0.setup();
#ifdef SP111
relay0Led.setup();
#endif
#ifdef Ch4Pro
button1.setup(); button1.setup();
button2.setup(); button2.setup();
button3.setup(); button3.setup();
status.setup();
relay0.setup();
relay1.setup(); relay1.setup();
relay2.setup(); relay2.setup();
relay3.setup(); relay3.setup();
#endif
} }
inline void ioLoop() { inline void ioLoop() {
status.loop();
button0.loop(); button0.loop();
relay0.loop();
#ifdef SP111
relay0Led.set(relay0.get());
relay0Led.loop();
#endif
#ifdef Ch4Pro
button1.loop(); button1.loop();
button2.loop(); button2.loop();
button3.loop(); button3.loop();
relay1.loop();
status.loop();
relay0.loop();
relay0.loop();
relay2.loop(); relay2.loop();
relay3.loop(); relay3.loop();
#endif
} }
#endif #endif

144
src/mqtt.cpp Normal file
View File

@ -0,0 +1,144 @@
#include "mqtt.h"
#ifdef ESP32
#include <WiFi.h>
#endif
#ifdef ESP8266
#include <ESP8266WiFi.h>
#endif
#include "config.h"
#include <WiFiClient.h>
#include "PubSubClient.h"
#define MQTT_HOST_KEY "/mqtt/host"
#define MQTT_HOST_FALLBACK "10.0.0.50"
#define MQTT_PORT_KEY "/mqtt/port"
#define MQTT_PORT_FALLBACK 1883
#define MQTT_USER_KEY "/mqtt/user"
#define MQTT_USER_FALLBACK ""
#define MQTT_PASSWORD_KEY "/mqtt/password"
#define MQTT_PASSWORD_FALLBACK ""
const String GRID_POWER_DELTA_TOPIC = "electricity/grid/power/signed/w";
WiFiClient wifiClient;
PubSubClient client(wifiClient);
String mqttHost = MQTT_HOST_FALLBACK;
long mqttPort = MQTT_PORT_FALLBACK;
String mqttUser = MQTT_USER_FALLBACK;
bool mqttShouldConnect = false;
unsigned long mqttLast = 0;
unsigned long warningLast = 0;
void mqttSetup() {
mqttShouldConnect = true;
}
double gridPowerDeltaValue = NAN;
double gridPowerDeltaMillis = 0;
void mqttReceive(const char *topic, const uint8_t *bytes, const unsigned int length) {
if (length >= 100) {
Serial.println("[MQTT] Inbound buffer overflow");
return;
}
char string[100];
memcpy(string, bytes, length);
const auto payload = String(string);
if (GRID_POWER_DELTA_TOPIC == topic) {
gridPowerDeltaValue = payload.toDouble();
gridPowerDeltaMillis = millis();
} else {
Serial.printf("[MQTT] Received unexpected topic: %s\n", topic);
}
}
void mqttLoop() {
if (client.loop()) {
if (!mqttShouldConnect) {
client.disconnect();
Serial.println("[MQTT] Stopped.");
}
} else if (mqttShouldConnect) {
mqttHost = loadString(MQTT_HOST_KEY, MQTT_HOST_FALLBACK);
mqttPort = loadLong(MQTT_PORT_KEY, MQTT_PORT_FALLBACK);
mqttUser = loadString(MQTT_USER_KEY, MQTT_USER_FALLBACK);
const auto mqttPass = loadPassword(MQTT_PASSWORD_KEY, MQTT_PASSWORD_FALLBACK);
if (mqttHost == "") {
return;
}
if (mqttLast == 0 || millis() - mqttLast >= 3000) {
mqttLast = max(1UL, millis());
client.setServer(mqttHost.c_str(), mqttPort);
client.setBufferSize(512);
Serial.printf("[MQTT] Connecting: %s:%ld\n", mqttHost.c_str(), mqttPort);
if (client.connect(WiFi.getHostname(), mqttUser.c_str(), mqttPass.c_str())) {
Serial.printf("[MQTT] Connected.\n");
client.subscribe(GRID_POWER_DELTA_TOPIC.c_str());
client.setCallback(mqttReceive);
} else {
Serial.printf("[MQTT] Failed to connect.\n");
}
delay(500);
}
}
}
void mqttStop() {
mqttShouldConnect = false;
}
void mqttPublish(const String &topic, const JsonDocument &json) {
if (!mqttShouldConnect) {
return;
}
char buffer[512];
serializeJson(json, buffer);
client.publish(topic.c_str(), buffer);
}
String getMqttUser() {
return mqttUser;
}
String getMqttHost() {
return mqttHost;
}
long getMqttPort() {
return mqttPort;
}
void mqttSetHost(const String &value) {
mqttHost = value;
storeString(MQTT_HOST_KEY, String(MQTT_HOST_FALLBACK), value);
}
void mqttSetPort(const long &value) {
mqttPort = value;
storeLong(MQTT_PORT_KEY,MQTT_PORT_FALLBACK, value);
}
void mqttSetUser(const String &value) {
mqttUser = value;
storeString(MQTT_USER_KEY, String(MQTT_USER_FALLBACK), value);
}
void mqttSetPassword(const String &value) {
storePassword(MQTT_PASSWORD_KEY,MQTT_PASSWORD_FALLBACK, value);
}

32
src/mqtt.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef MQTT_H
#define MQTT_H
#include <ArduinoJson.h>
extern double gridPowerDeltaValue;
extern double gridPowerDeltaMillis;
void mqttSetup();
void mqttLoop();
void mqttStop();
void mqttPublish(const String &topic, const JsonDocument &json);
String getMqttUser();
String getMqttHost();
long getMqttPort();
void mqttSetHost(const String &value);
void mqttSetPort(const long &value);
void mqttSetUser(const String &value);
void mqttSetPassword(const String &value);
#endif

View File

@ -3,16 +3,16 @@
#include "http.h" #include "http.h"
#include "io.h" #include "io.h"
#define CONFIG_HOSTNAME "/wifi/hostname"
#define CONFIG_WIFI_SSID "/wifi/ssid"
#define CONFIG_WIFI_PASSWORD "/wifi/password"
#define DEFAULT_HOSTNAME "PatrixSonoff4ChPro"
#define DEFAULT_WIFI_SSID "HappyNet"
#define DEFAULT_WIFI_PASSWORD "1Grausame!Sackratte7"
#include <ArduinoOTA.h> #include <ArduinoOTA.h>
#define WIFI_HOSTNAME_KEY "/wifi/hostname"
#define WIFI_SSID_KEY "/wifi/ssid"
#define WIFI_SSID_FALLBACK "HappyNet"
#define WIFI_PASSWORD_KEY "/wifi/password"
#define WIFI_PASSWORD_FALLBACK "1Grausame!Sackratte7"
bool wifiConnected = false; bool wifiConnected = false;
unsigned long wifiLast = 0; unsigned long wifiLast = 0;
@ -26,10 +26,11 @@ void wifiConnect() {
status.cycle(500, 500); status.cycle(500, 500);
const auto hostname = configRead(CONFIG_HOSTNAME, DEFAULT_HOSTNAME); const auto hostname = loadString(WIFI_HOSTNAME_KEY, WIFI_HOSTNAME_FALLBACK);
const auto wifiSSID = configRead(CONFIG_WIFI_SSID, DEFAULT_WIFI_SSID); const auto wifiSSID = loadString(WIFI_SSID_KEY, WIFI_SSID_FALLBACK);
const auto wifiPass = configRead(CONFIG_WIFI_PASSWORD, DEFAULT_WIFI_PASSWORD, true, true); const auto wifiPass = loadPassword(WIFI_PASSWORD_KEY, WIFI_PASSWORD_FALLBACK);
Serial.printf("[WiFi] Connecting: \"%s\"\n", wifiSSID.c_str());
WiFi.hostname(hostname); WiFi.hostname(hostname);
WiFi.begin(wifiSSID.c_str(), wifiPass.c_str()); WiFi.begin(wifiSSID.c_str(), wifiPass.c_str());
wifiLast = max(1UL, millis()); wifiLast = max(1UL, millis());
@ -41,18 +42,21 @@ void wifiLoop() {
if (wifiConnected) { if (wifiConnected) {
if (connected) { if (connected) {
httpLoop(); httpLoop();
mqttLoop();
} else { } else {
Serial.printf("[WiFi] Disconnected!\n"); Serial.printf("[WiFi] Disconnected!\n");
ArduinoOTA.end(); ArduinoOTA.end();
httpStop(); httpStop();
mqttStop();
wifiConnect(); wifiConnect();
} }
} else { } else {
if (connected) { if (connected) {
status.set(false); status.set(false);
Serial.printf("[WiFi] Connected as \"%s\" (%s)\n", WiFi.getHostname(), WiFi.localIP().toString().c_str()); Serial.printf("[WiFi] Connected \"%s\" as \"%s\" (%s)\n", WiFi.SSID().c_str(), WiFi.getHostname(), WiFi.localIP().toString().c_str());
ArduinoOTA.begin(); ArduinoOTA.begin();
httpSetup(); httpSetup();
mqttSetup();
} else if (wifiLast == 0 || millis() - wifiLast >= 10000) { } else if (wifiLast == 0 || millis() - wifiLast >= 10000) {
if (wifiLast > 0) { if (wifiLast > 0) {
Serial.printf("[WiFi] Timeout!\n"); Serial.printf("[WiFi] Timeout!\n");
@ -63,20 +67,6 @@ void wifiLoop() {
wifiConnected = connected; wifiConnected = connected;
} }
void wifiChangeHostname(const char *hostname) {
if (configWrite(CONFIG_HOSTNAME, DEFAULT_HOSTNAME, hostname)) {
WiFi.setHostname(hostname);
}
}
void wifiChangeSSID(const char *ssid) {
configWrite(CONFIG_WIFI_SSID, DEFAULT_WIFI_SSID, ssid);
}
void wifiChangePassword(const char *password) {
configWrite(CONFIG_WIFI_PASSWORD, DEFAULT_WIFI_PASSWORD, password, true);
}
void wifiSetup() { void wifiSetup() {
#ifdef ESP32 #ifdef ESP32
esp_log_level_set("wifi", ESP_LOG_NONE); esp_log_level_set("wifi", ESP_LOG_NONE);
@ -98,3 +88,17 @@ void wifiSetup() {
status.set(false); status.set(false);
}); });
} }
void wifiSetHostname(const String &hostname) {
if (storeString(WIFI_HOSTNAME_KEY, WIFI_HOSTNAME_FALLBACK, hostname)) {
WiFi.setHostname(hostname.c_str());
}
}
void wifiSetSSID(const String &ssid) {
storeString(WIFI_SSID_KEY, WIFI_SSID_FALLBACK, ssid);
}
void wifiSetPassword(const String &password) {
storePassword(WIFI_PASSWORD_KEY, WIFI_PASSWORD_FALLBACK, password);
}

View File

@ -1,14 +1,16 @@
#ifndef WIFI_H #ifndef WIFI_H
#define WIFI_H #define WIFI_H
void wifiChangeHostname(const char *hostname); #include <WString.h>
void wifiChangeSSID(const char *ssid); void wifiSetSSID(const String &ssid);
void wifiChangePassword(const char *password); void wifiSetPassword(const String &password);
void wifiSetup(); void wifiSetup();
void wifiLoop(); void wifiLoop();
void wifiSetHostname(const String &hostname);
#endif #endif

View File

@ -4,5 +4,13 @@ cd "$(dirname "$0")" || exit 1
minify index.html | sed 's|http://10.42.0.204||g' > index.html.min || exit 2 minify index.html | sed 's|http://10.42.0.204||g' > index.html.min || exit 2
curl -s 'http://10.42.0.204/upload/index' -F "file=@index.html.min" #curl -s 'http://10.42.0.204/upload/index' -F "file=@index.html.min"
curl -s 'http://10.42.0.204/upload/icon' -F "file=@icon.svg" #curl -s 'http://10.42.0.204/upload/icon' -F "file=@icon.svg"
# Greenhouse
curl -s 'http://10.0.0.178/upload/index' -F "file=@index.html.min"
#curl -s 'http://10.0.0.178/upload/icon' -F "file=@icon.svg"
# InfraredHeater
#curl -s 'http://10.0.0.179/upload/index' -F "file=@index.html.min"
#curl -s 'http://10.0.0.179/upload/icon' -F "file=@icon.svg"