⌛ History
Some time back, in 2019, I published a small blogpost (in Dutch) about QBoxNext
solution and how this was developed and deployed on Azure.
However, since I got a new Smart Meter, this solution did not work anymore so I needed to find a new way of reading data from the Smart Meter and process and store this data.
This blogpost describes the steps I’ve taken and technical challenges encountered when investigating a new solution and building a prototype using .NET.
1️⃣ P1
Here in the Netherlands, all Smart Meters are equipped with a P1 serial port. The port connector type is RJ-12 (“phone connector” with 6 positions and 6 contacts) port and the signal is inverted.
The table below shows the pin-reference:
Pin # | Signal name | Description | Remark |
---|---|---|---|
1 | +5V | +5V power supply | Power supply line |
2 | Data Request | Data Request | Input |
3 | Data GND | Data ground | |
4 | n.c. | Not connected | |
5 | Data Data | Line Output | Open collector |
6 | Power GND | Power ground | Power supply line |
The baud rate for the newer generation Smart Meters is 115200
.
To read data using the P1, you need a USB Serial converter cable. Several ready made cables are available and look like:
2️⃣ Reading data from Serial Port in .NET
There are several solutions available in other programming languages (e.g. Python), but I wanted to build a prototype using in C# .NET because using .NET (Core) should work on Windows and Linux (including Linux ARM which is used by the Raspberry PI 4B, more on that later in this blogpost).
The first step is accessing the serial port (via the USB Serial Device converter) using C#.
There are some custom projects (e.g. RJCP.DLL.SerialPortStream) which I investigated, but in the end I just used the ’new’ System.IO.Ports to open a serial port and receive data.
Get all available serial ports on the system
var portNames = SerialPort.GetPortNames();
Open a port with a specific baud rate
Example code on a Windows system:
var serialPort = new SerialPort("COM5", 115200);
serialPort.Open();
On a Linux system, the same NuGet (System.IO.Ports) can be used, but the port name differs:
var serialPort = new SerialPort("/dev/ttyUSB0", 115200);
serialPort.Open();
Reading data
The data which is sent by the P1 Smart Meter follows a specific protocol (more on that later) where each request line contains data.
And multiple lines combine to a Telegram.
Reading a single line using the SerialPort-class is straightforward, just use the ReadLine()
method:
while (serialPort.ReadLine() is { } line)
{
_logger.LogDebug(line);
if (line.StartsWith('/'))
{
// handle the start from the Telegram
}
if (line.StartsWith('!'))
{
// handle the end from the Telegram
}
}
ℹ Note that the ReadLine()
method actually removes the LF
(\n
) character when the line is read, so when combining all the lines to a valid DSMR Telegram, I need to append an extra \n
to each line.
3️⃣ Dutch Smart Meter Requirements Protocol
The protocol is based on NEN-EN-IEC 62056-21 Mode D.
Data transfer is requested with a request line and automatically initiated every second until request line is released.
Protocol
The protocol contains 3 main parts:
- The start character (
/
) + Smart Meter identification id (e.g.CTA5ZIV-METER
) and this line ends with twoCRLF
-characters. - The Data Lines: multiple COSEM (Companion Specification for Energy Metering) objects are encoded using OBIS (Object Identification System) and each line ends with a
CRLF
-character. - The end character (
!
) + CRC16
The CRC16 value calculated over the preceding characters in the data message (from/
to!
) and is using the polynomial calculation:x16 + x15 + x2 + 1
).
This CRC16 uses no XOR in, no XOR out and is computed with least significant bit first. The value is represented as 4 hexadecimal characters (MSB first).
And this line does also end with theCRLF
-character.
The transfer speed must be a fixed to 115200
baud. And the format of transmitted data must be defined as 8N1
.
Namely:
- 1 start bit
- 8 data bits
- no parity bit
- 1 stop bit
Example message (Telegram) looks like this:
/CTA5ZIV-METER
1-3:0.2.8(50)
0-0:1.0.0(221228194418W)
0-0:96.1.1(**********************************)
1-0:1.8.1(000106.240*kWh)
1-0:1.8.2(000071.890*kWh)
1-0:2.8.1(000000.198*kWh)
1-0:2.8.2(000003.657*kWh)
0-0:96.14.0(0002)
1-0:1.7.0(01.867*kW)
1-0:2.7.0(00.000*kW)
0-0:96.7.21(00015)
0-0:96.7.9(00008)
1-0:99.97.0(1)(0-0:96.7.19)(220420171713S)(0000000000*s)
1-0:32.32.0(00000)
1-0:52.32.0(00000)
1-0:72.32.0(00000)
1-0:32.36.0(00005)
1-0:52.36.0(00003)
1-0:72.36.0(00004)
0-0:96.13.0()
1-0:32.7.0(231.0*V)
1-0:52.7.0(229.0*V)
1-0:72.7.0(230.0*V)
1-0:31.7.0(000*A)
1-0:51.7.0(000*A)
1-0:71.7.0(008*A)
1-0:21.7.0(00.000*kW)
1-0:41.7.0(00.074*kW)
1-0:61.7.0(01.792*kW)
1-0:22.7.0(00.000*kW)
1-0:42.7.0(00.000*kW)
1-0:62.7.0(00.000*kW)
0-1:24.1.0(003)
0-1:96.1.0(**********************************)
0-1:24.2.1(221228194000W)(00097.867*m3)
!4E60
ℹ
Note that in the Telegram displayed above, the equipment identifiers 0-0:96.1.*
are obscured because of privacy reasons.
More details on each OBIS field can be found in the specification.
4️⃣ Parsing the Telegram
For parsing the Telegram (which follows the COSEM definition and is encoded using OBIS) I’ve used a helper library: DSMRParser.Net.
Parse the Telegram (all lines)
var lines = "*** Example Telegram from above ***";
var parser = new DSMRTelegramParser();
if (parser.TryParse(lines, out var telegram))
{
// Do something with the valid Telegram (e.g. trace, store this on disk / DB, etc).
}
else
{
// In case the parsing fails, log this and take action accordingly.
}
For all properties which are defined in the Telegram Model, see the C# class.
5️⃣ Producer - Consumer pattern
In order to separate the reading of the data using the System.IO.Ports
and parsing the Telegram using DSMRParser.Net
I decided to prototype my solution using the Producer - Consumer pattern which really fits in this scenario.
The technology used to implement a Producer and Consumer is System.Threading.Channels.
A very simple implementation from a Producer - Consumer pattern (not related to P1 data or a Telegram) can be like:
class Program
{
static async Task Main(string[] args)
{
var channel = Channel.CreateUnbounded<string>(); // 1
var producer = new Producer(channel.Writer); // 2
var consumer = new Consumer(channel.Reader); // 3
}
}
class Producer
{
private readonly ChannelWriter<string> _channelWriter;
public Producer(ChannelWriter<string> channelWriter)
{
_channelWriter = channelWriter;
}
}
class Consumer
{
private readonly ChannelReader<string> _channelReader;
public Consumer(ChannelReader<string> channelReader)
{
_channelReader = channelReader;
}
}
In the above simple example a main method is defined to show you how the creation of the writer/reader happen 1.
The important things to note are:
- for the Producer, I’ve passed it only a ChannelWriter, so it can only do write operations 2
- for the Consumer, a ChannelReader is passed, so it can only read data 3
However this simple example cannot be used for reading P1 data and parsing Telegrams because the data is constantly send by the P1 Smart Meter and should be processed (read) line by line from the serial port. And when a complete Telegram is found, this Telegram should be verified and parsed and eventually stored (out of scope).
So the required building blocks for this solution are:
- Producer
- Consumer
- Processor (which combines the Producer and the Consumer)
Producer : P1Reader
A class which needs to:
- Read lines from the serial port
- Assemble complete Telegram using these lines
- Use the
ChannelWriter<string>
to write the Telegram to the Channel
Reference implementation for this P1Reader
internal class P1Reader : IP1Reader
{
public async Task StartReadingAsync(CancellationToken cancellationToken = default)
{
var serialPort = _factory.CreateUsingFirstAvailableUSBSerialPort();
serialPort.Open();
await Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested && serialPort.ReadLine() is { } line)
{
// Use a StringBuilder to combine all lines to a Telegram
// If the line starts with '/', clear the StringBuilder
// Else append the line (including an extra '\n') to the StringBuilder
// If the line starts with '!', the Telegram has ended, write the Telegram to the write Channel
}
// Some housekeeping to complete the writer, close and dispose the serialPort.
}, cancellationToken);
}
}
For the complete file, see P1Reader.cs.
Consumer : TelegramParser
A class which needs to:
- Keep reading from the
ChannelReader<string>
Channel - Verify and parse the Telegram
- Forward the valid Telegram to a store (out of scope)
Reference implementation for this TelegramParser
The ReadAllAsync() method used in the reference code, as the name suggests, allows reading all the items from the channel. This returns an IAsyncEnumerable<string>
, which means the items from the channel will be streamed / processed asynchronously.
internal class TelegramParser : ITelegramParser
{
public async Task StartProcessingAsync(CancellationToken cancellationToken)
{
await foreach (var message in _reader.ReadAllAsync(cancellationToken))
{
if (_parserProxy.TryParse(message, out var telegram))
{
// Do something with the valid Telegram (e.g. trace, store this on disk / DB).
}
else
{
// In case the parsing fails, log this and take action accordingly.
}
}
}
}
For the complete file, see TelegramParser.cs.
Combine : Processor
A class which needs to:
- Create in instance of the
P1Reader
(using Dependency Injection) - Create in instance of the
TelegramParser
(using Dependency Injection) - Start reading using the
P1Reader
in a “Async Task - Fire and Forget” way - Also start processing the data using the
TelegramParser
in a “Async Task - Fire and Forget” way
Async Task - Fire and Forget
This means that the main process should start a new Task but should not wait on the result because the P1 port lines should be continuously read and also the data (Telegram) should be parsed continuously. However when exceptions occur (USB port does exists or reading serial data fails) these exceptions should be logged and if required, the total process should be stopped. To implement this requirement I’ve used a NuGet packaged called AsyncAwaitBestPractices. This package contains a useful extension method named SafeFireAndForget.
See example code below how this method is used.
Reference implementation for this Processor
internal class Processor : IProcessor
{
public void Run(CancellationToken cancellationToken)
{
_reader.StartReadingAsync(cancellationToken).SafeFireAndForget(e =>
{
Log.Logger.Fatal(e, "Unable to read.");
throw new Exception("Unable to read.", e);
});
_parser.StartProcessingAsync(cancellationToken).SafeFireAndForget(e =>
{
Log.Logger.Fatal(e, "Unable to parse.");
throw new Exception("Unable to parse.", e);
});
}
}
For the complete class, see Processor.cs.
💻 Demo
Windows
Compiling and running the .NET 7 console application on Windows is straightforward, in the screenshot below you can see that COM5
is the detected USB Serial Device port.
ℹ Note that also the ThreadID is logged, so it’s visible which Consumer Thread handles the data received via the Channel.
WSL (Windows Subsystem for Linux)
Running the example application on WSL needs some more steps because by default the USB (serial) ports are not attached to the WSL instance.
Follow these steps:
Install the latest version from usbipd on your Windows system
To get a list from all the USB devices, run the command:
1
C:\Windows\System32>usbipd list
On my system, the USB Serial Device converter adapter was located at bus id
1-4
.Now attach that specific USB device to the running WSL instance:
1
C:\Windows\System32>usbipd wsl attach --busid=1-4
When all is configured correctly, you can just do a sudo dotnet run
to start the example application.
Note that you need sudo
because the USB port is accessed.
ℹ Note that when you want to run the console application in Windows again, you need to detach the USB port from the WSL instance. See this page for more details.
Linux - ARM (Raspberry PI 4B)
It’s even possible to run the console app on a Raspberry PI 4B. You only need to install .NET SDK on the Raspberry Pi (link).
And when that’s done correctly, just plug-in the USB serial adapter and run the example console application.
My complete example GitHub project can be found here.
💡 Conclusion
This blog post explains some details on the P1 Smart Meter protocol and describes my journey on how to build a prototype console application to read and parse data from the P1 Smart Meter using .NET and implementing the Producer - Consumer pattern using Channels.
❔ Feedback or questions
Do you have any feedback or questions regarding this or one of my other blogs? Feel free to contact me!
📚 Additional resources
- https://github.com/StefH/DSMRParser.Example
- https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf
- https://github.com/RobThree/DSMR.Net
- https://github.com/brminnick/AsyncAwaitBestPractices