Hot water analog control via P1 meter XT211 (Sagemcom - Siconia) energy meter reading

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.");
  }
}

:clipboard: 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