esphome interface for DSMR P1 data port
Many modern Smart Meters have a "P1 Port" that provides a way to retrieve information about the power and gas usage. There are devices for smart homes that plug into the port, or you can build your own with an esp32 and the esphome firmware.
Schematic
The DSMR / P1 port specification is fairly straighforward. It is a plaintext protocol with relatively low speed serial signalling and a common modular jack for interfacing. The P1 port has an RJ12 with six pins, although it can also be connected with an RJ11 four pin if you provide external power for your device.
- +5V (Unused, not present on RJ11)
- Request (Black)
- Data GND (Red)
- No connection (Green, Unused)
- Data out (Yellow)
- Power ground (Unused, not present on RJ11)
When the Request pin is pulled up to 5V, the device will output a measurement continuously. This model had SMR5.0 firmware, which does so at 1 Hz. The data output is open collector, so a pull-up resistor is necessary to make a voltage appear on the input. This does not appear in the documentation, so it required some probing to figure out.
The board I'm using is a "TTGO Display", which has an esp32 and a 240x135 OLED. The pullup and filter cap fit tightly on the board. Note that with my RJ11 cable the +5v request signal is on the BLACK wire and the ground is on the RED wire. I've also wired a separate +5v supply to a USB charger; if you have a 6-pin cable you might be able to draw enough power from the smart meter itself.
+------------------------------+ +-------------------+
| | | |
Request ----------| +5V GPIO23 |--------| Reset |
| | | ST7789v |
| GPIO16 |--------| DC |
+----| +3.3V | | |
| | GPIO4 |--------| Backlight |
330 > | ESP32 | | |
Ohms < | TTGO Display GPIO5 |--------| CS |
| | | | 240x135 |
Data Out ----+----| GPIO13 GPIO18 |--------| CLK OLED |
| | | | |
| | GPIO19 |--------| MOSI |
10nf = | | | |
| | | +-------------------+
Data Ground -+----| Ground |
| |
+------------------------------+
esphome yaml
This is derived from Marcel Zuidwijk's "ESPHome powered P1 meter", and modified to draw the graph on the OLED. The main difference is that the esp32 includes a built-in inverter in the UART hardware, so the external inverter in Marcel's design is not required, although I did require a simple capacitor signal filter due to noise on the data line.
esphome:
name: p1meter
platform: ESP32
board: featheresp32
includes:
- dsmr_p1_sensor.h
libraries:
- "Dsmr"
wifi:
ssid: "SSID-GOES-HERE"
password: "PASSWORD-GOES-HERE"
# Enable logging during development, turn off when done
#logger:
# Do NOT Enable Home Assistant API
#api:
# Do enable OTA updates
ota:
# talk to mqtt broker for grafana updates; replace with your mqtt broker hostname
mqtt:
broker: dashboard
time:
- platform: sntp
id: ntp
# the P1 meters output inverted data as long as the request pin is tied high
# there is no tx pin; the meter outputs a measurement once per second
uart:
- rx_pin: 13
invert: true
baud_rate: 115200
id: p1_uart
sensor:
- platform: custom
lambda: |-
auto dsmr_p1_sensor = new CustomP1UartComponent(id(p1_uart));
App.register_component(dsmr_p1_sensor);
return {
dsmr_p1_sensor->s_energy_delivered_tariff1,
dsmr_p1_sensor->s_energy_delivered_tariff2,
dsmr_p1_sensor->s_energy_returned_tariff1,
dsmr_p1_sensor->s_energy_returned_tariff2,
dsmr_p1_sensor->s_power_delivered,
dsmr_p1_sensor->s_power_returned,
dsmr_p1_sensor->s_voltage_l1,
dsmr_p1_sensor->s_voltage_l2,
dsmr_p1_sensor->s_voltage_l3,
dsmr_p1_sensor->s_current_l1,
dsmr_p1_sensor->s_current_l2,
dsmr_p1_sensor->s_current_l3,
dsmr_p1_sensor->s_power_delivered_l1,
dsmr_p1_sensor->s_power_delivered_l2,
dsmr_p1_sensor->s_power_delivered_l3,
dsmr_p1_sensor->s_power_returned_l1,
dsmr_p1_sensor->s_power_returned_l2,
dsmr_p1_sensor->s_power_returned_l3,
dsmr_p1_sensor->s_gas_device_type,
dsmr_p1_sensor->s_gas_valve_position,
dsmr_p1_sensor->s_gas_delivered,
};
sensors:
- name: "Consumption Low Tarif Sensor"
unit_of_measurement: kWh
accuracy_decimals: 3
- name: "Consumption High Tarif Sensor"
unit_of_measurement: kWh
accuracy_decimals: 3
- name: "Return Low Tarif Sensor"
unit_of_measurement: kWh
accuracy_decimals: 3
- name: "Return High Tarif Sensor"
unit_of_measurement: kWh
accuracy_decimals: 3
- name: "Actual Consumption Sensor"
id: actual_consumption_sensor
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Actual Delivery Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Instant Voltage L1 Sensor"
unit_of_measurement: V
accuracy_decimals: 3
- name: "Instant Voltage L2 Sensor"
unit_of_measurement: V
accuracy_decimals: 3
- name: "Instant Voltage L3 Sensor"
unit_of_measurement: V
accuracy_decimals: 3
- name: "Instant Current L1 Sensor"
unit_of_measurement: A
accuracy_decimals: 3
- name: "Instant Current L2 Sensor"
unit_of_measurement: A
accuracy_decimals: 3
- name: "Instant Current L3 Sensor"
unit_of_measurement: A
accuracy_decimals: 3
- name: "Power Delivered L1 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Power Delivered L2 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Power Delivered L3 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Power Returned L1 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Power Returned L2 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Power Returned L3 Sensor"
unit_of_measurement: W
accuracy_decimals: 3
filters:
- multiply: 1000
- name: "Gas device type Sensor"
- name: "Gas valve position Sensor"
- name: "Gas Meter M3 Sensor"
unit_of_measurement: m3
accuracy_decimals: 3
# Display pins
spi:
clk_pin: GPIO18
mosi_pin: GPIO19
color:
- id: my_white
red: 100%
green: 100%
blue: 100%
- id: my_gray
red: 50%
green: 50%
blue: 50%
- id: my_gray2
red: 60%
green: 60%
blue: 60%
- id: my_blue
red: 40%
green: 10%
blue: 100%
- id: my_red
red: 100%
green: 40%
blue: 0%
font:
- file: 'IBMPlexMono-Bold.ttf'
id: din_big
size: 130
- file: 'IBMPlexMono-Bold.ttf'
id: din_med
size: 65
- file: 'DinBold.ttf'
id: din_small
size: 22
# ttgo uses a 240x135 OLED
display:
- platform: st7789v
backlight_pin: GPIO4
cs_pin: GPIO5
dc_pin: GPIO16
reset_pin: GPIO23
rotation: 270
update_interval: 1s
lambda: |-
const int max_y = 4000;
const int min_y = 0;
const int y_res = 135;
const int num_samples = 240;
static int watts[num_samples];
static int sample;
const float current = id(actual_consumption_sensor).state;
if (!isnan(current))
{
// store the sample in forward order
watts[sample] = current;
sample = (sample + 1) % num_samples;
// draw a bargraph of all of the samples, starting at the
// next one, which is now the oldest one in the data set
for(int x = 0 ; x < num_samples; x++)
{
int y = watts[(sample + x) % num_samples];
y = ((y - min_y) * y_res) / (max_y - min_y);
if (y < 0) y = 0;
if (y >= y_res) y = y_res - 1;
it.line(x, y_res-1, x, y_res - y, id(my_blue));
}
// print the current power overtop the bargraph
int w = current;
it.printf(0, 135, id(din_big), id(my_white), TextAlign::BOTTOM_LEFT, "%1d", w / 1000);
it.printf(85, 70, id(din_med), id(my_white), TextAlign::BOTTOM_LEFT, "%03d", w % 1000);
} else {
it.printf(0, 125, id(din_big), id(my_white), TextAlign::BOTTOM_LEFT, "INVALID");
}
it.printf(90, 80, id(din_small), id(my_gray), TextAlign::BOTTOM_LEFT, "kilowatts");
// and the time etc
it.strftime(0, 135, id(din_small), id(my_gray2), TextAlign::BOTTOM_LEFT, "%Y-%m-%d %A %H:%M:%S", id(ntp).now());
Custom UART component
This is derived from github.com/nldroid/CustomP1UartComponent
:
#include "esphome.h"
#include "dsmr.h"
using namespace esphome;
#define P1_MAXTELEGRAMLENGTH 1500
#define DELAY_MS 60000 // Delay in miliseconds before reading another telegram
#define WAIT_FOR_DATA_MS 2000
// Use data structure according to: https://github.com/matthijskooijman/arduino-dsmr
using MyData = ParsedData <
/* FixedValue */ energy_delivered_tariff1,
/* FixedValue */ energy_delivered_tariff2,
/* FixedValue */ energy_returned_tariff1,
/* FixedValue */ energy_returned_tariff2,
/* 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,
/* uint16_t */ gas_device_type,
/* uint8_t */ gas_valve_position,
/* TimestampedFixedValue */ gas_delivered
>;
class CustomP1UartComponent : public Component, public uart::UARTDevice {
protected:
char telegram[P1_MAXTELEGRAMLENGTH];
int telegramlen;
bool headerfound;
bool footerfound;
unsigned long lastread;
int bytes_read;
bool read_message() {
//ESP_LOGD("DmsrCustom","Read message");
headerfound = false;
footerfound = false;
telegramlen = 0;
bytes_read = 0;
unsigned long currentMillis = millis();
unsigned long previousMillis = currentMillis;
if (!available())
return false;
// Messages come in batches. Read until footer.
while (!footerfound && currentMillis - previousMillis < 5000) { // Loop while there's no footer found with a maximum of 5 seconds
currentMillis = millis();
// Loop while there's data to read
while (available()) { // Loop while there's data
if (telegramlen >= P1_MAXTELEGRAMLENGTH) { // Buffer overflow
headerfound = false;
footerfound = false;
ESP_LOGD("DmsrCustom","Error: Message larger than buffer");
}
const char c = read();
//Serial.print(c, HEX);
Serial.print(c);
bytes_read++;
if (c == '/') { // header: forward slash
// ESP_LOGD("DmsrCustom","Header found");
Serial.println("----");
headerfound = true;
telegramlen = 0;
}
if (!headerfound)
continue;
telegram[telegramlen] = c;
telegramlen++;
if (c == '!') { // footer: exclamation mark
footerfound = true;
continue;
}
// read until the last \n after footer
if (!footerfound || c != '\n')
continue;
// Parse message
MyData data;
Serial.println("+++");
// ESP_LOGD("DmsrCustom","Trying to parse");
// Parse telegram accoring to data definition. Ignore unknown values.
ParseResult<void> res = P1Parser::parse(&data, telegram, telegramlen, false);
if (!res.err) {
publish_sensors(data);
return true; // break out function
}
// Parsing error, show it
Serial.println(res.fullError(telegram, telegram + telegramlen));
}
}
return false;
}
void publish_sensors(MyData data){
if(data.energy_delivered_tariff1_present)s_energy_delivered_tariff1->publish_state(data.energy_delivered_tariff1);
if(data.energy_delivered_tariff2_present)s_energy_delivered_tariff2->publish_state(data.energy_delivered_tariff2);
if(data.energy_returned_tariff1_present)s_energy_returned_tariff1->publish_state(data.energy_returned_tariff1);
if(data.energy_returned_tariff2_present)s_energy_returned_tariff2->publish_state(data.energy_returned_tariff2);
if(data.power_delivered_present)s_power_delivered->publish_state(data.power_delivered);
if(data.power_returned_present)s_power_returned->publish_state(data.power_returned);
if(data.voltage_l1_present)s_voltage_l1->publish_state(data.voltage_l1);
if(data.voltage_l2_present)s_voltage_l2->publish_state(data.voltage_l2);
if(data.voltage_l3_present)s_voltage_l3->publish_state(data.voltage_l3);
if(data.current_l1_present)s_current_l1->publish_state(data.current_l1);
if(data.current_l2_present)s_current_l2->publish_state(data.current_l2);
if(data.current_l3_present)s_current_l3->publish_state(data.current_l3);
if(data.power_delivered_l1_present)s_power_delivered_l1->publish_state(data.power_delivered_l1);
if(data.power_delivered_l2_present)s_power_delivered_l2->publish_state(data.power_delivered_l2);
if(data.power_delivered_l3_present)s_power_delivered_l3->publish_state(data.power_delivered_l3);
if(data.power_returned_l1_present)s_power_returned_l1->publish_state(data.power_returned_l1);
if(data.power_returned_l2_present)s_power_returned_l2->publish_state(data.power_returned_l2);
if(data.power_returned_l3_present)s_power_returned_l3->publish_state(data.power_returned_l3);
if(data.gas_device_type_present)s_gas_device_type->publish_state(data.gas_device_type);
if(data.gas_valve_position_present)s_gas_valve_position->publish_state(data.gas_valve_position);
if(data.gas_delivered_present)s_gas_delivered->publish_state(data.gas_delivered);
};
public:
CustomP1UartComponent(UARTComponent *parent) : UARTDevice(parent) {}
Sensor *s_energy_delivered_tariff1 = new Sensor();
Sensor *s_energy_delivered_tariff2 = new Sensor();
Sensor *s_energy_returned_tariff1 = new Sensor();
Sensor *s_energy_returned_tariff2 = new Sensor();
Sensor *s_electricity_tariff = new Sensor();
Sensor *s_power_delivered = new Sensor();
Sensor *s_power_returned = new Sensor();
Sensor *s_electricity_threshold = new Sensor();
Sensor *s_voltage_l1 = new Sensor();
Sensor *s_voltage_l2 = new Sensor();
Sensor *s_voltage_l3 = new Sensor();
Sensor *s_current_l1 = new Sensor();
Sensor *s_current_l2 = new Sensor();
Sensor *s_current_l3 = new Sensor();
Sensor *s_power_delivered_l1 = new Sensor();
Sensor *s_power_delivered_l2 = new Sensor();
Sensor *s_power_delivered_l3 = new Sensor();
Sensor *s_power_returned_l1 = new Sensor();
Sensor *s_power_returned_l2 = new Sensor();
Sensor *s_power_returned_l3 = new Sensor();
Sensor *s_gas_device_type = new Sensor();
Sensor *s_gas_valve_position = new Sensor();
Sensor *s_gas_delivered = new Sensor();
void setup() override {
lastread = 0;
}
void loop() override {
unsigned long now = millis();
read_message();
}
};
Installation
Copy the p1meter.yaml
and dsmr_p1_sensor.h
file into the working directory, update the
SSID for your wifi and mqtt broker and run:
pip3 install esphome
esphome p1meter.yaml run
If you aren't seeing data on the display, try turning on logging
in the yaml file so that
it will print all the character received on the serial port, and hook a scope to the data input
line to verify that the signals look clean.
Noisy signals
Without filtering | With filtering |
The signal on the Data Out pin had massive ringing every few microseconds, so I added a small 10 nF cap between the output and ground, which greatly reduced the noise, although it also decreased the slew rate of the digital signal. The 10nF seemed to be an ok balance between removing the noise and still allowing 115200 baud data to pass; your millage may vary and it is worth checking on the scope if you are getting frequent checksum errors.