I have recently moved into a new house in Wallonia, Belgium, equipped with a solar installation (Huawei SUN2000-3K-LB0 inverter). Before moving in, I wanted to build a monitoring system that would help me understand my consumption patterns and be ready to diagnose potential inverter shutdown issues caused by grid over-voltage, a common problem in areas with high solar penetration.

For that purpose, I have built an Arduino-based system that reads the electrical meter’s P1 port and transmits aggregated data over LoRaWAN every 15 minutes. In my old house, I used it to analyze my consumption patterns, but the code was already prepared to detect the over-voltage conditions that can trigger inverter protective shutdowns.

The full source code is available on GitHub.

The Challenge: Understanding Consumption and Preparing for Solar

Belgian smart meters expose a wealth of data through their P1 port. I wanted to:

  1. Monitor consumption patterns: Understand when and how much electricity I use
  2. Prepare for solar production monitoring: Track production once I move in
  3. Be ready for over-voltage detection: In Belgium, grid-connected inverters must comply with Synergrid C10/11 and EN 50549-1 regulations, which require inverters to disconnect when:
    • Stage 1: Sustained over-voltage: Grid voltage exceeds 253V (1.10 × 230V nominal) for more than 1.5 seconds
    • Stage 2: Extreme over-voltage: Grid voltage instantaneously exceeds 264.5V (1.15 × 230V nominal)

Once triggered, the inverter waits approximately 60 seconds of stable voltage before reconnecting. This leads to lost production and reduced rentability.

Spoiler: my new house does not suffer from this issue, it was the case in my old house (but that house did not have solar panels).

The Solution: P1 Port + LoRaWAN + Environmental Monitoring

Belgian smart meters (ORES in Wallonia, Fluvius in Flanders) expose a P1 port that outputs detailed electrical data every second. The challenge? LoRaWAN’s duty cycle limitations don’t allow transmitting every second, but its low power consumption and long range (100m+) make it perfect for remote monitoring. Its long range allows usage for meters in a basement when the access point can be on the third floor, useful if you live in an apartment.

My solution: aggregate the per-second data and transmit a summary every 15 minutes, along with temperature and humidity readings from a DHT22 sensor.

Hardware Setup

Components

  • CubeCell 1/2AA Node (HTCC-AB02A): An Arduino-compatible LoRaWAN device with two hardware serial ports
  • BS170 MOSFET: For signal level conversion and inversion
  • RJ11 connector: To connect to the meter’s P1 port
  • DHT22 sensor: For ambient temperature and humidity monitoring

PCB and components before assembly

The Signal Challenge

The P1 port uses inverted 5V serial (5V = logic 0, 0V = logic 1), while the Arduino expects standard 3.3V serial. The BS170 MOSFET elegantly solves both problems:

  • 5V from the meter → 0V (GND) on Arduino
  • 0V from the meter → 3.3V on Arduino (pulled high by GPIO)

The Arduino is powered directly from the meter’s 5V line, and the P1 data request line is held high to enable continuous data output.

Electrical Meter P1 Port          Arduino CubeCell
        5V  ──────────────────────→  VIN (Power)
       GND  ──────────────────────→  GND
      Data  ────┬─────────────────→  Serial1 RX
                │
            BS170 MOSFET
            (Signal Inverter)

DHT22 Sensor
      VCC   ──────────────────────→  3.3V
      GND   ──────────────────────→  GND
      DATA  ──────────────────────→  GPIO9 (with 10kΩ pull-up)

Assembled PCB

FrontBack
PCB assembled, frontPCB assembled, back (DHT22 sensor)

Software Architecture

Parser: Standing on Giants’ Shoulders

The telegram parser is adapted from Maarten Pennings’ eMeterP1port project. It handles the P1 telegram format, including CRC validation.

A typical P1 telegram from a Belgian meter looks like:

/FLU5\253770234_A

0-0:96.1.4(50217)                           // Version
0-0:1.0.0(240904223307S)                    // Timestamp
1-0:1.8.1(000328.928*kWh)                   // Consumption Tariff 1 (Day)
1-0:1.8.2(000312.703*kWh)                   // Consumption Tariff 2 (Night)
1-0:2.8.1(000000.035*kWh)                   // Production Tariff 1
1-0:2.7.0(00.000*kW)                        // Current production
1-0:32.7.0(238.7*V)                         // Voltage L1
1-0:31.7.0(001.21*A)                        // Current L1
!E4C7                                       // CRC

Aggregation: Making Every Byte Count

Since I can only transmit every 15 minutes, I aggregate the 900 samples received during that period:

1
2
3
4
5
6
7
class ElecStatField {
public:
  uint32_t min_value;
  uint32_t max_value;
  uint32_t sum_values;
  uint16_t nb_samples;
};

For each measured value (consumption, production, voltage, current), I track:

  • Minimum: Lowest value in the 15-minute window
  • Maximum: Highest value (critical for voltage monitoring!)
  • Mean: Average calculated from sum/samples
  • Sample count: For data quality verification

We expect 900 samples per period but some can be lost (the Arduino can be busy and unable to read the serial buffer in time). The number of samples per transmission varies between 880 and 900, a reduction can also signal a bad connection.

Over-Voltage Detection (Synergrid C10/11 Compliant)

The key feature for my use case: detecting when voltage exceeds the regulatory thresholds that trigger inverter shutdown.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Synergrid C10/11 / EN 50549-1 thresholds for Belgium (230V nominal)
#define STAGE1_OVERVOLTAGE_DV 2530  // 253.0V - trip after 1.5s sustained
#define STAGE2_OVERVOLTAGE_DV 2645  // 264.5V - instant trip (0.2s)

void update_over_voltage_counter(int ix, uint32_t voltage_dv) {
  if (voltage_dv >= STAGE1_OVERVOLTAGE_DV) {
    over_voltage_counters[ix].total_samples_over_stage1++;
    over_voltage_counters[ix].consecutive_over_stage1++;

    if (over_voltage_counters[ix].consecutive_over_stage1 >
        over_voltage_counters[ix].max_consecutive_over_stage1) {
      over_voltage_counters[ix].max_consecutive_over_stage1 =
        over_voltage_counters[ix].consecutive_over_stage1;
    }
  } else {
    over_voltage_counters[ix].consecutive_over_stage1 = 0;
  }

  if (voltage_dv >= STAGE2_OVERVOLTAGE_DV) {
    over_voltage_counters[ix].total_samples_over_stage2++;
  }
}

The code tracks:

  • Total samples over 253V: How much time was spent in the danger zone
  • Total samples over 264.5V: Instant trip events
  • Maximum consecutive samples over 253V: If ≥2 consecutive samples (1 sample/second), the inverter likely tripped

Temperature and Humidity Monitoring

A DHT22 sensor provides environmental data, useful for correlating electrical behavior with ambient conditions. The readings are encoded in just 2 bytes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uint8_t dht_get_encoded_temperature() {
  // Encode: (temp + 40) * 2 — range: -40°C to +87.5°C, resolution: 0.5°C
  int16_t encoded = (int16_t)((temperature + 40.0f) * 2.0f);
  return (uint8_t)constrain(encoded, 0, 255);
}

uint8_t dht_get_encoded_humidity() {
  // Direct 0-100% with 1% resolution
  return (uint8_t)constrain(humidity + 0.5f, 0, 100);
}

To decode: temperature = (byte / 2.0) - 40.0

LoRaWAN Payload Format (v2)

Every byte matters in LoRaWAN. My 62–68 byte payload packs:

FieldSizeDescription
Frame version4 bitsProtocol version (2)
User button1 bitManual trigger flag
Phases2 bitsNumber of phases (1)
Timestamp32 bitsUnix timestamp (UTC)
Consumption indexes2×32 bitsDay/Night in Wh
Production indexes2×32 bitsDay/Night in Wh
Tariff indicator8 bitsCurrent tariff
Sample count16 bitsData quality
Consumption stats72 bitsMin/Max/Mean (W)
Production stats72 bitsMin/Max/Mean (W)
Voltage stats72 bitsMin/Max/Mean (decivolts)
Current stats72 bitsMin/Max/Mean (centiamps)
Temperature8 bitsDHT22: (val/2)-40 = °C
Humidity8 bitsDHT22: 0–100%
Stage 1 over-voltage count16 bitsSamples >253V (optional*)
Stage 2 over-voltage count16 bitsSamples >264.5V (optional*)
Max consecutive Stage 116 bitsLongest streak (optional*)

*Over-voltage fields are only included when Stage 1 count > 0, saving 6 bytes in normal operation.

Transmission Timing

To avoid all devices in a neighborhood transmitting simultaneously at quarter-hour boundaries, I add a random delay:

1
2
3
4
5
#define LORA_RANDOM_DELAY_TRANSMIT_S_MIN 15
#define LORA_RANDOM_DELAY_TRANSMIT_S_MAX 60

next_transmission_timestamp = elect_timestamp +
    random(LORA_RANDOM_DELAY_TRANSMIT_S_MIN, LORA_RANDOM_DELAY_TRANSMIT_S_MAX);

Timestamp Handling

Belgian meters report time in local format with a timezone indicator (S=Summer/CEST, W=Winter/CET). The conversion handles all edge cases including month/year rollover:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const uint32_t convertP1TimeStringToTimestamp(const char *meterTimestamp) {
  tmElements_t tm;
  tm.Year = year - 1970;
  tm.Month = month;
  tm.Day = day;
  tm.Hour = hour;
  tm.Minute = minute;
  tm.Second = second;

  uint32_t timestamp = (uint32_t)makeTime(tm);

  // Convert to UTC by subtracting timezone offset
  if (timeZone == 'W') {
    timestamp -= 3600;   // CET is UTC+1
  } else if (timeZone == 'S') {
    timestamp -= 7200;   // CEST is UTC+2
  }

  return timestamp;
}

Results and Insights

With the sensor installed on the electrical meter, I can now collect real data:

Sensor installed on the electrical meter

The Grafana dashboard surfaces both index readings and instantaneous consumption:

Consumption readings in Grafana

Index readings in Grafana

With this system deployed, I can:

  1. Analyze consumption patterns: Understand my electricity usage throughout the day and identify peak consumption periods
  2. Track voltage patterns: Monitor grid voltage stability and see when spikes occur
  3. Detect over-heating: The temperature sensor helps detect overheating inside the electrical panel
  4. Be prepared for solar: The system immediately starts tracking production and detecting potential over-voltage issues
  5. Document issues for DSO: If problems occur, I have concrete, timestamped data to share with my distribution system operator

The 15-minute resolution with min/max/mean gives enough detail to understand patterns without overwhelming the LoRaWAN network.

An unexpected but very practical benefit: when I am away on holiday, I can verify that consumption looks normal. A sudden spike would indicate someone entered the house, and a flat zero would signal a power outage: I can then ask someone to check and restore power for the refrigerator.

Lessons Learned

  1. Use correct thresholds: Synergrid C10/11 specifies 253V and 264.5V, not the commonly cited 254.5V and 267V
  2. Track consecutive samples: Total count isn’t enough; you need to know if voltage stayed high continuously
  3. Signal inversion matters: The BS170 MOSFET solution is elegant and reliable
  4. Timestamps are tricky: Convert to UTC immediately to avoid DST edge cases
  5. Compact encoding saves bytes: DHT22 data fits in 2 bytes with acceptable precision
  6. Random delays help: Prevents network congestion at quarter boundaries
  7. Meter data alone isn’t enough with solar: You also need production values from the inverter to get a complete picture of consumption

Future Improvements

  • Add gas meter reading (available on the P1 port) once a smart gas meter is installed
  • Add an ESP32 connected via Wi-Fi to the Huawei inverter, reading production values over Modbus, to combine all readings into a single unified dashboard

Resources