New code for ESP32, even better. Control of 2 boilers. With web reading of data.
#include <Arduino.h>
#include <dsmr2.h>
#include "Wire.h"
#include <WiFi.h>
#include <ESPmDNS.h>
#include <NetworkUdp.h>
#include <ArduinoOTA.h>
#include "Adafruit_GFX.h"
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//used pins
const int unused_pin_0 = 0;
const int ledPin = 4; //pin for system running
const int unused_pin_4 = 2;
const int unused_pin_5 = 5;
const int disconnect_relay_100 = 12; // relay to disconnect SSR_100 when not in use
const int unused_pin_13 = 13;
const int PWM_out_100 = 14; // board pin for PWM output for heating boiler 100L
const int unused_pin_15 = 15;
const int RXD2 = 16; //serial data incoming
const int TXD2 = 17; //not used, but shall be declated for communication functionality purpose
const int unused_pin_18 = 18;
const int unused_pin_19 = 19;
const int unused_pin_21 = 21;
const int unused_pin_22 = 22;
const int unused_pin_23 = 23;
const int disconnect_relay_200 = 25; // relay to disconnect SSR when not in use
const int full_power_relay_200 = 26; // relay to activete on full power, to by pass SSR
const int PWM_out_200 = 27; // board pin for PWM output for shower boiler 200L
const int buttonPin = 32; //pin for button display
const int summer_pin = 33; // pin for summer / winter switch
const int unused_pin_34 = 34;
const int unused_pin_35 = 35;
//WIFI connection
const char *ssid = "xxx";
const char *password = "xxx";
NetworkServer server(80);
uint32_t last_ota_time = 0;
// Static IP configuration
IPAddress staticIP(192, 168, x, xxx); // ESP32 static IP
IPAddress gateway(192, 168, x, x); // IP Address of your network gateway (router)
IPAddress subnet(255, 255, 255, 0); // Subnet mask
IPAddress primaryDNS(192, 168, x, x); // Primary DNS (optional)
IPAddress secondaryDNS(0, 0, 0, 0); // Secondary DNS (optional)
// Calibration
const int hist = 70; //minimum 160 [W] histeresis to avoid power to be taken from grid
const int max_boiler_200 = 2400;
const int max_boiler_100 = 1200;
const int calib = 130; // converter calibration pwm liniar range of 60 to 190pwm
// 60-190 is how my conveter works. Lower is almost no power, higher is almost full power
//End calibration
int buttonPushCounter = 0; // counter for the number of button presses
bool buttonState; // current state of the button
bool lastButtonState; // previous state of the button
bool buttonSummer; // Switch to define summer or winter = start w 400 on summer, 200 w on winter.
int summer = 1; //summer = 1; winter = 0
int parallel_run = 150; // pwm on shower when heating starts. From this point is a parallel run.
int wife_step = 100; //fast recovery when there are step loads.
long a1, a2, a3, a4, pr, pd, water;
float c_L1, c_L2, c_L3, v_L1, v_L2, v_L3, water2, gas;
float pd1, pr1, pd2, pr2, pd3, pr3;
long available_power = 0, available_power_read = 0, available_power_r[16];
long available_power_read_1 = 0, available_power_read_2 = 0, available_power_read_3 = 0, available_power_read_4 = 0;
int available_power_ABS = 0;
long boiler_100_power, boiler_200_power;
int SSR_101 = 0, SSR_100 = 0, i = 0, SSR_201 = 0, SSR_200 = 0;
byte ready_to_go1 = 0;
byte ready = 0;
unsigned long now = 0, now2 = 0, last = 0, last2 = 0, now3 = 0, last3 = 0;
unsigned long time_last_restart_m = 0, time_last_restart_h = 0, time_last_restart_d = 0;
int scan_time = 0, scan_time1 = 0;
int cp, cp1, rp, rp1, water1, gas1; // cp = consumed power; rp = returned power; boiler = power allocated to the electric load (boiler)
// cp, rp, gas are values to display only. In my case, I have one tariff (not day / night) so, cumulative value is what I pay.
// it is my way of store energy, other than batteries, while I do not have
// exported PWM value will need to "redirect" the energy from solar panels escaping the house, when overproducing.
// in same time, same function will also take care of not overloading the grig. As per my contract, any load over 2.5kW for 15 min will double the price.
/**
* Define the data we're interested in, as well as the datastructure to
* hold the parsed data. This list shows all supported fields, remove
* any fields you are not using from the below list to make the parsing
* and printing code smaller.
* Each template argument below results in a field of the same name.
*/
using MyData = ParsedData<
/* FixedValue */ energy_delivered_tariff1
/* FixedValue */ ,energy_delivered_tariff2
/* FixedValue */ ,energy_returned_tariff1
/* FixedValue */ ,energy_returned_tariff2
/* String */ ,electricity_tariff
/* FixedValue */ ,power_delivered
/* FixedValue */ ,power_returned
/* FixedValue */ ,voltage_l1
/* FixedValue */ ,voltage_l2
/* FixedValue */ ,voltage_l3
/* FixedValue */ ,current_l1
/* FixedValue */ ,current_l2
/* FixedValue */ ,current_l3
/* FixedValue */ ,power_delivered_l1
/* FixedValue */ ,power_delivered_l2
/* FixedValue */ ,power_delivered_l3
/* FixedValue */ ,power_returned_l1
/* FixedValue */ ,power_returned_l2
/* FixedValue */ ,power_returned_l3
/* String */ ,mbus1_equipment_id_tc
/* String */ ,mbus1_equipment_id_ntc
/* uint8_t */ ,mbus1_valve_position
/* TimestampedFixedValue */ ,mbus1_delivered
/* TimestampedFixedValue */ ,mbus1_delivered_ntc
/* TimestampedFixedValue */ ,mbus1_delivered_dbl
/* uint16_t */ ,mbus2_device_type
/* String */ ,mbus2_equipment_id_tc
/* String */ ,mbus2_equipment_id_ntc
/* uint8_t */ ,mbus2_valve_position
/* TimestampedFixedValue */ ,mbus2_delivered
/* TimestampedFixedValue */ ,mbus2_delivered_ntc
/* TimestampedFixedValue */ ,mbus2_delivered_dbl
/* uint16_t */ ,mbus3_device_type
/* String */ ,mbus3_equipment_id_tc
/* String */ ,mbus3_equipment_id_ntc
/* uint8_t */ ,mbus3_valve_position
/* TimestampedFixedValue */ ,mbus3_delivered
/* TimestampedFixedValue */ ,mbus3_delivered_ntc
/* TimestampedFixedValue */ ,mbus3_delivered_dbl
/* uint16_t */ ,mbus4_device_type
/* String */ ,mbus4_equipment_id_tc
/* String */ ,mbus4_equipment_id_ntc
/* uint8_t */ ,mbus4_valve_position
/* TimestampedFixedValue */ ,mbus4_delivered
/* TimestampedFixedValue */ ,mbus4_delivered_ntc
/* TimestampedFixedValue */ ,mbus4_delivered_dbl >;
struct Printer {
template<typename Item>
void apply(Item &i) {
if (i.present())
{
delay(10);
}
}
};
P1Reader reader(&Serial2, 2);
void setup() {
Serial.begin(115200);
// Begin of remote upload
Serial.println(F("Booting"));
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(F("."));
}
// Configuring static IP
if (!WiFi.config(staticIP, gateway, subnet, primaryDNS, secondaryDNS)) {
Serial.println(F("Failed to configure Static IP"));
} else {
Serial.println(F("Static IP configured!"));
}
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println(F("Connection Failed! Rebooting..."));
delay(5000);
ESP.restart();
}
// Port defaults to 3232
ArduinoOTA.setPort(3232);
// Hostname defaults to esp3232-[MAC]
ArduinoOTA.setHostname("xxx");
// No authentication by default
ArduinoOTA.setPassword("xxx");
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
{
type = "sketch";
}
else
{ // U_SPIFFS
type = "filesystem";
}
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println(F("\nEnd"));
})
.onProgress([](unsigned int progress, unsigned int total) {
if (millis() - last_ota_time > 500)
{
Serial.printf("Progress: %u%%\n", (progress / (total / 100)));
last_ota_time = millis();
}
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
Serial.println("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
Serial.println("Receive Failed");
} else if (error == OTA_END_ERROR) {
Serial.println("End Failed");
}
});
ArduinoOTA.begin();
// End of remote upload
Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
delay(500);
Serial.println(F(""));
Serial.println(F("WiFi connected."));
Serial.print(F("IP address: "));
Serial.println(WiFi.localIP());
pinMode(disconnect_relay_100, OUTPUT);
pinMode(disconnect_relay_200, OUTPUT);
pinMode(full_power_relay_200, OUTPUT); // PWM output for relay control at full load active. Bypass SSR.
pinMode(PWM_out_100, OUTPUT); // PWM output for analog control of 100L boiler (heating boiler)
pinMode(PWM_out_200, OUTPUT); // PWM output for analog control of 200L boiler (shower boiler)
pinMode(buttonPin, INPUT); // initialize the pushbutton pin as an input
pinMode(ledPin, OUTPUT); // to check / see that the unit is running.
pinMode(summer_pin, INPUT); // summer / winter mode
pinMode(unused_pin_0, OUTPUT);
pinMode(unused_pin_4, OUTPUT);
pinMode(unused_pin_5, OUTPUT);
pinMode(unused_pin_13, OUTPUT);
pinMode(unused_pin_15, OUTPUT);
pinMode(unused_pin_18, OUTPUT);
pinMode(unused_pin_19, OUTPUT);
pinMode(unused_pin_21, OUTPUT);
pinMode(unused_pin_22, OUTPUT);
pinMode(unused_pin_23, OUTPUT);
pinMode(unused_pin_34, INPUT);
pinMode(unused_pin_35, INPUT);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))
{
Serial.println(F("SSD1306 allocation failed"));
}
display.display();
display.clearDisplay();
reader.enable(true); // start a read right away
//last = millis();
//last2 = millis();
server.begin();
}
void loop()
{
ArduinoOTA.handle();
// 2. Safety Watchdog / Restart check
now2 = millis();
// 1. Keep the P1 serial buffer constantly reading every cycle (CRITICAL)
reader.loop();
if (now2 - last2 > 30000)
{
SSR_100 = 0;
analogWrite(PWM_out_100, SSR_100);
delay(50);
SSR_200 = 0;
analogWrite(PWM_out_200, SSR_200);
delay(50);
Serial.println(F("Watchdog Timeout - Restarting ESP32"));
delay(100);
ESP.restart();
ready = 0;
}
now = millis(); // Master unified loop timestamp
// 3. Scan rate calculation clock (Runs every 1 second for stats display)
if (now - last >= 900)
{
reader.enable(true);
scan_time1 = now - last;
scan_time = round(scan_time1 / 1000.0);
last = now;
Serial.println(F("Reading OK"));
ready = 1;
}
else {ready = 0;}
// 4. Process incoming Smart Meter data immediately when available
if (reader.available())
{
MyData data;
String err;
if (!reader.parse(&data, &err))
{
Serial.println(F("Reading NOT OK"));
Serial.println(err);
ready = 0;
return; // Clear and wait for next fresh packet
}
// Parse successful, print result
data.applyEach(Printer());
delay(50);
// Refresh watchdog timer immediately on valid data packet arrival
last2 = now2;
// Extracting values
a1 = round(data.energy_delivered_tariff1.val());
a2 = round(data.energy_delivered_tariff2.val());
a3 = round(data.energy_returned_tariff1.val());
a4 = round(data.energy_returned_tariff2.val());
pd = data.power_delivered.int_val();
pr = data.power_returned.int_val();
water = round(data.mbus1_delivered.val());
water2 = data.mbus1_delivered.val();
gas = data.mbus3_delivered_ntc.val();
c_L1 = data.current_l1.val();
c_L2 = data.current_l2.val();
c_L3 = data.current_l3.val();
v_L1 = data.voltage_l1.val();
v_L2 = data.voltage_l2.val();
v_L3 = data.voltage_l3.val();
pd1 = data.power_delivered_l1.val();
pr1 = data.power_returned_l1.val();
pd2 = data.power_delivered_l2.val();
pr2 = data.power_returned_l2.val();
pd3 = data.power_delivered_l3.val();
pr3 = data.power_returned_l3.val();
cp = a1 + a2;
rp = a3 + a4;
available_power_read = pr - pd; // Net balance (+ is export, - is import)
// Store reading in moving average array
available_power_r[i] = available_power_read;
i = (i + 1) % 8;
if (i >= 8) { i = 0; }
// Average of last 8 values
available_power = (available_power_r[0] + available_power_r[1] + available_power_r[2] + available_power_r[3]
+ available_power_r[4] + available_power_r[5] + available_power_r[6] + available_power_r[7]) / 8;
available_power = round(available_power);
now3 = millis();
if (available_power_read < - (5 * hist))
{
SSR_100 = 0;
analogWrite(PWM_out_100, SSR_100);
SSR_200 = 0;
delay(50);
analogWrite(PWM_out_200, SSR_200);
ready_to_go1 = 0;
}
else {ready_to_go1 = 1;}
if (available_power <= hist) { last3 = now3; }
if ((now3 - last3 > 10000 && SSR_200 == 0 && available_power > (3 * hist)) || SSR_200 > 0)
{
ready_to_go1 = 1;
last3 = now3;
}
else {
ready_to_go1 = 0;
SSR_100 = 0;
analogWrite(PWM_out_100, SSR_100);
SSR_200 = 0;
analogWrite(PWM_out_200, SSR_200);
}
reader.clear();
}
if (SSR_100 >= low_pwm) { boiler_100_power = map(SSR_100, low_pwm, high_pwm, 100, max_boiler_100); } else { boiler_100_power = 0; }
boiler_100_power = constrain(boiler_100_power, 0, max_boiler_100);
if (SSR_200 >= low_pwm) { boiler_200_power = map(SSR_200, low_pwm, high_pwm, 100, max_boiler_200); } else { boiler_200_power = 0; }
boiler_200_power = constrain(boiler_200_power, 0, max_boiler_200);
if (ready_to_go1 == 1 && ready == 1)
{
ready = 0;
ready_to_go1 = 0;
// ==========================================================================
// BOILER 100L REGULATION LOGIC (Physically First)
// ==========================================================================
if ((SSR_200 > parallel_run && summer == 0) || (SSR_200 > 250 && summer == 1))
{
// STEP 0: PRE-BALANCING
// If the 200L boiler is over the parallel run limit but 100L hasn't started,
// shave off a bit of 200L power to create headroom.
if (SSR_200 > (parallel_run + 3) && SSR_100 == 0)
{
SSR_200 = SSR_200 - 2;
analogWrite(PWM_out_200, SSR_200);
delay(5);
SSR_201 = SSR_200;
}
// STEP 1: SHUTDOWN (Low Power)
if (SSR_100 < 62 && available_power <= hist)
{
SSR_100 = 0;
Serial.println(F("SSR_100 = 0"));
analogWrite(PWM_out_100, SSR_100);
delay(5);
SSR_101 = SSR_100;
}
// STEP 2: KICK-START (High Excess Power Launch)
if (available_power - 100 > hist && SSR_100 == 0)
{
Serial.println(F("SSR_100 >= 60"));
SSR_100 = low_pwm;
SSR_101 = SSR_100;
analogWrite(PWM_out_100, SSR_100);
delay(5);
}
// STEP 3: ACTIVE REGULATION (Normal Running Power)
if ((SSR_100 <= 254 && SSR_100 >= low_pwm && available_power > (- hist)) ||
(available_power <= hist && SSR_100 == 255))
{
Serial.println(F("SSR_100 Dynamic Tuning"));
available_power_read_3 = (1.2 * max_boiler_100) - boiler_100_power;
available_power_read_4 = (0.6 * max_boiler_100) - boiler_100_power;
// Sub-step A: Calculate target power level
if (available_power > available_power_read_3 && SSR_100 < 240) {
SSR_100 = 240;
SSR_101 = SSR_100;
}
else if (available_power > available_power_read_4) {
if (SSR_100 < wife_step) {
SSR_100 = wife_step; // Instantly jump up to 100 if below it
SSR_101 = SSR_100;
} else {
SSR_100 = SSR_100 + round((available_power - hist) / (max_boiler_100 / calib));
}
}
else {
SSR_100 = SSR_100 + round((available_power - hist) / (max_boiler_100 / calib));
}
// Sub-step B: Enforce minimum safety bounds
if (SSR_100 < low_pwm) SSR_100 = low_pwm;
// Sub-step C: Apply step-climbing rate limits against last state (SSR_101)
if (SSR_100 - SSR_101 <= 5 || SSR_101 - SSR_100 <= 5)
{
if (SSR_100 - SSR_101 > 0) {SSR_100 = SSR_101 + 1;}
if (SSR_101 - SSR_100 > 0 || available_power_read < 0) {SSR_100 = SSR_101 - 1;}
}
if (SSR_100 - SSR_101 > 5) SSR_100 = SSR_101 + 3;
// Sub-step D: Enforce maximum regulation bounds
if (SSR_100 > 245) SSR_100 = 245;
// Sub-step E: Commit updates to hardware
SSR_101 = SSR_100;
analogWrite(PWM_out_100, SSR_100);
}
// STEP 4: MAXIMUM BOOST (Surplus Power Overdrive)
if ((available_power - 100) > hist && SSR_100 >= 245)
{
Serial.println(F("SSR_100 >= 255"));
SSR_100 = 255;
SSR_101 = SSR_100;
analogWrite(PWM_out_100, SSR_100);
delay(5);
}
}
else
{
// Safely turn off 100L boiler if conditions are not met
SSR_100 = SSR_100 - 10;
if (SSR_100 < low_pwm) SSR_100 = 0;
SSR_101 = SSR_100;
analogWrite(PWM_out_100, SSR_100);
}
// ==========================================================================
// BOILER 200L REGULATION LOGIC (Physically Second)
// ==========================================================================
if (SSR_100 == 0 || SSR_100 == 255 || summer == 1)
{
// STEP 1: SHUTDOWN (Low Power)
if (SSR_200 < 62 && available_power <= hist) {
SSR_200 = 0;
analogWrite(PWM_out_200, SSR_200);
Serial.println(F("ssr_200 = 0"));
delay(5);
SSR_201 = SSR_200;
}
// STEP 2: KICK-START (High Excess Power)
if (available_power - 100 > hist && SSR_200 == 0) {
Serial.println(F("ssr_200 >= low_pwm "));
SSR_200 = low_pwm;
SSR_201 = SSR_200;
analogWrite(PWM_out_200, SSR_200);
SSR_201 = SSR_200;
}
// STEP 3: ACTIVE REGULATION (Normal Running Power)
if ((SSR_200 <= 254 && SSR_200 >= low_pwm && available_power > (- hist)) ||
(available_power <= hist && SSR_200 == 255)) {
available_power_read_1 = (1.2 * max_boiler_200) - boiler_200_power;
available_power_read_2 = (0.6 * max_boiler_200) - boiler_200_power;
// Sub-step A: Calculate target power level
if (available_power > available_power_read_1 && SSR_200 < 240) {
SSR_200 = 240;
SSR_201 = SSR_200;
}
else if (available_power > available_power_read_2) {
if (SSR_200 < wife_step) {
SSR_200 = wife_step; // Instantly jump up to wife_step if below it
SSR_201 = SSR_200;
} else {
SSR_200 = SSR_200 + round((available_power - hist) / (max_boiler_200 / calib));
}
}
else {
SSR_200 = SSR_200 + round((available_power - hist) / (max_boiler_200 / calib));
}
// Sub-step B: Enforce minimum safety bounds
if (SSR_200 < low_pwm) SSR_200 = low_pwm;
// Sub-step C: Apply step-climbing rate limits against last state (SSR_201)
if (SSR_200 - SSR_201 <= 5 || SSR_201 - SSR_200 <= 5)
{
if (SSR_200 - SSR_201 > 0) {SSR_200 = SSR_201 + 1;}
if (SSR_201 - SSR_200 > 0 || available_power_read < 0) {SSR_200 = SSR_201 - 1;}
}
if (SSR_200 - SSR_201 > 5) SSR_200 = SSR_201 + 3;
// Sub-step D: Enforce maximum regulation bounds
if (SSR_200 > 250) SSR_200 = 250;
// Sub-step E: Commit updates to hardware
SSR_201 = SSR_200;
analogWrite(PWM_out_200, SSR_200);
Serial.print(F("SSR_200 updated to: "));
Serial.println(SSR_200);
}
// STEP 4: MAXIMUM BOOST (Surplus Power Overdrive)
if (available_power > hist && SSR_200 >= 250) {
SSR_200 = 255;
SSR_201 = SSR_200;
analogWrite(PWM_out_200, SSR_200);
delay(5);
}
}
SSR_101 = SSR_100;
SSR_201 = SSR_200;
}
// Summer / winter switch
buttonSummer = digitalRead(summer_pin);
if (buttonSummer == HIGH) { summer = 1; } else { summer = 0; }
summer = 1; // Temporary bypass code
// Display Interaction and Debouncing
buttonState = digitalRead(buttonPin);
if (buttonState != lastButtonState) {
if (buttonState == HIGH) {
buttonPushCounter = buttonPushCounter + 1;
Serial.println(F("on"));
Serial.print(F("number of button pushes: ")); Serial.println(buttonPushCounter);
}
delay(50); // Button Hardware anti-bounce
}
lastButtonState = buttonState;
if (buttonPushCounter >= 4) { buttonPushCounter = 0; }
// Display Output Control
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
if (buttonPushCounter == 0)
{
display.setCursor(0, 0);
if (available_power >= 0) { display.print(F("Exp:")); } else { display.print(F("Con:")); }
display.print(available_power);
display.print(F("/"));
display.println(scan_time);
display.setCursor(0, 25);
display.print(F("1:")); display.print(SSR_100); display.print(F("/")); display.print(boiler_100_power);
display.setCursor(0, 50);
display.print(F("2:")); display.print(SSR_200); display.print(F("/")); display.print(boiler_200_power);
}
else if (buttonPushCounter == 1)
{
display.setCursor(0, 0); display.print(F("Consumed"));
display.setCursor(0, 20); display.print(a1, 0); display.println(F("kwh"));
display.setCursor(0, 42); display.print(a2, 0); display.println(F("kwh"));
}
else if (buttonPushCounter == 2)
{
display.setCursor(0, 0); display.print(F("Injected"));
display.setCursor(0, 20); display.print(a3, 0); display.println(F("kwh"));
display.setCursor(0, 42); display.print(a4, 0); display.println(F("kwh"));
}
else if (buttonPushCounter == 3)
{
display.setCursor(0, 0); display.print(F("Water: MC"));
display.setTextSize(3);
display.setCursor(0, 30); display.print(water);
}
display.display();
// Diagnostics Trace Outputs
Serial.print("SSR 100= "); Serial.println(SSR_100);
Serial.print("SSR 200= "); Serial.println(SSR_200);
Serial.print("Available power= "); Serial.println(available_power);
Serial.println();
// Calculate relative time pieces
unsigned long total_seconds = now / 1000;
unsigned long total_minutes = total_seconds / 60;
unsigned long total_hours = total_minutes / 60;
time_last_restart_m = total_minutes % 60;
time_last_restart_h = total_hours % 24;
time_last_restart_d = total_hours / 24;
// Local annual offset calculations
cp1 = cp - 1341 - 436; // Old consumption extracted to indicate actual year consumption
rp1 = rp - 2352 - 971; // Old consumption extracted to indicate actual year consumption
water1 = water - 60; // Old consumption extracted to indicate actual year consumption
gas1 = gas - 71; // Old consumption extracted to indicate actual year consumption
// Handle incoming web client requests
NetworkClient client = server.accept();
if (client)
{
Serial.println("New Client.");
String currentLine = "";
unsigned long timeoutStart = millis();
const unsigned long TIMEOUT_MS = 2000;
while (client.connected() && (millis() - timeoutStart < TIMEOUT_MS))
{
if (client.available())
{
char c = client.read();
Serial.write(c);
timeoutStart = millis();
if (c == '\n') {
if (currentLine.length() == 0)
{
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
client.print("Scan time [milisec]: "); client.print(scan_time1); client.print("<br>");
client.print("Time from last restart [d:h:m]: "); client.print(time_last_restart_d); client.print(":"); client.print(time_last_restart_h); client.print(":");
if (time_last_restart_m < 10) { client.print("0");}
client.print(time_last_restart_m); client.print("<br><br>");
client.print("Boiler 100L: SSR 100 value: "); client.print(SSR_100); client.print(" PWM; "); client.print("Power: "); client.print(boiler_100_power, 0); client.print(" W<br>");
client.print("Boiler 200L: SSR 200 value: "); client.print(SSR_200); client.print(" PWM; "); client.print("Power: "); client.print(boiler_200_power, 0); client.print(" W<br>");
available_power_ABS = abs(available_power);
if (available_power >= 0) { client.print("Injecting: "); } else { client.print("Consuming: "); }
client.print(available_power_ABS, 0); client.print(" W<br>");
if (summer == 0) { client.print(" Winter mode selected.<br><br>"); } else { client.print(" Summer mode selected.<br><br>"); }
client.print("Electric energy reading: <br>");
client.print("Consumed: "); client.print(a1, 0); client.print(" + "); client.print(a2, 0); client.print(" = "); client.print(cp, 0); client.print(" kWh<br>");
client.print("Actual year: "); client.print(cp1, 0); client.print(" kWh<br>");
client.print("Injected: "); client.print(a3, 0); client.print(" + "); client.print(a4, 0); client.print(" = "); client.print(rp, 0); client.print(" kWh<br>");
client.print("Actual year: "); client.print(rp1, 0); client.print(" kWh<br><br>");
client.print("Gas reading: "); client.print(gas, 4); client.print(" m3 .");
client.print("Actual year: "); client.print(gas1, 4); client.print(" m3<br>");
client.print("Water reading: "); client.print(water2, 4); client.print(" m3 .");
client.print("Actual year: "); client.print(water1, 0); client.print(" m3<br><br>");
client.print("L1: "); client.print(v_L1,2); client.print(" V "); client.print(c_L1,2); client.print(" A<br>");
client.print("P1 consummed: "); client.print(pd1,3); client.print(" kW<br>");
client.print("P1 returned: "); client.print(pr1,3); client.print(" kW<br><br>");
client.print("L2: "); client.print(v_L2,2); client.print(" V "); client.print(c_L2,2); client.print(" A<br>");
client.print("P2 consummed: "); client.print(pd2,3); client.print(" kW<br>");
client.print("P2 returned: "); client.print(pr2,3); client.print(" kW<br><br>");
client.print("L3: "); client.print(v_L3,2); client.print(" V "); client.print(c_L3,2); client.print(" A<br>");
client.print("P3 consummed: "); client.print(pd3,3); client.print(" kW<br>");
client.print("P3 returned: "); client.print(pr3,3); client.print(" kW<br>");
break;
}
else
{
currentLine = "";
}
}
else if (c != '\r')
{
currentLine += c;
}
}
}
delay(5);
client.stop();
Serial.println("Client Disconnected.");
}
}
Complete & Finalized Bill of Materials (BOM)
| # | Component | Purpose / Operational Specification | Source Link |
|---|---|---|---|
| 1 | ESP32 NodeMCU Board | System Microcontroller. Parses physical P1 meter frames over native Serial2. |
AliExpress ESP32 Base |
| 2 | ESP32 Screw Terminal Base | DIN-rail breakout board ensuring stable screw connections for field deployment. | AliExpress Expansion Board |
| 3 | DIN-Rail Switching Power Supply | Drives the 5V/3.3V computing hardware electronics and the PWM module overhead loop. | AliExpress Power Supply |
| 4 | PWM to Voltage Linear Converter | Model: FV-1KHz2V. Converts digital ESP32 duty cycles into a calibrated 0-10V space. | AliExpress PWM-to-Voltage |
| 5 | Single Phase AC SSR Voltage Regulator | Model: DTY220D10-400P (25A). Modulates the primary heating power levels safely up to 25A. | Amazon.com.be SSR Regulator |
| 6 | 2-Channel Low-Level 5V SSR Module | Brand: GERUI. Acts as a low-power safety pilot switch to engage/disengage downstream contactors. | Amazon.com.be GERUI Relay |
| 7 | BC547 Inverter Kit | Generic NPN Transistor + 1x 1KΩ resistor + 1x 10KΩ (or 4.7KΩ) pull-up tracking resistors. | User Sourced Components |
| 8 | P1 Port Physical Patch Cable | Standard RJ12 (6p6c) telecommunications cable connecting the smart meter directly. | User Sourced Components |
| 9 | Heavy-Duty Contactor / RXN Relay | Coils driven by the GERUI board; heavy contacts rated for 16A+ to safely cut main power lines. | User Sourced Components |