mqtt.cpp 6.89 KB
Newer Older
1
2
3
4
#include "mqtt.h"

namespace config {
  // Values should be defined in config.h
5
  uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s]
6
7
8
9
10
11
12
13
14
  //INFO: Listen to every CO2 sensor which is connected to the server:
  //  mosquitto_sub -h MQTT_SERVER -t 'CO2sensors/#' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -v
  const char *mqtt_server = MQTT_SERVER;
  const uint16_t mqtt_port = MQTT_PORT;
  const char *mqtt_user = MQTT_USER;
  const char *mqtt_password = MQTT_PASSWORD;
  const bool allow_mqtt_commands = ALLOW_MQTT_COMMANDS;
  const unsigned long wait_after_fail = 900; // [s] Wait 15 minutes after an MQTT connection fail, before trying again.
}
15
16
17
18
19

#if MQTT_ENCRYPTED
#  if defined(ESP32)
#    include <WiFiClientSecure.h>
#  endif
20
WiFiClientSecure espClient;
21
22
23
24
#else
WiFiClient espClient;
#endif

25
26
27
28
29
PubSubClient mqttClient(espClient);

namespace mqtt {
  unsigned long last_sent_at = 0;
  unsigned long last_failed_at = 0;
30
  bool connected = false;
31

Eric Duminil's avatar
Eric Duminil committed
32
  char publish_topic[21]; // e.g. "CO2sensors/ESPxxxxxx\0"
33
  const char *json_sensor_format;
34
  char last_successful_publish[23] = "";
35

Eric Duminil's avatar
Eric Duminil committed
36
  void initialize(const char *sensorId) {
37
    json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
Eric Duminil's avatar
Eric Duminil committed
38
    snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId);
39
#if MQTT_ENCRYPTED
40
41
42
    // The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
    // on the sensor every 3 months. The connection can still be encrypted, though:
    espClient.setInsecure(); // If not available for ESP32, please update Arduino IDE / PlatformIO
43
#endif
44
    mqttClient.setServer(config::mqtt_server, config::mqtt_port);
Eric Duminil's avatar
Eric Duminil committed
45

46
    sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)"));
47
    sensor_console::defineCommand("send_local_ip", sendInfoAboutLocalNetwork,
48
        F("(Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
49
50
  }

51
  void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
52
    if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) {
53
      led_effects::onBoardLEDOn();
Eric Duminil's avatar
Eric Duminil committed
54
      Serial.print(F("MQTT - Publishing message ... "));
55
56

      char payload[75]; // Should be enough for json...
57
      snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity);
58
      // Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
Eric Duminil's avatar
Eric Duminil committed
59
      if (mqttClient.publish(publish_topic, payload)) {
Eric Duminil's avatar
Eric Duminil committed
60
        Serial.println(F("OK"));
61
        ntp::getLocalTime(last_successful_publish);
62
      } else {
Eric Duminil's avatar
Eric Duminil committed
63
        Serial.println(F("Failed."));
64
      }
65
      led_effects::onBoardLEDOff();
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    }
  }

  /**
   * Allows sensor to be controlled by commands over MQTT
   *
   * mosquitto_pub -h MQTT_SERVER -t 'CO2sensors/SENSOR_ID/control' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -m "reset"
   * mosquitto_pub -h MQTT_SERVER -t 'CO2sensors/SENSOR_ID/control' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -m "timer 30"
   * mosquitto_pub -h MQTT_SERVER -t 'CO2sensors/SENSOR_ID/control' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -m "mqtt 900"
   * mosquitto_pub -h MQTT_SERVER -t 'CO2sensors/SENSOR_ID/control' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -m "calibrate 700"
   */
  void controlSensorCallback(char *sub_topic, byte *message, unsigned int length) {
    if (length == 0) {
      return;
    }
81
    led_effects::onBoardLEDOn();
82
    Serial.print(F("Message arrived on topic: "));
Eric Duminil's avatar
Eric Duminil committed
83
84
    Serial.println(sub_topic);
    char command[length + 1];
85
    for (unsigned int i = 0; i < length; i++) {
Eric Duminil's avatar
Eric Duminil committed
86
      command[i] = message[i];
87
    }
Eric Duminil's avatar
Eric Duminil committed
88
    command[length] = 0;
89
    sensor_console::execute(command);
Eric Duminil's avatar
Eric Duminil committed
90
    led_effects::onBoardLEDOff();
91
92
93
  }

  void reconnect() {
Eric Duminil's avatar
Eric Duminil committed
94
    if (last_failed_at > 0 && (seconds() - last_failed_at < config::wait_after_fail)) {
95
96
97
98
99
100
101
      // It failed less than wait_after_fail ago. Not even trying.
      return;
    }
    if (WiFi.status() != WL_CONNECTED) { //NOTE: Sadly, WiFi.status is sometimes WL_CONNECTED even though it's really not
      // No WIFI
      return;
    }
102
103
104
105
106
107

    Serial.print(F("MQTT - Attempting connection ("));
    Serial.print(MQTT_ENCRYPTED ? F("Encrypted") : F("Unencrypted"));
    Serial.print(F(", port "));
    Serial.print(MQTT_PORT);
    Serial.print(F(") ..."));
108

109
    led_effects::onBoardLEDOn();
110
    // Wait for connection, at most 15s (default)
Eric Duminil's avatar
Eric Duminil committed
111
    mqttClient.connect(publish_topic, config::mqtt_user, config::mqtt_password);
112
    led_effects::onBoardLEDOff();
113

114
115
116
    connected = mqttClient.connected();

    if (connected) {
117
      if (config::allow_mqtt_commands) {
118
        char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
Eric Duminil's avatar
Eric Duminil committed
119
        snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic);
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
        mqttClient.subscribe(control_topic);
        mqttClient.setCallback(controlSensorCallback);
      }
      Serial.println(F(" Connected."));
      last_failed_at = 0;
    } else {
      last_failed_at = seconds();
      Serial.print(F(" Failed! Error code="));
      Serial.print(mqttClient.state());
      Serial.print(F(". Will try again in "));
      Serial.print(config::wait_after_fail);
      Serial.println("s.");
    }
  }

135
  void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum) {
136
137
    // Send message via MQTT according to sending interval
    unsigned long now = seconds();
138
    if (now - last_sent_at > config::mqtt_sending_interval) {
139
      last_sent_at = now;
140
      publish(timestamp, co2, temp, hum);
141
142
143
144
145
146
147
148
149
150
151
    }
  }

  void keepConnection() {
    // Keep MQTT connection
    if (!mqttClient.connected()) {
      reconnect();
    }
    mqttClient.loop();
  }

152
153
154
155
  /*****************************************************************
   * Callbacks for sensor commands                                 *
   *****************************************************************/
  void setMQTTinterval(int32_t sending_interval) {
156
    config::mqtt_sending_interval = sending_interval;
Eric Duminil's avatar
Eric Duminil committed
157
    Serial.print(F("Setting MQTT sending interval to : "));
158
    Serial.print(config::mqtt_sending_interval);
159
160
161
    Serial.println("s.");
    led_effects::showKITTWheel(color::green, 1);
  }
Eric Duminil's avatar
Eric Duminil committed
162
163
164
165
166
167

  // It can be hard to find the local IP of a sensor if it isn't connected to Serial port, and if mDNS is disabled.
  // If the sensor can be reach by MQTT, it can answer with info about local_ip and ssid.
  // The sensor will send the info to "CO2sensors/ESP123456/info".
  void sendInfoAboutLocalNetwork() {
    char info_topic[60]; // Should be enough for "CO2sensors/ESP123456/info"
Eric Duminil's avatar
Eric Duminil committed
168
    snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic);
Eric Duminil's avatar
Eric Duminil committed
169
170
171

    char payload[75]; // Should be enough for info json...
    const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}");
Eric Duminil's avatar
Eric Duminil committed
172
    snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, WIFI_SSID);
Eric Duminil's avatar
Eric Duminil committed
173
174
175

    mqttClient.publish(info_topic, payload);
  }
176
}