In deze handleiding tonen we hoe je een Lilygo T-Display gebruikt om je stroomverbruik van je digitale teller live weer te geven via een HomeWizard P1-meter. Optioneel kun je ook Energy Sockets aansturen afhankelijk van zonneproductie.
Sketch aanpassen voor de T-Display S3: zet GPIO 15 op HIGH in setup() voor een stabiele WiFi-verbinding.
Lilygo T-Display met de hier beschreven code
Open de HomeWizard app en activeer de lokale API voor de P1-meter en eventuele sockets via Instellingen > Meters.
Je hebt de IP-adressen nodig, liefst zelfs de namenHet inbrengen van de gegevens op naam in plaats van op IP-nummer heeft het voordeel dat je niets meer moet wijzigen, moest nadien het IP-adres wijzigen na bvb. een netwerkstoring, van je P1-meter en sockets voor de verdere instellingen.
Als er nog geen P1-meter was ingebracht (of als je die in de setup-html verwijderd hebt), zal het bordje je netwerk automatisch scannen en P1-meter en sockets zelf detecteren. Na de scan worden de hostnames van de gevonden toestellen naar het flashgeheugen weggeschreven. Dat gebeurt op naam, niet op IP-adres. Daarna herstart het bordje.
Als je alles graag zelf controleert kan je handmatig op je router inloggen om IP-adressen en netwerknamen te weten te komen. Er zijn ook handige apps zoals Net Scan voor Android of Fing. De P1-meter zendt zijn naam uit op het netwerk en heeft poort 80 open.
Na het opstarten zal het display proberen verbinding te maken met je WiFi. Lukt dit niet, dan start het een draadloos toegangspunt (Lily). Verbind hiermee draadloos (zet eerst je mobiele data uit), open je browser op 192.168.4.1 en vul het formulier in. De eerste keer is het zelfs aangeraden om enkel de WiFi-gegevens in te vullen. Dan scant het bordje naar apparaten van HomeWizard (mits hun lokale API werd opengezet op de app van HomeWizard).
p1meter-123abc.local) om problemen bij IP-wijzigingen te voorkomen. Als het bordje zelf je netwerk scant, wordt alles op naam opgeslagen.
Voor de meeste gebruikers is dit de gemakkelijkste manier om de software op je bordje te installeren. Je hebt geen Arduino IDE of andere software nodig - alles gebeurt direct in je webbrowser!
Ga naar https://esptool.spacehuhn.com/ in je webbrowser (Chrome, Edge of Opera).
Verbind je Lilygo T-Display via USB-datakabel met je computer.
Klik vervolgens op de knop "Connect" in de browser.
De webpagina van ESPTool - hier kun je direct vanuit je browser flashen zonder extra software te installeren.
Er verschijnt een pop-up waarin je de juiste COM-poort of USB-poort moet selecteren waar je ESP32 op is aangesloten. Selecteer de juiste poort en klik op "Verbinden".
Selecteer de USB-poort waarop je ESP32 is aangesloten. Dit is meestal iets als "USB Serial" of "CP210x".
Zet het adres op nul (0) en klik dan op "select".
Klik op "SELECT" en selecteer het .bin bestand (firmware) dat je zonet gedownload hebt.
Selecteer het .bin firmware bestand van je computer.
Klik op de knop "Program" om het flash-proces te starten. Je ziet in een voortgangsvak onderaan statusmeldingen verschijnen. Dit duurt enkele minuten - wacht rustig af en verbreek de USB-verbinding niet!
Het flash-proces is bezig. Je ziet de voortgang en eventuele statusmeldingen. Wacht tot het proces volledig is afgerond.
Wanneer het flashen succesvol is afgerond, zie je de melding "Done!" of "Flash successful".
Gelukt! De firmware is succesvol geflasht. Als je het bordje herstart (even de USB-kabel los en terug vastkoppelen), dan werkt het met de nieuwe software. Je mag het bordje nu aansluiten op een gewone voeding (zoals een oude oplader van een smartphone), het heeft geen PC meer nodig. Het bordje werkt nu op zichzelf. De eerste keer verbind je draadloos met het bordje (instructies staan op het display), en breng je liefst enkel de gegevens in van je lokale WiFi-netwerk. Uiteraard moet dat hetzelfde netwerk zijn waar de P1-meter mee verbonden is. Als je enkel de WiFi-credentials inbrengt, dan zal het script zelf je netwerk scannen naar HomeWizard-apparaten, en alles opslaan.
Let op: Deze methode is alleen bedoeld voor mensen die vertrouwd zijn met Arduino programmeren en het instellen van bibliotheken. Voor de meeste gebruikers is de browser-methode hierboven veel eenvoudiger!
Als je de code zelf wilt aanpassen of compileren, kun je de Arduino IDE gebruiken. Download dan de onderstaande sketch en zorg dat je alle benodigde bibliotheken geïnstalleerd hebt:
📥 Download de sketch (Hw_Lovyan_T.ino)
📥 Download de gebruiksaanwijzing (EspGo.be-gebruiksaanwijzing.pdf)
/**
* Copyright (c) 2025 johanok@gmail.com - https://espgo.be/
*
* This project is released under the Creative Commons Attribution 4.0 International License (CC BY 4.0).
*
* You are free to:
* - Copy and redistribute the material in any medium or format
* - Remix, transform, and build upon the material for any purpose, even commercially
*
* Under the following terms:
* - You must give appropriate credit to the original author (johanok@gmail.com - espgo.be)
* - Include a link to https://espgo.be/
* - Indicate if changes were made
*
* Full license text: https://creativecommons.org/licenses/by/4.0/
*
*
* @file Hw_Lovyan_T.ino
*
* @brief ESP32 sketch for Lilygo T-Display: HomeWizard Device Scanner and AutoSockets Controller.
*
* @author johanok@gmail.com
*
* @modes
* - Scanner Mode: Scans network for HomeWizard P1 meter and Energy Sockets
* - P1 Meter Mode: Real-time P1 meter monitoring with optional Automatic Socket Controller
*
* @features
* - Automatic network scanning for P1 meter and Energy Sockets
* - Multi-core FreeRTOS implementation for parallel polling
* - Dedicated WiFiClient per device for optimal parallel performance
* - Dedicated FreeRTOS task for P1 meter (priority 2 [highest], core 0)
* - Individual FreeRTOS tasks per socket for parallel polling (priority 1, core 0)
* - WiFi configuration via web interface (192.168.4.1)
* - Adaptive switchOnWatt threshold calibration based on actual power consumption
* - TFT display output for real-time data visualization
* - Persistent storage of settings in flash memory
* - Intelligent socket management based on power flow
* - Advanced network reliability with adaptive retry mechanisms
* - Thread-safe operations with separate httpClient, WiFiClient and mutex protection per device
*
* @hardware
* - ESP32 microcontroller: LilyGo TTGO T-Display
* - HomeWizard P1 meter (required)
* - HomeWizard Energy Sockets (optional, up to 3)
*
* @note
* - Automatic restart after 2 minutes of persistent connection failures
* - All HTTP requests are thread-safe via mutex protection per client
*
* @section usage Usage
* - Normal operation: Displays current power usage and socket states
* - Long press flash button (5 sec): Reset WiFi settings and restart
* - Short press other button: Toggle autocontrol on/off (if enabled)
* - Long press other button (5 sec): Reset polling intervals to defaults and restart
*
* @section BootBehavior Boot Behavior
* - **No P1 meter configured:** Automatically starts in Scanner Mode
* - **P1 meter configured:** Starts in P1 Meter Mode with AutoSockets Controller
* - **Scanner Mode:** Multi-core network scan (16 parallel tasks on core 0)
* - **P1 Meter Mode:** FreeRTOS tasks for continuous monitoring
*
* @section Architecture FreeRTOS Architecture
* **Core 0 (Network I/O):**
* - P1 meter polling task (priority 2 [highest], every second)
* - Socket polling tasks (priority 1, every 3 seconds per socket)
* - Button handler task (priority 1)
* - Scanner tasks (16 parallel tasks during network scan)
*
* **Core 1 (UI):**
* - Main loop: Display updates, Automatic socket switchOnWatt calibration, failure monitoring
* - Non-blocking UI updates only when data changes
*
* **Board Settings (Arduino IDE):**
* - Board: "ESP32 Dev Module" or "LyliGo T_Display"
* - Partition Scheme: "Default 4MB with spiffs"
* - Flash Size: 4MB (or larger)
*
* @section API HomeWizard API Endpoints
* **P1 Meter:**
* - GET http://[hostname]/api/v1/data
* Returns: active_tariff, active_power_w, montly_power_peak_w
*
* **Energy Socket:**
* - GET http://[hostname]/api/v1/data
* Returns: power_on, active_power_w
* - PUT http://[hostname]/api/v1/state
* Body: {"power_on": true/false}
*
* @version 2.3.1
* @date December 2025
*/
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h> // store data in EEPROM memory
#include <LovyanGFX.hpp> // display
#include <vector>
#define HTTP_PORT 80
WebServer server(HTTP_PORT);
Preferences flash;
class LGFX : public lgfx::LGFX_Device { // display settings for LovyanGFX library
lgfx::Bus_SPI bus;
lgfx::Panel_ST7789 panel;
lgfx::Light_PWM backlight;
public:
LGFX(void) {
{ // SPI bus setup
auto cfg = bus.config();
cfg.spi_host = SPI2_HOST;
cfg.spi_mode = 0;
cfg.freq_write = 40000000;
cfg.freq_read = 16000000;
cfg.pin_sclk = 18;
cfg.pin_mosi = 19;
cfg.pin_miso = -1;
cfg.pin_dc = 16;
bus.config(cfg);
panel.setBus(&bus);
}
{ // Display panel settings
auto cfg = panel.config();
cfg.pin_cs = 5;
cfg.pin_rst = 23;
cfg.pin_busy = -1;
cfg.memory_width = 135;
cfg.memory_height = 240;
cfg.panel_width = 135;
cfg.panel_height = 240;
cfg.offset_x = 52;
cfg.offset_y = -40; // MINUS 40
cfg.offset_rotation = 0;
cfg.invert = true;
cfg.rgb_order = false;
panel.config(cfg);
}
{ // Backlight
auto cfg = backlight.config();
cfg.pin_bl = 4;
cfg.invert = false;
cfg.freq = 44100;
cfg.pwm_channel = 7;
backlight.config(cfg);
panel.setLight(&backlight);
}
setPanel(&panel);
}
};
LGFX tft;
LGFX_Sprite sp(&tft);
#define SCREEN_ROTATION_USB_LEFT 3
// scanner defines & variables
#define PROGRESS_BAR_HEIGHT 15
#define PROGRESS_BAR_WIDTH 200
#define PROGRESS_BAR_X (tft.width() / 2 - PROGRESS_BAR_WIDTH / 2)
#define PROGRESS_BAR_Y 50
#define TASK_COMPLETE_BIT(num) (1 << (num))
const int IP_RANGE_START = 1, IP_RANGE_END = 254, NUM_TASKS = 16;
WiFiClient wifiClientScanners[NUM_TASKS];
int socketCount = 0, totalScanned = 0, currentIP = IP_RANGE_START;
String p1Hostname = "", socketHostnames[3];
SemaphoreHandle_t ipMutex;
EventGroupHandle_t scanEventGroup;
IPAddress baseIP;
bool p1Stored = false;
static uint32_t lastProgressUpdate = 0;
const uint32_t PROGRESS_UPDATE_INTERVAL = 200;
constexpr const char* UNCONFIGURED_IP = "192.168.0.0";
// p1 meter & socket handling variables & structures
constexpr uint8_t numSockets = 3, textLineSpacing = 12, MAX_IP_LENGTH = 64, MAX_NAME_LENGTH = 32;
constexpr uint8_t powerBufferSize = 5, maxConsecutiveFailures = 10;
constexpr uint16_t defaultP1MeterUpdateInterval = 1000, defaultSocketUpdateInterval = 3000;
constexpr uint32_t maxHttpFailureDuration = 2 * 60 * 1000, socketRetryInterval = 60 * 1000, extendedRetryInterval = 10 * 60 * 1000;
bool hasRegisteredSockets = false, scannerMode = false, allowAutoControl = false, autoControlEnabled = false, lastButtonStateOther = HIGH;
const unsigned long hysteresisDelay = 1000, RESTART_DELAY_MS = 5000, MIN_SOCKET_SWITCH_INTERVAL_MS = 10000;
uint8_t powerBufferIndex = 0, buttonOther = 0;
uint16_t spriteWidth, spriteHeight;
uint16_t socketUpdateInterval = defaultSocketUpdateInterval;
int16_t powerWattBuffer[powerBufferSize] = { 0 }, powerWattAverage = 0;
uint32_t lastButtonPressFlash = 0, lastButtonPressOther = 0;
std::vector<String> availableSSIDs;
// Separate HTTP clients per device
HTTPClient httpClientP1;
HTTPClient httpClientSocket1;
HTTPClient httpClientSocket2;
HTTPClient httpClientSocket3;
// Dedicated WiFi clients for each device
WiFiClient clientP1;
WiFiClient clientSocket1;
WiFiClient clientSocket2;
WiFiClient clientSocket3;
// FreeRTOS mutexes - one per client for parallel access
SemaphoreHandle_t httpMutexP1;
SemaphoreHandle_t httpMutexSocket1;
SemaphoreHandle_t httpMutexSocket2;
SemaphoreHandle_t httpMutexSocket3;
TaskHandle_t socketTaskHandles[numSockets] = { NULL };
TaskHandle_t p1TaskHandle = NULL;
struct Socket {
bool isOn = false;
char hostNameOrIP[MAX_IP_LENGTH];
char name[MAX_NAME_LENGTH]; // e.g. "boiler", "bathroom"
int power; // current power usage in Watts
uint8_t consecutiveFailures = 0;
bool useExtendedInterval = false;
bool lastRequestSuccess = false;
bool offline = false;
bool autoControlEnabled = false;
unsigned long lastTurnedOffMillis = 0;
uint16_t switchOnWatt = 10000; // threshold (in Watts) to automatically turn on the socket
uint16_t switchOffWatt = 0; // threshold (in Watts) to automatically switch off the socket
uint32_t offDelay = 2000; // delay (in ms) before auto turning off the socket
uint32_t lastUpdate = 0;
uint64_t lastCheck = 0;
bool needsSwitchOnUpdate = false;
uint16_t pendingNewThreshold = 0;
};
Socket sockets[numSockets];
struct P1Data {
char hostnameOrIp[MAX_IP_LENGTH];
int activeTariff; // (e.g., day or night tariff)
int activePowerW; // measured power flow in Watts
uint16_t monthlyPeak; // also in Watts
unsigned long lastUpdate = 0;
uint16_t updateInterval = defaultP1MeterUpdateInterval;
};
P1Data p1;
void setup() {
tft.begin();
tft.setRotation(SCREEN_ROTATION_USB_LEFT);
tft.setBrightness(224); // 0..255
tft.fillScreen(TFT_BLACK);
flash.begin("P1-app", true);
String savedP1Host = flash.getString("ipM", "");
flash.end();
if (savedP1Host.isEmpty()) {
scannerMode = true;
tft.setTextDatum(middle_center);
tft.setFont(&fonts::Font4);
tft.drawString("Scanner Mode", tft.width() / 2, 12);
tft.setFont(&fonts::Font2);
tft.drawString("No P1 meter found", tft.width() / 2, 36);
delay(1000);
setupScanner();
} else {
tft.setTextDatum(middle_center);
tft.setFont(&fonts::Font4);
tft.drawString("P1 meter Mode", tft.width() / 2, 12);
tft.setFont(&fonts::Font2);
tft.drawString("Normal startup", tft.width() / 2, 36);
delay(1000);
setupAutoSockets();
}
}
void loop() {
scannerMode ? loopScanner() : loopAutoSockets();
}
void showMessageNoConnection() { // message on display: no WiFi connection
const char* noC[] = { "WiFi: no connection.", "Connect to hotspot", "'Lily', open browser", "address 192.168.4.1", "for login + password." };
tft.fillScreen(TFT_NAVY);
tft.setTextColor(TFT_YELLOW);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
for (uint8_t i = 0; i < 5; i++) tft.drawString(noC[i], 120, 12 + i * 27);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
}
// ========== SCANNER MODE FUNCTIONS ==========
void setupScanner() {
tft.fillScreen(TFT_BLACK);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString("HomeWizard Scan", tft.width() / 2, 12);
flash.begin("login_data", true);
String wifiSsid = flash.getString("ssid", "");
String wifiPassword = flash.getString("pasw", "");
flash.end();
if (wifiSsid != "") {
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString("Connecting to WiFi...", tft.width() / 2, 36);
WiFi.begin(wifiSsid.c_str(), wifiPassword.c_str());
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startTime < 10000) delay(250);
}
if (WiFi.status() != WL_CONNECTED) {
showMessageNoConnection();
startWiFiAccessPoint();
return;
}
Serial.println("\nWiFi connected.");
tft.fillScreen(TFT_BLACK);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString("WiFi connected!", tft.width() / 2, 36);
delay(1000);
baseIP = WiFi.localIP();
String ipPrefix = String(baseIP[0]) + "." + String(baseIP[1]) + "." + String(baseIP[2]);
Serial.println("Scan IPs " + ipPrefix + ".1 --> " + ipPrefix + ".255");
tft.fillScreen(TFT_BLACK);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString("Scanning network", tft.width() / 2, 12);
tft.setFont(&fonts::Font2);
tft.drawString(ipPrefix + ".1 >> " + ipPrefix + ".255", tft.width() / 2, 38);
tft.drawRect(PROGRESS_BAR_X - 2, PROGRESS_BAR_Y - 2, PROGRESS_BAR_WIDTH + 4, PROGRESS_BAR_HEIGHT + 4, TFT_WHITE);
updateProgressBar(0);
ipMutex = xSemaphoreCreateMutex();
scanEventGroup = xEventGroupCreate();
for (int i = 0; i < NUM_TASKS; i++) {
char taskName[16];
snprintf(taskName, sizeof(taskName), "ScannerTask%d", i);
xTaskCreatePinnedToCore(scannerTask, taskName, 8192, (void*)i, 1, NULL, 0);
vTaskDelay(pdMS_TO_TICKS(50)); // TOEVOEGEN: 50ms delay tussen tasks
}
}
void loopScanner() {
EventBits_t bits = xEventGroupWaitBits(scanEventGroup, (1 << NUM_TASKS) - 1, true, true, portMAX_DELAY);
if (bits == ((1 << NUM_TASKS) - 1)) displayScanResults();
}
void updateProgressBar(int percentage) {
static int lastPercentage = -1;
if (percentage != lastPercentage || millis() - lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL) {
int progressWidth = (percentage * PROGRESS_BAR_WIDTH) / 100;
if (percentage > lastPercentage)
tft.fillRect(PROGRESS_BAR_X + (lastPercentage * PROGRESS_BAR_WIDTH / 100), PROGRESS_BAR_Y,
progressWidth - (lastPercentage * PROGRESS_BAR_WIDTH / 100), PROGRESS_BAR_HEIGHT, TFT_CYAN);
if (percentage != lastPercentage) {
char buffer[8];
snprintf(buffer, sizeof(buffer), "%3d%%", percentage);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString(buffer, tft.width() / 2, PROGRESS_BAR_Y + 36);
}
lastPercentage = percentage;
lastProgressUpdate = millis();
}
}
void displayScanResults() {
Serial.println("\n--- Scan complete ---");
tft.fillScreen(TFT_BLACK);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString("---Scan Complete---", tft.width() / 2, 14);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.setFont(&fonts::Font2);
tft.drawString("Results (found hostnames)", tft.width() / 2, 34);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setCursor(0, 24);
flash.begin("P1-app", false);
if (p1Stored) {
Serial.println("Found P1 meter: " + p1Hostname);
flash.putString("ipM", p1Hostname);
tft.setFont(&fonts::Font2);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString(p1Hostname, tft.width() / 2, 52);
}
tft.setTextDatum(middle_center);
for (int i = 0; i < socketCount; i++) {
Serial.printf("Found socket %d: %s\n", i + 1, socketHostnames[i].c_str());
String key = "ipS" + String(i + 1);
flash.putString(key.c_str(), socketHostnames[i]);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setFont(&fonts::Font2);
tft.drawString(socketHostnames[i], tft.width() / 2, 72 + i * 20);
}
flash.end();
tft.setTextDatum(middle_center);
if (!p1Stored && socketCount == 0) {
tft.setFont(&fonts::Font2);
tft.drawString("No HomeWizard devices found", tft.width() / 2, tft.height() - 20);
delay(2000);
startWiFiAccessPoint();
} else {
// tft.setTextColor(TFT_CYAN, TFT_BLACK);
// tft.setFont(&fonts::Font2);
// tft.drawString("Restarting for P1 reading mode", tft.width() / 2, tft.height());
delay(2000);
ESP.restart();
}
}
void scannerTask(void* parameter) {
int taskNum = (int)parameter;
WiFiClient& client = wifiClientScanners[taskNum];
while (true) {
int ipLastOctet = -1;
xSemaphoreTake(ipMutex, portMAX_DELAY);
if (currentIP <= IP_RANGE_END) ipLastOctet = currentIP++;
xSemaphoreGive(ipMutex);
if (ipLastOctet == -1) {
xEventGroupSetBits(scanEventGroup, TASK_COMPLETE_BIT(taskNum));
vTaskDelete(NULL);
}
IPAddress targetIP(baseIP[0], baseIP[1], baseIP[2], ipLastOctet);
String url = "http://" + targetIP.toString() + "/api";
HTTPClient http;
http.setTimeout(5000);
http.begin(client, url);
int httpCode = http.GET();
if (httpCode != 200) {
http.end();
vTaskDelay(pdMS_TO_TICKS(100)); // kort wachten
http.begin(client, url);
httpCode = http.GET(); // 2de poging
}
if (httpCode == 200) {
String payload = http.getString();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
const char* product_type = doc["product_type"];
const char* serial = doc["serial"];
String hostname;
if (String(product_type) == "HWE-P1") hostname = "p1meter-";
else if (String(product_type) == "HWE-SKT") hostname = "energysocket-";
else hostname = "unknown-";
hostname += String(serial).substring(strlen(serial) - 6);
hostname += ".local";
xSemaphoreTake(ipMutex, portMAX_DELAY);
if (String(product_type) == "HWE-P1" && !p1Stored) {
p1Hostname = hostname;
p1Stored = true;
} else if (String(product_type) == "HWE-SKT" && socketCount < 3) {
socketHostnames[socketCount++] = hostname;
}
xSemaphoreGive(ipMutex);
}
}
http.end();
xSemaphoreTake(ipMutex, portMAX_DELAY);
totalScanned++;
int progressPercentage = (totalScanned * 100) / (IP_RANGE_END - IP_RANGE_START + 1);
updateProgressBar(progressPercentage);
if (totalScanned % 16 == 0) {
Serial.printf("Scanned IP %s.%d\n", baseIP.toString().substring(0, baseIP.toString().lastIndexOf('.')).c_str(), totalScanned);
char buffer2[50];
snprintf(buffer2, sizeof(buffer2), "Scanned: %d/%d", totalScanned, IP_RANGE_END - IP_RANGE_START + 1);
tft.setFont(&fonts::Font4);
tft.setTextDatum(middle_center);
tft.drawString(buffer2, tft.width() / 2, PROGRESS_BAR_Y + 62);
}
xSemaphoreGive(ipMutex);
}
}
// ========== AUTOSOCKETS MODE FUNCTIONS ==========
void setupAutoSockets() {
initializeDisplay();
loadSavedSettings();
configureButtons();
connectToWiFi();
WiFi.setSleep(false);
httpMutexP1 = xSemaphoreCreateMutex();
httpMutexSocket1 = xSemaphoreCreateMutex();
httpMutexSocket2 = xSemaphoreCreateMutex();
httpMutexSocket3 = xSemaphoreCreateMutex();
// FreeRTOS tasks
startP1MeterPollingTask();
startSocketPollingTasks();
}
void loopAutoSockets() {
if (shouldUpdateDisplay()) updateDisplay();
checkHttpFailureAndRestart();
processPendingSwitchOnUpdates();
}
void initializeDisplay() {
sp.setColorDepth(8);
sp.createSprite(tft.width(), tft.height());
spriteWidth = sp.width();
spriteHeight = sp.height();
}
void loadSavedSettings() { // retrieve data from flash memory and assign it to variables
flash.begin("P1-app", true);
allowAutoControl = flash.getBool("autoControl", false);
autoControlEnabled = allowAutoControl; // autoControlEnabled can be toggled, allowAutoControl not
flash.getString("ipM", p1.hostnameOrIp, MAX_IP_LENGTH);
p1.updateInterval = flash.getUInt("p1Interval", defaultP1MeterUpdateInterval);
socketUpdateInterval = flash.getUInt("socketInterval", defaultSocketUpdateInterval);
for (int i = 0; i < numSockets; i++) { // socket settings
char keyBuffer[24];
snprintf(keyBuffer, sizeof(keyBuffer), "ipS%d", i + 1);
flash.getString(keyBuffer, sockets[i].hostNameOrIP, MAX_IP_LENGTH);
if (strlen(sockets[i].hostNameOrIP) < 7) strcpy(sockets[i].hostNameOrIP, UNCONFIGURED_IP);
if (strcmp(sockets[i].hostNameOrIP, UNCONFIGURED_IP) != 0) hasRegisteredSockets = true;
snprintf(keyBuffer, sizeof(keyBuffer), "nameS%d", i + 1);
flash.getString(keyBuffer, sockets[i].name, MAX_NAME_LENGTH);
snprintf(keyBuffer, sizeof(keyBuffer), "autoControlS%d", i + 1);
sockets[i].autoControlEnabled = flash.getBool(keyBuffer, false);
snprintf(keyBuffer, sizeof(keyBuffer), "offDelayS%d", i + 1);
sockets[i].offDelay = flash.getUInt(keyBuffer, 2000);
if (sockets[i].offDelay < 2000) sockets[i].offDelay = 2000;
snprintf(keyBuffer, sizeof(keyBuffer), "switchOnS%d", i + 1);
sockets[i].switchOnWatt = flash.getUInt(keyBuffer, 10000);
snprintf(keyBuffer, sizeof(keyBuffer), "switchOffS%d", i + 1);
sockets[i].switchOffWatt = flash.getUInt(keyBuffer, 0);
}
flash.end();
}
void handleButtonPress(void* parameter) { // function for FreeRTOS task
for (;;) { // intentional infinite loop
handleButtonInput(); // function handleButtonInput() is not in loop() because FreeRTOS does the handling
vTaskDelay(20 / portTICK_PERIOD_MS); // every 20 milliseconds
}
}
void configureButtons() {
if (tft.height() == 135) buttonOther = 35; // LilyGo T-Display
if (buttonOther != 0) pinMode(buttonOther, INPUT_PULLUP);
xTaskCreatePinnedToCore(handleButtonPress, "ButtonPressed", 4096, NULL, 1, NULL, 0);
}
void getSocketClientAndMutex(int socketIndex, WiFiClient*& client, HTTPClient*& httpClient, SemaphoreHandle_t& mutex) {
HTTPClient* httpClients[] = { &httpClientSocket1, &httpClientSocket2, &httpClientSocket3 };
WiFiClient* clientSockets[] = { &clientSocket1, &clientSocket2, &clientSocket3 };
SemaphoreHandle_t httpMutexes[] = { httpMutexSocket1, httpMutexSocket2, httpMutexSocket3 };
if (socketIndex >= 0 && socketIndex < 3) {
client = clientSockets[socketIndex];
httpClient = httpClients[socketIndex];
mutex = httpMutexes[socketIndex];
} else {
client = nullptr;
httpClient = nullptr;
mutex = nullptr;
}
}
/**
* @brief Processes pending switchOnWatt updates in main loop (thread-safe)
* Adaptive switchOnWatt threshold calibration based on actual power consumption
*/
void processPendingSwitchOnUpdates() {
static uint32_t lastCheck = 0;
if (millis() - lastCheck < 500) return;
lastCheck = millis();
for (int i = 0; i < numSockets; i++) {
if (sockets[i].needsSwitchOnUpdate) {
uint16_t oldValue = sockets[i].switchOnWatt;
uint16_t newValue = sockets[i].pendingNewThreshold;
sockets[i].switchOnWatt = newValue; // Update RAM
flash.begin("P1-app", false); // Update flash
char keyBuffer[24];
snprintf(keyBuffer, sizeof(keyBuffer), "switchOnS%d", i + 1);
flash.putUInt(keyBuffer, newValue);
flash.end();
sockets[i].needsSwitchOnUpdate = false; // Reset flag
}
}
}
bool processJsonRequestP1(const char* ipAddressOrName, std::function<void(JsonDocument&)> onSuccess, uint16_t customTimeout = 3000) {
uint32_t mutexTimeout = max(customTimeout + 2000, 5000); // wait time to prevent false failures during
if (xSemaphoreTake(httpMutexP1, pdMS_TO_TICKS(mutexTimeout)) != pdTRUE) return false; // take mutex
char apiUrl[64];
snprintf(apiUrl, sizeof(apiUrl), "http://%s/api/v1/data", ipAddressOrName);
httpClientP1.setTimeout(customTimeout);
httpClientP1.useHTTP10(true); // avoid chunked encoding overhead
httpClientP1.begin(clientP1, apiUrl);
int httpResponseCode = httpClientP1.GET();
bool success = false;
if (httpResponseCode == 200) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, httpClientP1.getStream());
if (!error) {
onSuccess(doc);
success = true;
}
}
httpClientP1.end();
xSemaphoreGive(httpMutexP1);
return success;
}
bool processJsonRequestSocket(int socketIndex, const char* ipAddressOrName,
std::function<void(JsonDocument&)> onSuccess, uint16_t customTimeout = 3000) {
WiFiClient* client;
HTTPClient* httpClient;
SemaphoreHandle_t mutex;
getSocketClientAndMutex(socketIndex, client, httpClient, mutex);
if (!client || !httpClient || !mutex) return false;
uint32_t mutexTimeout = max(customTimeout + 2000, 5000);
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(mutexTimeout)) != pdTRUE) return false;
char apiUrl[64];
snprintf(apiUrl, sizeof(apiUrl), "http://%s/api/v1/data", ipAddressOrName);
httpClient->setTimeout(customTimeout);
httpClient->useHTTP10(true);
httpClient->begin(*client, apiUrl);
int httpResponseCode = httpClient->GET();
bool success = false;
if (httpResponseCode == 200) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, httpClient->getStream());
if (!error) {
onSuccess(doc);
success = true;
}
}
httpClient->end();
xSemaphoreGive(mutex);
return success;
}
void parseP1MeterData(const JsonDocument& doc) {
if (!doc["active_tariff"].isNull()) p1.activeTariff = doc["active_tariff"].as<int>();
if (!doc["active_power_w"].isNull()) p1.activePowerW = doc["active_power_w"].as<int>();
if (!doc["montly_power_peak_w"].isNull()) p1.monthlyPeak = doc["montly_power_peak_w"].as<int>();
p1.lastUpdate = millis();
}
/**
* @brief FreeRTOS task voor P1 meter polling
* Polls P1 meter every second and handles autocontrol
*/
void p1PollingTask(void* parameter) {
for (;;) { // infinite loop
if (strlen(p1.hostnameOrIp) < 7) { // skip if no valid P1 meter configured
vTaskDelay(pdMS_TO_TICKS(5000)); // wait 5 seconds before checking again
continue;
}
if (hasRegisteredSockets) controlSocketsBasedOnPowerFlow();
bool p1RequestSucceeded = processJsonRequestP1(
p1.hostnameOrIp,
[](JsonDocument& doc) {
parseP1MeterData(doc);
},
4000);
if (p1RequestSucceeded) {
p1.lastUpdate = millis();
} else { // fallback: set error indicators
p1.activeTariff = 3; // connection error indicator
p1.activePowerW = 4040; // error value
}
vTaskDelay(pdMS_TO_TICKS(p1.updateInterval));
}
}
void startP1MeterPollingTask() { // Start P1 meter polling task (FreeRTOS)
xTaskCreatePinnedToCore(
p1PollingTask, // Task function
"P1PollingTask", // Task name
10240, // Stack size (10KB)
NULL, // Parameter
2, // Priority (higher than sockets)
&p1TaskHandle, // Task handle
0); // Core 0
}
/**
* @brief Checks if switchOnWatt needs to be updated
*/
void autoCalibrateSwitchOnThreshold(int socketIndex) {
if (sockets[socketIndex].power <= 0) return; // only calibrate if socket is consuming power
if (sockets[socketIndex].power <= sockets[socketIndex].switchOnWatt) return; // only calibrate if measured power exceeds current threshold
uint16_t newThreshold = sockets[socketIndex].power * 1.05; // calculate new threshold with 5% safety margin
if (newThreshold < 100) newThreshold = 100;
sockets[socketIndex].pendingNewThreshold = newThreshold; // set flag for update in main loop
sockets[socketIndex].needsSwitchOnUpdate = true;
}
void startSocketPollingTasks() { // start individual FreeRTOS tasks for each socket: parallel polling
if (!hasRegisteredSockets) return;
for (int i = 0; i < numSockets; i++) {
if (strcmp(sockets[i].hostNameOrIP, UNCONFIGURED_IP) != 0 && strlen(sockets[i].hostNameOrIP) >= 7) {
char taskName[24];
snprintf(taskName, sizeof(taskName), "SocketTask%d", i);
xTaskCreatePinnedToCore(
socketPollingTask,
taskName,
10240,
(void*)i,
1,
&socketTaskHandles[i],
0);
}
}
}
/**
* @brief FreeRTOS task for polling a single socket
* Runs independently for each socket with its own timing
*/
void socketPollingTask(void* parameter) {
int socketIndex = (int)parameter;
for (;;) {
if (strcmp(sockets[socketIndex].hostNameOrIP, UNCONFIGURED_IP) == 0 || strlen(sockets[socketIndex].hostNameOrIP) < 7) {
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}
uint32_t currentTime = millis();
uint32_t updateInterval;
if (sockets[socketIndex].useExtendedInterval) {
updateInterval = extendedRetryInterval;
} else if (sockets[socketIndex].offline) {
updateInterval = socketRetryInterval;
} else {
updateInterval = socketUpdateInterval;
}
if (currentTime - sockets[socketIndex].lastUpdate >= updateInterval) {
sockets[socketIndex].lastUpdate = currentTime;
uint16_t timeout;
if (sockets[socketIndex].offline || sockets[socketIndex].useExtendedInterval) timeout = 800;
else timeout = 3000;
// Make HTTP request (thread-safe via mutex)
int previousPower = sockets[socketIndex].power;
bool requestSuccess = processJsonRequestSocket(
socketIndex,
sockets[socketIndex].hostNameOrIP,
[socketIndex](JsonDocument& doc) {
if (!doc["active_power_w"].isNull()) {
sockets[socketIndex].power = doc["active_power_w"].as<int>();
}
if (!doc["power_on"].isNull()) {
sockets[socketIndex].isOn = doc["power_on"].as<bool>();
}
},
timeout);
if (!requestSuccess) { // don't mark as failed if mutex timeout (only if actual HTTP failure)
// Check if it was a mutex timeout vs real network failure
// by attempting a very short retry after brief delay
vTaskDelay(pdMS_TO_TICKS(100));
bool retrySuccess = processJsonRequestSocket(
socketIndex,
sockets[socketIndex].hostNameOrIP,
[socketIndex](JsonDocument& doc) {
if (!doc["active_power_w"].isNull()) sockets[socketIndex].power = doc["active_power_w"].as<int>();
if (!doc["power_on"].isNull()) sockets[socketIndex].isOn = doc["power_on"].as<bool>();
},
500); // short timeout for retry
requestSuccess = retrySuccess;
}
// Reset lastCheck timer when socket starts drawing power (transition from 0 to >0)
if (requestSuccess && previousPower == 0 && sockets[socketIndex].power > 0) sockets[socketIndex].lastCheck = millis();
if (requestSuccess) { // update socket state based on request result
// Socket is back online - reset failure counters
sockets[socketIndex].consecutiveFailures = 0;
sockets[socketIndex].useExtendedInterval = false;
sockets[socketIndex].offline = false;
autoCalibrateSwitchOnThreshold(socketIndex); // Adaptive switchOnWatt threshold calibration based on actual power consumption
} else {
// Request failed - increment failure counter (but be lenient)
sockets[socketIndex].consecutiveFailures++;
// Only mark as offline after 3 consecutive real failures
if (sockets[socketIndex].consecutiveFailures >= 3) sockets[socketIndex].offline = true;
// Switch to extended interval after max consecutive failures
if (sockets[socketIndex].consecutiveFailures >= maxConsecutiveFailures && !sockets[socketIndex].useExtendedInterval) {
sockets[socketIndex].useExtendedInterval = true;
}
}
sockets[socketIndex].lastRequestSuccess = requestSuccess;
}
vTaskDelay(pdMS_TO_TICKS(100)); // small delay to prevent task from hogging CPU
}
}
/**
* @brief Controls sockets based on measured consumption and surplus.
* - Switches OFF sockets when total power consumption exceeds their switchOffWatt threshold.
* - Switches ON sockets when sufficient power surplus is available.
* - Prioritizes by offDelay: shortest delay → least important (OFF first), longest → most important (ON first).
* - Round-robin rotation when multiple sockets have equal priority
* - Enforces a minimum interval between ON activations.
* - Maintains a running average (powerWattAverage) for stable decisions.
*/
void controlSocketsBasedOnPowerFlow() { // called by p1PollingTask() ~ every second
static int32_t runningSum = 0;
static unsigned long lastTurnOnMillis = 0;
static int lastSocketTurnedOn = -1; // Track last socket turned ON for round-robin
static int lastSocketTurnedOff = -1; // Track last socket turned OFF for round-robin
unsigned long now = millis();
constexpr uint32_t minDelayForOn = 2000; // 2 seconds before socket can be turned ON again
// === 1. Update running average ===
runningSum -= powerWattBuffer[powerBufferIndex];
runningSum += p1.activePowerW;
powerWattBuffer[powerBufferIndex] = p1.activePowerW;
powerBufferIndex = (powerBufferIndex + 1) % powerBufferSize;
powerWattAverage = runningSum / powerBufferSize;
// === 2. Find best candidate to turn OFF (with round-robin for equal priority) ===
int bestOff = -1;
uint32_t shortestOffDelay = UINT32_MAX;
for (int i = 0; i < numSockets; i++) {
if (!autoControlEnabled || !sockets[i].autoControlEnabled) {
sockets[i].lastCheck = now; // reset when disabled
continue;
}
unsigned long sinceLast = now - sockets[i].lastCheck;
bool offDelayElapsed = (sinceLast >= sockets[i].offDelay);
if (offDelayElapsed && sockets[i].power > 0
&& powerWattAverage > sockets[i].switchOffWatt
&& p1.activePowerW > sockets[i].switchOffWatt) {
// Priority selection with round-robin for equal priority
if (sockets[i].offDelay < shortestOffDelay) {
// This socket has higher priority (shorter delay = less important)
shortestOffDelay = sockets[i].offDelay;
bestOff = i;
} else if (sockets[i].offDelay == shortestOffDelay && bestOff != -1) {
// Equal priority: use round-robin to distribute wear evenly
// Prefer the next socket after the last one we turned off
if ((lastSocketTurnedOff >= 0 && i > lastSocketTurnedOff && bestOff <= lastSocketTurnedOff)
|| (lastSocketTurnedOff >= 0 && i > bestOff && bestOff <= lastSocketTurnedOff)) {
bestOff = i;
}
}
}
}
// === 3. Find best candidate to turn ON (only if nothing needs to turn OFF) ===
int bestOn = -1;
uint32_t longestOffDelay = 0;
if (bestOff < 0) {
for (int i = 0; i < numSockets; i++) {
if (!autoControlEnabled || !sockets[i].autoControlEnabled) continue;
unsigned long sinceLast = now - sockets[i].lastCheck;
bool minDelayElapsed = (sinceLast >= minDelayForOn);
bool hysteresisPassed = ((now - sockets[i].lastTurnedOffMillis) >= hysteresisDelay);
if (minDelayElapsed && !sockets[i].isOn
&& sockets[i].switchOnWatt > 0
&& hysteresisPassed
&& powerWattAverage < -sockets[i].switchOnWatt) {
// Priority selection with round-robin for equal priority
if (sockets[i].offDelay > longestOffDelay) {
// This socket has higher priority (longer delay = more important)
longestOffDelay = sockets[i].offDelay;
bestOn = i;
} else if (sockets[i].offDelay == longestOffDelay && bestOn != -1) {
// Equal priority: use round-robin to distribute wear evenly
// Prefer the next socket after the last one we turned on
if ((lastSocketTurnedOn >= 0 && i > lastSocketTurnedOn && bestOn <= lastSocketTurnedOn)
|| (lastSocketTurnedOn >= 0 && i > bestOn && bestOn <= lastSocketTurnedOn)) {
bestOn = i;
}
}
}
}
}
// === 4. Execute best action ===
if (bestOff >= 0) {
vTaskDelay(pdMS_TO_TICKS(50));
setSocketPowerState(bestOff, false);
sockets[bestOff].lastTurnedOffMillis = now;
sockets[bestOff].lastCheck = now;
lastSocketTurnedOff = bestOff; // Remember for round-robin
} else if (bestOn >= 0 && (now - lastTurnOnMillis >= MIN_SOCKET_SWITCH_INTERVAL_MS)) {
vTaskDelay(pdMS_TO_TICKS(50));
setSocketPowerState(bestOn, true);
lastTurnOnMillis = now;
sockets[bestOn].lastCheck = now;
lastSocketTurnedOn = bestOn; // Remember for round-robin
}
}
void setSocketPowerState(int socketIndex, bool powerOn) {
if (strcmp(sockets[socketIndex].hostNameOrIP, UNCONFIGURED_IP) == 0 || strlen(sockets[socketIndex].hostNameOrIP) == 0) return;
WiFiClient* client;
HTTPClient* httpClient;
SemaphoreHandle_t mutex;
getSocketClientAndMutex(socketIndex, client, httpClient, mutex);
if (!client || !httpClient || !mutex) return;
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return;
char urlBuffer[64];
snprintf(urlBuffer, sizeof(urlBuffer), "http://%s/api/v1/state", sockets[socketIndex].hostNameOrIP);
if (!httpClient->begin(*client, urlBuffer)) {
xSemaphoreGive(mutex);
return;
}
httpClient->addHeader("Content-Type", "application/json");
char payloadBuffer[32];
snprintf(payloadBuffer, sizeof(payloadBuffer), "{\"power_on\": %s}", powerOn ? "true" : "false");
httpClient->PUT(payloadBuffer); // no confirmation is requested, result is visible on display
httpClient->end();
sockets[socketIndex].isOn = powerOn; // record socket state
xSemaphoreGive(mutex);
}
void handleButtonInput() { // adjust this code if a module other than T-Display is used
constexpr uint16_t debounceDelay = 50;
constexpr uint16_t longPressDuration = 5000; // 5 seconds for long press
unsigned long now = millis();
// Other button handling (if configured)
if (buttonOther != 0) {
bool currentOtherState = digitalRead(buttonOther);
static unsigned long otherPressStart = 0;
static bool longPressHandled = false;
if (currentOtherState == LOW && lastButtonStateOther == HIGH) { // button just pressed
if (now - lastButtonPressOther > debounceDelay) {
otherPressStart = now;
longPressHandled = false;
}
}
if (currentOtherState == LOW && lastButtonStateOther == LOW) { // button held down
if (!longPressHandled && otherPressStart > 0 && (now - otherPressStart >= longPressDuration)) {
// Long press: reset intervals and switchOnWatt thresholds (always works)
flash.begin("P1-app", false);
flash.remove("p1Interval"); // p1Interval then becomes defaultP1MeterUpdateInterval
flash.remove("socketInterval"); // socketInterval then becomes defaultSocketUpdateInterval
for (int i = 0; i < numSockets; i++) { // reset switchOnWatt thresholds to 150W for all sockets
char keyBuffer[24];
snprintf(keyBuffer, sizeof(keyBuffer), "switchOnS%d", i + 1);
flash.putUInt(keyBuffer, 150);
}
flash.end();
longPressHandled = true;
vTaskDelay(500 / portTICK_PERIOD_MS);
ESP.restart(); // restart ESP32 to apply new defaults
}
}
if (currentOtherState == HIGH && lastButtonStateOther == LOW) {
if (!longPressHandled && otherPressStart > 0 && (now - otherPressStart < longPressDuration) && (now - otherPressStart > debounceDelay)) {
// Short press: toggle auto control (only if autoControl is allowed)
if (allowAutoControl) {
autoControlEnabled = !autoControlEnabled;
lastButtonPressOther = now;
}
}
otherPressStart = 0;
longPressHandled = false;
}
lastButtonStateOther = currentOtherState;
}
// Flash button handling
static unsigned long flashPressStart = 0;
if (!digitalRead(0)) {
if (now - lastButtonPressFlash > debounceDelay) {
if (flashPressStart == 0) {
flashPressStart = now;
lastButtonPressFlash = now;
}
if (now - flashPressStart >= 5000) { // flash button: long press detected
flash.begin("login_data", false); // reset WiFi SSID = force setup html on next startup
flash.putString("ssid", "");
flash.end();
vTaskDelay(500 / portTICK_PERIOD_MS);
ESP.restart();
}
}
} else {
flashPressStart = 0;
lastButtonPressFlash = 0;
}
}
uint16_t mapPowerToColor(int power) {
if (power < 0) return TFT_GREEN;
if (power < 500) return TFT_CYAN;
if (power < 1000) return TFT_YELLOW;
return (power < 2500) ? TFT_ORANGE : TFT_RED;
}
bool shouldUpdateDisplay() { // only update if activePowerW changes or if 1 second has elapsed
static int lastPower = -9999;
static uint32_t lastDisplayUpdate = 0;
bool powerChanged = (lastPower != p1.activePowerW);
bool timeElapsed = (millis() - lastDisplayUpdate > 1000);
if (powerChanged || timeElapsed) {
lastPower = p1.activePowerW;
lastDisplayUpdate = millis();
return true;
}
return false;
}
void updateDisplay() { // visual feedback to user
sp.fillSprite(TFT_BLACK);
static const uint16_t colors[] = { TFT_YELLOW, TFT_GREEN, TFT_RED };
if (p1.monthlyPeak < 2500) sp.setTextColor(TFT_WHITE); // assign colors according to monthly peak
else if (p1.monthlyPeak < 3500) sp.setTextColor(TFT_YELLOW);
else if (p1.monthlyPeak < 4500) sp.setTextColor(TFT_ORANGE);
else sp.setTextColor(TFT_RED);
char peakBuffer[10];
snprintf(peakBuffer, sizeof(peakBuffer), "%u", p1.monthlyPeak);
sp.setFont(&fonts::Font4);
sp.drawRightString(peakBuffer, spriteWidth - 2, 0); // monthly peak
if (p1.activeTariff > 0 && p1.activeTariff <= 3) { // day/night rate information
static const char* messages[] = { "Watt", "Watt", "Conn ERR" };
sp.setFont(&fonts::Font4);
sp.setTextColor(colors[p1.activeTariff - 1]); // using arrays is much more compact than if..else or switch..case loops
sp.setFont(&fonts::Font4);
sp.drawString(messages[p1.activeTariff - 1], spriteWidth / 5, spriteHeight / 5);
}
sp.setTextColor(mapPowerToColor(p1.activePowerW));
char powerBuffer[10];
snprintf(powerBuffer, sizeof(powerBuffer), "%d", p1.activePowerW);
sp.setFont(&fonts::Font8);
sp.drawRightString(powerBuffer, spriteWidth, spriteHeight * 0.44); // total power usage
for (int i = 0; i < numSockets; i++) {
if (strcmp(sockets[i].hostNameOrIP, UNCONFIGURED_IP) != 0) { // only for registered sockets
displaySocketInfo(i, sockets[i].hostNameOrIP, sockets[i].power, sockets[i].lastRequestSuccess);
int offDelaySec = sockets[i].offDelay / 1000;
char delayBuffer[5];
snprintf(delayBuffer, sizeof(delayBuffer), "%d", offDelaySec);
if (allowAutoControl) {
sp.setFont(&fonts::Font2);
sp.drawRightString(delayBuffer, spriteWidth / 1.7, spriteHeight / 6.4 + (i * textLineSpacing));
}
}
}
if (autoControlEnabled && hasRegisteredSockets) { // show auto control information
sp.setTextColor(TFT_GREEN);
sp.setFont(&fonts::Font4);
sp.drawString(F("Auto ctrl"), 2, 0);
sp.setFont(&fonts::Font2);
sp.drawString(F("Delay"), spriteWidth / 2.1, -3);
sp.drawString(F("seconds"), spriteWidth / 2.1, 8);
}
sp.pushSprite(0, 0);
}
void shortenHostname(const char* hostname, char* result, size_t resultSize) {
size_t startPos = 0;
size_t length;
const char* dashPtr = strchr(hostname, '-'); // extract part between '-' and '.', store result in buffer
const char* dotPtr = strchr(hostname, '.');
if (dashPtr != NULL && dotPtr != NULL && dotPtr > dashPtr) {
startPos = dashPtr - hostname + 1;
length = dotPtr - dashPtr - 1;
if (length > resultSize - 1) length = resultSize - 1;
} else length = strnlen(hostname, resultSize - 1);
strncpy(result, hostname + startPos, length);
result[length] = '\0';
}
void displaySocketInfo(int index, const char* ipAddressOrName, int activePowerW, bool success) {
uint16_t textColor;
if (success) { // socket is online
if (sockets[index].power > 0) textColor = TFT_CYAN; // socket supplies power: cyan
else if (sockets[index].isOn) textColor = TFT_BLUE; // socket on but no power consumption: blue
else if (autoControlEnabled && sockets[index].autoControlEnabled) {
textColor = TFT_BROWN; // socket off, autocontrol enabled: brown
} else textColor = TFT_DARKGREY; // socket off, autocontrol disabled: dark grey
} else if (sockets[index].useExtendedInterval) {
textColor = TFT_MAGENTA; // socket is offline - extended interval
} else textColor = TFT_RED; // socket is offline - normal interval
char shortNameBuffer[MAX_NAME_LENGTH];
shortenHostname(ipAddressOrName, shortNameBuffer, sizeof(shortNameBuffer));
sp.setTextColor(textColor); // next line: use socket name if available, otherwise use shortened hostname
const char* displayName = (strlen(sockets[index].name) > 0) ? sockets[index].name : shortNameBuffer;
sp.setFont(&fonts::Font2);
sp.drawRightString(displayName, spriteWidth, spriteHeight / 6.4 + (index * textLineSpacing));
char powerBuffer[10];
snprintf(powerBuffer, sizeof(powerBuffer), "%d", activePowerW);
sp.setFont(&fonts::Font2);
sp.drawRightString(powerBuffer, spriteWidth / 7.5, spriteHeight / 6.4 + (index * textLineSpacing));
if (tft.height() > 170) sp.setTextColor(TFT_BLACK, textColor);
}
void checkHttpFailureAndRestart() { // check every 10 seconds; restart ESP32 if P1 meter JSON-failure for 2 minutes
static unsigned long restartTimestamp = 0, lastCheck = 0;
if (millis() - lastCheck <= 10000) return;
lastCheck = millis();
if (millis() - p1.lastUpdate <= maxHttpFailureDuration) {
restartTimestamp = 0;
return;
}
if (restartTimestamp == 0) {
for (int i = 0; i < numSockets; i++) setSocketPowerState(i, false); // switch all sockets off
restartTimestamp = millis();
}
if (millis() - restartTimestamp > RESTART_DELAY_MS) ESP.restart();
}
void startWiFiAccessPoint() {
showMessageNoConnection();
WiFi.disconnect(true);
WiFi.scanDelete();
WiFi.mode(WIFI_MODE_AP);
WiFi.softAP("Lily", "");
int numNetworks = WiFi.scanNetworks();
availableSSIDs.clear();
for (int i = 0; i < numNetworks; i++) availableSSIDs.push_back(WiFi.SSID(i));
server.on("/", serveWiFiSetupPage);
server.on("/setting", processWiFiSettingsSubmission);
server.begin();
unsigned long startTime = millis();
const unsigned long maxDuration = 1800000; // 30 minutes in milliseconds
for (;;) { // intentional infinite loop
server.handleClient();
if (millis() - startTime > maxDuration) ESP.restart(); // restart ESP32 after 30 minutes in case of e.g. WiFi problems
delay(10); // small delay to avoid watchdog timer problems
}
}
void connectToWiFi() {
constexpr uint8_t wifiMaxAttempts = 50, wifiRetryDelay = 160;
WiFi.mode(WIFI_MODE_STA);
flash.begin("login_data", true);
String wifiSsid = flash.getString("ssid", "");
String wifiPassword = flash.getString("pasw", "");
flash.end();
if (wifiSsid.isEmpty()) { // SSID is empty, immediately launch the web interface
startWiFiAccessPoint();
return;
}
WiFi.begin(wifiSsid.c_str(), wifiPassword.c_str());
tft.fillScreen(TFT_BLACK);
tft.setFont(&fonts::Font2);
tft.setTextDatum(middle_center);
tft.drawString("Connecting to", spriteWidth / 2, spriteHeight / 2 - 10);
tft.setFont(&fonts::Font4);
tft.drawString(wifiSsid.c_str(), spriteWidth / 2, spriteHeight / 2 + 20);
for (uint8_t i = 0; i < wifiMaxAttempts; ++i) {
if (WiFi.isConnected()) {
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(middle_center);
tft.setFont(&fonts::Font4);
tft.drawString("Connected", spriteWidth / 2, spriteHeight / 2 - 10);
tft.setFont(&fonts::Font2);
tft.drawString(p1.hostnameOrIp, spriteWidth / 2, spriteHeight / 2 + 20);
unsigned long messageStartTime = millis();
while (millis() - messageStartTime < 2000) delay(10);
return;
}
delay(wifiRetryDelay);
}
startWiFiAccessPoint(); // if connect fails, launch web interface
}
void serveWiFiSetupPage() {
String webText = F("<!DOCTYPE HTML><html lang='en'><head><meta charset='UTF-8'><title>WiFi Setup</title>");
webText += F("<meta name='viewport' content='width=device-width, initial-scale=1.0'><style>");
webText += F("body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 0; text-align: center; }");
webText += F(".container { max-width: 440px; background: white; padding: 20px; margin: 20px auto; border-radius: 10px;");
webText += F("box-shadow: 0 0 10px rgba(0,0,0,0.1); } h1 { font-size: 24px; color: #333; margin-bottom: 10px; }");
webText += F("p { font-size: 16px; color: #555; } label { text-align: left; display: block; margin-top: 12px; color: #333; }");
webText += F("input, select, button { width: 100%; padding: 10px; margin-top: 4px; border-radius: 5px;");
webText += F("border: 1px solid #ccc; font-size: 16px; box-sizing: border-box; } .socket-fieldset { background: #f5fff5; }");
webText += F("input[type=submit], button { background: #007BFF; color: white; border: none; cursor: pointer; }");
webText += F("input[type=submit]:hover, button:hover { background: #0056b3; }");
webText += F("fieldset { border: 2px solid #007BFF; border-radius: 10px; padding: 16px; margin-top: 20px; background: #f0f8ff;");
webText += F("box-shadow: inset 0 0 5px rgba(0,123,255,0.1); } legend { font-weight: bold; color: white; background: ");
webText += F("#006400; padding: 4px 10px; border-radius: 5px; font-size: 16px; }");
webText += F("#ssid-buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 16px; }");
webText += F("#ssid-buttons button { width: auto; padding: 8px 14px; font-size: 14px; }</style></head><body>");
webText += F("<div class='container'><h1>WiFi Setup</h1><p>Select a network or enter details below.</p>");
webText += F("<div id='ssid-buttons'>");
for (const String& ssid : availableSSIDs) {
webText += "<button onclick=\"document.getElementsByName('ssid')[0].value='" + ssid + "'\">" + ssid + "</button>";
}
webText += F("</div><form method='get' action='setting'>");
webText += F("<fieldset><legend>WiFi Settings</legend>");
webText += F("<label for='ssid'><b>SSID:</b></label><input type='text' id='ssid' name='ssid' required>");
webText += F("<label for='pass'><b>Password: </b></label>");
webText += F("<input type='password' id='pass' name='pass'></fieldset>");
webText += F("<fieldset><legend>P1 Meter</legend>");
webText += F("<label for='ip_P1'><b>Hostname (or IP Address):</b></label>");
webText += F("<small>Leave empty for automatic scan on restart</small><br>");
webText += "<input type='text' id='ip_P1' name='ip_P1' value='" + String(p1.hostnameOrIp) + "'></fieldset>";
if (p1.hostnameOrIp[0] != '\0') {
webText += F("<fieldset><legend>Socket Settings</legend>");
for (int i = 0; i < numSockets; i++) {
webText += "<fieldset class='socket-fieldset'><legend>Socket " + String(i + 1) + "</legend>";
webText += "<label for='ip_s" + String(i + 1) + "'><b>Hostname (or IP Address):</b></label>";
webText += "<input type='text' id='ip_s" + String(i + 1) + "' name='ip_s" + String(i + 1) + "' value='" + String(sockets[i].hostNameOrIP) + "'>";
webText += "<small style='color:red;'>If unused, set to 192.168.0.0 or leave empty</small><br>";
webText += "<label for='name_s" + String(i + 1) + "'><b>Custom Name:</b></label>";
webText += "<input type='text' id='name_s" + String(i + 1) + "' name='name_s" + String(i + 1) + "' value='" + String(sockets[i].name) + "'><br>";
webText += "<label for='autoControl_s" + String(i + 1) + "'><b>Allow autocontrol for this socket:</b>";
webText += "<input type='checkbox' style='margin-left:10px; width:auto;' id='autoControl_s" + String(i + 1) + "' ";
webText += "name='autoControl_s" + String(i + 1) + "' value='1'";
webText += sockets[i].autoControlEnabled ? " checked>" : ">";
webText += "</label><small>If checked, the module can control this socket</small><br><br>";
webText += "<label for='switchOn_s" + String(i + 1) + "'><b>Switch ON Threshold (Watt):</b></label>";
webText += "<input type='number' id='switchOn_s" + String(i + 1) + "' name='switchOn_s" + String(i + 1) + "' value='";
webText += String(sockets[i].switchOnWatt) + "' min='0' ";
webText += "onkeypress='return (event.charCode !=8 && event.charCode ==0 || (event.charCode >= 48 && event.charCode <= 57))'>";
webText += "<small>Switch ON this socket if power generation exceeds this value</small><br>";
webText += "<label for='switchOff_s" + String(i + 1) + "'><b>Switch OFF Threshold (Watt):</b></label>";
webText += "<input type='number' id='switchOff_s" + String(i + 1) + "' name='switchOff_s" + String(i + 1) + "' value='";
webText += String(sockets[i].switchOffWatt) + "' min='0' ";
webText += "onkeypress='return (event.charCode !=8 && event.charCode ==0 || (event.charCode >= 48 && event.charCode <= 57))'>";
webText += "<small>Switch OFF this socket if power generation drops below this value</small><br>";
webText += "<label for='offDelay_s" + String(i + 1) + "'><b>Autocontrol Switch OFF Delay (seconds):</b></label>";
webText += "<input type='number' id='offDelay_s" + String(i + 1) + "' name='offDelay_s" + String(i + 1) + "' value='";
webText += String(sockets[i].offDelay / 1000) + "' min='2' ";
webText += "onkeypress='return (event.charCode !=8 && event.charCode ==0 || (event.charCode >= 48 && event.charCode <= 57))'>";
webText += "</fieldset>";
}
webText += F("</fieldset>");
}
webText += F("<fieldset><legend>Updates & Settings</legend>");
webText += F("<label for='p1Upd'><b>P1 Meter Update Interval (millisec):</b></label>");
webText += "<input type='number' id='p1Upd' name='p1Upd' value='" + String(p1.updateInterval) + "' min='0' ";
webText += "onkeypress='return (event.charCode !=8 && event.charCode ==0 || (event.charCode >= 48 && event.charCode <= 57))'>";
webText += F("<label for='scktUpd'><b>Socket Update Interval (millisec):</b></label>");
webText += "<input type='number' id='scktUpd' name='scktUpd' value='" + String(socketUpdateInterval) + "' min='0' ";
webText += "onkeypress='return (event.charCode !=8 && event.charCode ==0 || (event.charCode >= 48 && event.charCode <= 57))'>";
webText += F("<label for='autoControl'><b>Allow automatic socket control:</b>");
webText += "<input type='checkbox' style='margin-left:10px; width:auto;' id='autoControl' name='autoControl' value='1'";
webText += autoControlEnabled ? " checked>" : ">";
webText += "</label><small>If checked, the module can switch sockets</small>";
webText += F("</fieldset>");
webText += F("<br><input type='submit' value='Save'></form></div></body></html>");
server.send(200, "text/html", webText);
}
void processWiFiSettingsSubmission() {
String newSsid = server.arg("ssid");
String newPassword = server.arg("pass");
String ipAddr = server.arg("ip_P1");
uint16_t newP1Interval = server.arg("p1Upd").toInt();
uint16_t newSocketInterval = server.arg("scktUpd").toInt();
autoControlEnabled = server.hasArg("autoControl");
for (int i = 0; i < numSockets; i++) { // === read all socket fields on the form ===
String key;
key = "ip_s" + String(i + 1);
strncpy(sockets[i].hostNameOrIP, server.arg(key).c_str(), sizeof(sockets[i].hostNameOrIP) - 1);
sockets[i].hostNameOrIP[sizeof(sockets[i].hostNameOrIP) - 1] = '\0';
key = "name_s" + String(i + 1);
server.arg(key).toCharArray(sockets[i].name, sizeof(sockets[i].name));
key = "autoControl_s" + String(i + 1);
sockets[i].autoControlEnabled = server.hasArg(key);
key = "offDelay_s" + String(i + 1);
int offDelaySeconds = server.arg(key).toInt();
if (offDelaySeconds < 2) offDelaySeconds = 2;
sockets[i].offDelay = offDelaySeconds * 1000; // convert to millisec
key = "switchOn_s" + String(i + 1);
sockets[i].switchOnWatt = server.arg(key).toInt();
key = "switchOff_s" + String(i + 1);
sockets[i].switchOffWatt = server.arg(key).toInt();
}
if (newSsid.length() > 0) {
flash.begin("login_data", false);
flash.putString("ssid", newSsid);
flash.putString("pasw", newPassword);
flash.end();
flash.begin("P1-app", false);
flash.putString("ipM", ipAddr);
if (newP1Interval != 0 && newP1Interval != defaultP1MeterUpdateInterval) flash.putUInt("p1Interval", newP1Interval);
if (newSocketInterval > 0 && newSocketInterval != defaultSocketUpdateInterval) flash.putUInt("socketInterval", newSocketInterval);
for (int i = 0; i < numSockets; i++) {
String key;
key = "ipS" + String(i + 1);
flash.putString(key.c_str(), sockets[i].hostNameOrIP);
key = "nameS" + String(i + 1);
if (strlen(sockets[i].name) > 0) flash.putString(key.c_str(), sockets[i].name);
key = "autoControlS" + String(i + 1);
flash.putBool(key.c_str(), sockets[i].autoControlEnabled);
key = "offDelayS" + String(i + 1);
flash.putUInt(key.c_str(), sockets[i].offDelay);
key = "switchOnS" + String(i + 1);
flash.putUInt(key.c_str(), sockets[i].switchOnWatt);
key = "switchOffS" + String(i + 1);
flash.putUInt(key.c_str(), sockets[i].switchOffWatt);
}
flash.putBool("autoControl", autoControlEnabled);
flash.end();
}
String webText = F(
"<!DOCTYPE HTML><html lang='en'><head><title>Setup</title>"
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
"<style>*{font-family:Arial,Helvetica,sans-serif;font-size:45px;font-weight:600;margin:0;text-align:center;}"
"@keyframes op_en_neer{0%{height:0px;}50%{height:40px;}100%{height:0px;}}"
".opneer{margin:auto;text-align:center;animation:op_en_neer 2s infinite;}</style></head>"
"<body><div class='opneer'></div>ESP will reboot<br>Close this window</body></html>");
server.send(200, "text/html", webText);
delay(500);
ESP.restart();
}
Deze oplossing (mits je de sketch.ino aanpast) werkt ook op grotere schermen zoals de ESP32-2432S028. Zo kun je de gegevens van grotere afstand aflezen. Beschikbaar via AliExpress of Amazon.
Bij testen in meerdere thuisnetwerken blijkt dat de Lilygo T-Display een veel betere netwerkverbinding heeft dan de T-Display S3. Die vindt soms apparaten niet of heeft soms een slechte verbinding, terwijl de Lilygo T-Display geen problemen heeft.
Moest je toch een T-Display S3 hebben en deze code er op wil zetten: gebruik dan het bestand AutoSockets_S3.ino.merged.bin (enkel voor LilyGo T-Display S3) en doe hiermee de stappen van punt 7 hierboven. Deze (gecompileerde) code is aangepast voor een zo stabiel mogelijke WiFi-verbinding.
Deze modules van Lilygo zijn gebaseerd op de ESP32-chip. Die heeft alles aan boord voor draadloze WiFi, en werkt dus gewoon met een standaard 5 volt-aansluiting op de USB-C poort. De module moet dus niet op een PC aangesloten zijn voor de reguliere werking.
De sketch gebruikt FreeRTOS om parallelle taken uit te voeren. Dat is veel sneller dan de sequentiële uitvoering. Dit wordt onder andere gebruikt bij het scannen naar apparaten op het netwerk, en voor het pollen van de sockets.