initial release
This commit is contained in:
parent
fe267a81e2
commit
bb9da629fb
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
tic2mqtt
|
||||
*.d
|
||||
*.o
|
24
Makefile
Normal file
24
Makefile
Normal file
@ -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
|
11
README.md
11
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
|
||||
|
||||
|
112
homeassistant.c
Normal file
112
homeassistant.c
Normal file
@ -0,0 +1,112 @@
|
||||
#include "homeassistant.h"
|
||||
#include "mqtt.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
8
homeassistant.h
Normal file
8
homeassistant.h
Normal file
@ -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
|
32
logger.c
Normal file
32
logger.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "logger.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
|
||||
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);
|
||||
}
|
17
logger.h
Normal file
17
logger.h
Normal file
@ -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
|
91
mqtt.c
Normal file
91
mqtt.c
Normal file
@ -0,0 +1,91 @@
|
||||
#include "mqtt.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <mosquitto.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
13
mqtt.h
Normal file
13
mqtt.h
Normal file
@ -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
|
||||
|
326
tic.c
Normal file
326
tic.c
Normal file
@ -0,0 +1,326 @@
|
||||
#include "tic.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
19
tic.h
Normal file
19
tic.h
Normal file
@ -0,0 +1,19 @@
|
||||
#ifndef __TIC_H__
|
||||
#define __TIC_H__
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#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
|
227
tic2mqtt.c
Normal file
227
tic2mqtt.c
Normal file
@ -0,0 +1,227 @@
|
||||
#include "logger.h"
|
||||
#include "tic.h"
|
||||
#include "mqtt.h"
|
||||
#include "homeassistant.h"
|
||||
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user