Created
November 7, 2020 10:49
-
-
Save yorickvP/a690945bd0694c3ae100779c0456475b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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