From 4274a207fd17cbe1a014c7657df2df1d779faf1e Mon Sep 17 00:00:00 2001 From: Megamannen Date: Wed, 19 Apr 2023 07:18:37 +0200 Subject: [PATCH] feat: added HDLC support Basically merged Erik's code with the current branch, also reworked how it's integrated There is room for improvment but it works, which is good enough for me :) https://github.com/forsberg/esphome-p1reader/tree/branschrekommendation-1.2-tekniska-verken Co-authored-by: Erik Forsberg --- README.md | 11 +++ p1reader.h | 217 +++++++++++++++++++++++++++++++++++++++++++++++++- p1reader.yaml | 8 +- 3 files changed, 232 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a643340..d1e36d7 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,17 @@ uart: ## Installation Clone the repository and create a companion `secrets.yaml` file with the following fields: + +If your electricity supplier is using Aidon 6442SE or Aidon 653X, you might also need to change the instantiation of the meter_sensor. +As these meters are sometimes using the HDLC-protocol instead of ASCII. Open [p1reader.yaml](./p1reader.yaml) +```c +// Change +auto meter_sensor = new P1Reader(id(uart_bus)); +// To +auto meter_sensor = new P1ReaderHDLC(id(uart_bus)); +``` + +Create a companion `secrets.yaml` file with the following fields: ``` wifi_ssid: wifi_password: diff --git a/p1reader.h b/p1reader.h index 3ed00bc..fff6854 100644 --- a/p1reader.h +++ b/p1reader.h @@ -4,6 +4,7 @@ // // History // 0.1.0 2020-11-05: Initial release +// 0.x.0 2023-04-18: Added HDLC support // // MIT License // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -68,7 +69,9 @@ class ParsedMessage { class P1Reader : public Component, public UARTDevice { const char* DELIMITERS = "()*:"; const char* DATA_ID = "1-0"; - char buffer[BUF_SIZE]; + + protected: + char buffer[BUF_SIZE]; public: Sensor *cumulativeActiveImport = new Sensor(); @@ -113,11 +116,12 @@ class P1Reader : public Component, public UARTDevice { void setup() override { } - void loop() override { + void loop() override + { readP1Message(); } - private: + protected: uint16_t crc16_update(uint16_t crc, uint8_t a) { int i; crc ^= a; @@ -252,6 +256,7 @@ class P1Reader : public Component, public UARTDevice { currentL3->publish_state(parsed->currentL3); } + private: void readP1Message() { if (available()) { uint16_t crc = 0x0000; @@ -311,4 +316,210 @@ class P1Reader : public Component, public UARTDevice { } } } + + }; + +class P1ReaderHDLC : public P1Reader { + const int8_t OUTSIDE_FRAME = 0; + const int8_t FOUND_FRAME = 1; + + + int8_t parseHDLCState = OUTSIDE_FRAME; + + public: + P1ReaderHDLC(UARTComponent *parent) : P1Reader(parent) {} + + void loop() override { + readHDLCMessage(); + } + + private: + bool readHDLCStruct(ParsedMessage* parsed) { + if(Serial.readBytes(buffer, 3) != 3) + return false; + + if(buffer[0] != 0x02) { + return false; + } + + char obis[7]; + + uint8_t struct_len = buffer[1]; + //ESP_LOGD("hdlc", "Struct length is %d", struct_len); + + uint8_t tag = buffer[2]; + + if(tag != 0x09) { + ESP_LOGE("hdlc", "Unexpected tag %X in struct, bailing out", tag); + return false; + } + + uint8_t str_length = read(); + if(Serial.readBytes(buffer, str_length) != str_length) { + ESP_LOGE("hdlc", "Unable to read %d bytes of OBIS code", str_length); + return false; + } + buffer[str_length] = 0; // Null-terminate + sprintf(obis, "%d.%d.%d", buffer[2], buffer[3], buffer[4]); + + tag = read(); + + char value_buf[24]; + bool is_signed = false; + uint32_t uvalue = 0; + int32_t value = 0; + if (tag == 0x09) { + str_length = read(); + if(Serial.readBytes(buffer, str_length) != str_length) { + ESP_LOGE("hdlc", "Unable to read %d bytes of string", str_length); + return false; + } + + buffer[str_length] = 0; + //ESP_LOGD("hdlc", "Read string length %d", str_length); + } else if(tag == 0x06) { + Serial.readBytes(buffer, 4); + //uvalue = buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24; + uvalue = buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24; + //ESP_LOGD("hdlc", "Value of uvalue is %u", uvalue); + } else if (tag == 0x10) { + Serial.readBytes(buffer, 2); + //value = buffer[0] | buffer[1] << 8; // + is_signed = true; + value = buffer[1] | buffer[0] << 8; + //ESP_LOGD("hdlc", "(Signed) Value of value is %d", value); + } else if (tag == 0x12) { + Serial.readBytes(buffer, 2); + //uvalue = buffer[0] | buffer[1] << 8; // + uvalue = buffer[1] | buffer[0] << 8; + //ESP_LOGD("hdlc", "(Unsigned) Value of uvalue is %u", uvalue); + } else { + ESP_LOGE("hdlc", "unknown tag %X", tag); + } + + int8_t scaler; + uint8_t unit; + if(struct_len == 3) { + Serial.readBytes(buffer, 6); + scaler = buffer[3]; + unit = buffer[5]; + //ESP_LOGD("hdlc", "Scaler %u", scaler); + //ESP_LOGD("hdlc", "Unit %d", buffer[5]); + + if(!is_signed) + value = uvalue; + + double scaled_value = pow(10, scaler) * value; + + // Volt and Ampere are the only two units where p1reader.yaml doesn't specify + // we should report in 1/1000, all others should be divided. + if(unit != 33 && unit != 35) + scaled_value = scaled_value / 1000; + + snprintf(value_buf, 24, "%f", scaled_value); + parseRow(parsed, obis, value_buf); + } + + + + + return true; + } + + /* Reads messages formatted according to "Branschrekommendation v1.2", which + at the time of writing (20210207) is used by Tekniska Verken's Aidon 6442SE + meters. This is a binary format, with a HDLC Frame. + + This code is in no way a generic HDLC Frame parser, but it does the job + of decoding this particular data stream. + */ + void readHDLCMessage() + { + if (available()) + { + uint8_t data = 0; + uint16_t crc = 0x0000; + ParsedMessage parsed = ParsedMessage(); + + while (parseHDLCState == OUTSIDE_FRAME) + { + data = read(); + if (data == 0x7e) + { + // ESP_LOGD("hdlc", "Found start of frame"); + parseHDLCState = FOUND_FRAME; + break; + + int8_t next = peek(); + + // ESP_LOGD("hdlc", "Next is %d", next); + + if (next == 0x7e) + { + read(); // We were actually at the end flag, consume the start flag of the next frame. + } else if (next == -1) { + ESP_LOGE("hdlc", "No char available after flag, out of sync. Returning"); + parseHDLCState = OUTSIDE_FRAME; + return; + } + } + } + + if (parseHDLCState == FOUND_FRAME) + { + // Read various static HDLC Frame information we don't care about + int len = Serial.readBytes(buffer, 12); + if (len != 12) { + ESP_LOGE("hdlc", "Expected 12 bytes, got %d bytes - out of sync. Returning", len); + parseHDLCState = OUTSIDE_FRAME; + return; + } + // ESP_LOGD("hdlc", "Got %d HDLC bytes, now reading 4 Invoke ID And Priority bytes", len); + len = Serial.readBytes(buffer, 4); + if (len != 4 || buffer[0] != 0x40 || buffer[1] != 0x00 || buffer[2] != 0x00 || buffer[3] != 0x00) + { + ESP_LOGE("hdlc", "Expected 0x40 0x00 0x00 0x00, got %X %X %X %X - out of sync, returning.", buffer[0], buffer[1], buffer[2], buffer[3]); + parseHDLCState = OUTSIDE_FRAME; + return; + } + } + + data = read(); // Expect length of time field, usually 0 + //ESP_LOGD("hdlc", "Length of datetime field is %d", data); + Serial.readBytes(buffer, data); + + data = read(); + ESP_LOGD("hdlc", "Expect 0x01 (array tag), got 0x%02x", data); + if(data != 0x01) { + parseHDLCState = OUTSIDE_FRAME; + return; + } + + uint8_t array_length = read(); + ESP_LOGD("hdlc", "Array length is %d", array_length); + + for(int i=0;i