#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }