From: Martin Mares Date: Tue, 16 Jul 2019 21:43:39 +0000 (+0200) Subject: Power daemon: A new daemon for relaying power meter data to MQTT X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=71d8e81533dc24d06cae545edac06c0fe393b374;p=home-hw.git Power daemon: A new daemon for relaying power meter data to MQTT --- diff --git a/power/Makefile b/power/Makefile deleted file mode 100644 index 973d6e6..0000000 --- a/power/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -TOPDIR=/root/turris - -include $(TOPDIR)/rules.mk -include $(TOPDIR)/include/package.mk - -PC := PATH=$(STAGING_DIR_HOST)/bin:$(PATH) PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_LIBDIR=$(PKG_CONFIG_PATH) STAGING_PREFIX=$(STAGING_DIR)/usr $(PKG_CONFIG) -MB_CFLAGS := $(shell $(PC) --cflags libmodbus) -MB_LDFLAGS := $(shell $(PC) --libs libmodbus) - -export PATH=$(TARGET_PATH_PKG) -CC=$(TARGET_CC_NOCACHE) -LD=$(TARGET_LD_NOCACHE) -CFLAGS=$(TARGET_CFLAGS) $(MB_CFLAGS) -std=gnu99 -LDFLAGS=$(TARGET_LDFLAGS) $(MB_LDFLAGS) - -all: power-cgi upload - -power-cgi: power-cgi.c - -upload: - rsync -av power-cgi micac:/www/burrow/power.cgi diff --git a/power/cgi/Makefile b/power/cgi/Makefile new file mode 100644 index 0000000..973d6e6 --- /dev/null +++ b/power/cgi/Makefile @@ -0,0 +1,21 @@ +TOPDIR=/root/turris + +include $(TOPDIR)/rules.mk +include $(TOPDIR)/include/package.mk + +PC := PATH=$(STAGING_DIR_HOST)/bin:$(PATH) PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_LIBDIR=$(PKG_CONFIG_PATH) STAGING_PREFIX=$(STAGING_DIR)/usr $(PKG_CONFIG) +MB_CFLAGS := $(shell $(PC) --cflags libmodbus) +MB_LDFLAGS := $(shell $(PC) --libs libmodbus) + +export PATH=$(TARGET_PATH_PKG) +CC=$(TARGET_CC_NOCACHE) +LD=$(TARGET_LD_NOCACHE) +CFLAGS=$(TARGET_CFLAGS) $(MB_CFLAGS) -std=gnu99 +LDFLAGS=$(TARGET_LDFLAGS) $(MB_LDFLAGS) + +all: power-cgi upload + +power-cgi: power-cgi.c + +upload: + rsync -av power-cgi micac:/www/burrow/power.cgi diff --git a/power/cgi/power-cgi.c b/power/cgi/power-cgi.c new file mode 100644 index 0000000..ac0f361 --- /dev/null +++ b/power/cgi/power-cgi.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include + +typedef unsigned int uint; + +void die(const char *msg, ...) +{ + va_list args; + va_start(args, msg); + vsyslog(LOG_ERR, msg, args); + // vfprintf(stderr, msg, args); + // fputc('\n', stderr); + printf("Status: 500\n"); + printf("Content-Type: text/plain\n\n"); + printf("Internal error, see syslog.\n"); + exit(0); +} + +static uint16_t reg[22]; + +int32_t get_s32(uint i) +{ + if (reg[i+1] < 0x8000) + return (reg[i+1] << 16) | reg[i]; + else + return ((reg[i+1] - 65536) * 65536) + reg[i]; +} + +int main(void) +{ + openlog("power.cgi", LOG_PID, LOG_LOCAL0); + + modbus_t *ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); + if (!ctx) + die("Unable to establish Modbus context"); + modbus_set_slave(ctx, 42); + if (modbus_connect(ctx) < 0) + die("Unable to connect to Modbus"); + + if (modbus_read_registers(ctx, 0, 22, reg) < 0) + die("Modbus read failed"); + + printf("Content-type: text/plain; version=0.0.4\n\n"); + + printf("# HELP pm_voltage Voltage between phases and neutral [V]\n"); + printf("# TYPE pm_voltage gauge\n"); + printf("pm_voltage{phase=\"L1N\"} %.1f\n", get_s32(0) / 10.); + printf("pm_voltage{phase=\"L2N\"} %.1f\n", get_s32(2) / 10.); + printf("pm_voltage{phase=\"L3N\"} %.1f\n", get_s32(4) / 10.); + + printf("# HELP pm_current Current through phases [A]\n"); + printf("# TYPE pm_current gauge\n"); + printf("pm_current{phase=\"L1\"} %.3f\n", get_s32(6) / 1000.); + printf("pm_current{phase=\"L2\"} %.3f\n", get_s32(8) / 1000.); + printf("pm_current{phase=\"L3\"} %.3f\n", get_s32(10) / 1000.); + + printf("# HELP pm_power Total power [W]\n"); + printf("# TYPE pm_power gauge\n"); + printf("pm_power %.1f\n", get_s32(12) / 10.); + + printf("# HELP pm_energy Total energy [kWh]\n"); + printf("# TYPE pm_energy gauge\n"); + printf("pm_energy %.1f\n", get_s32(14) / 10.); + + printf("# HELP pm_reactive_power Reactive power [VAr]\n"); + printf("# TYPE pm_reactive_power gauge\n"); + printf("pm_reactive_power %.1f\n", get_s32(18) / 10.); + + printf("# HELP pm_reactive_energy Total reactive energy [kVArh]\n"); + printf("# TYPE pm_reactive_energy gauge\n"); + printf("pm_reactive_energy %.1f\n", get_s32(20) / 10.); + + return 0; +} diff --git a/power/daemon/Makefile b/power/daemon/Makefile new file mode 100644 index 0000000..b779cc9 --- /dev/null +++ b/power/daemon/Makefile @@ -0,0 +1,23 @@ +TOPDIR=/root/turris + +include $(TOPDIR)/rules.mk +include $(TOPDIR)/include/package.mk + +PC := PATH=$(STAGING_DIR_HOST)/bin:$(PATH) PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_LIBDIR=$(PKG_CONFIG_PATH) STAGING_PREFIX=$(STAGING_DIR)/usr $(PKG_CONFIG) +MB_CFLAGS := $(shell $(PC) --cflags libmodbus) +MB_LDFLAGS := $(shell $(PC) --libs libmodbus) +UCW_CFLAGS := $(shell $(PC) --cflags libucw) +UCW_LDFLAGS := $(shell $(PC) --libs libucw) + +export PATH=$(TARGET_PATH_PKG) +CC=$(TARGET_CC_NOCACHE) +LD=$(TARGET_LD_NOCACHE) +CFLAGS=$(TARGET_CFLAGS) $(MB_CFLAGS) $(UCW_CFLAGS) -std=gnu99 +LDFLAGS=$(TARGET_LDFLAGS) $(MB_LDFLAGS) $(UCW_LDFLAGS) -lmosquitto + +all: burrow-powerd upload + +burrow-powerd: burrow-powerd.c + +upload: + rsync -av burrow-powerd micac:/root/burrow/ diff --git a/power/daemon/burrow-powerd.c b/power/daemon/burrow-powerd.c new file mode 100644 index 0000000..93e37bb --- /dev/null +++ b/power/daemon/burrow-powerd.c @@ -0,0 +1,229 @@ +/* + * A MQTT Gateway Daemon for the Power Meter + * + * (c) 2019 Martin Mares + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/*** MQTT ***/ + +static struct mosquitto *mosq; +static bool mqtt_connected; + +static void mqtt_error(const char *operation, int err, bool teardown) +{ + msg(L_ERROR, "Mosquitto: %s failed: error %d", operation, err); + + if (teardown) { + mosquitto_destroy(mosq); + mosq = NULL; + mqtt_connected = false; + } else if (err == MOSQ_ERR_NO_CONN || err == MOSQ_ERR_CONN_REFUSED || err == MOSQ_ERR_CONN_LOST) { + mqtt_connected = false; + } +} + +static void mqtt_publish(const char *topic, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + + if (mqtt_connected) { + char m[256]; + int l = vsnprintf(m, sizeof(m), fmt, args); + msg(L_DEBUG, "MQTT > %s %s", topic, m); + int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true); + if (err != MOSQ_ERR_SUCCESS) + mqtt_error("publish", err, false); + } + + va_end(args); +} + +static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message) +{ + // msg(L_INFO, "MQTT(%d): %s", level, message); +} + +static bool mqtt_connect(void) +{ + int err; + + if (mqtt_connected) + return true; + + if (!mosq) { + mosq = mosquitto_new("powerd", 1, NULL); + if (!mosq) + die("Mosquitto: initialization failed"); + + mosquitto_log_callback_set(mosq, mqtt_log_callback); + + err = mosquitto_will_set(mosq, "status/power-meter", 4, "dead", 0, true); + if (err != MOSQ_ERR_SUCCESS) { + mqtt_error("will_set", err, true); + return false; + } + + err = mosquitto_connect(mosq, "127.0.0.1", 1883, 60); + if (err != MOSQ_ERR_SUCCESS) { + mqtt_error("connect", err, true); + return false; + } + } else { + err = mosquitto_reconnect(mosq); + if (err != MOSQ_ERR_SUCCESS) { + mqtt_error("reconnect", err, false); + return false; + } + } + + mqtt_connected = true; + + mqtt_publish("status/power-meter", "ok"); + + return mqtt_connected; +} + +/*** MODBUS ***/ + +static modbus_t *modbus; +static bool mb_is_open; + +static void mb_error(const char *operation) +{ + msg(L_ERROR, "MODBUS: %s failed: %s", operation, modbus_strerror(errno)); + + if (modbus) { + if (mb_is_open) { + modbus_close(modbus); + mb_is_open = false; + } + modbus_free(modbus); + modbus = NULL; + } +} + +static bool mb_connect(void) +{ + if (modbus) + return true; + + // FIXME: Find the right device. Reconnect if needed. + modbus = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); + if (!modbus) { + mb_error("open"); + return false; + } + + modbus_set_slave(modbus, 42); + + if (modbus_connect(modbus) < 0) { + mb_error("connect"); + return false; + } + + mb_is_open = true; +} + +/*** Main loop ***/ + +static u16 mb_regs[22]; + +s32 get_s32(uint i) +{ + if (mb_regs[i+1] < 0x8000) + return (mb_regs[i+1] << 16) | mb_regs[i]; + else + return ((mb_regs[i+1] - 65536) * 65536) + mb_regs[i]; +} + +static void scan_power_meter(time_t now) +{ + long int lnow = now; + + if (modbus_read_registers(modbus, 0, 22, mb_regs) < 0) { + mb_error("read"); + return; + } + + mqtt_publish("burrow/power/voltage/l1n", "%.1f %ld", get_s32(0) / 10., lnow); + mqtt_publish("burrow/power/voltage/l2n", "%.1f %ld", get_s32(2) / 10., lnow); + mqtt_publish("burrow/power/voltage/l3n", "%.1f %ld", get_s32(4) / 10., lnow); + + mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(6) / 1000., lnow); + mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(8) / 1000., lnow); + mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(10) / 1000., lnow); + + mqtt_publish("burrow/power/power", "%.1f %ld", get_s32(12) / 10., lnow); + mqtt_publish("burrow/power/energy", "%.1f %ld", get_s32(14) / 10., lnow); + + mqtt_publish("burrow/power/reactive/power", "%.1f %ld", get_s32(18) / 10., lnow); + mqtt_publish("burrow/power/reactive/energy", "%.1f %ld", get_s32(20) / 10., lnow); +} + +static int use_daemon; +static int use_debug; + +static struct opt_section options = { + OPT_ITEMS { + OPT_HELP("A daemon for sending power meter readings to MQTT"), + OPT_HELP(""), + OPT_HELP("Options:"), + OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"), + OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"), + OPT_HELP_OPTION, + OPT_CONF_OPTIONS, + OPT_END + } +}; + +int main(int argc UNUSED, char **argv) +{ + log_init(argv[0]); + opt_parse(&options, argv+1); + + if (use_daemon) { + struct log_stream *ls = log_new_syslog("daemon", LOG_PID); + log_set_default_stream(ls); + } + if (!use_debug) + log_default_stream()->levels &= ~(1U << L_DEBUG); + + mosquitto_lib_init(); + + time_t next_run = 0; + for (;;) { + if (!mqtt_connect() || !mb_connect()) { + sleep(5); + continue; + } + + time_t now = time(NULL); + if (now < next_run) { + int err = mosquitto_loop(mosq, (next_run - now) * 1000, 1); + if (err != MOSQ_ERR_SUCCESS) + mqtt_error("loop", err, false); + continue; + } + + next_run = now + 10; + scan_power_meter(now); + } +} diff --git a/power/power-cgi.c b/power/power-cgi.c deleted file mode 100644 index ac0f361..0000000 --- a/power/power-cgi.c +++ /dev/null @@ -1,77 +0,0 @@ -#include -#include -#include -#include -#include - -typedef unsigned int uint; - -void die(const char *msg, ...) -{ - va_list args; - va_start(args, msg); - vsyslog(LOG_ERR, msg, args); - // vfprintf(stderr, msg, args); - // fputc('\n', stderr); - printf("Status: 500\n"); - printf("Content-Type: text/plain\n\n"); - printf("Internal error, see syslog.\n"); - exit(0); -} - -static uint16_t reg[22]; - -int32_t get_s32(uint i) -{ - if (reg[i+1] < 0x8000) - return (reg[i+1] << 16) | reg[i]; - else - return ((reg[i+1] - 65536) * 65536) + reg[i]; -} - -int main(void) -{ - openlog("power.cgi", LOG_PID, LOG_LOCAL0); - - modbus_t *ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); - if (!ctx) - die("Unable to establish Modbus context"); - modbus_set_slave(ctx, 42); - if (modbus_connect(ctx) < 0) - die("Unable to connect to Modbus"); - - if (modbus_read_registers(ctx, 0, 22, reg) < 0) - die("Modbus read failed"); - - printf("Content-type: text/plain; version=0.0.4\n\n"); - - printf("# HELP pm_voltage Voltage between phases and neutral [V]\n"); - printf("# TYPE pm_voltage gauge\n"); - printf("pm_voltage{phase=\"L1N\"} %.1f\n", get_s32(0) / 10.); - printf("pm_voltage{phase=\"L2N\"} %.1f\n", get_s32(2) / 10.); - printf("pm_voltage{phase=\"L3N\"} %.1f\n", get_s32(4) / 10.); - - printf("# HELP pm_current Current through phases [A]\n"); - printf("# TYPE pm_current gauge\n"); - printf("pm_current{phase=\"L1\"} %.3f\n", get_s32(6) / 1000.); - printf("pm_current{phase=\"L2\"} %.3f\n", get_s32(8) / 1000.); - printf("pm_current{phase=\"L3\"} %.3f\n", get_s32(10) / 1000.); - - printf("# HELP pm_power Total power [W]\n"); - printf("# TYPE pm_power gauge\n"); - printf("pm_power %.1f\n", get_s32(12) / 10.); - - printf("# HELP pm_energy Total energy [kWh]\n"); - printf("# TYPE pm_energy gauge\n"); - printf("pm_energy %.1f\n", get_s32(14) / 10.); - - printf("# HELP pm_reactive_power Reactive power [VAr]\n"); - printf("# TYPE pm_reactive_power gauge\n"); - printf("pm_reactive_power %.1f\n", get_s32(18) / 10.); - - printf("# HELP pm_reactive_energy Total reactive energy [kVArh]\n"); - printf("# TYPE pm_reactive_energy gauge\n"); - printf("pm_reactive_energy %.1f\n", get_s32(20) / 10.); - - return 0; -}