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.
|
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