Skip to content

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

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.

Hacks 2021 ESP


Last update: June 6, 2021