summaryrefslogblamecommitdiff
path: root/mqtt-quail.c
blob: f76d745413ca5c8d583cefe7e8fd838c442c984f (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16















                          



                   
                            

                 
                           


                      



                                    
                                      
 



























































































                                                                                                      
 
 

                                
 
 

                                                                                        


   





                                                              































                                                                                                                   
                                               
                             
                                                         












































































































































































                                                                                           
                                  





























                                                                
                                           
 

                                                                     

















                                     
#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 <time.h>
#include <unistd.h>

#ifndef VERSION
#  define VERSION "20240218"
#endif
#ifndef MQTT_NAME
#  define MQTT_NAME "quail"
#endif

#define I2C_BUS      1

#define DEFAULT_HOSTNAME "localhost"
#define DEFAULT_PORT     1883

#define MQTT_TOPIC "shack/quail/state"



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
sht3x_setup (unsigned i2c_bus) {


   /* Set up period data acquisition mode, 1 measurement per sec., high repeatability */

}

int
sht3x_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\":\"sht3x%c0001\","
               "\"device\":{"
                 "\"identifiers\":[\"tinyhouse-sht3x\"],"
                 "\"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 = sht3x_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 sht3x_co2, sht3x_t, sht3x_p;

      if (sht3x_read (i2c_fd, &sht3x_co2, &sht3x_t, &sht3x_p) == 0) {
         publish (mosq, sht3x_co2, sht3x_t, sht3x_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;
}