2 * A gateway between MQTT and InfluxDB
4 * (c) 2018--2020 Martin Mares <mj@ucw.cz>
8 #include <ucw/fastbuf.h>
11 #include <ucw/string.h>
21 #include <curl/curl.h>
22 #include <mosquitto.h>
24 #define MEASUREMENT_TIMEOUT 120
26 #define INFLUX_URL "http://localhost:8086/write?db=burrow&precision=s"
27 #define INFLUX_TIMEOUT 30
28 #define INFLUX_VERBOSE 0
29 #define INFLUX_INTERVAL 60
31 static struct mosquitto *mosq;
35 const char *value_name;
41 static const struct attr attr_table[] = {
43 .metric = "temp,where=loft",
45 .topic = "burrow/temp/loft",
48 .metric = "temp,where=ursarium",
50 .topic = "burrow/temp/ursarium"
53 .metric = "temp,where=catarium",
55 .topic = "burrow/temp/catarium"
58 .metric = "temp,where=garage",
60 .topic = "burrow/temp/garage"
63 .metric = "temp,where=terarium",
65 .topic = "burrow/temp/terarium"
68 .metric = "rh,where=ursarium",
70 .topic = "burrow/temp/ursarium-rh"
73 .metric = "temp,where=catarium_clock",
75 .topic = "burrow/temp/clock"
78 .metric = "pressure,where=catarium_clock",
80 .topic = "burrow/pressure/clock"
83 .metric = "air,where=inside_intake",
85 .topic = "burrow/air/inside-intake",
88 .metric = "air,where=inside_exhaust",
90 .topic = "burrow/air/inside-exhaust",
93 .metric = "air,where=outside_intake",
95 .topic = "burrow/air/outside-intake",
98 .metric = "air,where=outside_exhaust",
100 .topic = "burrow/air/outside-exhaust",
103 .metric = "air,where=mixed",
105 .topic = "burrow/air/mixed",
108 .metric = "air,where=inside_intake_avg",
110 .topic = "burrow/avg/air/inside-intake",
113 .metric = "air,where=outside_intake_avg",
115 .topic = "burrow/avg/air/outside-intake",
118 .metric = "air_bypass",
120 .topic = "burrow/air/bypass"
123 .metric = "air_fan_pwm",
125 .topic = "burrow/air/exchanger-fan"
128 .metric = "loft_fan",
130 .topic = "burrow/loft/fan"
133 .metric = "water_circ",
135 .topic = "burrow/loft/circulation"
138 .metric = "pm_voltage,phase=L1N",
140 .topic = "burrow/power/voltage/l1n",
143 .metric = "pm_voltage,phase=L2N",
145 .topic = "burrow/power/voltage/l2n",
148 .metric = "pm_voltage,phase=L3N",
150 .topic = "burrow/power/voltage/l3n",
153 .metric = "pm_current,phase=L1",
155 .topic = "burrow/power/current/l1",
158 .metric = "pm_current,phase=L2",
160 .topic = "burrow/power/current/l2",
163 .metric = "pm_current,phase=L3",
165 .topic = "burrow/power/current/l3",
168 .metric = "pm_power",
170 .topic = "burrow/power/power",
173 .metric = "pm_energy",
175 .topic = "burrow/power/energy",
178 .metric = "pm_reactive_power",
180 .topic = "burrow/power/reactive/power",
183 .metric = "pm_reactive_energy",
184 .value_name = "kVArh",
185 .topic = "burrow/power/reactive/energy",
188 .metric = "heating_water_pressure",
190 .topic = "burrow/heating/water-pressure",
194 .metric = "heating_outside_temp",
196 .topic = "burrow/heating/outside-temp",
200 .metric = "heating_room_temp,circuit=1",
202 .topic = "burrow/heating/circuit1/room-temp",
206 .metric = "heating_mix-temp,circuit=1",
208 .topic = "burrow/heating/circuit1/mix-temp",
212 .metric = "heating_mix-valve,circuit=1",
214 .topic = "burrow/heating/circuit1/mix-valve",
218 .metric = "heating_active,circuit=1",
220 .topic = "burrow/heating/circuit1/active",
224 .metric = "heating_pump_active,circuit=1",
226 .topic = "burrow/heating/circuit1/pump",
230 .metric = "heating_room_temp,circuit=2",
232 .topic = "burrow/heating/circuit2/room-temp",
236 .metric = "heating_active,circuit=2",
238 .topic = "burrow/heating/circuit2/active",
242 .metric = "heating_active,circuit=water",
244 .topic = "burrow/heating/water/active",
248 .metric = "heating_error",
250 .topic = "burrow/heating/error",
254 .metric = "heating_clock",
256 .topic = "burrow/heating/clock",
264 static char *attr_values[ARRAY_SIZE(attr_table)];
265 static pthread_mutex_t attr_mutex = PTHREAD_MUTEX_INITIALIZER;
267 static void mqtt_publish(const char *topic, const char *fmt, ...)
272 int l = vsnprintf(m, sizeof(m), fmt, args);
273 if (mosquitto_publish(mosq, NULL, topic, l, m, 0, true) != MOSQ_ERR_SUCCESS)
274 msg(L_ERROR, "Mosquitto: publish failed");
278 static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status)
281 msg(L_DEBUG, "MQTT: Connection established, subscribing");
282 if (mosquitto_subscribe(mosq, NULL, "burrow/#", 1) != MOSQ_ERR_SUCCESS)
283 die("Mosquitto: subscribe failed");
285 mqtt_publish("status/influx", "ok");
289 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
291 // msg(L_INFO, "MQTT(%d): %s", level, message);
294 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
297 if (m->payloadlen >= sizeof(val) - 1) {
298 msg(L_ERROR, "Invalid value for topic %s", m->topic);
301 memcpy(val, m->payload, m->payloadlen);
302 val[m->payloadlen] = 0;
303 msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
305 for (uint i=0; i < ARRAY_SIZE(attr_table); i++) {
306 if (attr_table[i].topic && !strcmp(attr_table[i].topic, m->topic)) {
307 pthread_mutex_lock(&attr_mutex);
308 if (attr_values[i]) {
309 xfree(attr_values[i]);
310 attr_values[i] = NULL;
313 attr_values[i] = xstrdup(val);
314 pthread_mutex_unlock(&attr_mutex);
323 static struct fastbuf *influx_fb;
324 static struct fastbuf *influx_err_fb;
326 static size_t influx_write_callback(char *ptr, size_t size, size_t nmemb, void *userdata UNUSED)
329 for (size_t i=0; i<nmemb; i++) {
330 int c = *(byte *)ptr;
336 else if (c < 0x20 || c > 0x7e)
338 bputc(influx_err_fb, c);
343 static void influx_init(void)
345 curl = curl_easy_init();
347 die("libcurl init failed");
349 curl_easy_setopt(curl, CURLOPT_URL, INFLUX_URL);
350 curl_easy_setopt(curl, CURLOPT_VERBOSE, (long) INFLUX_VERBOSE);
351 curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long) INFLUX_TIMEOUT);
352 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, influx_write_callback);
354 influx_fb = fbgrow_create(4096);
355 influx_err_fb = fbgrow_create(256);
358 static struct fastbuf *influx_write_start(void)
360 fbgrow_reset(influx_fb);
364 static void influx_write_flush(void)
368 fbgrow_get_buf(influx_fb, &buf);
369 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf);
371 msg(L_DEBUG, "InfluxDB: Sending data (%u bytes)", strlen(buf));
373 fprintf(stderr, "%s", buf);
375 fbgrow_reset(influx_err_fb);
377 CURLcode code = curl_easy_perform(curl);
379 if (code != CURLE_OK) {
380 msg(L_ERROR, "Write to InfluxDB failed: %s", curl_easy_strerror(code));
385 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc);
386 if (rc != 200 && rc != 204) {
387 bputc(influx_err_fb, 0);
389 fbgrow_get_buf(influx_err_fb, &err);
390 msg(L_DEBUG, "InfluxDB HTTP error %ld: %s [...]", rc, err);
396 static int use_daemon;
397 static int use_debug;
399 static struct opt_section options = {
401 OPT_HELP("A daemon for transferring MQTT data to InfluxDB"),
403 OPT_HELP("Options:"),
404 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
405 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
412 int main(int argc UNUSED, char **argv)
415 opt_parse(&options, argv+1);
418 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
419 log_set_default_stream(ls);
422 log_default_stream()->levels &= ~(1U << L_DEBUG);
424 mosquitto_lib_init();
425 mosq = mosquitto_new("influx", 1, NULL);
427 die("Mosquitto: initialization failed");
429 mosquitto_connect_callback_set(mosq, mqtt_conn_callback);
430 mosquitto_log_callback_set(mosq, mqtt_log_callback);
431 mosquitto_message_callback_set(mosq, mqtt_msg_callback);
433 if (mosquitto_will_set(mosq, "status/influx", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
434 die("Mosquitto: unable to set will");
436 if (mosquitto_tls_set(mosq, "/etc/burrow-mqtt/ca.crt", NULL, "/etc/burrow-mqtt/client.crt", "/etc/burrow-mqtt/client.key", NULL) != MOSQ_ERR_SUCCESS)
437 die("Mosquitto: unable to set TLS parameters");
439 if (mosquitto_connect_async(mosq, "burrow-mqtt", 8883, 60) != MOSQ_ERR_SUCCESS)
440 die("Mosquitto: connect failed");
442 if (mosquitto_loop_start(mosq))
443 die("Mosquitto: cannot start service thread");
448 struct fastbuf *f = influx_write_start();
449 char val[256], *w[2];
450 time_t now = time(NULL);
452 for (uint i=0; i < ARRAY_SIZE(attr_table); i++) {
453 const struct attr *a = &attr_table[i];
455 pthread_mutex_lock(&attr_mutex);
456 snprintf(val, sizeof(val), "%s", attr_values[i] ? : "");
457 pthread_mutex_unlock(&attr_mutex);
461 int fields = str_wordsplit(val, w, ARRAY_SIZE(w));
465 time_t t = atoll(w[1]);
466 uint timeout = a->timeout ? : MEASUREMENT_TIMEOUT;
467 if (t < now - timeout)
472 bprintf(f, "%s %s=%s\n", a->metric, a->value_name, val);
474 bprintf(f, "%s %s=\"%s\"\n", a->metric, a->value_name, val);
476 influx_write_flush();
477 sleep(INFLUX_INTERVAL);