This guide explains how to use a Lilygo T-Display to show the electricity usage (digital counter data) live via a HomeWizard P1 meter. Optionally, you can also control Energy Sockets based on solar production.
Testing across multiple home networks shows that the Lilygo T-Display has a much better network connection than the T-Display S3. If you modify the sketch for the T-Display S3 you must set GPIO 15 (PWD) to HIGH in setup() to ensure stable WiFi.
These modules from Lilygo are based on the ESP32 chip. It has everything you need for wireless WiFi, so it works with a standard 5-volt connection to the USB-C port. This means that the module does not need to be connected to a PC for normal operation.
Modify sketch for the T-Display S3: set GPIO 15 to HIGH in setup() for a stable WiFi connection.
Lilygo T-Display running the current code
Open the HomeWizard app and enable the local API for your P1 meter and sockets under Settings > Meters.
You need the IP address or preferably the hostnameInserting the data by name instead of IP number has the advantage that you don't have to change anything, should the IP address change afterwards of your P1 meter and sockets.
If no P1 meter was already inserted (or if you removed it in the setup html), the board will automatically scan your network and detect P1 meter and sockets itself. After the scan, the hostnames of the found devices will be written to flash memory. After that, the board reboots.
If you want to check this yourself, you can log into your router manually or use tools such as Net Scan or Fing.
The P1 meter advertises its name and has port 80 open.
On startup, the display will try to connect to WiFi. If this fails, it starts an access point (Lily). Connect to it (first turn off your mobile data), open your browser to 192.168.4.1 and complete the form.
p1meter-123abc.local) to avoid problems with changing IPs.
Use the Arduino IDE to upload the sketch below to your Lilygo T-Display. Download this sketch
/**
* 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();
}
This solution also works with larger displays like the ESP32-2432S028, allowing easier viewing from a distance. Available on AliExpress or Amazon.
The sketch uses FreeRTOS to perform parallel tasks. This is much faster than sequential execution. It is used, among other things, when scanning for devices on the network and for polling the sockets.