#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <i2c/smbus.h>
#include <limits.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <mosquitto.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#ifndef VERSION
# define VERSION "20240214"
#endif
#ifndef MQTT_NAME
# define MQTT_NAME "auk"
#endif
#define I2C_BUS 1
#define CO2_ADDR 0x5e
#define CO2_CO2_PPM 0
#define CO2_T_C 2
#define CO2_P_MBAR 6
#define DEFAULT_HOSTNAME "localhost"
#define DEFAULT_PORT 1883
#define MQTT_TOPIC "tiny-house/ee895/state"
#define info(m,...) message (LOG_INFO, "info[%s:%u]: " m, __func__, __LINE__, ##__VA_ARGS__);
#define warning(m,...) message (LOG_WARNING, "warning[%s:%u]: " m, __func__, __LINE__, ##__VA_ARGS__);
#define error(m,...) message (LOG_ERR, "error[%s:%u]: " m, __func__, __LINE__, ##__VA_ARGS__);
#define fatal(m,...) do { \
message (LOG_EMERG, "fatal[%s:%u]: " m, __func__, __LINE__, ##__VA_ARGS__); \
exit (EXIT_FAILURE); \
} while (0);
bool foreground = false;
const char *
mqtt_strerror (int err) {
static const char *const mqtt_errors[] = {
[MOSQ_ERR_SUCCESS] = "MOSQ_ERR_SUCCESS",
[MOSQ_ERR_NOMEM] = "MOSQ_ERR_NOMEM",
[MOSQ_ERR_PROTOCOL] = "MOSQ_ERR_PROTOCOL",
[MOSQ_ERR_INVAL] = "MOSQ_ERR_INVAL",
[MOSQ_ERR_NO_CONN] = "MOSQ_ERR_NO_CONN",
[MOSQ_ERR_CONN_REFUSED] = "MOSQ_ERR_CONN_REFUSED",
[MOSQ_ERR_NOT_FOUND] = "MOSQ_ERR_NOT_FOUND",
[MOSQ_ERR_CONN_LOST] = "MOSQ_ERR_CONN_LOST",
[MOSQ_ERR_TLS] = "MOSQ_ERR_TLS",
[MOSQ_ERR_PAYLOAD_SIZE] = "MOSQ_ERR_PAYLOAD_SIZE",
[MOSQ_ERR_NOT_SUPPORTED] = "MOSQ_ERR_NOT_SUPPORTED",
[MOSQ_ERR_AUTH] = "MOSQ_ERR_AUTH",
[MOSQ_ERR_ACL_DENIED] = "MOSQ_ERR_ACL_DENIED",
[MOSQ_ERR_UNKNOWN] = "MOSQ_ERR_UNKNOWN",
[MOSQ_ERR_ERRNO] = "MOSQ_ERR_ERRNO",
[MOSQ_ERR_EAI] = "MOSQ_ERR_EAI",
[MOSQ_ERR_PROXY] = "MOSQ_ERR_PROXY",
[MOSQ_ERR_PLUGIN_DEFER] = "MOSQ_ERR_PLUGIN_DEFER",
[MOSQ_ERR_MALFORMED_UTF8] = "MOSQ_ERR_MALFORMED_UTF8",
[MOSQ_ERR_KEEPALIVE] = "MOSQ_ERR_KEEPALIVE",
[MOSQ_ERR_LOOKUP] = "MOSQ_ERR_LOOKUP",
[MOSQ_ERR_MALFORMED_PACKET] = "MOSQ_ERR_MALFORMED_PACKET",
[MOSQ_ERR_DUPLICATE_PROPERTY] = "MOSQ_ERR_DUPLICATE_PROPERTY",
[MOSQ_ERR_TLS_HANDSHAKE] = "MOSQ_ERR_TLS_HANDSHAKE",
[MOSQ_ERR_QOS_NOT_SUPPORTED] = "MOSQ_ERR_QOS_NOT_SUPPORTED",
[MOSQ_ERR_OVERSIZE_PACKET] = "MOSQ_ERR_OVERSIZE_PACKET",
[MOSQ_ERR_OCSP] = "MOSQ_ERR_OCSP",
[MOSQ_ERR_TIMEOUT] = "MOSQ_ERR_TIMEOUT",
[MOSQ_ERR_RETAIN_NOT_SUPPORTED] = "MOSQ_ERR_RETAIN_NOT_SUPPORTED",
[MOSQ_ERR_TOPIC_ALIAS_INVALID] = "MOSQ_ERR_TOPIC_ALIAS_INVALID",
[MOSQ_ERR_ADMINISTRATIVE_ACTION] = "MOSQ_ERR_ADMINISTRATIVE_ACTION",
[MOSQ_ERR_ALREADY_EXISTS] = "MOSQ_ERR_ALREADY_EXISTS",
};
if (err < 0 || err >= sizeof (mqtt_errors) / sizeof (mqtt_errors[0]) || mqtt_errors[err] == NULL) {
return "Unknown MQTT error";
}
if (err == MOSQ_ERR_ERRNO) {
return strerror (errno);
}
return mqtt_errors[err];
}
void
message (int level, char *const m, ...) {
char buf[8192] = {0};
va_list ap;
va_start (ap, m);
vsnprintf (buf, sizeof (buf), m, ap);
va_end (ap);
if (foreground) {
puts (buf);
} else {
syslog (LOG_DAEMON|level, "%s", buf);
}
}
static char secret[256];
void
read_secret_file (char *file, char **username, char **password) {
FILE *fp = fopen (file, "rt");
if (fp == NULL) {
fatal ("failed to open secret file '%s': %s", file, strerror (errno));
}
if (fgets (secret, sizeof (secret), fp) == NULL) {
fatal ("failed to read secret file '%s': %s", file, strerror (errno));
}
if (fclose (fp)) {
warning ("error closing secret file '%s': %s", file, strerror (errno));
}
if (strlen (secret) > 0) {
char *end = NULL;
while ((end = strrchr (secret, '\n'))) { *end = 0; }
}
char *col = strchr (secret, ':');
if (col) {
*username = secret;
*col = 0;
*password = col + 1;
} else {
*password = secret;
}
}
int
ee895_setup (unsigned i2c_bus) {
char i2c_dev[PATH_MAX] = {0};
int fd;
snprintf (i2c_dev, sizeof (i2c_dev), "/dev/i2c-%u", i2c_bus);
fd = open (i2c_dev, O_RDWR);
if (fd < 0) {
fatal ("open(%s): %s", i2c_dev, strerror (errno));
}
if (ioctl(fd, I2C_SLAVE, CO2_ADDR) < 0) {
fatal ("ioctl(%s): %s", i2c_dev, strerror (errno));
}
return fd;
}
int
ee895_read (int fd, uint16_t *co2, uint16_t *t, uint16_t *p) {
int i, v;
struct { uint16_t *out; uint8_t addr; } to_read[] = { { co2, CO2_CO2_PPM }, { t, CO2_T_C }, { p, CO2_P_MBAR } };
*co2 = *t = *p = 0;
for (i = 0; i < sizeof (to_read) / sizeof (to_read[0]); i++) {
if ((v = i2c_smbus_read_word_data (fd, to_read[i].addr)) < 0) {
error ("i2c read error @%u: %s", to_read[i].addr, strerror (errno));
return 1;
}
*to_read[i].out = (uint16_t)v;
}
return 0;
}
void
discovery (struct mosquitto *mosq, char *ident, char *class, char *unit) {
char topic[256] = {0};
char payload[512] = {0};
/*
* https://mosquitto.org/api/files/mosquitto-h.html
* Discover messages: https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
* device_class: https://www.home-assistant.io/integrations/sensor/#device-class
*/
snprintf (topic, sizeof (topic), "homeassistant/sensor/%s/config", ident);
snprintf (payload, sizeof (payload),
"{"
"\"device_class\":\"%s\","
"\"state_topic\":\"%s\","
"\"unit_of_measurement\":\"%s\","
"\"value_template\":\"{{ value_json.%s }}\","
"\"unique_id\":\"ee895%c0001\","
"\"device\":{"
"\"identifiers\":[\"tinyhouse-ee895\"],"
"\"name\":\"EE895\","
"\"model\":\"EE895\","
"\"manufacturer\":\"E+E Elektronik\","
"\"sw_version\":\""VERSION"\""
"}"
"}",
class, MQTT_TOPIC, unit, ident, ident[0]);
int rc = mosquitto_publish (mosq, NULL, topic, strlen (payload), payload, 0, true);
if (rc != MOSQ_ERR_SUCCESS) error ("discovery %s: %s", ident, mqtt_strerror (rc));
if (foreground) info ("%s", payload);
}
void
publish (struct mosquitto *mosq, uint16_t co2, uint16_t t, uint16_t p) {
char payload[512] = {0};
uint16_t t_ip, t_fp, p_ip, p_fp;
t_ip = betoh16 (t);
t_fp = t_ip % 100;
t_ip = t_ip / 100;
p_ip = betoh16 (p);
p_fp = p_ip % 10;
p_ip = p_ip / 10;
snprintf (payload, sizeof (payload),
"{\"co2\":%u,\"temperature\":%u.%u,\"pressure\":%u.%u}",
betoh16 (co2), t_ip, t_fp, p_ip, p_fp);
int rc = mosquitto_publish (mosq, NULL, MQTT_TOPIC, strlen (payload), payload, 0, true);
if (rc != MOSQ_ERR_SUCCESS) error ("publish: %s", mqtt_strerror (rc));
if (foreground) info ("%s", payload);
}
bool keep_going = true;
bool exit_immediately = true;
void
sigint (int sig) {
if (exit_immediately) exit (EXIT_SUCCESS);
keep_going = false;
}
void
help (void) {
printf ("Usage:\n");
}
void
version (void) {
printf ("Version %s\n", VERSION);
}
int
main (int argc, char *argv[]) {
int i2c_fd, rc, o;
struct mosquitto *mosq;
unsigned int interval = 10;
char *hostname = DEFAULT_HOSTNAME;
int port = DEFAULT_PORT;
char *username = NULL;
char *password = "";
char *pidfile = NULL;
unsigned int i2c_bus = I2C_BUS;
struct option opts[] = {
{ "hostname", required_argument, NULL, 'H' },
{ "port", required_argument, NULL, 'P' },
{ "username", required_argument, NULL, 'u' },
{ "password", required_argument, NULL, 'p' },
{ "secret", required_argument, NULL, 's' },
{ "interval", required_argument, NULL, 'i' },
{ "bus", required_argument, NULL, 'b' },
{ "pidfile", required_argument, NULL, 1 },
{ "foreground", no_argument, NULL, 'f' },
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
while ((o = getopt_long (argc, argv, "H:P:u:p:s:i:b:fhv", opts, NULL)) != -1) {
switch (o) {
case 'H':
hostname = optarg;
break;
case 'P':
{
errno = 0;
unsigned long p = strtoul (optarg, NULL, 0);
if (p == 0 || p > 65535 || errno) {
fatal ("invalid port argument '%s'", optarg);
}
port = (int)p;
}
break;
case 'u':
username = optarg;
break;
case 'p':
password = optarg;
break;
case 's':
read_secret_file (optarg, &username, &password);
break;
case 'i':
{
errno = 0;
unsigned long i = strtoul (optarg, NULL, 0);
if (i == 0 || i > UINT_MAX || errno) {
fatal ("invalid interval argument '%s'", optarg);
} else if (i > 60 * 60) {
warning ("interval '%s' seems uselessly large", optarg);
}
interval = (unsigned int)i;
}
break;
case 'b':
{
errno = 0;
unsigned long b = strtoul (optarg, NULL, 0);
if (b == 0 || b > UINT_MAX || errno) {
fatal ("invalid I2C bus id '%s'", optarg);
} else if (b > 16) {
warning ("uh, I doubt your device has %s I2C busses, but whatever", optarg);
}
i2c_bus = (unsigned int)b;
}
break;
case 'f':
foreground = true;
break;
case 'h':
help ();
exit (EXIT_SUCCESS);
case 'v':
version ();
exit (EXIT_SUCCESS);
case 1:
pidfile = optarg;
break;
case '?':
default:
printf ("%c\n", o);
help ();
exit (EXIT_FAILURE);
}
}
if (! foreground) {
if (daemon (0, 0) < 0) {
fatal ("failed to daemonize: %s", strerror (errno));
}
openlog ("mqtt-publish", 0, LOG_DAEMON);
if (pidfile) {
FILE *fp = fopen (pidfile, "wt");
if (fp == NULL) {
error ("failed to create pid file '%s': %s", pidfile, strerror (errno));
} else {
fprintf (fp, "%u\n", getpid ());
fclose (fp);
}
}
}
if (signal (SIGINT, sigint) < 0 || signal (SIGTERM, sigint) < 0) {
fatal ("signal: %s", strerror (errno));
}
info ("publishing to %s:%u every %us", hostname, port, interval);
mosquitto_lib_init ();
i2c_fd = ee895_setup (i2c_bus);
mosq = mosquitto_new (MQTT_NAME, false, NULL);
if (mosq == NULL) {
fatal ("mosquitto_new: %s", strerror (errno));
}
if (username) {
rc = mosquitto_username_pw_set (mosq, username, password);
if (rc != MOSQ_ERR_SUCCESS) {
fatal ("%s", mqtt_strerror (rc));
}
}
rc = mosquitto_connect (mosq, hostname, port, 60);
if (rc != MOSQ_ERR_SUCCESS) {
fatal ("mosquitto_connect: %s", mqtt_strerror (rc));
}
exit_immediately = false;
rc = mosquitto_loop_start (mosq);
if (rc != MOSQ_ERR_SUCCESS) {
fatal ("%s", mqtt_strerror (rc));
}
discovery (mosq, "co2", "carbon_dioxide", "ppm");
discovery (mosq, "temperature", "temperature", "°C");
discovery (mosq, "pressure", "pressure", "mbar");
while (keep_going) {
uint16_t ee895_co2, ee895_t, ee895_p;
if (ee895_read (i2c_fd, &ee895_co2, &ee895_t, &ee895_p) == 0) {
publish (mosq, ee895_co2, ee895_t, ee895_p);
}
sleep (interval);
}
close (i2c_fd);
mosquitto_disconnect (mosq);
mosquitto_loop_stop (mosq, false);
mosquitto_destroy (mosq);
mosquitto_lib_cleanup ();
if (pidfile) {
unlink (pidfile);
}
return 0;
}