]> mj.ucw.cz Git - home-hw.git/blob - power/daemon/burrow-powerd.c
Power daemon: A new daemon for relaying power meter data to MQTT
[home-hw.git] / power / daemon / burrow-powerd.c
1 /*
2  *      A MQTT Gateway Daemon for the Power Meter
3  *
4  *      (c) 2019 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <ucw/lib.h>
8 #include <ucw/log.h>
9 #include <ucw/opt.h>
10 #include <ucw/strtonum.h>
11 #include <ucw/stkstring.h>
12
13 #include <errno.h>
14 #include <stdio.h>
15 #include <stdarg.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <syslog.h>
19 #include <unistd.h>
20
21 #include <modbus.h>
22 #include <mosquitto.h>
23
24 /*** MQTT ***/
25
26 static struct mosquitto *mosq;
27 static bool mqtt_connected;
28
29 static void mqtt_error(const char *operation, int err, bool teardown)
30 {
31         msg(L_ERROR, "Mosquitto: %s failed: error %d", operation, err);
32
33         if (teardown) {
34                 mosquitto_destroy(mosq);
35                 mosq = NULL;
36                 mqtt_connected = false;
37         } else if (err == MOSQ_ERR_NO_CONN || err == MOSQ_ERR_CONN_REFUSED || err == MOSQ_ERR_CONN_LOST) {
38                 mqtt_connected = false;
39         }
40 }
41
42 static void mqtt_publish(const char *topic, const char *fmt, ...)
43 {
44         va_list args;
45         va_start(args, fmt);
46
47         if (mqtt_connected) {
48                 char m[256];
49                 int l = vsnprintf(m, sizeof(m), fmt, args);
50                 msg(L_DEBUG, "MQTT > %s %s", topic, m);
51                 int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
52                 if (err != MOSQ_ERR_SUCCESS)
53                         mqtt_error("publish", err, false);
54         }
55
56         va_end(args);
57 }
58
59 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
60 {
61         // msg(L_INFO, "MQTT(%d): %s", level, message);
62 }
63
64 static bool mqtt_connect(void)
65 {
66         int err;
67
68         if (mqtt_connected)
69                 return true;
70
71         if (!mosq) {
72                 mosq = mosquitto_new("powerd", 1, NULL);
73                 if (!mosq)
74                         die("Mosquitto: initialization failed");
75
76                 mosquitto_log_callback_set(mosq, mqtt_log_callback);
77
78                 err = mosquitto_will_set(mosq, "status/power-meter", 4, "dead", 0, true);
79                 if (err != MOSQ_ERR_SUCCESS) {
80                         mqtt_error("will_set", err, true);
81                         return false;
82                 }
83
84                 err = mosquitto_connect(mosq, "127.0.0.1", 1883, 60);
85                 if (err != MOSQ_ERR_SUCCESS) {
86                         mqtt_error("connect", err, true);
87                         return false;
88                 }
89         } else {
90                 err = mosquitto_reconnect(mosq);
91                 if (err != MOSQ_ERR_SUCCESS) {
92                         mqtt_error("reconnect", err, false);
93                         return false;
94                 }
95         }
96
97         mqtt_connected = true;
98
99         mqtt_publish("status/power-meter", "ok");
100
101         return mqtt_connected;
102 }
103
104 /*** MODBUS ***/
105
106 static modbus_t *modbus;
107 static bool mb_is_open;
108
109 static void mb_error(const char *operation)
110 {
111         msg(L_ERROR, "MODBUS: %s failed: %s", operation, modbus_strerror(errno));
112
113         if (modbus) {
114                 if (mb_is_open) {
115                         modbus_close(modbus);
116                         mb_is_open = false;
117                 }
118                 modbus_free(modbus);
119                 modbus = NULL;
120         }
121 }
122
123 static bool mb_connect(void)
124 {
125         if (modbus)
126                 return true;
127
128         // FIXME: Find the right device. Reconnect if needed.
129         modbus = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
130         if (!modbus) {
131                 mb_error("open");
132                 return false;
133         }
134
135         modbus_set_slave(modbus, 42);
136
137         if (modbus_connect(modbus) < 0) {
138                 mb_error("connect");
139                 return false;
140         }
141
142         mb_is_open = true;
143 }
144
145 /*** Main loop ***/
146
147 static u16 mb_regs[22];
148
149 s32 get_s32(uint i)
150 {
151         if (mb_regs[i+1] < 0x8000)
152                 return (mb_regs[i+1] << 16) | mb_regs[i];
153         else
154                 return ((mb_regs[i+1] - 65536) * 65536) + mb_regs[i];
155 }
156
157 static void scan_power_meter(time_t now)
158 {
159         long int lnow = now;
160
161         if (modbus_read_registers(modbus, 0, 22, mb_regs) < 0) {
162                 mb_error("read");
163                 return;
164         }
165
166         mqtt_publish("burrow/power/voltage/l1n", "%.1f %ld", get_s32(0) / 10., lnow);
167         mqtt_publish("burrow/power/voltage/l2n", "%.1f %ld", get_s32(2) / 10., lnow);
168         mqtt_publish("burrow/power/voltage/l3n", "%.1f %ld", get_s32(4) / 10., lnow);
169
170         mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(6) / 1000., lnow);
171         mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(8) / 1000., lnow);
172         mqtt_publish("burrow/power/current/l1", "%.3f %ld", get_s32(10) / 1000., lnow);
173
174         mqtt_publish("burrow/power/power", "%.1f %ld", get_s32(12) / 10., lnow);
175         mqtt_publish("burrow/power/energy", "%.1f %ld", get_s32(14) / 10., lnow);
176
177         mqtt_publish("burrow/power/reactive/power", "%.1f %ld", get_s32(18) / 10., lnow);
178         mqtt_publish("burrow/power/reactive/energy", "%.1f %ld", get_s32(20) / 10., lnow);
179 }
180
181 static int use_daemon;
182 static int use_debug;
183
184 static struct opt_section options = {
185         OPT_ITEMS {
186                 OPT_HELP("A daemon for sending power meter readings to MQTT"),
187                 OPT_HELP(""),
188                 OPT_HELP("Options:"),
189                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
190                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
191                 OPT_HELP_OPTION,
192                 OPT_CONF_OPTIONS,
193                 OPT_END
194         }
195 };
196
197 int main(int argc UNUSED, char **argv)
198 {
199         log_init(argv[0]);
200         opt_parse(&options, argv+1);
201
202         if (use_daemon) {
203                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
204                 log_set_default_stream(ls);
205         }
206         if (!use_debug)
207                 log_default_stream()->levels &= ~(1U << L_DEBUG);
208
209         mosquitto_lib_init();
210
211         time_t next_run = 0;
212         for (;;) {
213                 if (!mqtt_connect() || !mb_connect()) {
214                         sleep(5);
215                         continue;
216                 }
217
218                 time_t now = time(NULL);
219                 if (now < next_run) {
220                         int err = mosquitto_loop(mosq, (next_run - now) * 1000, 1);
221                         if (err != MOSQ_ERR_SUCCESS)
222                                 mqtt_error("loop", err, false);
223                         continue;
224                 }
225
226                 next_run = now + 10;
227                 scan_power_meter(now);
228         }
229 }