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>
<html lang="de">
<meta charset="UTF-8">
<head>
<title id="title"></title>
<link rel="icon" type="image/svg" href="icon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: sans-serif;
@ -27,7 +29,7 @@
padding: 0.25em;
}
input, select {
input[type=text], input[type=number], select {
all: unset;
width: 100%;
}
@ -55,29 +57,59 @@
.state {
}
.countdown {
.topic {
flex: 1;
padding: 0;
}
label {
white-space: nowrap;
}
.stateOn {
background-color: palegreen;
background-color: #7aca7a;
}
.stateOff {
background-color: indianred;
background-color: #ae4d4d;
}
.switchOn {
border-radius: 0.5em;
filter: brightness(50%);
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 {
border-radius: 0.5em;
filter: brightness(50%);
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 {
border-radius: 0.5em;
filter: brightness(50%);
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 {
flex: 1;
}
@ -87,10 +119,19 @@
flex: 1;
}
.initial {
text-align: right;
}
.adminHidden {
display: none;
}
@media (min-width: 1000px) {
body {
font-size: 16px;
}
.relayBox {
width: 400px;
}
@ -102,18 +143,26 @@
<div id="relayList"></div>
<button id="admin" onclick="toggleAdmin()">Admin</button>
<div id="gridPowerDelta"></div>
<script>
let admin = false;
function toggleAdmin() {
admin = !admin;
update();
}
const title = document.getElementById("title");
const gridPowerDelta = document.getElementById("gridPowerDelta");
const relayList = document.getElementById("relayList");
function getUrl(path) {
return `http://10.42.0.204/${path}`;
}
function setState(index, value) {
set("state", index, value ? 'true' : 'false');
}
function set(key, index, value) {
request(`${key}${index}=${encodeURIComponent(value)}`);
}
@ -122,21 +171,20 @@
function updateValue(tag, clazz, innerTag, value) {
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;
}
}
function updateState(relayTag, state) {
const tag = relayTag.getElementsByClassName("state")[0];
if (state) {
tag.innerText = "Ein";
tag.classList.add("stateOn");
tag.classList.remove("stateOff");
relayTag.classList.add("stateOn");
relayTag.classList.remove("stateOff");
} else {
tag.innerText = "Aus";
tag.classList.add("stateOff");
tag.classList.remove("stateOn");
relayTag.classList.add("stateOff");
relayTag.classList.remove("stateOn");
}
}
@ -146,10 +194,12 @@
const DAY = (24 * HOUR);
function countdownString(relayData, millis) {
const rest = Math.ceil((millis - relayData.stateMillis - (Date.now() - dataAge)) / SECOND) * SECOND;
if (millis <= 0 || (relayData.onCount === 0 && !relayData.state)) {
const rest = Math.ceil((millis - relayData.stateAgeMillis - (Date.now() - dataAge)) / SECOND) * SECOND;
const cycle = relayData.onCount !== 0 && relayData.onMillis > 0 && relayData.offMillis > 0;
if (millis <= 0 || (!cycle && !relayData.state)) {
return "";
}
const days = rest / DAY;
const hours = rest / HOUR;
const minutes = rest / MINUTE;
@ -189,30 +239,61 @@
timeout = setTimeout(() => request(), 2000);
const r = new XMLHttpRequest();
r.open("GET", getUrl(`set?${query}`));
r.open("GET", getUrl(`status?${query}`));
r.onreadystatechange = () => {
if (r.readyState === 4 && r.status === 200) {
data = JSON.parse(r.response);
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++) {
const relayData = data.relays[index];
const relay = document.getElementById("relay" + index) || create(index);
updateValue(relay, "name", "input", relayData.name);
updateState(relay, relayData.state);
updateCountdown(relay, relayData);
updateValue(relay, "onMillis", "input", relayData.onMillis);
updateValue(relay, "offMillis", "input", relayData.offMillis);
updateValue(relay, "initial", "select", relayData.initial);
const relayTag = document.getElementById("relay" + index) || create(index);
updateValue(relayTag, "name", "input", relayData.name);
updateValue(relayTag, "topic", "input", relayData.topic);
updateState(relayTag, relayData.state);
updateCountdown(relayTag, relayData);
updateValue(relayTag, "onMillis", "input", relayData.onMillis);
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();
}
function newDiv(parent, name) {
function newDiv(parent, clazz) {
const div = document.createElement("div")
div.className = name;
div.className = clazz;
parent.append(div);
return div;
}
@ -228,6 +309,22 @@
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) {
const div = newDiv(parent, clazz);
@ -269,17 +366,29 @@
const header = newDiv(relay, "flex");
newInput(relayIndex, header, "name", "name", "text");
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 offMillis", "offMillis", "number");
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");
newButton(relayIndex, switches, "switch switchCycle", "onCount", -1, "Zyklus");
newButton(relayIndex, switches, "switch switchOn", "state", "true", "Ein");
newButton(relayIndex, switches, "switch switchOff", "state", "false", "Aus");
newButton(relayIndex, switches, "switch switchCycle", "onCount", -1, "Zyklus");
return relay;
}
@ -293,6 +402,13 @@
const relayData = data.relays[index];
const relayTag = document.getElementById("relay" + index) || create(index);
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
board = esp8285
framework = arduino
upload_speed = 921600
upload_port = 10.0.0.178
monitor_speed = 115200
build.filesystem = littlefs
lib_deps = bblanchon/ArduinoJson @ 7.4.2
framework = ${common.framework}
upload_speed = ${common.upload_speed}
monitor_speed = ${common.monitor_speed}
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
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
board = esp32dev
framework = arduino
upload_speed = 921600
monitor_speed = 115200
build.filesystem = littlefs
lib_deps = bblanchon/ArduinoJson @ 7.4.2
build_flags = -D STATUS_PIN=2 -D STATUS_INVERT=false -D CORE_DEBUG_LEVEL=0
framework = ${common.framework}
upload_speed = ${common.upload_speed}
monitor_speed = ${common.monitor_speed}
build_type = debug
debug_tool = esp-prog
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;
void _write(const bool state) {
if (state != get()) {
virtual void publish() {
// nothing
}
void _write(const bool wanted) {
const auto current = get();
if (wanted != current) {
digitalWrite(pin, wanted ^ inverted ? HIGH : LOW);
publish();
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();
}
digitalWrite(pin, state ^ inverted ? HIGH : LOW);
}
void _applyInitial() {
@ -63,9 +69,9 @@ public:
return (digitalRead(pin) == HIGH) ^ inverted;
}
void set(const bool state) {
_write(state);
virtual void set(const bool state) {
onCount = 0;
_write(state);
}
void toggle() {
@ -73,19 +79,21 @@ public:
}
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->offMillis = offMillis_;
this->onCount = onCount_;
set(false);
}
void loop() {
if (get()) {
if (onMillis > 0 && millis() - stateMillis > onMillis) {
virtual void loop() {
const auto status = get();
const auto ageMillis = millis() - stateMillis;
if (status) {
if (onMillis > 0 && ageMillis >= onMillis) {
_write(false);
}
} else {
if (offMillis > 0 && millis() - stateMillis > offMillis && onCount != 0) {
if (offMillis > 0 && ageMillis >= offMillis && onCount != 0) {
_write(true);
if (onCount > 0) {
onCount--;
@ -114,30 +122,6 @@ public:
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

View File

@ -3,46 +3,164 @@
#include "config.h"
#include "Output.h"
#include "mqtt.h"
class Relay final : public Output {
String nameFallback;
String topicFallback;
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:
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 {
Output::setup();
Output::setName(configRead(path("name"), nameFallback));
Output::setInitial(configRead(path("initial"), INITIAL_OFF));
Output::setOnMillis(configRead(path("onMillis"), 0L));
Output::setOffMillis(configRead(path("offMillis"), 0L));
Output::setName(loadString(path("name"), nameFallback));
Output::setInitial(loadInitial(path("initial"), INITIAL_OFF));
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();
}
void loop() override {
Output::loop();
doGridPowerDelta();
}
void setName(const String &value) override {
Output::setName(value);
configWrite(path("name"), nameFallback, value, false);
storeString(path("name"), nameFallback, value);
publish();
}
void setInitial(const Initial value) override {
Output::setInitial(value);
configWrite(path("initial"), INITIAL_OFF, value);
storeInitial(path("initial"), INITIAL_OFF, value);
publish();
}
void setOnMillis(const unsigned long value) override {
Output::setOnMillis(value);
configWrite(path("onMillis"), 0L, value);
storeLong(path("onMillis"), 0L, value);
publish();
}
void setOffMillis(const unsigned long value) override {
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:
@ -53,6 +171,59 @@ private:
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

View File

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

View File

@ -4,6 +4,8 @@
#include <ArduinoJson.h>
#include <LittleFS.h>
#include "wifi.h"
#ifdef ESP8266
#include <ESP8266WebServer.h>
@ -18,73 +20,86 @@ WebServer server(80);
bool httpRunning = false;
void httpRelay(const int index, Output &relay) {
const auto nameKey = String("name") + index;
if (server.hasArg(nameKey)) {
const auto name = server.arg(nameKey);
relay.setName(name);
File httpUploadFile;
void httpString(const String &key, const std::function<void(const String &)> &modifier) {
if (server.hasArg(key)) {
const auto name = server.arg(key);
modifier(name);
}
}
const auto stateKey = String("state") + index;
if (server.hasArg(stateKey)) {
const auto state = server.arg(stateKey);
void httpBool(const String &key, const std::function<void(const bool &)> &modifier) {
if (server.hasArg(key)) {
const auto state = server.arg(key);
if (state == "true") {
relay.set(true);
modifier(true);
} else if (state == "false") {
relay.set(false);
modifier(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);
void httpLong(const String &key, const std::function<void(const long &)> &modifier) {
if (server.hasArg(key)) {
modifier(server.arg(key).toInt());
}
}
const auto onCountKey = String("onCount") + index;
if (server.hasArg(onCountKey)) {
const auto value = server.arg(onCountKey).toInt();
relay.setOnCount(value);
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);
}
});
}
const auto onMillisKey = String("onMillis") + index;
if (server.hasArg(onMillisKey)) {
const auto value = server.arg(onMillisKey).toInt();
relay.setOnMillis(value);
}
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); });
const auto offMillisKey = String("offMillis") + index;
if (server.hasArg(offMillisKey)) {
const auto value = server.arg(offMillisKey).toInt();
relay.setOffMillis(value);
}
}
httpString("topic" + suffix, [&relay](const String &value) { relay.setTopic(value); });
void httpRelayJson(const Output &relay, const JsonObject json) {
json["name"] = relay.getName();
json["state"] = relay.get();
json["stateMillis"] = relay.getStateMillis();
json["initial"] = initialToString(relay.getInitial());
json["onCount"] = relay.getOnCount();
json["onMillis"] = relay.getOnMillis();
json["offMillis"] = relay.getOffMillis();
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() {
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>();
httpRelayJson(relay0, relays.add<JsonObject>());
httpRelayJson(relay1, relays.add<JsonObject>());
httpRelayJson(relay2, relays.add<JsonObject>());
httpRelayJson(relay3, relays.add<JsonObject>());
relay0.json(relays.add<JsonObject>());
#ifdef Ch4Pro
relay1.json(relays.add<JsonObject>());
relay2.json(relays.add<JsonObject>());
relay3.json(relays.add<JsonObject>());
#endif
String response;
serializeJson(json, response);
@ -92,24 +107,27 @@ void httpStatus() {
}
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);
#ifdef Ch4Pro
httpRelay(1, relay1);
httpRelay(2, relay2);
httpRelay(3, relay3);
#endif
httpStatus();
}
void httpOff() {
relay0.set(false);
relay1.set(false);
relay2.set(false);
relay3.set(false);
httpStatus();
}
File httpUploadFile;
void httpUpload(const char *name) {
yield();
const auto upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
char path[64];
@ -136,17 +154,15 @@ void httpSetup() {
server.serveStatic("/", LittleFS, "/index.html");
server.serveStatic("/icon.svg", LittleFS, "/icon.svg");
server.on("/set", httpSet);
server.on("/set/", httpSet);
server.on("/off", httpOff);
server.on("/off/", httpOff);
server.on("/status", httpSet);
server.on("/status/", httpSet);
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.begin();
Serial.println("HTTP server started");
Serial.println("[HTTP] Server started");
httpRunning = true;
}
@ -158,6 +174,6 @@ void httpStop() {
if (httpRunning) {
httpRunning = false;
server.stop();
Serial.println("HTTP server stopped");
Serial.println("[HTTP] Server stopped");
}
}

View File

@ -1,22 +1,28 @@
#include "io.h"
#ifndef STATUS_PIN
#define STATUS_PIN 13
#endif
#ifdef SP111
#ifndef STATUS_INVERT
#ifndef ESP32_TESTBOARD
#define STATUS_PIN 0
#define STATUS_INVERT true
#endif
Relay relay0(0, "RELAY #0", 12, false, true);
Button button0(13, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); });
Relay relay1(1, "RELAY #1", 5, false, true);
Relay relay0(0, "fallback/relay0", "RELAY #0", 15, false, true);
Relay relay2(2, "RELAY #2", 4, false, true);
#ifdef SP111
Output relay0Led("relay0Led", 2, true, false);
#endif
Relay relay3(3, "RELAY #3", 15, false, true);
#endif
Output status("Status", STATUS_PIN, STATUS_INVERT, false);
#ifdef Ch4Pro
#ifndef ESP32_TESTBOARD
#define STATUS_PIN 13
#define STATUS_INVERT true
#endif
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); });
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) {
if (event == BUTTON_PRESSED) {
output.toggle();

View File

@ -6,50 +6,65 @@
void buttonCallback(Output &output, ButtonEvent event);
extern Relay relay0;
extern Relay relay1;
extern Relay relay2;
extern Relay relay3;
extern Output status;
extern Button button0;
extern Relay relay0;
#ifdef SP111
extern Output relay0Led;
#endif
#ifdef Ch4Pro
extern Button button1;
extern Button button2;
extern Button button3;
extern Relay relay1;
extern Relay relay2;
extern Relay relay3;
#endif
inline void ioSetup() {
status.setup();
button0.setup();
relay0.setup();
#ifdef SP111
relay0Led.setup();
#endif
#ifdef Ch4Pro
button1.setup();
button2.setup();
button3.setup();
status.setup();
relay0.setup();
relay1.setup();
relay2.setup();
relay3.setup();
#endif
}
inline void ioLoop() {
status.loop();
button0.loop();
relay0.loop();
#ifdef SP111
relay0Led.set(relay0.get());
relay0Led.loop();
#endif
#ifdef Ch4Pro
button1.loop();
button2.loop();
button3.loop();
status.loop();
relay0.loop();
relay0.loop();
relay1.loop();
relay2.loop();
relay3.loop();
#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 "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>
#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;
unsigned long wifiLast = 0;
@ -26,10 +26,11 @@ void wifiConnect() {
status.cycle(500, 500);
const auto hostname = configRead(CONFIG_HOSTNAME, DEFAULT_HOSTNAME);
const auto wifiSSID = configRead(CONFIG_WIFI_SSID, DEFAULT_WIFI_SSID);
const auto wifiPass = configRead(CONFIG_WIFI_PASSWORD, DEFAULT_WIFI_PASSWORD, true, true);
const auto hostname = loadString(WIFI_HOSTNAME_KEY, WIFI_HOSTNAME_FALLBACK);
const auto wifiSSID = loadString(WIFI_SSID_KEY, WIFI_SSID_FALLBACK);
const auto wifiPass = loadPassword(WIFI_PASSWORD_KEY, WIFI_PASSWORD_FALLBACK);
Serial.printf("[WiFi] Connecting: \"%s\"\n", wifiSSID.c_str());
WiFi.hostname(hostname);
WiFi.begin(wifiSSID.c_str(), wifiPass.c_str());
wifiLast = max(1UL, millis());
@ -41,18 +42,21 @@ void wifiLoop() {
if (wifiConnected) {
if (connected) {
httpLoop();
mqttLoop();
} else {
Serial.printf("[WiFi] Disconnected!\n");
ArduinoOTA.end();
httpStop();
mqttStop();
wifiConnect();
}
} else {
if (connected) {
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();
httpSetup();
mqttSetup();
} else if (wifiLast == 0 || millis() - wifiLast >= 10000) {
if (wifiLast > 0) {
Serial.printf("[WiFi] Timeout!\n");
@ -63,20 +67,6 @@ void wifiLoop() {
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() {
#ifdef ESP32
esp_log_level_set("wifi", ESP_LOG_NONE);
@ -98,3 +88,17 @@ void wifiSetup() {
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
#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 wifiLoop();
void wifiSetHostname(const String &hostname);
#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
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/index' -F "file=@index.html.min"
#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"