Skip to content

Instantly share code, notes, and snippets.

@yorickvP
Created November 7, 2020 10:49
Show Gist options
  • Save yorickvP/a690945bd0694c3ae100779c0456475b to your computer and use it in GitHub Desktop.
Save yorickvP/a690945bd0694c3ae100779c0456475b to your computer and use it in GitHub Desktop.
// code is MIT licensed etc etc copyright 2018 @puckipedia
// - dsmrv5 parser / @yorickvp
// -- Libraries used --
#pragma GCC diagnostic warning "-Wall"
#pragma GCC diagnostic warning "-Wextra"
// ESP8266 OTA
#include <ArduinoOTA.h>
// look I'm not going to pay for a signal inverter. hooray for software
#define INVERT_SERIAL
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
struct {
char identifier[97];
uint32_t delivered[2];
uint32_t received[2];
uint16_t tariff;
uint32_t delivering;
uint32_t receiving;
uint32_t power_failures;
uint32_t long_power_failures;
uint32_t gas;
char gastime[14];
char measure_time[14];
struct {
uint32_t voltage_sags;
uint32_t voltage_swells;
uint32_t voltage;
uint32_t current;
uint32_t delivered_active_power;
uint32_t received_active_power;
} phases[3];
} data;
#define DATA_MAX 512
struct {
uint16_t index;
char data[DATA_MAX];
} buffer;
WiFiClient wclient;
struct obis {
uint8_t a:4;
uint8_t b:4;
uint8_t c:8;
uint8_t d:8;
uint8_t e:8;
};
static_assert(sizeof(obis) == sizeof(uint32_t), "obis struct should be 4 bytes");
//#define DBG(str) if (wclient && wclient.connected()) wclient.println(str)
#define DBG(str) {}
void on_byte(uint8_t val) {
//Serial.print("Got byte: ");
//Serial.write(val);
// if (wclient && wclient.connected()) {
// wclient.write(val);
// }
if (val == '\n' || val == '\r') {
buffer.data[buffer.index] = 0;
if (buffer.index > 0)
process(buffer.data);
buffer.index = 0;
} else if (val < 128) {
if (buffer.index > DATA_MAX - 2) return; // buffer full
buffer.data[buffer.index++] = val;
} else {
buffer.index = 0;
buffer.data[buffer.index] = 0; // garbage
}
}
#define THEN(x) if (!(x)) {DBG("failed " #x); return 0;}
class parser {
private:
const char* line;
public:
parser(const char *line) : line(line) {}
inline int obis(struct obis& target) {
uint8_t a, b, c, d, e;
THEN(dec(a)); THEN(chr<'-'>());
THEN(dec(b)); THEN(chr<':'>());
THEN(dec(c)); THEN(chr<'.'>());
THEN(dec(d)); THEN(chr<'.'>());
THEN(dec(e));
target = {a, b, c, d, e};
return 1;
}
template<char t> inline int chr() {
if (*line == t) {
line++;
return 1;
}
return 0;
}
inline int fixed(uint32_t &target) {
unsigned int hi, lo;
THEN(dec(hi));
THEN(chr<'.'>());
const char* ln = line;
THEN(dec(lo));
target = hi * std::pow(10, line-ln) + lo;
return 1;
}
template<typename T> inline int dec(T &target) {
return integral<10>(target);
}
template<typename T> inline int hex(T &target) {
return integral<16>(target);
}
template<uint8_t base, typename T> inline int integral(T &target) {
target = 0;
int ret = 0;
while(1) {
char l = *line++;
if ('0' <= l && l <= '9') {
target *= base;
target += l - '0';
ret = 1;
} else if ('A' <= l && l < ('A' + base - 10)) {
target *= base;
target += l - 'A';
ret = 1;
} else {
line--;
return ret;
}
}
}
template<int n> inline int str(char* target) {
register unsigned int i = 0;
while(i < n && line[i] != ')' && line[i] != '\n' && line[i] != 0) {
target[i] = line[i];
i++;
}
line += i;
return i;
}
inline int timestamp(char *target) {
return str<13>(target);
}
};
constexpr uint32_t sw(struct obis param) { // switchable
return (param.a << 28) | (param.b << 24) | (param.c << 16) | (param.d << 8) | (param.e);
}
int process(const char *line) {
if (wclient && wclient.connected()) {
wclient.println(line);
}
obis id;
parser p(line);
THEN(p.obis(id));
THEN(p.chr<'('>());
switch(sw(id)) {
case sw({0, 0, 1, 0, 0}): return p.timestamp(data.measure_time);
case sw({0, 0, 96, 1, 1}): return p.str<96>(data.identifier);
case sw({1, 0, 1, 8, 1}): return p.fixed(data.delivered[0]);
case sw({1, 0, 1, 8, 2}): return p.fixed(data.delivered[1]);
case sw({1, 0, 2, 8, 1}): return p.fixed(data.received[0]);
case sw({1, 0, 2, 8, 2}): return p.fixed(data.received[1]);
case sw({1, 0, 1, 7, 0}): return p.fixed(data.delivering);
case sw({1, 0, 2, 7, 0}): return p.fixed(data.receiving);
case sw({0, 0, 96, 14, 0}): return p.hex(data.tariff);
case sw({0, 0, 96, 7, 21}): return p.dec(data.power_failures);
case sw({0, 0, 96, 7, 9}): return p.dec(data.long_power_failures);
case sw({1, 0, 32, 7, 0}): return p.fixed(data.phases[0].voltage);
case sw({1, 0, 21, 7, 0}): return p.fixed(data.phases[0].delivered_active_power);
case sw({1, 0, 22, 7, 0}): return p.fixed(data.phases[0].received_active_power);
case sw({1, 0, 32, 32, 0}): return p.dec(data.phases[0].voltage_sags);
case sw({1, 0, 32, 36, 0}): return p.dec(data.phases[0].voltage_swells);
case sw({1, 0, 31, 7, 0}): return p.dec(data.phases[0].current);
case sw({0, 1, 24, 2, 1}): {
THEN(p.timestamp(data.gastime));
THEN(p.chr<')'>());
THEN(p.chr<'('>());
return p.fixed(data.gas);
}
}
// power failure log: 1-0:99.97.0(1)(0-0:96.7.19)(190201235139W)(0000003233*s)
// // 0-0:96.13.0: text message
// // 0-1:24.1.0(003): m-bus device type (gas)
return 0;
}
// int process(const char* line) {
// int ret = ptest(line);
// if (wclient && wclient.connected()) {
// wclient.print(line);
// wclient.print(": ");
// wclient.println(ret);
// }
// }
// connect via wifi
ESP8266WebServer server(80);
WiFiServer wserv(8888);
#define metric(name, type, help) base += "# HELP " name " " help "\n# TYPE " name " " type "\n"
void send_redirect() {
server.sendHeader("Location", String("/metrics"), true);
server.send(302, "text/plain", "");
}
void send_data() {
char buf[2048];
String base = "";
metric("power_watthours_total", "counter", "The amount of power delivered to the meter");
for(int i = 0; i < 2; i++) {
sprintf(buf, "power_watthours_total{direction=\"delivered\", tariff=\"%u\"} %u\npower_watthours_total{direction=\"returned\"} %u\n",
i+1, data.delivered[i], data.received[i]);
base += buf;
}
metric("power_watts", "gauge", "The power usage measured by the meter");
sprintf(buf, "power_watts{direction=\"delivered\"} %d\n", (int32_t) data.delivering);
base += buf;
sprintf(buf, "power_watts{direction=\"returned\"} %d\n", ((int32_t) data.receiving));
base += buf;
metric("power_tarriff", "gauge", "The current power tariff");
sprintf(buf, "power_tariff %d\n", (int32_t)data.tariff);
base += buf;
metric("gas_m3_total", "counter", "The total gas in m3");
sprintf(buf, "gas_m3_total %d.%03d\n", data.gas / 1000, data.gas % 1000);
base += buf;
metric("voltage_volts", "gauge", "The voltage per phase");
metric("current_volts", "gauge", "The current per phase");
for (int i = 0; i < 1; i++) {
sprintf(buf, "voltage_volts{phase=\"%d\"} %u.%u\ncurrent_amperes{phase=\"%d\"} %u\npower_watts{phase=\"%d\"} %d\n",
i+1, data.phases[i].voltage/10, data.phases[i].voltage%10,
i+1, data.phases[i].current,
i+1, ((int32_t) data.phases[i].delivered_active_power) - ((int32_t) data.phases[i].received_active_power)
);
base += buf;
}
base += "# state is ";
base += buffer.index;
base += buffer.data;
base += "\n";
server.send(200, "text/plain; version=0.0.4", base);
}
void setup() {
// setup serial, both debug + non-debug
Serial.setRxBufferSize(2048);
Serial.begin(115200, SERIAL_8N1);
// connect to the wifi
WiFi.mode(WIFI_STA);
WiFi.begin("[INSERT WIFI NETWORK NAME]", "[INSERT PASSWORD]");
WiFi.hostname("SmartMeter");
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println(WiFi.localIP());
ArduinoOTA.setHostname("SmartMeter");
ArduinoOTA.onStart([]() { });
ArduinoOTA.onEnd([]() { });
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { });
ArduinoOTA.begin();
MDNS.addService("prometheus-http", "tcp", 80);
server.on("/metrics", send_data);
server.on("/", send_redirect);
server.begin();
wserv.begin();
Serial.println("Flipping RX bit now... Goodbye!");
delay(1000);
// use GPIO13/D7 for receiving from the P1 port, to allow for less work when debugging
//Serial.pins(15, 13);
#ifdef INVERT_SERIAL
U0C0 |= BIT(UCRXI);// Inverse RX
#endif
}
uint32_t bytes = 0;
void loop() {
server.handleClient();
if (!wclient || !wclient.connected()) {
wclient = wserv.available();
}
ArduinoOTA.handle();
while (Serial.available()) {
bytes++;
on_byte(Serial.read());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment