Compare commits

..

11 Commits

16 changed files with 526 additions and 223 deletions

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="de">
<meta charset="UTF-8">
<head>
<title id="title"></title>
<link rel="icon" type="image/svg" href="icon.svg">
@ -27,7 +28,7 @@
padding: 0.25em;
}
input, select {
input[type=text], input[type=number], select {
all: unset;
width: 100%;
}
@ -60,6 +61,10 @@
padding: 0;
}
label {
white-space: nowrap;
}
.stateOn {
background-color: #7aca7a;
}
@ -139,6 +144,8 @@
<button id="admin" onclick="toggleAdmin()">Admin</button>
<div id="gridPowerDelta"></div>
<script>
let admin = false;
@ -148,6 +155,7 @@
}
const title = document.getElementById("title");
const gridPowerDelta = document.getElementById("gridPowerDelta");
const relayList = document.getElementById("relayList");
function getUrl(path) {
@ -162,7 +170,9 @@
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;
}
}
@ -228,12 +238,13 @@
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 relayTag = document.getElementById("relay" + index) || create(index);
@ -244,6 +255,12 @@
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;
@ -273,9 +290,9 @@
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;
}
@ -291,6 +308,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);
@ -341,6 +374,16 @@
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");

View File

@ -1,6 +0,0 @@
#Name,Type,SubType,Offset,Size,Flags
nvs,data,nvs,0x9000,0x4000,
otadata,data,ota,0xd000,0x2000,
app0,app,ota_0,0x10000,0x3C000,
app1,app,ota_1,,0x3C000,
spiffs,data,spiffs,,0x0E000,
1 #Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x4000
3 otadata data ota 0xd000 0x2000
4 app0 app ota_0 0x10000 0x3C000
5 app1 app ota_1 0x3C000
6 spiffs data spiffs 0x0E000

View File

@ -1,6 +1,4 @@
[common]
platform = espressif8266
board = esp8285
framework = arduino
upload_speed = 921600
monitor_speed = 115200
@ -8,66 +6,68 @@ build.filesystem = littlefs
lib_deps = bblanchon/ArduinoJson @ 7.4.2
knolleary/PubSubClient
[Sonoff4ChPro]
platform = espressif8266
board = esp8285
build_flags = -D Sonoff4ChPro -D WIFI_HOSTNAME_FALLBACK=\"PatrixSonoff4ChPro\"
[GosundSP111]
platform = espressif8266
board = esp8285
build_flags = -D GosundSP111 -D WIFI_HOSTNAME_FALLBACK=\"PatrixGosundSP111\"
[ESP32_TEST]
platform = espressif32
board = esp32dev
build_type = debug
debug_tool = esp-prog
monitor_filters = esp32_exception_decoder
build_flags = -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0
[env:Sonoff4ChPro]
platform = ${common.platform}
board = ${common.board}
platform = ${Sonoff4ChPro.platform}
board = ${Sonoff4ChPro.board}
framework = ${common.framework}
upload_speed = ${common.upload_speed}
monitor_speed = ${common.monitor_speed}
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
upload_protocol = espota
upload_port = 10.0.0.178
build_flags = -D Sonoff4ChPro
build_flags = ${Sonoff4ChPro.build_flags}
[env:Sonoff4ChPro_ESP32]
platform = espressif32
board = esp32dev
platform = ${ESP32_TEST.platform}
board = ${ESP32_TEST.board}
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_type = ${ESP32_TEST.build_type}
debug_tool = ${ESP32_TEST.debug_tool}
monitor_filters = ${ESP32_TEST.monitor_filters}
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
build_flags = -D Sonoff4ChPro -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0
build_flags = ${Sonoff4ChPro.build_flags} ${ESP32_TEST.build_flags}
[env:GosundSP111_USB]
platform = ${common.platform}
board = ${common.board}
[env:GosundSP111]
platform = ${GosundSP111.platform}
board = ${GosundSP111.board}
framework = ${common.framework}
upload_speed = ${common.upload_speed}
monitor_speed = ${common.monitor_speed}
board_build.partitions = partitions.csv
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
build_flags = -D GosundSP111
[env:GosundSP111_02]
platform = ${common.platform}
board = ${common.board}
framework = ${common.framework}
upload_speed = ${common.upload_speed}
monitor_speed = ${common.monitor_speed}
board_build.partitions = partitions.csv
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
upload_port = 10.0.0.179
build_flags = -D GosundSP111
; -Os
; -flto
; -fno-exceptions
; -fno-rtti
build_flags = ${GosundSP111.build_flags}
[env:GosundSP111_ESP32]
platform = espressif32
board = esp32dev
platform = ${ESP32_TEST.platform}
board = ${ESP32_TEST.board}
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_type = ${ESP32_TEST.build_type}
debug_tool = ${ESP32_TEST.debug_tool}
monitor_filters = ${ESP32_TEST.monitor_filters}
build.filesystem = ${common.build.filesystem}
lib_deps = ${common.lib_deps}
build_flags = -D GosundSP111 -D ESP32_TESTBOARD -D CORE_DEBUG_LEVEL=0
build_flags = ${GosundSP111.build_flags} ${ESP32_TEST.build_flags}

View File

@ -69,7 +69,7 @@ public:
return (digitalRead(pin) == HIGH) ^ inverted;
}
void set(const bool state) {
virtual void set(const bool state) {
_write(state);
onCount = 0;
}
@ -85,7 +85,7 @@ public:
this->onCount = onCount_;
}
void loop() {
virtual void loop() {
const auto status = get();
const auto ageMillis = millis() - stateMillis;
if (status) {

View File

@ -11,68 +11,135 @@ class Relay final : public Output {
String topicFallback;
const uint8_t index;
String topic;
const uint8_t index;
bool gridPowerDeltaOffEnabled = false;
long gridPowerDeltaOnThreshold = 0;
long gridPowerDeltaOnDelay = 0;
bool gridPowerDeltaOnEnabled = false;
long gridPowerDeltaOffThreshold = 0;
long gridPowerDeltaOffDelay = 0;
unsigned long gridPowerDeltaLast = 0;
public:
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) {
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));
topic = configRead(path("topic"), topicFallback);
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);
}
void setTopic(const String &value) {
topic = value;
configWrite(path("topic"), topicFallback, value);
storeString(path("name"), nameFallback, value);
}
void setInitial(const Initial value) override {
Output::setInitial(value);
configWrite(path("initial"), INITIAL_OFF, value);
storeInitial(path("initial"), INITIAL_OFF, value);
}
void setOnMillis(const unsigned long value) override {
Output::setOnMillis(value);
configWrite(path("onMillis"), 0L, value);
storeLong(path("onMillis"), 0L, value);
}
void setOffMillis(const unsigned long value) override {
Output::setOffMillis(value);
configWrite(path("offMillis"), 0L, value);
storeLong(path("offMillis"), 0L, value);
}
void setTopic(const String &value) {
topic = value;
storeString(path("topic"), topicFallback, value);
}
void setGridPowerDeltaOnEnabled(const bool value) {
gridPowerDeltaOnEnabled = value;
storeBool(path("gridPowerDeltaOnEnabled"), false, value);
}
void setGridPowerDeltaOnThreshold(const long value) {
gridPowerDeltaOnThreshold = value;
storeLong(path("gridPowerDeltaOnThreshold"), 0L, value);
}
void setGridPowerDeltaOnDelay(const long value) {
gridPowerDeltaOnDelay = value;
storeLong(path("gridPowerDeltaOnDelay"), 0L, value);
}
void setGridPowerDeltaOffEnabled(const bool value) {
gridPowerDeltaOffEnabled = value;
storeBool(path("gridPowerDeltaOffEnabled"), false, value);
}
void setGridPowerDeltaOffThreshold(const long value) {
gridPowerDeltaOffThreshold = value;
storeLong(path("gridPowerDeltaOffThreshold"), 0L, value);
}
void setGridPowerDeltaOffDelay(const long value) {
gridPowerDeltaOffDelay = value;
storeLong(path("gridPowerDeltaOffDelay"), 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;
json["topic"] = topic;
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;
}
private:
String path(const char *name) const {
char path[64];
snprintf(path, sizeof(path), "/relay%d/%s", index, name);
return String(path);
void set(const bool state) override {
Output::set(state);
gridPowerDeltaLast = 0;
}
protected:
@ -83,6 +150,65 @@ protected:
mqttPublish(topic, doc);
}
private:
String path(const char *name) const {
char path[64];
snprintf(path, sizeof(path), "/relay%d/%s", index, name);
return String(path);
}
void doGridPowerDelta() {
if (get()) {
gridPowerDeltaOff();
} else {
gridPowerDeltaOn();
}
}
bool gridPowerDeltaConfigInvalid() {
return gridPowerDeltaOffThreshold < gridPowerDeltaOnThreshold;
}
void gridPowerDeltaOff() {
if (!gridPowerDeltaOffEnabled) {
return;
}
const auto invalid = isnan(gridPowerDeltaValue) || millis() - gridPowerDeltaMillis > 10000 || gridPowerDeltaConfigInvalid();
if (gridPowerDeltaValue > gridPowerDeltaOffThreshold || invalid) {
if (gridPowerDeltaLast == 0 && gridPowerDeltaOffDelay > 0) {
Serial.printf("[RELAY] \"%s\": CONSUMING TOO MUCH: Preparing to power OFF...\n", name.c_str());
gridPowerDeltaLast = max(1UL, millis());
} else {
if (millis() - gridPowerDeltaLast > gridPowerDeltaOffDelay) {
set(false);
}
}
} else if (gridPowerDeltaLast > 0) {
Serial.printf("[RELAY] \"%s\": Powering off CANCELED!\n", name.c_str());
gridPowerDeltaLast = 0;
}
}
void gridPowerDeltaOn() {
if (!gridPowerDeltaOnEnabled) {
return;
}
if (gridPowerDeltaValue < gridPowerDeltaOnThreshold && !gridPowerDeltaConfigInvalid()) {
if (gridPowerDeltaLast == 0 && gridPowerDeltaOnDelay > 0) {
Serial.printf("[RELAY] \"%s\": PRODUCING TOO MUCH: Preparing to power ON...\n", name.c_str());
gridPowerDeltaLast = max(1UL, millis());
} else {
if (millis() - gridPowerDeltaLast > gridPowerDeltaOnDelay) {
set(true);
}
}
} else if (gridPowerDeltaLast > 0) {
Serial.printf("[RELAY] \"%s\": Powering on CANCELED!\n", name.c_str());
gridPowerDeltaLast = 0;
}
}
};
#endif

View File

@ -55,7 +55,7 @@ void configSetup() {
}
#endif
Serial.println("Filesystem-content:");
listDir("/", "");
listDir("/", " ");
}
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;
}
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,47 +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 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));
}
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;
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;
}
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;
}
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,22 +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);
String configRead(const String &path, const String &fallback, bool log = true, bool isPassword = false);
bool storeString(const String &path, const String &fallback, const String &value);
bool configWrite(const String &path, const String &fallback, const String &value, bool isPassword = false);
String loadPassword(const String &path, const String &fallback, bool log = true);
Initial configRead(const String &path, const Initial &fallback);
bool storePassword(const String &path, const String &fallback, const String &value);
bool configWrite(const String &path, const Initial &fallback, const Initial &value);
Initial loadInitial(const String &path, const Initial &fallback, bool log = true);
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,63 +20,78 @@ WebServer server(80);
bool httpRunning = false;
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);
}
File httpUploadFile;
const auto topicKey = String("topic") + index;
if (server.hasArg(topicKey)) {
const auto topic = server.arg(topicKey);
relay.setTopic(topic);
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());
}
}
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 onCountKey = String("onCount") + index;
if (server.hasArg(onCountKey)) {
const auto value = server.arg(onCountKey).toInt();
relay.setOnCount(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 onMillisKey = String("onMillis") + index;
if (server.hasArg(onMillisKey)) {
const auto value = server.arg(onMillisKey).toInt();
relay.setOnMillis(value);
}
httpString("topic" + suffix, [&relay](const String &value) { relay.setTopic(value); });
const auto offMillisKey = String("offMillis") + index;
if (server.hasArg(offMillisKey)) {
const auto value = server.arg(offMillisKey).toInt();
relay.setOffMillis(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() {
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>();
relay0.json(relays.add<JsonObject>());
@ -90,27 +107,25 @@ 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 Sonoff4ChPro
httpRelay(1, relay1);
httpRelay(2, relay2);
httpRelay(3, relay3);
#endif
httpStatus();
}
void httpOff() {
relay0.set(false);
#ifdef Sonoff4ChPro
relay1.set(false);
relay2.set(false);
relay3.set(false);
#endif
httpStatus();
}
File httpUploadFile;
void httpUpload(const char *name) {
const auto upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
@ -138,17 +153,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;
}
@ -160,6 +173,6 @@ void httpStop() {
if (httpRunning) {
httpRunning = false;
server.stop();
Serial.println("HTTP server stopped");
Serial.println("[HTTP] Server stopped");
}
}

View File

@ -11,7 +11,9 @@ Button button0(13, true, true, [](const ButtonEvent event) { buttonCallback(rela
Relay relay0(0, "fallback/relay0", "RELAY #0", 15, false, true);
Output relay0Led("relay0Led", 2, STATUS_INVERT, false);
#ifdef GosundSP111
Output relay0Led("relay0Led", 3, true, false);
#endif
#endif

View File

@ -12,7 +12,9 @@ extern Button button0;
extern Relay relay0;
#ifdef GosundSP111
extern Output relay0Led;
#endif
#ifdef Sonoff4ChPro

View File

@ -19,5 +19,7 @@ void loop() {
wifiLoop();
ioLoop();
ArduinoOTA.handle();
#ifdef GosundSP111
relay0Led.set(relay0.get());
#endif
}

View File

@ -1,22 +1,73 @@
#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) {
@ -24,20 +75,21 @@ void mqttLoop() {
Serial.println("[MQTT] Stopped.");
}
} else if (mqttShouldConnect) {
const auto host = configRead("/mqtt/host", "", false);
if (host == "") {
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) {
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())) {
client.setServer(mqttHost.c_str(), mqttPort);
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");
}
@ -58,3 +110,34 @@ void mqttPublish(const String &topic, const JsonDocument &json) {
const auto size = serializeJson(json, buffer);
client.publish(topic.c_str(), buffer, size);
}
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);
}

View File

@ -3,6 +3,10 @@
#include <ArduinoJson.h>
extern double gridPowerDeltaValue;
extern double gridPowerDeltaMillis;
void mqttSetup();
void mqttLoop();
@ -11,4 +15,18 @@ 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,18 +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"
#ifndef DEFAULT_HOSTNAME
#define DEFAULT_HOSTNAME "PatrixSonoff4ChPro"
#endif
#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;
@ -28,9 +26,9 @@ 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);
@ -69,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);
@ -104,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,11 +4,9 @@ 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"
#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"
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"
#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"