summaryrefslogtreecommitdiff
path: root/mqtt-ee895.c
diff options
context:
space:
mode:
Diffstat (limited to 'mqtt-ee895.c')
-rw-r--r--mqtt-ee895.c419
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;
+}