diff options
Diffstat (limited to 'mqtt-ee895.c')
| -rw-r--r-- | mqtt-ee895.c | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/mqtt-ee895.c b/mqtt-ee895.c new file mode 100644 index 0000000..c48fd57 --- /dev/null +++ b/mqtt-ee895.c @@ -0,0 +1,419 @@ +#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; +} |
