From 71d8e81533dc24d06cae545edac06c0fe393b374 Mon Sep 17 00:00:00 2001 From: Martin Mares Date: Tue, 16 Jul 2019 23:43:39 +0200 Subject: [PATCH] Power daemon: A new daemon for relaying power meter data to MQTT --- power/{ => cgi}/Makefile | 0 power/{ => cgi}/power-cgi.c | 0 power/daemon/Makefile | 23 ++++ power/daemon/burrow-powerd.c | 229 +++++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) rename power/{ => cgi}/Makefile (100%) rename power/{ => cgi}/power-cgi.c (100%) create mode 100644 power/daemon/Makefile create mode 100644 power/daemon/burrow-powerd.c diff --git a/power/Makefile b/power/cgi/Makefile similarity index 100% rename from power/Makefile rename to power/cgi/Makefile diff --git a/power/power-cgi.c b/power/cgi/power-cgi.c similarity index 100% rename from power/power-cgi.c rename to power/cgi/power-cgi.c 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); + } +} -- 2.39.5