diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58fbb11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tic2mqtt +*.d +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9feaf1e --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +SRCS = tic2mqtt.c tic.c mqtt.c homeassistant.c logger.c + +OBJS = $(SRCS:%.c=%.o) + +LIBS = -lmosquitto -ljson-c + +CFLAGS += -Wall -Werror + +DEPFLAGS = -MT $@ -MMD -MP -MF .$*.d +DEPS=$(SRCS:%.c=.%.d) +CFLAGS += $(DEPFLAGS) + +all: tic2mqtt + +tic2mqtt: $(OBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + +clean: + rm -f tic2mqtt $(OBJS) $(DEPS) + +$(OBJS): $(MAKEFILE_LIST) +-include $(DEPS) + +.PHONY: all clean diff --git a/README.md b/README.md index 5c6487d..b42ab58 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,15 @@ tic2mqtt is a MQTT client for Linky Electricity Meters installed by Enedis in France. Hence the rest of this document is in french. +References +========== + +Librement inspiré du code de pmelly93: https://github.com/pmelly93/tic2mqtt + +Specification Téléinformation Client - Enedis: +* Linky: https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_54E.pdf +* Pre-linky: https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_02E.pdf + +Spécification du champ ADS: +https://euridis.org/wp-content/uploads/IdentifiantsEuridisListeCCTTV304A_143027.pdf diff --git a/homeassistant.c b/homeassistant.c new file mode 100644 index 0000000..754d2cc --- /dev/null +++ b/homeassistant.c @@ -0,0 +1,112 @@ +#include "homeassistant.h" +#include "mqtt.h" +#include "logger.h" + +#include + +#include +#include + + +typedef struct { + const char *tag; // TIC Tag + const char *state_class; // HA State Class + const char *device_class; // HA Device Class + const char *name; // HA name + const char *unit; // HA unit + const bool last_reset; // HA LastReset present + const char *value_template; // HA Template for value +} ha_config_desc; + + +static ha_config_desc ha_config_descs[] = { + { "PAPP", "measurement", "power", "Puissance Apparente", "VA", }, + { "IINST", "measurement", "current", "Intensité Instantanée", "A", }, + { "HCHP", "measurement", "energy", "Energie Heures Pleines", "kWh", true, "{{ (value | float) / 1000 }}" }, + { "HCHC", "measurement", "energy", "Energie Heures Creuses", "kWh", true, "{{ (value | float) / 1000 }}" }, + { "PTEC", NULL, NULL, "Période Tarifaire en cours", NULL, false, + "{% if value == \"TH..\" %}Toutes les Heures" + "{% elif value == \"HC..\" %}Heures Creuses" + "{% elif value == \"HP..\" %}Heures Pleines" + "{% elif value == \"HN..\" %}Heures Normales" + "{% elif value == \"PM..\" %}Heures de Pointe Mobile" + "{% else %}{{value}}" + "{% endif %}" }, + { NULL } +}; + +/* workaround for libjson-c 0.12 which always escape + * strings while serializing + */ +static void unescape_str(const char *str) +{ + const char *s; + char *d; + + for (s = str, d = (char*)str; *s; s++, d++) { + if (*s == '\\' && *(s+1) == '/') + s++; + *d = *s; + } + *d = '\0'; +} + +void ha_config_init(const char *tic_name, struct mosquitto *mosq_tic) +{ + ha_config_desc *desc; + // char payload[512]; + + json_object *device = json_object_new_object(); + json_object *dev_ids = json_object_new_array(); + json_object_array_add(dev_ids, json_object_new_string(tic_name)); + json_object_object_add(device, "identifiers", dev_ids); + json_object_object_add(device, "manufacturer", json_object_new_string("Enedis")); + json_object_object_add(device, "model", json_object_new_string("Linky Monophasé")); + json_object_object_add(device, "name", json_object_new_string(tic_name)); + json_object_object_add(device, "sw_version", json_object_new_string("0.0.1")); + + for (desc = &ha_config_descs[0]; desc->tag != NULL; desc++) { + char topic[TOPIC_MAXLEN+1]; + char vtopic[TOPIC_MAXLEN+1]; + char uid[TOPIC_MAXLEN+1]; + char name[TOPIC_MAXLEN+1]; + + snprintf(topic, TOPIC_MAXLEN, "homeassistant/sensor/%s/%s/config", tic_name, desc->tag); + snprintf(vtopic, TOPIC_MAXLEN, "tic2mqtt/%s/%s", tic_name, desc->tag); + snprintf(name, TOPIC_MAXLEN, "%s %s", tic_name, desc->name); + snprintf(uid, TOPIC_MAXLEN, "tic2mqtt_%s_%s", tic_name, desc->tag); + + json_object *obj = json_object_new_object(); + json_object_object_add(obj, "device", json_object_get(device)); + json_object_object_add(obj, "name", json_object_new_string(name)); + json_object_object_add(obj, "unique_id", json_object_new_string(uid)); + json_object_object_add(obj, "state_topic", json_object_new_string(vtopic)); + if (desc->state_class) + json_object_object_add(obj, "state_class", json_object_new_string(desc->state_class)); + if (desc->device_class) + json_object_object_add(obj, "device_class", json_object_new_string(desc->device_class)); + if (desc->unit) + json_object_object_add(obj, "unit_of_measurement", json_object_new_string(desc->unit)); + if (desc->value_template) + json_object_object_add(obj, "value_template", json_object_new_string(desc->value_template)); + if (desc->last_reset) { + json_object_object_add(obj, "last_reset_topic", json_object_new_string(vtopic)); + json_object_object_add(obj, "last_reset_value_template", json_object_new_string("1970-01-01T00:00:00+00:00")); + } + + const char *payload = json_object_to_json_string(obj); + unescape_str(payload); + + log_info("%s\n%s\n", topic, payload); + + if (mosq_tic) { + int res = mqtt_publish(mosq_tic, topic, NULL, payload, TIC_QOS); + if (res != 0) + log_error("Cannot publish topic %s: %s\n", topic, mqtt_strerror(res)); + } + json_object_put(obj); + } + + json_object_put(device); +} + diff --git a/homeassistant.h b/homeassistant.h new file mode 100644 index 0000000..7f91f56 --- /dev/null +++ b/homeassistant.h @@ -0,0 +1,8 @@ +#ifndef __HOME_ASSISTANT_H__ +#define __HOME_ASSISTANT_H__ + +#include "mosquitto.h" + +extern void ha_config_init(const char *tic_name, struct mosquitto *mosq_tic); + +#endif diff --git a/logger.c b/logger.c new file mode 100644 index 0000000..810d6f5 --- /dev/null +++ b/logger.c @@ -0,0 +1,32 @@ +#include "logger.h" + +#include +#include + + +static char* log_level_str[] = { + [LOG_DEBUG] = "DEBUG", + [LOG_INFO] = "INFO", + [LOG_WARNING] = "WARNING", + [LOG_ERROR] = "ERROR", +}; + +static int log_level; + + +void set_loglevel(int level) +{ + log_level = level; +} + +void logger(int priority, const char *fmt, ...) +{ + if (priority < log_level) + return; + + printf("%s: ", log_level_str[priority]); + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); +} diff --git a/logger.h b/logger.h new file mode 100644 index 0000000..01109c4 --- /dev/null +++ b/logger.h @@ -0,0 +1,17 @@ +#ifndef __LOGGER_H__ +#define __LOGGER_H__ + +#define LOG_DEBUG 1 +#define LOG_INFO 2 +#define LOG_WARNING 3 +#define LOG_ERROR 4 + +#define log_debug(...) logger(LOG_DEBUG, __VA_ARGS__) +#define log_info(...) logger(LOG_INFO, __VA_ARGS__) +#define log_warning(...) logger(LOG_WARNING, __VA_ARGS__) +#define log_error(...) logger(LOG_ERROR, __VA_ARGS__) + +extern void set_loglevel(int level); +extern void logger(int priority, const char *fmt, ...); + +#endif diff --git a/mqtt.c b/mqtt.c new file mode 100644 index 0000000..972fc28 --- /dev/null +++ b/mqtt.c @@ -0,0 +1,91 @@ +#include "mqtt.h" +#include "logger.h" + +#include + +#include +#include +#include + + +static void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level, const char *str) +{ + switch (level) { + case MOSQ_LOG_WARNING: log_warning("%s", str); break; + case MOSQ_LOG_ERR: log_error("%s", str); break; + } +} + +struct mosquitto *mqtt_open(const char *host, int port, int keepalive) +{ + struct mosquitto *mosq; + int res; + + mosquitto_lib_init(); + + /* Create MQTT client. */ + mosq = mosquitto_new(NULL, 1, NULL); + if (mosq == NULL) { + log_error("Cannot create mosquitto client: %s", strerror(errno)); + return NULL; + } + + mosquitto_log_callback_set(mosq, mosq_log_callback); + + /* Connect to broker. */ + res = mosquitto_connect(mosq, host, port, keepalive); + if (res != MOSQ_ERR_SUCCESS) { + log_error("Unable to connect to MQTT broker %s:%d: %s", host, port, mosquitto_strerror(res)); + return NULL; + } + + /* Start network loop. */ + res = mosquitto_loop_start(mosq); + if (res != MOSQ_ERR_SUCCESS) { + log_error("Unable to start loop: %s", mosquitto_strerror(res)); + return NULL; + } + + return mosq; +} + +void mqtt_close(struct mosquitto *mosq) +{ + mosquitto_loop_stop(mosq, 1); + mosquitto_disconnect(mosq); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); +} + +int mqtt_publish(struct mosquitto *mosq, const char *topic_prefix, const char *topic_suffix, const void *payload, int qos) +{ + char topic[TOPIC_MAXLEN + 1]; + int res; + + if (topic_prefix != NULL) { + if (topic_suffix != NULL) { + sprintf(topic, "%s%s", topic_prefix, topic_suffix); + } else { + sprintf(topic, "%s", topic_prefix); + } + } else { + if (topic_suffix != NULL) { + sprintf(topic, "%s", topic_suffix); + } else { + return -1; + } + } + + res = mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, qos, 1); + if (res != 0) + log_error("Cannot publish topic %s: %s\n", topic, mosquitto_strerror(res)); + + return res; +} + + +const char *mqtt_strerror(int err) +{ + return mosquitto_strerror(err); +} + diff --git a/mqtt.h b/mqtt.h new file mode 100644 index 0000000..8524d29 --- /dev/null +++ b/mqtt.h @@ -0,0 +1,13 @@ +#ifndef __MQTT_H__ +#define __MQTT_H__ + +#define TOPIC_MAXLEN 255 +#define TIC_QOS 0 + +extern struct mosquitto *mqtt_open(const char *host, int port, int keepalive); +extern void mqtt_close(struct mosquitto *mosq); +extern int mqtt_publish(struct mosquitto *mosq, const char *topic_prefix, const char *topic_suffix, const void *payload, int qos); +extern const char *mqtt_strerror(int err); + +#endif + diff --git a/tic.c b/tic.c new file mode 100644 index 0000000..71c92b7 --- /dev/null +++ b/tic.c @@ -0,0 +1,326 @@ +#include "tic.h" +#include "logger.h" + +#include +#include +#include +#include +#include +#include + +#define TIC_FRAME_MAX 2048 // enough for all possible data groups in standard mode +#define TIC_DATA_MAX 128 + +#define TIC_STX 0x02 +#define TIC_ETX 0x03 +#define TIC_SGR 0x0a +#define TIC_SEP_STD 0x09 +#define TIC_SEP_HIS 0x20 +#define TIC_EGR 0x0d + + +struct tic_info_s { + int fd; + int is_tty; + int mode; + + tic_cb_data_f *cb_data; + + int frame_len; + char frame[TIC_FRAME_MAX]; +}; + + + + +static void tic_init_tty(int fd, int speed) +{ + struct termios termios; + + tcgetattr(fd, &termios); + + // input and output speed + cfsetispeed(&termios, speed); + cfsetospeed(&termios, speed); + + // set input modes: + // - Disable XON/XOFF flow control on input. + // - Do not translate carriage return to newline on input. + // - Enable input parity checking. + // - Strip off eighth bit. + termios.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL); + termios.c_iflag |= INPCK | ISTRIP; + + // set output modes: + // - Disable implementation-defined output processing (raw mode). + termios.c_oflag &= ~OPOST; + + // set control modes: + // - Enable receiver. + // - Ignore modem control lines. + // - 7 bit. + // - 1 stop bit. + // - Enable parity generation on output and parity checking for input. + // - Disable RTS/CTS (hardware) flow control. + termios.c_cflag |= CLOCAL | CREAD; + termios.c_cflag &= ~(CSIZE | PARODD | CSTOPB); + termios.c_cflag |= CS7 | PARENB; + termios.c_cflag &= ~CRTSCTS; + + // set local modes: + // - Do bot generate signal when the characters INTR, QUIT, SUSP or DSUSP are received. + // - Disable canonical mode. + // - Do not echo input characters. + termios.c_lflag &= ~(ISIG | ICANON | ECHO | ECHOE); + + // set special characters: + // - Timeout set to 1 s. + // - Minimum number of characters for noncanonical read set to 1. + termios.c_cc[VTIME] = 10; + termios.c_cc[VMIN] = 0; + tcsetattr(fd, TCSANOW, &termios); + + tcflush(fd, TCIFLUSH); +} + +/* check received stream + * returns: + * - 0: ok, received at least 20 valid control chars without any + * illegal characters + * - -1: read() error + * - 1: timeout, didn't received enough valid chars in the allowed + * time (2s). + */ +static int tic_check_read(int fd) +{ + char c; + int nb_ok = 0; + time_t stamp = time(NULL); + + tcflush(fd, TCIFLUSH); + + while (nb_ok < 20) { + int ret = read(fd, &c, 1); + + if (ret < 0) + return -1; + + if (ret == 1) { + switch (c) { + case TIC_STX: + case TIC_ETX: + case TIC_SGR: + case TIC_SEP_STD: + case TIC_EGR: + nb_ok++; + break; + default: + if (c < 0x20 || c > 0x7f) + nb_ok = 0; + } + } + + if (difftime(time(NULL), stamp) > 2) + return 1; + } + + return 0; +} + + +tic_info_t* tic_init(const char *fname) +{ + tic_info_t* tic = calloc(1, sizeof(tic_info_t)); + if (tic == NULL) { + log_error("cannot allocate memory\n"); + return NULL; + } + tic->cb_data = NULL; + + log_info("opening %s\n", fname); + + tic->fd = open(fname, O_RDWR | O_NOCTTY); + if (tic->fd < 0) { + log_error("Cannot open %s: %s\n", fname, strerror(errno)); + free(tic); + return NULL; + } + + tic->is_tty = isatty(tic->fd); + + if (tic->is_tty) { + int ret; + + log_debug("trying standard mode\n"); + tic_init_tty(tic->fd, B9600); + + ret = tic_check_read(tic->fd); + if (ret == 0) { + tic->mode = TIC_MODE_STD; + log_info("TIC standard mode detected\n"); + } else { + log_debug("trying historical mode\n"); + tic_init_tty(tic->fd, B1200); + + ret = tic_check_read(tic->fd); + if (ret == 0) { + tic->mode = TIC_MODE_HIST; + log_info("TIC historical mode detected\n"); + } else { + log_error("no valid TIC stream detected\n"); + free(tic); + return NULL; + } + } + } + + if (tic_read_frame(tic)) { + free(tic); + return NULL; + } + tic_process_frame(tic); + + return tic; +} + + +int tic_mode(tic_info_t *tic) +{ + return tic->mode; +} + +void tic_set_cb_data(tic_info_t *tic, tic_cb_data_f *func) +{ + tic->cb_data = func; +} + + +int tic_read_frame(tic_info_t *tic) +{ + char c; + + do { + if (read(tic->fd, &c, 1) <= 0) { + log_error("read error (waiting for STX)\n"); + return -1; + } + } while (c != TIC_STX); + + for (int i = 0; i < TIC_FRAME_MAX; i++) { + if (read(tic->fd, &c, 1) <= 0) { + log_error("read error (reading frame)\n"); + return -1; + } + + tic->frame[i] = c; + if (c == TIC_ETX) { + return 0; + } + } + + log_error("frame too long\n"); + return -1; +} + + +static inline int twodigits2int(const char *digits) +{ + return (digits[0]-'0')*10 + (digits[1]-'0'); +} + +static time_t tic_horodate_to_time(const char *horodate) +{ + if (horodate == NULL) + return (time_t)0L; + + // format: "SAAMMJJhhmmss" Saison-year-month-day-hour-minutes-seconds + // 01 3 5 7 9 11 + struct tm tm; + tm.tm_sec = twodigits2int(&horodate[11]); + tm.tm_min = twodigits2int(&horodate[9]); + tm.tm_hour = twodigits2int(&horodate[7]); + tm.tm_mday = twodigits2int(&horodate[5]); + tm.tm_mon = twodigits2int(&horodate[3]); + tm.tm_year = twodigits2int(&horodate[1]) + 2000 - 1900; + switch(horodate[0]) { + case 'E': tm.tm_isdst = 1; break; + case 'H': tm.tm_isdst = 0; break; + default: tm.tm_isdst = -1; + } + return mktime(&tm); +} + +void tic_process_frame(tic_info_t *tic) +{ + char *frame = tic->frame; + + for (int i = 0;;) { + int grp_start; + int grp_end; + + if (frame[i] == TIC_ETX) + return; + + while (frame[i++] != '\n') { + if (i >= TIC_FRAME_MAX) + return; + } + grp_start = i; + + while (frame[i++] != '\r') { + if (i >= TIC_FRAME_MAX) + return; + } + grp_end = i - 1; + + int grp_end_sum = tic->mode == TIC_MODE_STD ? grp_end - 1 : grp_end - 2; + char sum = 0; + for (int j = grp_start; j < grp_end_sum; j++) + sum += frame[j]; + sum = (sum & 0x3f) + 0x20; + + if (sum != frame[grp_end - 1]) { + log_warning("checksum error\n"); + continue; + } + + char sep = tic->mode == TIC_MODE_STD ? '\t' : ' '; + int nsep = 0; + char *p_label = &frame[grp_start]; + char *p_horodate; + char *p_data; + for (int j = grp_start; j < grp_end - 1; j++) { + if (frame[j] == sep) { + frame[j] = '\0'; + nsep++; + switch(nsep) { + case 1: p_horodate = &frame[j+1]; break; + case 2: p_data = &frame[j+1]; break; + } + } + } + if (nsep > 3 || (nsep == 3 && tic->mode == TIC_MODE_HIST) || nsep < 2) { + log_warning("bad format group\n"); + continue; + } + + if (nsep == 2) { + p_data = p_horodate; + p_horodate = NULL; + } + + if (tic->cb_data) { + time_t date = tic_horodate_to_time(p_horodate); + (*tic->cb_data)(p_label, p_data, date); + } + } +} + + +void tic_exit(tic_info_t *tic) +{ + if (tic) { + close(tic->fd); + free(tic); + } +} diff --git a/tic.h b/tic.h new file mode 100644 index 0000000..8da0b96 --- /dev/null +++ b/tic.h @@ -0,0 +1,19 @@ +#ifndef __TIC_H__ +#define __TIC_H__ + +#include + +#define TIC_MODE_HIST 0 +#define TIC_MODE_STD 1 + +typedef struct tic_info_s tic_info_t; +typedef void (tic_cb_data_f)(const char *label, const char *data, time_t date); + +extern tic_info_t* tic_init(const char *fname); +extern void tic_set_cb_data(tic_info_t *tic, tic_cb_data_f *func); +extern int tic_mode(tic_info_t *tic); +extern int tic_read_frame(tic_info_t *tic); +extern void tic_process_frame(tic_info_t *tic); +extern void tic_exit(tic_info_t *tic); + +#endif diff --git a/tic2mqtt.c b/tic2mqtt.c new file mode 100644 index 0000000..bcfea00 --- /dev/null +++ b/tic2mqtt.c @@ -0,0 +1,227 @@ +#include "logger.h" +#include "tic.h" +#include "mqtt.h" +#include "homeassistant.h" + + +#include +#include +#include +#include +#include +#include + +#define TIC2MQTT_VERSION "1.0.1" + +#define DEFAULT_TTY "/dev/ttyUSB0" + +#define DEFAULT_PORT 1883 +#define DEFAULT_KEEPALIVE 60 + +#define DEFAULT_TIC_NAME "Linky" + + +static tic_info_t *tic_info = NULL; + +struct tag_desc { + const char *tag; // Name of tag. + const int len; // Length of data. + time_t stamp; // last time received + char *data; // Last data received. +}; + +static struct tag_desc tag_descs[] = +{ + { "ADCO", 12 }, + { "OPTARIF", 4 }, + { "ISOUSC", 2 }, + + { "BASE", 9 }, + + { "HCHC", 9 }, + { "HCHP", 9 }, + + { "EJPHN", 9 }, + { "EJPHPM", 9 }, + + { "BBRHCJB", 9 }, + { "BBRHPJB", 9 }, + { "BBRHCJW", 9 }, + { "BBRHPJW", 9 }, + { "BBRHCJR", 9 }, + { "BBRHPJR", 9 }, + + { "PEJP", 2 }, + { "PTEC", 4 }, + { "DEMAIN", 4 }, + { "IINST", 3 }, + { "ADPS", 3 }, + { "IMAX", 3 }, + { "PAPP", 5 }, + { "HHPHC", 1 }, + { "MOTDETAT", 6 }, + + { NULL, 0 } /* End of table marker. */ +}; + +static struct mosquitto *mosq_tic = NULL; +static int log_level = LOG_WARNING; +static int ha_config = 0; + +char *tic_name = DEFAULT_TIC_NAME; + + +static void tic2mqtt_process_group(const char *tag, const char *data, time_t date) +{ + struct tag_desc *ptag_desc; + + for (ptag_desc = tag_descs; ptag_desc->tag != NULL; ptag_desc++) { + if (strcmp(ptag_desc->tag, tag) == 0) { + if (ptag_desc->data == NULL) { + ptag_desc->data = calloc(1, ptag_desc->len + 1); + if (ptag_desc->data == NULL) { + log_error("Cannot alloc data for tag %s: %s\n", tag, strerror(errno)); + } + } + + time_t stamp = time(NULL); + double elapsed = difftime(stamp, ptag_desc->stamp); + + if (elapsed >= 60 || strcmp(ptag_desc->data, data)) { + ptag_desc->stamp = stamp; + strncpy(ptag_desc->data, data, ptag_desc->len); + + char topic[TOPIC_MAXLEN + 1]; + snprintf(topic, TOPIC_MAXLEN, "tic2mqtt/%s/%s", tic_name, tag); + + log_info("%ld %s %s\n", stamp, topic, data); + + if (mosq_tic) { + int res = mqtt_publish(mosq_tic, topic, NULL, data, TIC_QOS); + if (res != 0) + log_error("Cannot publish topic %s: %s\n", topic, mqtt_strerror(res)); + } + } + return; // No more processing. + } + } +} + + + +static void sighandler(int signum) +{ + log_info("Catch signal #%d (%s)\n", signum, strsignal(signum)); + exit(EXIT_SUCCESS); +} + +static void cleanup(void) +{ + tic_exit(tic_info); + if (mosq_tic) + mqtt_close(mosq_tic); +} + + +static void usage(const char *progname) +{ + fprintf(stderr, "Usage: %s [-Hv] [-a] [-t tty] [-n name] [-h host] [-p port] [-k keepalive]\n", progname); +} + + +static void set_progname(char *argv0) +{ + char *p; + + if ((p = strrchr(argv0, '/')) != NULL) + strcpy(argv0, p + 1); // argv[0] contains a slash. +} + + +int main(int argc, char *argv[]) +{ + int opt; + const char *tty = DEFAULT_TTY; + const char *host = NULL; + int port = DEFAULT_PORT; + int keepalive = DEFAULT_KEEPALIVE; + + set_progname(argv[0]); + + /* Decode options. */ + opterr = 1; + while ((opt = getopt(argc, argv, "vdat:n:h:p:k:H")) != -1) { + switch (opt) { + case 'v': + log_level = LOG_INFO; + break; + case 'd': + log_level = LOG_DEBUG; + break; + + case 'a': + ha_config = 1; + break; + + case 't': + tty = optarg; + break; + + case 'n': + tic_name = optarg; + break; + + case 'h': + host = optarg; + break; + + case 'p': + port = atoi(optarg); + break; + + case 'k': + keepalive = atoi(optarg); + break; + + case 'H': + printf("version " TIC2MQTT_VERSION "\n"); + usage(argv[0]); + exit(EXIT_SUCCESS); + break; + + default: + usage(argv[0]); + exit(EXIT_FAILURE); + break; + } + } + + atexit(cleanup); + signal(SIGINT, sighandler); + signal(SIGQUIT, sighandler); + signal(SIGTERM, sighandler); + signal(SIGHUP, sighandler); + + if (host) { + mosq_tic = mqtt_open(host, port, keepalive); + if (mosq_tic == NULL) + return EXIT_FAILURE; + } + + tic_info = tic_init(tty); + if (tic_info == NULL) + return EXIT_FAILURE; + + if (ha_config) + ha_config_init(tic_name, mosq_tic); + + tic_set_cb_data(tic_info, tic2mqtt_process_group); + + for (;;) { + if (tic_read_frame(tic_info) < 0) + return EXIT_FAILURE; + tic_process_frame(tic_info); + } + + return EXIT_SUCCESS; +}