From: Martin Mares Date: Tue, 16 Jul 2019 19:36:12 +0000 (+0200) Subject: Aircon daemon: First bits X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=25ebbf744a095be742a9be429c9b621374a6635b;p=home-hw.git Aircon daemon: First bits --- diff --git a/aircon/daemon/Makefile b/aircon/daemon/Makefile new file mode 100644 index 0000000..b7651ad --- /dev/null +++ b/aircon/daemon/Makefile @@ -0,0 +1,23 @@ +TOPDIR=/root/turris + +include $(TOPDIR)/rules.mk +include $(TOPDIR)/include/package.mk + +PC := PATH=$(STAGING_DIR_HOST)/bin:$(PATH) PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) PKG_CONFIG_LIBDIR=$(PKG_CONFIG_PATH) STAGING_PREFIX=$(STAGING_DIR)/usr $(PKG_CONFIG) +MB_CFLAGS := $(shell $(PC) --cflags libmodbus) +MB_LDFLAGS := $(shell $(PC) --libs libmodbus) +UCW_CFLAGS := $(shell $(PC) --cflags libucw) +UCW_LDFLAGS := $(shell $(PC) --libs libucw) + +export PATH=$(TARGET_PATH_PKG) +CC=$(TARGET_CC_NOCACHE) +LD=$(TARGET_LD_NOCACHE) +CFLAGS=$(TARGET_CFLAGS) $(MB_CFLAGS) $(UCW_CFLAGS) -std=gnu99 +LDFLAGS=$(TARGET_LDFLAGS) $(MB_LDFLAGS) $(UCW_LDFLAGS) -lmosquitto + +all: burrow-aircond upload + +burrow-aircond: burrow-aircond.c + +upload: + rsync -av burrow-aircond micac:/root/burrow/ diff --git a/aircon/daemon/burrow-aircond.c b/aircon/daemon/burrow-aircond.c new file mode 100644 index 0000000..629c202 --- /dev/null +++ b/aircon/daemon/burrow-aircond.c @@ -0,0 +1,181 @@ +/* + * A MQTT Gateway Daemon for the Air Conditioning Controller + * + * (c) 2019 Martin Mares + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../firmware/registers.h" + +static struct mosquitto *mosq; +static modbus_t *modbus; + +static void mqtt_publish(const char *topic, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + char m[256]; + int l = vsnprintf(m, sizeof(m), fmt, args); + if (mosquitto_publish(mosq, NULL, topic, l, m, 0, true) != MOSQ_ERR_SUCCESS) + msg(L_ERROR, "Mosquitto: publish failed"); + va_end(args); +} + +static void mqtt_setup(void) +{ + if (mosquitto_subscribe(mosq, NULL, "burrow/air/#", 1) != MOSQ_ERR_SUCCESS) + die("Mosquitto: subscribe failed"); + + mqtt_publish("status/aircon", "ok"); +} + +static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message) +{ + // msg(L_INFO, "MQTT(%d): %s", level, message); +} + +static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m) +{ + char val[256]; + if (m->payloadlen >= sizeof(val) - 1) { + msg(L_ERROR, "Invalid value for topic %s", m->topic); + return; + } + memcpy(val, m->payload, m->payloadlen); + val[m->payloadlen] = 0; + msg(L_DEBUG, "MQTT < %s %s", m->topic, val); + + if (!strcmp(m->topic, "burrow/air/bypass")) { + uint x; + const char *err; + if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 2) { + msg(L_ERROR, "Received invalid bypass setting %s: %s", val, err); + return; + } + if (modbus_write_bit(modbus, AIRCON_COIL_EXCHANGER_BYPASS, x) < 0) + msg(L_ERROR, "Modbus write failed: %s", modbus_strerror(errno)); + } else if (!strcmp(m->topic, "burrow/air/exchanger-fan")) { + uint x; + const char *err; + if ((err = str_to_uint(&x, val, NULL, 10 | STN_WHOLE)) || x >= 256) { + msg(L_ERROR, "Received invalid exchanger fan setting %s: %s", val, err); + return; + } + if (modbus_write_register(modbus, AIRCON_HREG_EXCHANGER_FAN, x) < 0) + msg(L_ERROR, "Modbus write failed: %s", modbus_strerror(errno)); + } +} + +static void scan_temperatures(time_t now) +{ + static const char * const temp_names[] = { + "inside-intake", + "inside-exhaust", + "outside-intake", + "outside-exhaust", + "mixed", + }; + + u16 regs[5]; + if (modbus_read_input_registers(modbus, AIRCON_IREG_TEMP_FROM_INSIDE, 5, regs) < 0) { + msg(L_ERROR, "Modbus read failed: %s", modbus_strerror(errno)); + return; + } + + for (uint i=0; i<5; i++) { + if (regs[i] != 0x8000) { + int t = regs[i]; + if (t >= 0x8000) + t -= 0x10000; + mqtt_publish(stk_printf("burrow/air/%s", temp_names[i]), "%.3f %llu", t / 100., (unsigned long long) now); + } + } +} + +static int use_daemon; +static int use_debug; + +static struct opt_section options = { + OPT_ITEMS { + OPT_HELP("A daemon for controlling the air conditioning controller via MQTT"), + OPT_HELP(""), + OPT_HELP("Options:"), + OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"), + OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"), + OPT_HELP_OPTION, + OPT_CONF_OPTIONS, + OPT_END + } +}; + +int main(int argc UNUSED, char **argv) +{ + log_init(argv[0]); + opt_parse(&options, argv+1); + + if (use_daemon) { + struct log_stream *ls = log_new_syslog("daemon", LOG_PID); + log_set_default_stream(ls); + } + if (!use_debug) + log_default_stream()->levels &= ~(1U << L_DEBUG); + + // FIXME: Find the right device. Reconnect if needed. + modbus = modbus_new_rtu("/dev/ttyUSB1", 19200, 'E', 8, 1); + if (!modbus) + die("Unable to establish Modbus context"); + modbus_set_slave(modbus, 42); + if (modbus_connect(modbus) < 0) + die("Unable to connect to Modbus"); + + mosquitto_lib_init(); + mosq = mosquitto_new("aircond", 1, NULL); + if (!mosq) + die("Mosquitto: initialization failed"); + + mosquitto_log_callback_set(mosq, mqtt_log_callback); + mosquitto_message_callback_set(mosq, mqtt_msg_callback); + + if (mosquitto_will_set(mosq, "status/aircon", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS) + die("Mosquitto: unable to set will"); + + if (mosquitto_connect(mosq, "127.0.0.1", 1883, 60) != MOSQ_ERR_SUCCESS) + die("Mosquitto: connect failed"); + + mqtt_setup(); + + time_t next_run = 0; + for (;;) { + time_t now = time(NULL); + if (now < next_run) { + int err = mosquitto_loop(mosq, (next_run - now) * 1000, 1); + if (err == MOSQ_ERR_NO_CONN) { + err = mosquitto_reconnect(mosq); + if (err == MOSQ_ERR_SUCCESS) + mqtt_setup(); + else + msg(L_ERROR, "Mosquitto: cannot reconnect, error %d", err); + } else if (err != MOSQ_ERR_SUCCESS) + msg(L_ERROR, "Mosquitto: loop returned error %d", err); + continue; + } + + next_run = now + 10; + scan_temperatures(now); + } +}