]> mj.ucw.cz Git - home-hw.git/blob - aircon/daemon/burrow-aircond.c
Merge branch 'master' of ssh://git.ucw.cz/home/mj/GIT/home-hw
[home-hw.git] / aircon / daemon / burrow-aircond.c
1 /*
2  *      A MQTT Gateway Daemon for the Air Conditioning Controller
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
20 #include <modbus.h>
21 #include <mosquitto.h>
22
23 #include "../firmware/registers.h"
24
25 static struct mosquitto *mosq;
26 static modbus_t *modbus;
27
28 static void mqtt_publish(const char *topic, const char *fmt, ...)
29 {
30         va_list args;
31         va_start(args, fmt);
32         char m[256];
33         int l = vsnprintf(m, sizeof(m), fmt, args);
34         if (mosquitto_publish(mosq, NULL, topic, l, m, 0, true) != MOSQ_ERR_SUCCESS)
35                 msg(L_ERROR, "Mosquitto: publish failed");
36         va_end(args);
37 }
38
39 static void mqtt_setup(void)
40 {
41         if (mosquitto_subscribe(mosq, NULL, "burrow/air/#", 1) != MOSQ_ERR_SUCCESS)
42                 die("Mosquitto: subscribe failed");
43
44         mqtt_publish("status/aircon", "ok");
45 }
46
47 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
48 {
49         // msg(L_INFO, "MQTT(%d): %s", level, message);
50 }
51
52 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
53 {
54         char val[256];
55         if (m->payloadlen >= sizeof(val) - 1) {
56                 msg(L_ERROR, "Invalid value for topic %s", m->topic);
57                 return;
58         }
59         memcpy(val, m->payload, m->payloadlen);
60         val[m->payloadlen] = 0;
61         msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
62
63         if (!strcmp(m->topic, "burrow/air/bypass")) {
64                 uint x;
65                 const char *err;
66                 if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 2) {
67                         msg(L_ERROR, "Received invalid bypass setting %s: %s", val, err);
68                         return;
69                 }
70                 if (modbus_write_bit(modbus, AIRCON_COIL_EXCHANGER_BYPASS, x) < 0)
71                         msg(L_ERROR, "Modbus write failed: %s", modbus_strerror(errno));
72         } else if (!strcmp(m->topic, "burrow/air/exchanger-fan")) {
73                 uint x;
74                 const char *err;
75                 if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 256) {
76                         msg(L_ERROR, "Received invalid exchanger fan setting %s: %s", val, err);
77                         return;
78                 }
79                 if (modbus_write_register(modbus, AIRCON_HREG_EXCHANGER_FAN, x) < 0)
80                         msg(L_ERROR, "Modbus write failed: %s", modbus_strerror(errno));
81         }
82 }
83
84 static void scan_temperatures(time_t now)
85 {
86         static const char * const temp_names[] = {
87                 "inside-intake",
88                 "inside-exhaust",
89                 "outside-intake",
90                 "outside-exhaust",
91                 "mixed",
92         };
93
94         u16 regs[5];
95         if (modbus_read_input_registers(modbus, AIRCON_IREG_TEMP_FROM_INSIDE, 5, regs) < 0) {
96                 msg(L_ERROR, "Modbus read failed: %s", modbus_strerror(errno));
97                 return;
98         }
99
100         for (uint i=0; i<5; i++) {
101                 if (regs[i] != 0x8000) {
102                         int t = regs[i];
103                         if (t >= 0x8000)
104                                 t -= 0x10000;
105                         mqtt_publish(stk_printf("burrow/air/%s", temp_names[i]), "%.3f %llu", t / 100., (unsigned long long) now);
106                 }
107         }
108 }
109
110 static int use_daemon;
111 static int use_debug;
112
113 static struct opt_section options = {
114         OPT_ITEMS {
115                 OPT_HELP("A daemon for controlling the air conditioning controller via MQTT"),
116                 OPT_HELP(""),
117                 OPT_HELP("Options:"),
118                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
119                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
120                 OPT_HELP_OPTION,
121                 OPT_CONF_OPTIONS,
122                 OPT_END
123         }
124 };
125
126 int main(int argc UNUSED, char **argv)
127 {
128         log_init(argv[0]);
129         opt_parse(&options, argv+1);
130
131         if (use_daemon) {
132                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
133                 log_set_default_stream(ls);
134         }
135         if (!use_debug)
136                 log_default_stream()->levels &= ~(1U << L_DEBUG);
137
138         // FIXME: Find the right device. Reconnect if needed.
139         modbus = modbus_new_rtu("/dev/ttyUSB1", 19200, 'E', 8, 1);
140         if (!modbus)
141                 die("Unable to establish Modbus context");
142         modbus_set_slave(modbus, 42);
143         if (modbus_connect(modbus) < 0)
144                 die("Unable to connect to Modbus");
145
146         mosquitto_lib_init();
147         mosq = mosquitto_new("aircond", 1, NULL);
148         if (!mosq)
149                 die("Mosquitto: initialization failed");
150
151         mosquitto_log_callback_set(mosq, mqtt_log_callback);
152         mosquitto_message_callback_set(mosq, mqtt_msg_callback);
153
154         if (mosquitto_will_set(mosq, "status/aircon", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
155                 die("Mosquitto: unable to set will");
156
157         if (mosquitto_connect(mosq, "127.0.0.1", 1883, 60) != MOSQ_ERR_SUCCESS)
158                 die("Mosquitto: connect failed");
159
160         mqtt_setup();
161
162         time_t next_run = 0;
163         for (;;) {
164                 time_t now = time(NULL);
165                 if (now < next_run) {
166                         int err = mosquitto_loop(mosq, (next_run - now) * 1000, 1);
167                         if (err == MOSQ_ERR_NO_CONN) {
168                                 err = mosquitto_reconnect(mosq);
169                                 if (err == MOSQ_ERR_SUCCESS)
170                                         mqtt_setup();
171                                 else
172                                         msg(L_ERROR, "Mosquitto: cannot reconnect, error %d", err);
173                         } else if (err != MOSQ_ERR_SUCCESS)
174                                 msg(L_ERROR, "Mosquitto: loop returned error %d", err);
175                         continue;
176                 }
177
178                 next_run = now + 10;
179                 scan_temperatures(now);
180         }
181 }