Start SDM series energy meters tools repo
This commit is contained in:
commit
4011b2d8a6
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.o
|
||||||
|
**.swp
|
17
Makefile
Normal file
17
Makefile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
sdm2mqtt: sdm.o sdm2mqtt.o
|
||||||
|
cc -o sdm2mqtt sdm2mqtt.o sdm.o -lmosquitto -lmodbus
|
||||||
|
|
||||||
|
sdm2mqtt.o: sdm2mqtt.c config.h
|
||||||
|
cc -c sdm2mqtt.c
|
||||||
|
|
||||||
|
sdm.o: sdm.c sdm*_registers.h
|
||||||
|
cc -c sdm.c
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf *.o
|
||||||
|
|
||||||
|
install: sdm2mqtt
|
||||||
|
envsubst <sdm2mqtt.service >/etc/systemd/system/sdm2mqtt.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable sdm2mqtt.service
|
||||||
|
systemctl start sdm2mqtt.service
|
31
README.md
Normal file
31
README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## SDM2MQTT Bridge
|
||||||
|
|
||||||
|
Reads SDM series energy meters modbus registers and pushes them to mqtt topic
|
||||||
|
as json object.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt install git make libmodbus-dev libmosquitto-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure
|
||||||
|
|
||||||
|
Modify `config.h` to your needs
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copyright
|
||||||
|
|
||||||
|
2024 Waijb
|
||||||
|
Documentation by Sprinterfreak
|
55
config.h
Normal file
55
config.h
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include "sdm.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int addr;
|
||||||
|
char *friendlyname;
|
||||||
|
enum sdm_types type;
|
||||||
|
} config_meters_t;
|
||||||
|
|
||||||
|
static config_meters_t meters[] = {
|
||||||
|
{ 11,
|
||||||
|
"Feld",
|
||||||
|
SDM630M
|
||||||
|
},
|
||||||
|
{ 12,
|
||||||
|
"Vorzelt",
|
||||||
|
SDM630M
|
||||||
|
},
|
||||||
|
{ 0,
|
||||||
|
NULL,
|
||||||
|
SDMNONE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *ser_device;
|
||||||
|
int ser_baud;
|
||||||
|
int ser_databits;
|
||||||
|
char ser_parity;
|
||||||
|
int ser_stopbits;
|
||||||
|
config_meters_t *meters;
|
||||||
|
const char *mosq_name;
|
||||||
|
const char *mosq_topicprefix;
|
||||||
|
const char *mosq_host;
|
||||||
|
const int mosq_port;
|
||||||
|
const int mosq_keepalive;
|
||||||
|
} config_t;
|
||||||
|
|
||||||
|
static config_t cfg = {
|
||||||
|
"/dev/ttyUSB0",
|
||||||
|
38400,
|
||||||
|
8,
|
||||||
|
'N',
|
||||||
|
1,
|
||||||
|
meters,
|
||||||
|
"sdm2mqtt2",
|
||||||
|
"sdm2mqtt2",
|
||||||
|
"localhost",
|
||||||
|
1883,
|
||||||
|
5
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
1001
grafana/dashboard.json
Normal file
1001
grafana/dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
0
logs/.keep
Normal file
0
logs/.keep
Normal file
333
sdm.c
Normal file
333
sdm.c
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
#include "sdm.h"
|
||||||
|
|
||||||
|
#include "sdm230m_registers.h"
|
||||||
|
#include "sdm630m_registers.h"
|
||||||
|
#include "sdm72dm_registers.h"
|
||||||
|
|
||||||
|
static char *sdm_typenames[] = {
|
||||||
|
"none",
|
||||||
|
"SDM230M",
|
||||||
|
"SDM630M",
|
||||||
|
"SDM72DM"
|
||||||
|
};
|
||||||
|
|
||||||
|
static void swap(sdm_values_t *value) {
|
||||||
|
uint16_t tmp;
|
||||||
|
|
||||||
|
tmp = value->v16[0];
|
||||||
|
value->v16[0] = value->v16[1];
|
||||||
|
value->v16[1] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sdm_gettypename(sdm_t *sdm) {
|
||||||
|
return sdm_typenames[sdm->type];
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm_t *sdm_new(modbus_t *mb, int address, char *friendlyname, enum sdm_types type, int *error) {
|
||||||
|
sdm_t *sdm;
|
||||||
|
|
||||||
|
if (mb == NULL) {
|
||||||
|
*error = SDM_MB_ERR;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address <= 0 || address > 247) {
|
||||||
|
*error = SDM_ADDR_ERR;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friendlyname == NULL) {
|
||||||
|
*error = SDM_NAME_ERR;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm = (sdm_t *)calloc(sizeof(sdm_t), 1);
|
||||||
|
|
||||||
|
if (sdm == NULL) {
|
||||||
|
*error = SDM_MEM_ERR;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm->mb = mb;
|
||||||
|
sdm->address = address;
|
||||||
|
sdm->friendlyname = strdup(friendlyname);
|
||||||
|
|
||||||
|
if (sdm->friendlyname == NULL) {
|
||||||
|
free(sdm);
|
||||||
|
*error = SDM_MEM_ERR;
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm->type = type;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SDM230M:
|
||||||
|
sdm->values = (sdm_values_t *)calloc(sizeof(sdm_values_t) * SMD230M_NRVAL, 1);
|
||||||
|
break;
|
||||||
|
case SDM630M:
|
||||||
|
sdm->values = (sdm_values_t *)calloc(sizeof(sdm_values_t) * SMD630M_NRVAL, 1);
|
||||||
|
break;
|
||||||
|
case SDM72DM:
|
||||||
|
sdm->values = (sdm_values_t *)calloc(sizeof(sdm_values_t) * SMD72DM_NRVAL, 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
free(sdm->friendlyname);
|
||||||
|
free(sdm);
|
||||||
|
*error = SDM_MEM_ERR;
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdm->values == NULL) {
|
||||||
|
free(sdm->friendlyname);
|
||||||
|
free(sdm);
|
||||||
|
*error = SDM_MEM_ERR;
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm->valid = false;
|
||||||
|
|
||||||
|
|
||||||
|
return sdm;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sdm_free(sdm_t *sdm) {
|
||||||
|
if (sdm == NULL) {
|
||||||
|
return SDM_EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(sdm->friendlyname);
|
||||||
|
free(sdm->values);
|
||||||
|
free(sdm);
|
||||||
|
|
||||||
|
return SDM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sdm_update(sdm_t *sdm) {
|
||||||
|
sdm_registers_t *regs;
|
||||||
|
sdm_values_t *value = sdm->values;
|
||||||
|
|
||||||
|
sdm->valid = false;
|
||||||
|
|
||||||
|
switch (sdm->type) {
|
||||||
|
case SDM230M:
|
||||||
|
regs = sdm230m_registers;
|
||||||
|
break;
|
||||||
|
case SDM630M:
|
||||||
|
regs = sdm630m_registers;
|
||||||
|
break;
|
||||||
|
case SDM72DM:
|
||||||
|
regs = sdm72dm_registers;
|
||||||
|
break;
|
||||||
|
case SDMNONE:
|
||||||
|
regs = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
modbus_flush(sdm->mb);
|
||||||
|
modbus_set_slave(sdm->mb, sdm->address);
|
||||||
|
|
||||||
|
while (regs->name != NULL) {
|
||||||
|
if (-1 == modbus_read_input_registers(sdm->mb, regs->regno, 2, value->v16)) {
|
||||||
|
return SDM_COMM_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
swap(value);
|
||||||
|
|
||||||
|
regs++;
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdm->valid = true;
|
||||||
|
|
||||||
|
return SDM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sdm_print_csv_header(sdm_t *sdm, char **header) {
|
||||||
|
char tmp[4096], *ret;
|
||||||
|
int free = 4095, used = 0, count = 0;
|
||||||
|
sdm_registers_t *regs;
|
||||||
|
sdm_values_t *value = sdm->values;
|
||||||
|
|
||||||
|
switch (sdm->type) {
|
||||||
|
case SDM230M:
|
||||||
|
regs = sdm230m_registers;
|
||||||
|
break;
|
||||||
|
case SDM630M:
|
||||||
|
regs = sdm630m_registers;
|
||||||
|
break;
|
||||||
|
case SDM72DM:
|
||||||
|
regs = sdm72dm_registers;
|
||||||
|
break;
|
||||||
|
case SDMNONE:
|
||||||
|
regs = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = snprintf(tmp, free, "time;friendlyname;address;typename;valid;");
|
||||||
|
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
while (regs->name != NULL) {
|
||||||
|
count = snprintf(tmp + used, free, "%s;", regs->name);
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
regs++;
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = strdup(tmp);
|
||||||
|
if (ret == NULL) {
|
||||||
|
return SDM_MB_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*header = ret;
|
||||||
|
return SDM_OK;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sdm_print_csv(sdm_t *sdm, char **csv) {
|
||||||
|
char tmp[4096], *ret;
|
||||||
|
int free = 4095, used = 0, count = 0;
|
||||||
|
time_t now;
|
||||||
|
sdm_registers_t *regs;
|
||||||
|
sdm_values_t *value = sdm->values;
|
||||||
|
|
||||||
|
time(&now);
|
||||||
|
|
||||||
|
tmp[4095] = 0;
|
||||||
|
|
||||||
|
switch (sdm->type) {
|
||||||
|
case SDM230M:
|
||||||
|
regs = sdm230m_registers;
|
||||||
|
break;
|
||||||
|
case SDM630M:
|
||||||
|
regs = sdm630m_registers;
|
||||||
|
break;
|
||||||
|
case SDM72DM:
|
||||||
|
regs = sdm72dm_registers;
|
||||||
|
break;
|
||||||
|
case SDMNONE:
|
||||||
|
regs = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = strftime(tmp, free, "%FT%TZ;", gmtime(&now));
|
||||||
|
count += snprintf(tmp + count, free, "%s;%03d;%s;%s;", sdm->friendlyname, sdm->address, sdm_gettypename(sdm), sdm->valid?"true":"false");
|
||||||
|
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
while (regs->name != NULL && sdm->valid == true) {
|
||||||
|
count = snprintf(tmp + used, free, "%f;", value->vf);
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
regs++;
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = strdup(tmp);
|
||||||
|
if (ret == NULL) {
|
||||||
|
return SDM_MB_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*csv = ret;
|
||||||
|
return SDM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sdm_print_json(sdm_t *sdm, char **json) {
|
||||||
|
char tmp[4096], *ret;
|
||||||
|
int free = 4095, used = 0, count = 0;
|
||||||
|
sdm_registers_t *regs;
|
||||||
|
sdm_values_t *value = sdm->values;
|
||||||
|
|
||||||
|
tmp[4095] = 0;
|
||||||
|
|
||||||
|
switch (sdm->type) {
|
||||||
|
case SDM230M:
|
||||||
|
regs = sdm230m_registers;
|
||||||
|
break;
|
||||||
|
case SDM630M:
|
||||||
|
regs = sdm630m_registers;
|
||||||
|
break;
|
||||||
|
case SDM72DM:
|
||||||
|
regs = sdm72dm_registers;
|
||||||
|
break;
|
||||||
|
case SDMNONE:
|
||||||
|
regs = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = snprintf(tmp, free, "{\"name\":\"%s\",\"address\":%d,\"type\":\"%s\",\"valid\":%s", sdm->friendlyname, sdm->address, sdm_gettypename(sdm), sdm->valid?"true":"false");
|
||||||
|
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
while (regs->name != NULL && sdm->valid == true) {
|
||||||
|
count = snprintf(tmp + used, free, ",\"%s\":%f", regs->name, value->vf);
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
free -= count;
|
||||||
|
used += count;
|
||||||
|
count = 0;
|
||||||
|
regs++;
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = snprintf(tmp + used, free, "}");
|
||||||
|
|
||||||
|
if (count < 0 || count >= free) {
|
||||||
|
//FIXME ERRORHANDLING
|
||||||
|
|
||||||
|
return SDM_MEM_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = strdup(tmp);
|
||||||
|
if (ret == NULL) {
|
||||||
|
return SDM_MB_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*json = ret;
|
||||||
|
return SDM_OK;
|
||||||
|
}
|
||||||
|
|
56
sdm.h
Normal file
56
sdm.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#ifndef sdm_h
|
||||||
|
#define sdm_h
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <modbus/modbus.h>
|
||||||
|
|
||||||
|
#define SDM_OK 0
|
||||||
|
#define SDM_EMPTY -1
|
||||||
|
#define SDM_MB_ERR -2
|
||||||
|
#define SDM_ADDR_ERR -3
|
||||||
|
#define SDM_NAME_ERR -4
|
||||||
|
#define SDM_TYPE_ERR -5
|
||||||
|
#define SDM_COMM_ERR -6
|
||||||
|
#define SDM_NOT_IMPL -7
|
||||||
|
#define SDM_MEM_ERR -8
|
||||||
|
|
||||||
|
enum sdm_types {
|
||||||
|
SDMNONE,
|
||||||
|
SDM230M,
|
||||||
|
SDM630M,
|
||||||
|
SDM72DM
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
const char *unit;
|
||||||
|
const unsigned int regno;
|
||||||
|
} sdm_registers_t;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
uint16_t v16[2];
|
||||||
|
float vf;
|
||||||
|
} __attribute__ ((packed)) sdm_values_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
modbus_t *mb;
|
||||||
|
int address;
|
||||||
|
char *friendlyname;
|
||||||
|
enum sdm_types type;
|
||||||
|
sdm_values_t *values;
|
||||||
|
bool valid;
|
||||||
|
} sdm_t;
|
||||||
|
|
||||||
|
sdm_t *sdm_new(modbus_t *mb, int address, char *friendlyname, enum sdm_types type, int *error);
|
||||||
|
int sdm_free(sdm_t *sdm);
|
||||||
|
int sdm_update(sdm_t *sdm);
|
||||||
|
const char *sdm_gettypename(sdm_t *sdm);
|
||||||
|
int sdm_print_csv(sdm_t *sdm, char **csv);
|
||||||
|
int sdm_print_csv_header(sdm_t *sdm, char **header);
|
||||||
|
int sdm_print_json(sdm_t *sdm, char **json);
|
||||||
|
|
||||||
|
#endif
|
35
sdm230m_registers.h
Normal file
35
sdm230m_registers.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#ifndef sdm230m_registers_h
|
||||||
|
#define sdm230m_registers_h
|
||||||
|
|
||||||
|
#define SMD230M_NRVAL 24
|
||||||
|
|
||||||
|
|
||||||
|
// 24 Messwerte
|
||||||
|
static sdm_registers_t sdm230m_registers[] = {
|
||||||
|
{ "L-N Voltage", "V" , 0 },
|
||||||
|
{ "Current", "A" , 6 },
|
||||||
|
{ "Power", "W" , 12 },
|
||||||
|
{ "Apparent Power", "VA" , 18 },
|
||||||
|
{ "Reactive Power", "VAr" , 24 },
|
||||||
|
{ "Power Factor", "1" , 30 },
|
||||||
|
{ "Phase Angle", "°" , 36 },
|
||||||
|
{ "Frequency", "Hz" , 70 },
|
||||||
|
{ "Energy Import", "kWh" , 72 },
|
||||||
|
{ "Energy Export", "kWh" , 74 },
|
||||||
|
{ "Reactive Energy Import", "kVArh" , 76 },
|
||||||
|
{ "Reactive Energy Export", "kVArh" , 78 },
|
||||||
|
{ "Total Power Demand", "W" , 84 },
|
||||||
|
{ "Total Power Demand (Max)", "W" , 86 },
|
||||||
|
{ "Current Power Demand (Import)", "W" , 88 },
|
||||||
|
{ "Max Power Demand (Import)", "W" , 90 },
|
||||||
|
{ "Current Power Demand (Export)", "W" , 92 },
|
||||||
|
{ "Max Power Demand (Export)", "W" , 94 },
|
||||||
|
{ "L Current Demand", "A" , 258 },
|
||||||
|
{ "L Current Demand (Max)", "A" , 264 },
|
||||||
|
{ "Total Energy", "kWh" , 342 },
|
||||||
|
{ "Total Reactive Energy", "kVArh" , 344 },
|
||||||
|
{ "Total Energy (resettable)", "kWh" , 384 },
|
||||||
|
{ "Total Reactive Energy (resettable)", "kVArh" , 386 },
|
||||||
|
{ NULL, NULL , 0 }
|
||||||
|
};
|
||||||
|
#endif
|
163
sdm2mqtt.c
Normal file
163
sdm2mqtt.c
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include <mosquitto.h>
|
||||||
|
#include <modbus/modbus.h>
|
||||||
|
|
||||||
|
#include "sdm.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int nmeters, i, err;
|
||||||
|
|
||||||
|
struct mosquitto *mosq;
|
||||||
|
modbus_t *mb;
|
||||||
|
sdm_t **sdms;
|
||||||
|
FILE **logfiles;
|
||||||
|
|
||||||
|
char *printme;
|
||||||
|
char buf[4096];
|
||||||
|
|
||||||
|
mosquitto_lib_init();
|
||||||
|
mosq = mosquitto_new(cfg.mosq_name, false, NULL);
|
||||||
|
|
||||||
|
if (mosq == NULL) {
|
||||||
|
//FIXME
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mosquitto_connect(mosq, cfg.mosq_host, cfg.mosq_port, cfg.mosq_keepalive);
|
||||||
|
|
||||||
|
if (err != MOSQ_ERR_SUCCESS) {
|
||||||
|
//FIXME
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mb = modbus_new_rtu(cfg.ser_device, cfg.ser_baud, cfg.ser_parity, cfg.ser_databits, cfg.ser_stopbits);
|
||||||
|
|
||||||
|
if (mb == NULL) {
|
||||||
|
fprintf(stderr, "%s:%d: failed to create Modbus Context\n", __FILE__, __LINE__);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modbus_connect(mb) == -1) {
|
||||||
|
fprintf(stderr, "%s:%d: Modbus connection failed: %s\n", __FILE__, __LINE__, modbus_strerror(errno));
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
nmeters = 0;
|
||||||
|
while (cfg.meters[nmeters].friendlyname != NULL) {
|
||||||
|
nmeters++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdms = (sdm_t **)calloc(sizeof(sdm_t *) * nmeters, 1);
|
||||||
|
logfiles = (FILE **)calloc(sizeof(FILE *) * nmeters, 1);
|
||||||
|
|
||||||
|
for (i = 0; i < nmeters; i++) {
|
||||||
|
err = 0;
|
||||||
|
sdms[i] = sdm_new(mb, cfg.meters[i].addr, cfg.meters[i].friendlyname, cfg.meters[i].type, &err);
|
||||||
|
|
||||||
|
if (sdms[i] == NULL) {
|
||||||
|
fprintf(stderr, "%s:%d: Failed to create meter %s\n", __FILE__, __LINE__, cfg.meters[i].friendlyname);
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(buf, 4095, "logs/%s.log", sdms[i]->friendlyname);
|
||||||
|
logfiles[i] = fopen(buf, "r");
|
||||||
|
if (logfiles[i] == NULL) {
|
||||||
|
char *header;
|
||||||
|
|
||||||
|
logfiles[i] = fopen(buf, "a");
|
||||||
|
if (logfiles[i] == NULL) {
|
||||||
|
fprintf(stderr, "%s:%d: Failed to open logfile %s:\n", __FILE__, __LINE__, cfg.meters[i].friendlyname);
|
||||||
|
perror("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdm_print_csv_header(sdms[i], &header)) {
|
||||||
|
//FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(logfiles[i], "%s\n", header);
|
||||||
|
free(header);
|
||||||
|
} else {
|
||||||
|
logfiles[i] = freopen(buf, "a", logfiles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logfiles[i] == NULL) {
|
||||||
|
fprintf(stderr, "%s:%d: Failed to open logfile %s:\n", __FILE__, __LINE__, cfg.meters[i].friendlyname);
|
||||||
|
perror("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
for (i = 0; i < nmeters; i++) {
|
||||||
|
if (sdm_update(sdms[i]) != SDM_OK) {
|
||||||
|
int errsv = errno;
|
||||||
|
fprintf(stderr, "%s:%d: %s: %s (%d)\n", __FILE__, __LINE__, modbus_strerror(errsv), sdms[i]->friendlyname, sdms[i]->address);
|
||||||
|
if (errsv == ECONNRESET || errsv == EBADF) {
|
||||||
|
// USB Connection failed?
|
||||||
|
// try to reconnect...
|
||||||
|
modbus_close(mb);
|
||||||
|
if (modbus_connect(mb) == -1) {
|
||||||
|
fprintf(stderr, "%s:%d: Modbus reconnect failed: %s\n", __FILE__, __LINE__, modbus_strerror(errno));
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdm_print_csv(sdms[i], &printme) != SDM_OK) {
|
||||||
|
fprintf(stderr, "%s:%d: error in sdm_print_csv!\n", __FILE__, __LINE__);
|
||||||
|
} else {
|
||||||
|
fprintf(logfiles[i], "%s\n", printme);
|
||||||
|
fflush(logfiles[i]);
|
||||||
|
free(printme);
|
||||||
|
}
|
||||||
|
if (sdm_print_json(sdms[i], &printme) != SDM_OK) {
|
||||||
|
fprintf(stderr, "%s:%d: error in sdm_print_json!\n", __FILE__, __LINE__);
|
||||||
|
} else {
|
||||||
|
int err;
|
||||||
|
//FIXME
|
||||||
|
char topic[256];
|
||||||
|
|
||||||
|
snprintf(topic, 255, "%s/%s", cfg.mosq_topicprefix, sdms[i]->friendlyname);
|
||||||
|
|
||||||
|
err = mosquitto_publish(mosq, NULL, topic, strlen(printme), printme, 0, false);
|
||||||
|
if (err != MOSQ_ERR_SUCCESS) {
|
||||||
|
fprintf(stderr, "%s:%d: mosquitto error! (%d)\n", __FILE__, __LINE__, err);
|
||||||
|
err = mosquitto_reconnect(mosq);
|
||||||
|
if (err != MOSQ_ERR_SUCCESS) {
|
||||||
|
//FIXME
|
||||||
|
fprintf(stderr, "%s:%d: Mosquitto reconnect failed.\n", __FILE__, __LINE__);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mosquitto_publish(mosq, NULL, topic, strlen(printme), printme, 0, false);
|
||||||
|
if (err != MOSQ_ERR_SUCCESS) {
|
||||||
|
//FIXME
|
||||||
|
fprintf(stderr, "%s:%d: publish failed after reconnect.\n", __FILE__, __LINE__);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(printme);
|
||||||
|
}
|
||||||
|
usleep(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < nmeters; i++) {
|
||||||
|
sdm_free(sdms[i]);
|
||||||
|
fclose(logfiles[i]);
|
||||||
|
}
|
||||||
|
free(logfiles);
|
||||||
|
free(sdms);
|
||||||
|
modbus_close(mb);
|
||||||
|
//broken: "free(): double free detected in tcache 2"
|
||||||
|
//modbus_free(mb);
|
||||||
|
|
||||||
|
mosquitto_lib_cleanup();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
12
sdm2mqtt.service
Normal file
12
sdm2mqtt.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=SDM-Series Energy Meter MQTT Bridge
|
||||||
|
After=multi-user.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$PWD
|
||||||
|
ExecStart=sdm2mqtt
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
95
sdm630m_registers.h
Normal file
95
sdm630m_registers.h
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#ifndef sdm630m_registers_h
|
||||||
|
#define sdm630m_registers_h
|
||||||
|
|
||||||
|
#define SMD630M_NRVAL 85
|
||||||
|
|
||||||
|
// 85 Messwerte
|
||||||
|
static sdm_registers_t sdm630m_registers[] = {
|
||||||
|
{ "L1-N Voltage", "V" , 0 },
|
||||||
|
{ "L2-N Voltage", "V" , 2 },
|
||||||
|
{ "L3-N Voltage", "V" , 4 },
|
||||||
|
{ "L1 Current", "A" , 6 },
|
||||||
|
{ "L2 Current", "A" , 8 },
|
||||||
|
{ "L3 Current", "A" , 10 },
|
||||||
|
{ "L1 Power", "W" , 12 },
|
||||||
|
{ "L2 Power", "W" , 14 },
|
||||||
|
{ "L3 Power", "W" , 16 },
|
||||||
|
{ "L1 Apparent Power", "VA" , 18 },
|
||||||
|
{ "L2 Apparent Power", "VA" , 20 },
|
||||||
|
{ "L3 Apparent Power", "VA" , 22 },
|
||||||
|
{ "L1 Reactive Power", "VAr" , 24 },
|
||||||
|
{ "L2 Reactive Power", "VAr" , 26 },
|
||||||
|
{ "L3 Reactive Power", "VAr" , 28 },
|
||||||
|
{ "L1 Power Factor", "1" , 30 },
|
||||||
|
{ "L2 Power Factor", "1" , 32 },
|
||||||
|
{ "L3 Power Factor", "1" , 34 },
|
||||||
|
{ "L1 Phase Angle", "°" , 36 },
|
||||||
|
{ "L2 Phase Angle", "°" , 38 },
|
||||||
|
{ "L3 Phase Angle", "°" , 40 },
|
||||||
|
{ "Average Voltage", "V" , 42 },
|
||||||
|
{ "Average Current", "A" , 46 },
|
||||||
|
{ "Current (Sum)", "A" , 48 },
|
||||||
|
{ "Total Power", "W" , 52 },
|
||||||
|
{ "Total Apparent Power", "VA" , 56 },
|
||||||
|
{ "Total Reactive Power", "VAr" , 60 },
|
||||||
|
{ "Total Power Factor", "1" , 62 },
|
||||||
|
{ "Total Phase Angle", "°" , 66 },
|
||||||
|
{ "Frequency", "Hz" , 70 },
|
||||||
|
{ "Energy Import", "kWh" , 72 },
|
||||||
|
{ "Energy Export", "kWh" , 74 },
|
||||||
|
{ "Reactive Energy Import", "kVArh" , 76 },
|
||||||
|
{ "Reactive Energy Export", "kVArh" , 78 },
|
||||||
|
{ "Total VAh", "kVAh" , 80 },
|
||||||
|
{ "Total Ah", "kAh" , 82 },
|
||||||
|
{ "Total Power Demand", "W" , 84 },
|
||||||
|
{ "Total Power Demand (Max)", "W" , 86 },
|
||||||
|
{ "Total Apparent Power Demand", "VA" , 100 },
|
||||||
|
{ "Total Apparent Power Demand (Max)", "VA" , 102 },
|
||||||
|
{ "Neutral Current Demand", "A" , 104 },
|
||||||
|
{ "Neutral Current Demand (Max)", "A" , 106 },
|
||||||
|
{ "L1-L2 Voltage", "V" , 200 },
|
||||||
|
{ "L2-L3 Voltage", "V" , 202 },
|
||||||
|
{ "L3-L1 Voltage", "V" , 204 },
|
||||||
|
{ "Average Line-Line Voltage", "V" , 206 },
|
||||||
|
{ "Neutral Current", "A" , 224 },
|
||||||
|
{ "L1-N THD (U)", "%" , 234 },
|
||||||
|
{ "L2-N THD (U)", "%" , 236 },
|
||||||
|
{ "L3-N THD (U)", "%" , 238 },
|
||||||
|
{ "L1 THD (I)", "%" , 240 },
|
||||||
|
{ "L2 THD (I)", "%" , 242 },
|
||||||
|
{ "L3 THD (I)", "%" , 244 },
|
||||||
|
{ "Average THD (U)", "%" , 248 },
|
||||||
|
{ "Average THD (I)", "%" , 250 },
|
||||||
|
{ "L1 Current Demand", "A" , 258 },
|
||||||
|
{ "L2 Current Demand", "A" , 260 },
|
||||||
|
{ "L3 Current Demand", "A" , 262 },
|
||||||
|
{ "L1 Current Demand (Max)", "A" , 264 },
|
||||||
|
{ "L2 Current Demand (Max)", "A" , 266 },
|
||||||
|
{ "L3 Current Demand (Max)", "A" , 268 },
|
||||||
|
{ "L1-L2 THD (U)", "%" , 334 },
|
||||||
|
{ "L2-L3 THD (U)", "%" , 336 },
|
||||||
|
{ "L3-L1 THD (U)", "%" , 338 },
|
||||||
|
{ "Average L-L THD", "%" , 340 },
|
||||||
|
{ "Total Energy", "kWh" , 342 },
|
||||||
|
{ "Total Reactive Energy", "kVArh" , 344 },
|
||||||
|
{ "L1 Energy Import", "kWh" , 346 },
|
||||||
|
{ "L2 Energy Import", "kWh" , 348 },
|
||||||
|
{ "L3 Energy Import", "kWh" , 350 },
|
||||||
|
{ "L1 Energy Export", "kWh" , 352 },
|
||||||
|
{ "L2 Energy Export", "kWh" , 354 },
|
||||||
|
{ "L3 Energy Export", "kWh" , 356 },
|
||||||
|
{ "L1 Energy (Total)", "kWh" , 358 },
|
||||||
|
{ "L2 Energy (Total)", "kWh" , 360 },
|
||||||
|
{ "L3 Energy (Total)", "kWh" , 362 },
|
||||||
|
{ "L1 Reactive Energy Import", "kVArh" , 364 },
|
||||||
|
{ "L2 Reactive Energy Import", "kVArh" , 366 },
|
||||||
|
{ "L3 Reactive Energy Import", "kVArh" , 368 },
|
||||||
|
{ "L1 Reactive Energy Export", "kVArh" , 370 },
|
||||||
|
{ "L2 Reactive Energy Export", "kVArh" , 372 },
|
||||||
|
{ "L3 Reactive Energy Export", "kVArh" , 374 },
|
||||||
|
{ "L1 Reactive Energy (Total)", "kVArh" , 376 },
|
||||||
|
{ "L2 Reactive Energy (Total)", "kVArh" , 378 },
|
||||||
|
{ "L3 Reactive Energy (Total)", "kVArh" , 380 },
|
||||||
|
{ NULL, NULL , 0 }
|
||||||
|
};
|
||||||
|
#endif
|
20
sdm72dm_registers.h
Normal file
20
sdm72dm_registers.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#ifndef sdm72dm_registers_h
|
||||||
|
#define sdm72dm_registers_h
|
||||||
|
|
||||||
|
#define SMD72DM_NRVAL 9
|
||||||
|
|
||||||
|
|
||||||
|
// 9 Messwerte
|
||||||
|
static sdm_registers_t sdm72dm_registers[] = {
|
||||||
|
{ "Power", "W" , 52 },
|
||||||
|
{ "Energy Import", "kWh" , 72 },
|
||||||
|
{ "Energy Export", "kWh" , 74 },
|
||||||
|
{ "Energy (Total)", "kWh" , 342 },
|
||||||
|
{ "Total Energy (resettable)", "kWh" , 384 },
|
||||||
|
{ "Energy Import (resettable)", "kWh" , 388 },
|
||||||
|
{ "Energy Export (resettable)", "kWh" , 390 },
|
||||||
|
{ "Power Import", "W" , 1280 },
|
||||||
|
{ "Power Export", "W" , 1282 },
|
||||||
|
{ NULL, NULL , 0 }
|
||||||
|
};
|
||||||
|
#endif
|
24
telegraf/sdm2mqtt.conf
Normal file
24
telegraf/sdm2mqtt.conf
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# # Read metrics from MQTT topic(s)
|
||||||
|
[[inputs.mqtt_consumer]]
|
||||||
|
servers = ["tcp://127.0.0.1:1883"]
|
||||||
|
topics = [
|
||||||
|
"sdm2mqtt2/#",
|
||||||
|
]
|
||||||
|
name_override = "sdm"
|
||||||
|
|
||||||
|
# ## Username and password to connect MQTT server.
|
||||||
|
# # username = "telegraf"
|
||||||
|
# # password = "metricsmetricsmetricsmetrics"
|
||||||
|
#
|
||||||
|
# ## Optional TLS Config
|
||||||
|
# # tls_ca = "/etc/telegraf/ca.pem"
|
||||||
|
# # tls_cert = "/etc/telegraf/cert.pem"
|
||||||
|
# # tls_key = "/etc/telegraf/key.pem"
|
||||||
|
# ## Use TLS but skip chain & host verification
|
||||||
|
# # insecure_skip_verify = false
|
||||||
|
#
|
||||||
|
data_format = "json_v2"
|
||||||
|
[[inputs.mqtt_consumer.json_v2]]
|
||||||
|
[[inputs.mqtt_consumer.json_v2.object]]
|
||||||
|
path = "@this"
|
||||||
|
tags = [ "name", "address", "type" ]
|
Loading…
x
Reference in New Issue
Block a user