]> mj.ucw.cz Git - home-hw.git/commitdiff
Power daemon: A new daemon for relaying power meter data to MQTT
authorMartin Mares <mj@ucw.cz>
Tue, 16 Jul 2019 21:43:39 +0000 (23:43 +0200)
committerMartin Mares <mj@ucw.cz>
Tue, 16 Jul 2019 21:43:39 +0000 (23:43 +0200)
power/Makefile [deleted file]
power/cgi/Makefile [new file with mode: 0644]
power/cgi/power-cgi.c [new file with mode: 0644]
power/daemon/Makefile [new file with mode: 0644]
power/daemon/burrow-powerd.c [new file with mode: 0644]
power/power-cgi.c [deleted file]

diff --git a/power/Makefile b/power/Makefile
deleted file mode 100644 (file)
index 973d6e6..0000000
+++ /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 (file)
index 0000000..973d6e6
--- /dev/null
@@ -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 (file)
index 0000000..ac0f361
--- /dev/null
@@ -0,0 +1,77 @@
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <syslog.h>
+#include <modbus.h>
+
+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 (file)
index 0000000..b779cc9
--- /dev/null
@@ -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 (file)
index 0000000..93e37bb
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ *     A MQTT Gateway Daemon for the Power Meter
+ *
+ *     (c) 2019 Martin Mares <mj@ucw.cz>
+ */
+
+#include <ucw/lib.h>
+#include <ucw/log.h>
+#include <ucw/opt.h>
+#include <ucw/strtonum.h>
+#include <ucw/stkstring.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include <modbus.h>
+#include <mosquitto.h>
+
+/*** 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 (file)
index ac0f361..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-#include <stdio.h>
-#include <stdarg.h>
-#include <stdlib.h>
-#include <syslog.h>
-#include <modbus.h>
-
-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;
-}