mqtt + admin ui + cycle FIX

This commit is contained in:
Patrick Haßel 2025-09-01 12:46:14 +02:00
parent 801b99a3a7
commit b6ea584a4b
9 changed files with 227 additions and 73 deletions

View File

@ -55,29 +55,55 @@
.state {
}
.countdown {
.topic {
flex: 1;
padding: 0;
}
.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;
}
@ -91,6 +117,10 @@
text-align: right;
}
.adminHidden {
display: none;
}
@media (min-width: 1000px) {
body {
font-size: 16px;
@ -107,7 +137,16 @@
<div id="relayList"></div>
<button id="admin" onclick="toggleAdmin()">Admin</button>
<script>
let admin = false;
function toggleAdmin() {
admin = !admin;
update();
}
const title = document.getElementById("title");
const relayList = document.getElementById("relayList");
@ -115,10 +154,6 @@
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)}`);
}
@ -133,15 +168,10 @@
}
function updateState(relayTag, state) {
const tag = relayTag.getElementsByClassName("state")[0];
if (state) {
tag.classList.add("stateOn");
tag.classList.remove("stateOff");
relayTag.classList.add("stateOn");
relayTag.classList.remove("stateOff");
} else {
tag.classList.add("stateOff");
tag.classList.remove("stateOn");
relayTag.classList.add("stateOff");
relayTag.classList.remove("stateOn");
}
@ -153,10 +183,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;
@ -204,13 +236,37 @@
title.innerText = data.hostname;
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);
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");
}
}
}
}
@ -276,9 +332,11 @@
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"]]);
@ -300,6 +358,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

@ -6,6 +6,7 @@ upload_speed = 921600
monitor_speed = 115200
build.filesystem = littlefs
lib_deps = bblanchon/ArduinoJson @ 7.4.2
knolleary/PubSubClient
[env:Sonoff4ChPro]
platform = ${common.platform}
@ -24,6 +25,9 @@ board = esp32dev
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 Sonoff4ChPro -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0
@ -45,6 +49,9 @@ board = esp32dev
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 GosundSP111 -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0

View File

@ -27,15 +27,20 @@ protected:
unsigned long stateMillis = 0;
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(), wanted ? "ON" : "OFF");
}
stateMillis = millis();
}
digitalWrite(pin, wanted ^ inverted ? HIGH : LOW);
}
void _applyInitial() {
@ -117,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,22 +3,28 @@
#include "config.h"
#include "Output.h"
#include "mqtt.h"
class Relay final : public Output {
String nameFallback;
String topicFallback;
String topic;
const uint8_t index;
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), topic(topic), index(index) {
//
}
void setup() override {
Output::setup();
Output::setName(configRead(path("name"), nameFallback));
topic = configRead(path("topic"), topicFallback);
Output::setInitial(configRead(path("initial"), INITIAL_OFF));
Output::setOnMillis(configRead(path("onMillis"), 0L));
Output::setOffMillis(configRead(path("offMillis"), 0L));
@ -27,7 +33,12 @@ public:
void setName(const String &value) override {
Output::setName(value);
configWrite(path("name"), nameFallback, value, false);
configWrite(path("name"), nameFallback, value);
}
void setTopic(const String &value) {
topic = value;
configWrite(path("topic"), topicFallback, value);
}
void setInitial(const Initial value) override {
@ -45,6 +56,17 @@ public:
configWrite(path("offMillis"), 0L, value);
}
void json(const JsonObject json) const {
json["name"] = name;
json["topic"] = topic;
json["state"] = get();
json["stateAgeMillis"] = millis() - stateMillis;
json["initial"] = initialToString(initial);
json["onCount"] = onCount;
json["onMillis"] = onMillis;
json["offMillis"] = offMillis;
}
private:
String path(const char *name) const {
@ -53,6 +75,14 @@ private:
return String(path);
}
protected:
void publish() override {
JsonDocument doc;
json(doc.to<JsonObject>());
mqttPublish(topic, doc);
}
};
#endif

View File

@ -18,13 +18,19 @@ WebServer server(80);
bool httpRunning = false;
void httpRelay(const int index, Output &relay) {
void httpRelay(const int index, Relay &relay) {
const auto nameKey = String("name") + index;
if (server.hasArg(nameKey)) {
const auto name = server.arg(nameKey);
relay.setName(name);
}
const auto topicKey = String("topic") + index;
if (server.hasArg(topicKey)) {
const auto topic = server.arg(topicKey);
relay.setTopic(topic);
}
const auto stateKey = String("state") + index;
if (server.hasArg(stateKey)) {
const auto state = server.arg(stateKey);
@ -66,26 +72,16 @@ void httpRelay(const int index, Output &relay) {
}
}
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();
}
void httpStatus() {
JsonDocument json;
json["hostname"] = WiFi.getHostname();
const auto relays = json["relays"].to<JsonArray>();
httpRelayJson(relay0, relays.add<JsonObject>());
relay0.json(relays.add<JsonObject>());
#ifdef Sonoff4ChPro
httpRelayJson(relay1, relays.add<JsonObject>());
httpRelayJson(relay2, relays.add<JsonObject>());
httpRelayJson(relay3, relays.add<JsonObject>());
relay1.json(relays.add<JsonObject>());
relay2.json(relays.add<JsonObject>());
relay3.json(relays.add<JsonObject>());
#endif
String response;

View File

@ -9,7 +9,7 @@
Button button0(13, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); });
Relay relay0(0, "RELAY #0", 15, false, true);
Relay relay0(0, "fallback/relay0", "RELAY #0", 15, false, true);
#endif
@ -20,8 +20,6 @@ Relay relay0(0, "RELAY #0", 15, false, true);
#define STATUS_INVERT true
#endif
Output status("Status", 13, true, false);
Button button0(0, true, true, [](const ButtonEvent event) { buttonCallback(relay0, event); });
Button button1(9, true, true, [](const ButtonEvent event) { buttonCallback(relay1, event); });
@ -30,13 +28,13 @@ Button button2(10, true, true, [](const ButtonEvent event) { buttonCallback(rela
Button button3(14, true, true, [](const ButtonEvent event) { buttonCallback(relay3, event); });
Relay relay0(0, "RELAY #0", 12, false, true);
Relay relay0(0, "fallback/relay0", "RELAY #0", 12, false, true);
Relay relay1(1, "RELAY #1", 5, false, true);
Relay relay1(1, "fallback/relay1", "RELAY #1", 5, false, true);
Relay relay2(2, "RELAY #2", 4, false, true);
Relay relay2(2, "fallback/relay2", "RELAY #2", 4, false, true);
Relay relay3(3, "RELAY #3", 15, false, true);
Relay relay3(3, "fallback/relay3", "RELAY #3", 15, false, true);
#endif
@ -45,7 +43,7 @@ Relay relay3(3, "RELAY #3", 15, false, true);
#define STATUS_INVERT false
#endif
Output status("Status", STATUS_PIN, STATUS_INVERT, true);
Output status("Status", STATUS_PIN, STATUS_INVERT, false);
void buttonCallback(Output &output, const ButtonEvent event) {
if (event == BUTTON_PRESSED) {

60
src/mqtt.cpp Normal file
View File

@ -0,0 +1,60 @@
#include "mqtt.h"
#include "config.h"
#include <WiFiClient.h>
#include "PubSubClient.h"
WiFiClient wifiClient;
PubSubClient client(wifiClient);
bool mqttShouldConnect = false;
unsigned long mqttLast = 0;
void mqttSetup() {
mqttShouldConnect = true;
}
void mqttLoop() {
if (client.loop()) {
if (!mqttShouldConnect) {
client.disconnect();
Serial.println("[MQTT] Stopped.");
}
} else if (mqttShouldConnect) {
const auto host = configRead("/mqtt/host", "", false);
if (host == "") {
return;
}
if (mqttLast == 0 || millis() - mqttLast >= 3000) {
const auto id = configRead("/mqtt/id", "test", false);
const auto user = configRead("/mqtt/user", "", false);
const auto pass = configRead("/mqtt/pass", "", false, true);
const auto port = configRead("/mqtt/port", 1883L, false);
mqttLast = max(1UL, millis());
client.setServer(host.c_str(), port);
Serial.printf("[MQTT] Connecting: %s:%ld\n", host.c_str(), port);
if (client.connect(id.c_str(), user.c_str(), pass.c_str())) {
Serial.printf("[MQTT] Connected.\n");
} 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[256];
const auto size = serializeJson(json, buffer);
client.publish(topic.c_str(), buffer, size);
}

14
src/mqtt.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef MQTT_H
#define MQTT_H
#include <ArduinoJson.h>
void mqttSetup();
void mqttLoop();
void mqttStop();
void mqttPublish(const String &topic, const JsonDocument &json);
#endif

View File

@ -42,10 +42,12 @@ void wifiLoop() {
if (wifiConnected) {
if (connected) {
httpLoop();
mqttLoop();
} else {
Serial.printf("[WiFi] Disconnected!\n");
ArduinoOTA.end();
httpStop();
mqttStop();
wifiConnect();
}
} else {
@ -54,6 +56,7 @@ void wifiLoop() {
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");