What a smart meter is

A smart meter is an electricity meter that records consumption and production in short intervals, stores the readings, and can be read remotely by its owner. In the Netherlands the owner is the distribution system operator (the regional grid operator, such as Liander, Stedin or Enexis): under the Energiewet the grid operator provides, installs and manages the meter, and the customer pays meter rent. The energy supplier, such as the fictional Kestrel Energy on this site, does not own the meter and never talks to it directly.

Dutch smart meters are built to the DSMR (Dutch Smart Meter Requirements), the specifications of Netbeheer Nederland, the association of grid operators. The rules require the meter to register electricity per quarter of an hour and gas per hour. A gas meter does not have its own internet connection: it reports to the electricity meter, which passes its readings on with its own.

This page follows one meter. Lotte Kortenhoeve, a Kestrel customer in Utrecht (C-10003), switched to a dynamic contract (CT-20004, with hourly prices) on 1 July 2026. On the same day her grid operator installed a new DSMR 5 meter with registers for her solar panels. Lotte wants to see her power use live, so she built her own reader for the meter's P1 port: an ESP32 board that sends the data to Home Assistant on a Raspberry Pi 5 in her hallway.

Three identifiers for one meter

People who handle meter data mix up three different numbers. Keep them apart:

Identifier Example What it identifies Who uses it
EAN code 871687400000000017 The connection (the address's link to the grid) Grid operator, suppliers, switching between suppliers
Equipment identifier DEMO000000000017 The physical meter, sent in every P1 telegram Grid operator; anyone reading the P1 port
Internal meter ID M-30004 The meter as a row in Kestrel's own systems Kestrel only

The EAN code is an 18-digit number for each electricity or gas connection. It stays the same when the meter is replaced or the customer moves out, which is why suppliers use it when a customer switches. The equipment identifier changes with every new meter, and Kestrel's internal ID exists only in Kestrel's database. None of the three belongs in a log line or an MQTT topic that many people can read: together with an address they identify a household.

How readings reach the supplier

A smart meter has three data paths, named after the interfaces in DSMR:

  • P3 connects the meter to the grid operator's central system. The meter uses its own telecom module for this: GPRS (2G), CDMA on 450 MHz, or 4G in replacement meters. Netbeheer Nederland reports that about 2.6 million meters with 2G modules are being replaced because the 2G networks are closing (see Wireless Basics). Meters with a CDMA module use the 450 MHz band, where the grid operators' joint venture Utility Connect holds a license for a utility network (see private LTE). The meter does not use the customer's Wi-Fi or the P1 port.
  • P4 is the name in DSMR for the link between the central systems and suppliers or other service providers. It is not a port on the meter.
  • P1 is the customer's own port, described in the rest of this page.

Remote meter reading is the grid operator reading the meter over P3. Under the Energiewet the grid operator collects meter data at most once a day and no finer than one value per quarter of an hour. Readings reach the suppliers through the sector's central systems, run by EDSN (Energie Data Services Nederland). EDSN keeps, among other things, the central register of connections and a central register of daily meter readings, which it has run since 1 October 2020.

A supplier receives monthly readings by default: enough for the monthly overview, the annual bill and a switch of supplier. Daily, hourly and quarter-hour readings go to the supplier only with the customer's consent. Kestrel asks for that consent on dynamic contracts, because those are billed per hour; Lotte gave it when she signed CT-20004. The full Dutch rules, including a draft regulation that could change the consent rule for dynamic contracts, are on Data Governance and Privacy.

The Dutch meter data paths. Top row: the smart meter, owned by the grid operator, sends readings over P3, its own mobile link, at most once a day to the grid operator, which passes them to EDSN's central systems, which pass them to the supplier, such as Kestrel: monthly by default, finer only with consent. Bottom row: the meter's P1 port sends a read-only telegram every second to the customer's own P1 reader, an ESP32, which sends it to Home Assistant on a Raspberry Pi 5. That data stays at home. A dashed path shows the optional Kestrel Home pilot, which receives live power only with the customer's consent. Sector path (as of October 2026) Smart meter grid operator's Grid operator collects, validates EDSN central systems Supplier such as Kestrel P3, own mobile link, at most once a day P4 to suppliers monthly by default, finer with consent P1: read-only, every second P1 reader ESP32 board Home Assistant Raspberry Pi 5 the customer's data stays at home Kestrel Home pilot, opt-in pilot dongle: live power only, only with consent
Readings reach a supplier through the grid operator and EDSN, never from the meter directly. The P1 port feeds only devices in the customer's home.

The meter also keeps a short history of its own. Grid operator Liander's meter manual lists daily readings at midnight for 40 days, and quarter-hour electricity and hourly gas values for 10 days. So a missed daily collection can be caught up the next day, but not after weeks.

The simplified model elsewhere on this site

Elsewhere on this site Kestrel's smart meters "send a reading to meter-api every 15 minutes". That is a simplified teaching model. In the Netherlands a smart meter reports to its grid operator, at most once a day, over its own mobile connection; a supplier like Kestrel receives the readings from there, monthly by default and in quarter-hour detail only with the customer's consent. In the simplified model, meter-api stands for Kestrel's intake of those readings, and the "meter gateways" some pages mention belong to the same simplification.

The P1 port

The P1 port is the customer interface of a Dutch smart meter: a socket on the meter that sends the meter's readings and live values as text, so that the customer can connect a display, an energy monitor or home automation. It is read-only by design. The DSMR 5.0.2 P1 Companion Standard (26 February 2016, still the current P1 specification as of October 2026) says that it must not be possible to influence the meter in any way through P1, apart from requesting and receiving data and drawing 5 V power. In the Netherlands the port is always on and the data is not encrypted: no activation, no key.

Connector and pinout

The connector is an RJ12: six positions, six contacts (a telephone RJ11 plug has six positions but usually only four contacts, so not every "telephone cable" works).

Pin Signal What it does
1 +5 V Power for the device: up to 250 mA continuously in DSMR 5 (DSMR 4.2.2 meters: 100 mA)
2 Data Request Input: pull it high (4.0–5.5 V, 4–10 mA) and the meter starts sending; leave it open to stop. Never drive it low.
3 Data GND Ground for the data lines
4 n.c. Not connected
5 Data Output: open collector, logically inverted
6 Power GND Ground for the +5 V supply

Signal levels: inverted, open collector

The Data pin is an open-collector output behind an opto-coupler: the meter can only pull the line low, so the reader must provide a pull-up resistor (see GPIO, PWM and Interrupts and Bus Protocols for pull-ups and open-drain outputs). The signal is also logically inverted compared with a normal UART: a "1" (mark) is below 1 V and a "0" (space) is high. A serial line is at mark when it is idle, so the P1 data line idles low, and with a 5 V pull-up its high pulses go above 4 V.

That has two consequences for a 3.3 V board such as the ESP32:

  • Pull the Data line up to 3.3 V, not 5 V, or put a transistor or level shifter in between. An ESP32 pin is not 5 V tolerant; a 5 V pull-up straight into a GPIO can damage it.
  • Invert the received signal, in software or with a transistor. MicroPython's UART on the ESP32 has invert=UART.INV_RX; ESPHome has inverted: true on the receive pin.

Data Request needs 4.0–5.5 V at 4–10 mA, which a 3.3 V GPIO cannot supply. Most do-it-yourself readers simply tie it to the +5 V pin, so the meter sends all the time, or switch it with a transistor.

Typical wiring of a do-it-yourself P1 reader. P1 pin 1, +5 V, goes to the ESP32 board's 5 V input. Pin 2, Data Request, is tied to +5 V so the meter keeps sending. Pins 3 and 6, the two grounds, go to the board's ground. Pin 4 is not connected. Pin 5, Data, goes to a GPIO used as the UART receive pin, with a pull-up resistor to the board's 3.3 V output; the signal is inverted in software. P1 port (RJ12, on the meter) ESP32 board 1 +5 V 2 Data Request 3 Data GND 4 n.c. 5 Data 6 Power GND 5V in 3V3 out GPIO16 (RX) GND tied high pull-up to 3.3 V idle low, invert in software illustrative: check your board's documentation
A common do-it-yourself layout. The pull-up goes to 3.3 V because the ESP32's pins are not 5 V tolerant; the inversion is done by the UART.

Serial settings and timing

DSMR 4 and 5 meters send at 115200 baud, 8N1 (8 data bits, no parity, 1 stop bit). Older DSMR 2.2 and 3.0 meters are generally documented as using 9600 baud with 7 data bits and even parity, and their telegrams have no CRC. A DSMR 5 meter sends a telegram every second while Data Request is high; a DSMR 4.2.2 meter sends every ten seconds. The example telegram below is 631 bytes; at 115200 baud with 10 bits per byte it takes about 55 ms on the wire (the calculation is on Bus Protocols).

Lotte's first reader ran MicroPython on the ESP32. The UART setup is short; this block is not run here, because the machine module exists only on the board (the full reader is on Firmware and Embedded Programming):

from machine import UART

# P1: 115200 baud, 8N1, inverted; pin number as wired above
uart = UART(1, baudrate=115200, bits=8, parity=None, stop=1,
            rx=16, invert=UART.INV_RX)

A port supplies one device. DSMR 5.0.2 expects a single device on P1; to connect more, such as Lotte's own reader and a second dongle, you need a P1 hub or repeater (often sold as an active P1 splitter) that copies the data to several outputs.

After P1: DSMR 6 and X1

The next generation of meters replaces P1 with an interface called X1. In the draft DSMR 6 X1 Companion Standard (version 6.0.0, 17 December 2025; still a draft as of October 2026) it is a wired RJ45 Ethernet port with an HTTP and JSON or WebSocket interface, a token per connected device, at most one live message per second, and signed 15-minute values. It can still deliver DSMR 5.0.2 telegrams for existing devices, and the grid operator can switch it off remotely. Meters with P1 will be in homes for many years, so P1 readers stay relevant.

Anatomy of a telegram

A P1 telegram is one complete message from the P1 port: a header line that starts with "/", an empty line, one line per data object, and a last line with "!" and a four-digit hexadecimal CRC. Every line ends with CR LF (carriage return, line feed). The content is plain ASCII.

This is a telegram as Lotte's meter could send it at 19:30:15 on Wednesday 7 October 2026. The header and the meter identifiers are invented for this site; the format, the field widths and the CRC are correct for DSMR 5.0.2. Do not use the example telegram printed in the specification itself as a test: its printed CRC does not match its content, and neither does the one in DSMR 4.2.2.

/XYZ5\2DEMO-P1-METER

1-3:0.2.8(50)
0-0:1.0.0(261007193015S)
0-0:96.1.1(44454D4F303030303030303030303137)
1-0:1.8.1(000412.338*kWh)
1-0:1.8.2(000467.905*kWh)
1-0:2.8.1(000061.214*kWh)
1-0:2.8.2(000598.770*kWh)
0-0:96.14.0(0002)
1-0:1.7.0(01.234*kW)
1-0:2.7.0(00.000*kW)
0-0:96.7.21(00002)
0-0:96.7.9(00001)
1-0:99.97.0(1)(0-0:96.7.19)(260812021511S)(0000000397*s)
1-0:32.32.0(00000)
1-0:32.36.0(00000)
0-0:96.13.0()
1-0:32.7.0(231.4*V)
1-0:31.7.0(005*A)
1-0:21.7.0(01.234*kW)
1-0:22.7.0(00.000*kW)
0-1:24.1.0(003)
0-1:96.1.0(44454D4F474153303030303030303137)
0-1:24.2.1(261007192500S)(00092.418*m3)
!F1F4

OBIS codes

Each data line starts with an OBIS code (Object Identification System, from the international IEC 62056 metering standards): an address such as 1-0:1.8.1 that says what the value is. The values follow in brackets, a number with its unit after an asterisk. Roughly: 1-0 is electricity, 0-1 is the first device on the meter's M-Bus (here the gas meter), 1.8 is energy delivered to the customer and 2.8 energy delivered by the customer.

OBIS code Meaning Value in the example
1-3:0.2.8 P1 version 50 (DSMR 5.0)
0-0:1.0.0 Time of the telegram 2026-10-07 19:30:15, summer time
0-0:96.1.1 Equipment identifier, as hexadecimal ASCII DEMO000000000017
1-0:1.8.1 / 1.8.2 Energy delivered to the customer, tariff 1 / 2 412.338 / 467.905 kWh
1-0:2.8.1 / 2.8.2 Energy delivered by the customer (solar), tariff 1 / 2 61.214 / 598.770 kWh
0-0:96.14.0 Tariff in use now 0002: tariff 2
1-0:1.7.0 / 2.7.0 Power now, imported / exported (1 W resolution) 1.234 / 0.000 kW
0-0:96.7.21 / 96.7.9 Number of power failures / long power failures 2 / 1
1-0:99.97.0 Log of long power failures: end time and duration ended 12 Aug 2026 02:15:11, lasted 397 s
1-0:32.32.0 / 32.36.0 Voltage sags / swells on phase L1 0 / 0
0-0:96.13.0 Text message from the grid operator (empty)
1-0:32.7.0 / 31.7.0 Voltage / current on L1 (current in whole amperes) 231.4 V / 5 A
1-0:21.7.0 / 22.7.0 Power imported / exported on L1 1.234 / 0.000 kW
0-1:24.1.0 / 96.1.0 M-Bus device type (003 = gas) / its identifier gas meter DEMOGAS000000017
0-1:24.2.1 Last gas reading, with its own time 92.418 m³ at 19:25:00

Lotte's meter is single-phase, so it sends only the L1 lines; a three-phase meter adds the same values for L2 and L3. DSMR does not fix the number or the order of the objects, so a parser must look values up by OBIS code, never by line number.

Timestamps and gas

Timestamps have the form YYMMDDhhmmssX, where X is S for summer time (UTC+2) or W for winter time (UTC+1). The letter tells summer from winter time, which matters when the clocks go back: on 25 October 2026 the hour from 02:00 to 03:00 happens twice, and only the letter separates the two (see time zones and daylight saving time). The gas line carries its own timestamp because the gas value is not live: a DSMR 5 meter repeats the last 5-minute gas value every second (DSMR 4 sent the last hourly value). A power failure log entry is stamped with the end of the failure.

Two numbers that look odd are fine. The power, 1.234 kW, is the sum of everything running in the house at that second. The current, 5 A, is 1234 W divided by 231.4 V (5.33 A) rounded down to whole amperes, the only precision the format allows.

Checking the CRC

The last line carries a cyclic redundancy check. DSMR defines it as a CRC16 over all characters from "/" up to and including "!", with the polynomial x16 + x15 + x2 + 1, computed least significant bit first, with no XOR at the start or the end, written as four hexadecimal characters, most significant first. That matches the variant CRC catalogs call CRC-16/ARC: reflected polynomial 0xA001 and a start value of 0. (The specification does not state the start value; 0 is the value that reproduces the CRCs of real meters.) The CRC page explains how the calculation works.

TELEGRAM = (
    "/XYZ5\\2DEMO-P1-METER\r\n"
    "\r\n"
    "1-3:0.2.8(50)\r\n"
    "0-0:1.0.0(261007193015S)\r\n"
    "0-0:96.1.1(44454D4F303030303030303030303137)\r\n"
    "1-0:1.8.1(000412.338*kWh)\r\n"
    "1-0:1.8.2(000467.905*kWh)\r\n"
    "1-0:2.8.1(000061.214*kWh)\r\n"
    "1-0:2.8.2(000598.770*kWh)\r\n"
    "0-0:96.14.0(0002)\r\n"
    "1-0:1.7.0(01.234*kW)\r\n"
    "1-0:2.7.0(00.000*kW)\r\n"
    "0-0:96.7.21(00002)\r\n"
    "0-0:96.7.9(00001)\r\n"
    "1-0:99.97.0(1)(0-0:96.7.19)(260812021511S)(0000000397*s)\r\n"
    "1-0:32.32.0(00000)\r\n"
    "1-0:32.36.0(00000)\r\n"
    "0-0:96.13.0()\r\n"
    "1-0:32.7.0(231.4*V)\r\n"
    "1-0:31.7.0(005*A)\r\n"
    "1-0:21.7.0(01.234*kW)\r\n"
    "1-0:22.7.0(00.000*kW)\r\n"
    "0-1:24.1.0(003)\r\n"
    "0-1:96.1.0(44454D4F474153303030303030303137)\r\n"
    "0-1:24.2.1(261007192500S)(00092.418*m3)\r\n"
    "!F1F4\r\n"
)


def crc16_arc(data: bytes) -> int:
    """CRC-16/ARC: reflected polynomial 0xA001, start value 0, no final XOR."""
    crc = 0x0000
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc


def check(telegram: str) -> bool:
    end = telegram.index("!") + 1          # the CRC covers "/" up to and including "!"
    computed = crc16_arc(telegram[:end].encode("ascii"))
    sent = int(telegram[end:end + 4], 16)  # four hex digits, most significant first
    print(f"sent {sent:04X}, computed {computed:04X}")
    return computed == sent


print(check(TELEGRAM))
print(check(TELEGRAM.replace("01.234*kW", "01.284*kW", 1)))
sent F1F4, computed F1F4
True
sent F1F4, computed FDDD
False

The second check changes one digit of the power value, 1.234 kW to 1.284 kW, and the CRC no longer matches. A reader that skips the check would happily show 1,284 W. A CRC catches noise on the line, a loose contact or a byte lost because the reader was too slow. It does not protect against a deliberate change: anyone who alters a telegram can recompute the CRC, because there is no secret involved (see accidents versus attacks).

The same check in C

On the device itself the check is usually written in C. The function is the same loop; the standard way to test any CRC implementation is its check value, the CRC of the ASCII string "123456789", which catalogs list as BB3D for CRC-16/ARC.

#include <stdint.h>
#include <stdio.h>
#include <string.h>

/* CRC-16/ARC as a P1 reader's firmware would compute it:
   reflected polynomial 0xA001, start value 0, no final XOR. */
static uint16_t crc16_arc(const uint8_t *data, size_t length)
{
    uint16_t crc = 0x0000;
    for (size_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (int bit = 0; bit < 8; bit++) {
            if (crc & 1u) {
                crc = (uint16_t)((crc >> 1) ^ 0xA001u);
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

int main(void)
{
    const char *check = "123456789";   /* the standard check string */
    uint16_t crc = crc16_arc((const uint8_t *)check, strlen(check));
    printf("CRC-16/ARC(\"%s\") = %04X\n", check, crc);
    printf("matches the published check value BB3D: %s\n",
           crc == 0xBB3D ? "yes" : "no");
    return 0;
}
CRC-16/ARC("123456789") = BB3D
matches the published check value BB3D: yes

Parsing a telegram

Once the CRC is right, parsing is mostly text handling. A regular expression splits each data line into its OBIS code and its bracketed values; values become Decimal numbers so that 412.338 kWh stays exactly 412.338; timestamps get the right UTC offset from their S or W. The equipment identifier is hexadecimal ASCII: 44454D4F…3137 decodes to "DEMO000000000017".

TELEGRAM = (
    "/XYZ5\\2DEMO-P1-METER\r\n"
    "\r\n"
    "1-3:0.2.8(50)\r\n"
    "0-0:1.0.0(261007193015S)\r\n"
    "0-0:96.1.1(44454D4F303030303030303030303137)\r\n"
    "1-0:1.8.1(000412.338*kWh)\r\n"
    "1-0:1.8.2(000467.905*kWh)\r\n"
    "1-0:2.8.1(000061.214*kWh)\r\n"
    "1-0:2.8.2(000598.770*kWh)\r\n"
    "0-0:96.14.0(0002)\r\n"
    "1-0:1.7.0(01.234*kW)\r\n"
    "1-0:2.7.0(00.000*kW)\r\n"
    "0-0:96.7.21(00002)\r\n"
    "0-0:96.7.9(00001)\r\n"
    "1-0:99.97.0(1)(0-0:96.7.19)(260812021511S)(0000000397*s)\r\n"
    "1-0:32.32.0(00000)\r\n"
    "1-0:32.36.0(00000)\r\n"
    "0-0:96.13.0()\r\n"
    "1-0:32.7.0(231.4*V)\r\n"
    "1-0:31.7.0(005*A)\r\n"
    "1-0:21.7.0(01.234*kW)\r\n"
    "1-0:22.7.0(00.000*kW)\r\n"
    "0-1:24.1.0(003)\r\n"
    "0-1:96.1.0(44454D4F474153303030303030303137)\r\n"
    "0-1:24.2.1(261007192500S)(00092.418*m3)\r\n"
    "!F1F4\r\n"
)
import re
from datetime import datetime, timedelta, timezone
from decimal import Decimal

LINE = re.compile(r"^(\d+-\d+:\d+\.\d+\.\d+)((?:\([^)]*\))+)$")
SUMMER = timezone(timedelta(hours=2), "CEST")
WINTER = timezone(timedelta(hours=1), "CET")


def parse(telegram: str) -> dict:
    """Returns {OBIS code: [values in brackets]} for every data line."""
    data = telegram[: telegram.index("!")]
    objects = {}
    for line in data.split("\r\n")[2:]:     # skip the header and the empty line
        match = LINE.match(line)
        if match:
            objects[match[1]] = re.findall(r"\(([^)]*)\)", match[2])
    return objects


def number(text: str) -> Decimal:
    return Decimal(text.split("*")[0])      # "000412.338*kWh" -> 412.338


def timestamp(text: str) -> datetime:
    zone = SUMMER if text.endswith("S") else WINTER   # YYMMDDhhmmss + S or W
    return datetime.strptime(text[:-1], "%y%m%d%H%M%S").replace(tzinfo=zone)


p1 = parse(TELEGRAM)
imported = number(p1["1-0:1.8.1"][0]) + number(p1["1-0:1.8.2"][0])
exported = number(p1["1-0:2.8.1"][0]) + number(p1["1-0:2.8.2"][0])
net_kw = number(p1["1-0:1.7.0"][0]) - number(p1["1-0:2.7.0"][0])
gas_time, gas_value = p1["0-1:24.2.1"]
tariff = {"0001": "1 (low)", "0002": "2 (normal)"}[p1["0-0:96.14.0"][0]]

print("meter      ", bytes.fromhex(p1["0-0:96.1.1"][0]).decode("ascii"))
print("time       ", timestamp(p1["0-0:1.0.0"][0]).isoformat())
print("tariff     ", tariff)
print("imported   ", imported, "kWh")
print("exported   ", exported, "kWh")
print("power now  ", int(net_kw * 1000), "W", "import" if net_kw >= 0 else "export")
print("gas        ", number(gas_value), "m3 at", timestamp(gas_time).strftime("%H:%M %Z"))
meter       DEMO000000000017
time        2026-10-07T19:30:15+02:00
tariff      2 (normal)
imported    880.243 kWh
exported    659.984 kWh
power now   1234 W import
gas         92.418 m3 at 19:25 CEST

Imported energy is the sum of both tariff registers (412.338 + 467.905 = 880.243 kWh). Net power is imported minus exported power: at 19:30 Lotte's panels produce nothing, so everything comes from the grid. The registers count up forever; to know the energy used in a quarter of an hour, subtract two readings (see meter data for the units, and IoT Data: Time Series and Streams for working with many readings).

Be strict about content, tolerant about format

In September 2026 Kestrel started the Kestrel Home pilot: about 250 households on dynamic contracts got a Kestrel-branded P1 dongle that sends their live power to Kestrel, with their explicit consent. Lotte joined as pilot home h0017, using an active P1 splitter (a powered P1 hub) so that her own reader keeps working. On 24 September 2026 the service desk passed on a call that became BUG-1060: one pilot dongle rejected every single telegram. The meter in that home sent its CRC as three digits, B9F instead of 0B9F, and the dongle's parser accepted exactly four. This is a real quirk: the open-source dsmr_parser project keeps a telegram from such a meter among its test cases. The specification says four characters, but meters are not always right.

import re


def sent_crc_strict(tail: str) -> int:
    """The first version: exactly four hex digits, then CR LF."""
    if not re.fullmatch(r"[0-9A-F]{4}\r\n", tail):
        raise ValueError(f"bad CRC field {tail!r}")
    return int(tail[:4], 16)


def sent_crc(tail: str) -> int:
    """The fix: one to four hex digits, then CR LF."""
    match = re.fullmatch(r"([0-9A-Fa-f]{1,4})\r\n", tail)
    if not match:
        raise ValueError(f"bad CRC field {tail!r}")
    return int(match[1], 16)


for tail in ["F1F4\r\n", "0B9F\r\n", "B9F\r\n"]:
    try:
        old = f"{sent_crc_strict(tail):04X}"
    except ValueError:
        old = "rejected"
    print(f"{tail.strip():5} old: {old:9} new: {sent_crc(tail):04X}")
F1F4  old: F1F4      new: F1F4
0B9F  old: 0B9F      new: 0B9F
B9F   old: rejected  new: 0B9F

The fix accepts one to four hexadecimal digits and still rejects anything else. That is the balance a parser for device data needs: tolerate harmless format quirks of real devices, but never accept a telegram whose content cannot be verified. The parser is also the part of the dongle that reads untrusted input, which makes it a good candidate for fuzzing: truncated telegrams, missing brackets, a value of 10,000 digits.

Troubleshooting a P1 reader

Kestrel's service desk handles pilot calls with KB-0610, "Connecting the Kestrel Home dongle to the P1 port". The same symptoms appear with every P1 reader, bought or home-made:

Symptom Likely cause What to check or do
Nothing arrives at all Data Request not high; wrong cable; no pull-up on Data Tie Data Request to +5 V; use a six-contact RJ12 cable; check the pull-up. A multimeter on pin 1 should show about 5 V.
Random characters instead of text Signal not inverted, or wrong speed Invert the receive pin; use 115200 8N1 (9600 7E1 only for DSMR 2.2/3.0 meters)
Readable text, but many CRC errors Receive buffer smaller than a telegram, or the program reads too slowly and loses bytes Make the buffer larger than the longest telegram; read continuously; log the CRC failure rate
Values only every 10 seconds A DSMR 4 meter Nothing is wrong; DSMR 5 sends every second
The board resets when Wi-Fi starts Not enough power from P1 (100 mA on DSMR 4.2.2) Power the board from USB on older meters
The second device on the port stops working The port is meant for one device Use a P1 hub or repeater, not a passive cable splitter
Every telegram rejected on one meter only A format quirk, such as BUG-1060's short CRC Log the raw telegram and compare it with the parser's rules

Privacy and the P1 port

One-second data says more than the quarter-hour values the rules are written for. In Lotte's telegrams you can see the moment the kettle goes on, how long the oven runs and when the house goes quiet at night. That is fine while the data stays in her home, which is where P1 data goes by default: the port does not send anything anywhere by itself, and her Home Assistant server is on her own network.

It changes as soon as a reader sends data out of the house. A cloud-connected P1 dongle makes its vendor a recipient of personal data that is more detailed than anything the supplier receives through the official path. For the Kestrel Home pilot that meant:

  • Explicit consent for the pilot, separate from the consent for quarter-hour readings on the dynamic contract.
  • Only live power leaves the home, not the whole telegram with its meter identifiers and registers.
  • Topics and logs use the pilot home number (kestrel/home/h0017/p1/power), never the EAN code or the customer number (see MQTT and IoT Messaging).
  • A way out: leaving the pilot stops the data and deletes what was collected (see IoT Security).

There is also a physical side. P1 is unencrypted and needs no login, so anyone who can plug into the port can read it. In a house that is the household; in a shared meter room in an apartment building it may be anyone with the key. The data protection rules for meter data, including retention, are on Data Governance and Privacy and are not repeated here.