+++ /dev/null
-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
--- /dev/null
+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
--- /dev/null
+#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;
+}
--- /dev/null
+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/
--- /dev/null
+/*
+ * 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);
+ }
+}
+++ /dev/null
-#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;
-}