]> mj.ucw.cz Git - home-hw.git/blob - aircon/daemon/burrow-aircond.c
7fb6c4248efa7de6beb675814d5759b26052249e
[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 #include <time.h>
20 #include <unistd.h>
21
22 #include <modbus.h>
23 #include <mosquitto.h>
24
25 #include "../firmware/registers.h"
26
27 /*** MQTT ***/
28
29 static struct mosquitto *mosq;
30 static bool mqtt_connected;
31
32 static void mqtt_error(const char *operation, int err, bool teardown)
33 {
34         msg(L_ERROR, "Mosquitto: %s failed: error %d", operation, err);
35
36         if (teardown) {
37                 mosquitto_destroy(mosq);
38                 mosq = NULL;
39                 mqtt_connected = false;
40         } else if (err == MOSQ_ERR_NO_CONN || err == MOSQ_ERR_CONN_REFUSED || err == MOSQ_ERR_CONN_LOST) {
41                 mqtt_connected = false;
42         }
43 }
44
45 static void mqtt_publish(const char *topic, const char *fmt, ...)
46 {
47         va_list args;
48         va_start(args, fmt);
49
50         if (mqtt_connected) {
51                 char m[256];
52                 int l = vsnprintf(m, sizeof(m), fmt, args);
53                 int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
54                 if (err != MOSQ_ERR_SUCCESS)
55                         mqtt_error("publish", err, false);
56         }
57
58         va_end(args);
59 }
60
61 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level UNUSED, const char *message UNUSED)
62 {
63         // msg(L_INFO, "MQTT(%d): %s", level, message);
64 }
65
66 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m);
67
68 static bool mqtt_connect(void)
69 {
70         int err;
71
72         if (mqtt_connected)
73                 return true;
74
75         if (!mosq) {
76                 mosq = mosquitto_new("aircond", 1, NULL);
77                 if (!mosq)
78                         die("Mosquitto: initialization failed");
79
80                 mosquitto_log_callback_set(mosq, mqtt_log_callback);
81                 mosquitto_message_callback_set(mosq, mqtt_msg_callback);
82
83                 err = mosquitto_will_set(mosq, "status/aircon", 4, "dead", 0, true);
84                 if (err != MOSQ_ERR_SUCCESS) {
85                         mqtt_error("will_set", err, true);
86                         return false;
87                 }
88
89                 err = mosquitto_connect(mosq, "127.0.0.1", 1883, 60);
90                 if (err != MOSQ_ERR_SUCCESS) {
91                         mqtt_error("connect", err, true);
92                         return false;
93                 }
94         } else {
95                 err = mosquitto_reconnect(mosq);
96                 if (err != MOSQ_ERR_SUCCESS) {
97                         mqtt_error("reconnect", err, false);
98                         return false;
99                 }
100         }
101
102         err = mosquitto_subscribe(mosq, NULL, "burrow/air/#", 1);
103         if (err != MOSQ_ERR_SUCCESS) {
104                 mqtt_error("subscribe", err, false);
105                 return false;
106         }
107
108         mqtt_connected = true;
109
110         mqtt_publish("status/aircon", "ok");
111
112         return mqtt_connected;
113 }
114
115 /*** MODBUS ***/
116
117 static modbus_t *modbus;
118 static bool mb_is_open;
119
120 static void mb_error(const char *operation, bool need_close)
121 {
122         msg(L_ERROR, "MODBUS: %s failed: %s", operation, modbus_strerror(errno));
123
124         if (need_close && modbus) {
125                 if (mb_is_open) {
126                         modbus_close(modbus);
127                         mb_is_open = false;
128                 }
129                 modbus_free(modbus);
130                 modbus = NULL;
131         }
132 }
133
134 static bool mb_connect(void)
135 {
136         if (modbus)
137                 return true;
138
139         modbus = modbus_new_tcp("127.0.0.1", 4301);
140         // modbus = modbus_new_rtu("/dev/modbus-aircon", 19200, 'E', 8, 1);
141         if (!modbus) {
142                 mb_error("open", true);
143                 return false;
144         }
145
146         // modbus_set_debug(modbus, 1);
147         modbus_set_slave(modbus, 42);
148         modbus_set_response_timeout(modbus, 5, 0);
149
150         if (modbus_connect(modbus) < 0) {
151                 mb_error("connect", true);
152                 return false;
153         }
154
155         mb_is_open = true;
156         return true;
157 }
158
159 /*** Main loop ***/
160
161 static void scan_temperatures(time_t now)
162 {
163         static const char * const temp_names[] = {
164                 "inside-intake",
165                 "inside-exhaust",
166                 "outside-intake",
167                 "outside-exhaust",
168                 "mixed",
169         };
170
171         u16 regs[5];
172         if (modbus_read_input_registers(modbus, AIRCON_IREG_TEMP_FROM_INSIDE, 5, regs) < 0) {
173                 mb_error("read", true);
174                 return;
175         }
176
177         for (uint i=0; i<5; i++) {
178                 if (regs[i] != 0x8000) {
179                         int t = regs[i];
180                         if (t >= 0x8000)
181                                 t -= 0x10000;
182                         mqtt_publish(stk_printf("burrow/air/%s", temp_names[i]), "%.3f %llu", t / 100., (unsigned long long) now);
183                 }
184         }
185 }
186
187 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
188 {
189         char val[256];
190         if (m->payloadlen >= sizeof(val) - 1) {
191                 msg(L_ERROR, "Invalid value for topic %s", m->topic);
192                 return;
193         }
194         memcpy(val, m->payload, m->payloadlen);
195         val[m->payloadlen] = 0;
196         msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
197
198         if (!strcmp(m->topic, "burrow/air/bypass")) {
199                 uint x;
200                 const char *err;
201                 if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 2) {
202                         msg(L_ERROR, "Received invalid bypass setting %s: %s", val, err);
203                         return;
204                 }
205                 if (modbus_write_bit(modbus, AIRCON_COIL_EXCHANGER_BYPASS, x) < 0)
206                         mb_error("coil write", false);
207         } else if (!strcmp(m->topic, "burrow/air/exchanger-fan")) {
208                 uint x;
209                 const char *err;
210                 if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 256) {
211                         msg(L_ERROR, "Received invalid exchanger fan setting %s: %s", val, err);
212                         return;
213                 }
214                 if (modbus_write_register(modbus, AIRCON_HREG_EXCHANGER_FAN, x) < 0)
215                         mb_error("fan register write", false);
216         } else if (!strcmp(m->topic, "burrow/air/aircon-remote")) {
217                 if (strlen(val) != 1) {
218                         msg(L_ERROR, "Received invalid aircon remote command %s", val);
219                         return;
220                 }
221                 if (modbus_write_register(modbus, AIRCON_HREG_REMOTE_CONTROL, val[0]) < 0)
222                         mb_error("remote control register write", false);
223         }
224 }
225
226 static int use_daemon;
227 static int use_debug;
228
229 static struct opt_section options = {
230         OPT_ITEMS {
231                 OPT_HELP("A daemon for controlling the air conditioning controller via MQTT"),
232                 OPT_HELP(""),
233                 OPT_HELP("Options:"),
234                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
235                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
236                 OPT_HELP_OPTION,
237                 OPT_CONF_OPTIONS,
238                 OPT_END
239         }
240 };
241
242 int main(int argc UNUSED, char **argv)
243 {
244         log_init(argv[0]);
245         opt_parse(&options, argv+1);
246
247         if (use_daemon) {
248                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
249                 log_set_default_stream(ls);
250         }
251         if (!use_debug)
252                 log_default_stream()->levels &= ~(1U << L_DEBUG);
253
254         mosquitto_lib_init();
255
256         time_t next_run = 0;
257         for (;;) {
258                 if (!mqtt_connect() || !mb_connect()) {
259                         sleep(5);
260                         continue;
261                 }
262
263                 time_t now = time(NULL);
264                 if (now < next_run) {
265                         int err = mosquitto_loop(mosq, (next_run - now) * 1000, 1);
266                         if (err != MOSQ_ERR_SUCCESS)
267                                 mqtt_error("loop", err, false);
268                         continue;
269                 }
270
271                 next_run = now + 10;
272                 scan_temperatures(now);
273         }
274 }