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