mqtt.cpp 7.58 KB
Newer Older
1
2
#include "mqtt.h"

Eric Duminil's avatar
Eric Duminil committed
3
#include "web_config.h"
Eric Duminil's avatar
Eric Duminil committed
4
5
6
#include "led_effects.h"
#include "sensor_console.h"
#include "wifi_util.h"
Eric Duminil's avatar
Eric Duminil committed
7
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
8
9
#include "src/lib/PubSubClient/src/PubSubClient.h"

Eric Duminil's avatar
Eric Duminil committed
10
11
12
13
14
15
#if defined(ESP8266)
#  include <ESP8266WiFi.h>
#elif defined(ESP32)
#  include <WiFi.h>
#endif

16
namespace config {
Eric Duminil's avatar
Eric Duminil committed
17
  // Values should be defined in config.h or over webconfig
18
19
20
21
  //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 unsigned long wait_after_fail = 900; // [s] Wait 15 minutes after an MQTT connection fail, before trying again.
}
22

Eric Duminil's avatar
Eric Duminil committed
23
24
#if defined(ESP32)
#  include <WiFiClientSecure.h>
25
26
#endif

27
WiFiClient *espClient;
Eric Duminil's avatar
Eric Duminil committed
28
29

PubSubClient mqttClient;
30
31
32
33

namespace mqtt {
  unsigned long last_sent_at = 0;
  unsigned long last_failed_at = 0;
34
  bool connected = false;
35

36
  char publish_topic[42]; // "MQTT_TOPIC_PREFIX/ESPxxxxxx\0", e.g. "CO2sensors/ESPxxxxxx\0"
37
  const char *json_sensor_format;
38
  char last_successful_publish[23] = "";
39

Eric Duminil's avatar
Eric Duminil committed
40
  void initialize(const char *sensorId) {
41
    json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
42
    snprintf(publish_topic, sizeof(publish_topic), "%s%s", config::mqtt_topic_prefix, sensorId);
Eric Duminil's avatar
Eric Duminil committed
43
44
45
46

    if (config::mqtt_encryption) {
      // 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:
47
48
49
      WiFiClientSecure *secureClient = new WiFiClientSecure();
      secureClient->setInsecure();
      espClient = secureClient;
Eric Duminil's avatar
Eric Duminil committed
50
    } else {
51
      espClient = new WiFiClient();
Eric Duminil's avatar
Eric Duminil committed
52
    }
53
    mqttClient.setClient(*espClient);
Eric Duminil's avatar
Eric Duminil committed
54

55
    mqttClient.setServer(config::mqtt_server, config::mqtt_port);
Eric Duminil's avatar
Eric Duminil committed
56

57
    sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)"));
58
    sensor_console::defineCommand("send_local_ip", sendInfoAboutLocalNetwork,
59
        F("(Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
60
61
  }

62
  void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
Eric Duminil's avatar
Eric Duminil committed
63
    if (wifi::connected() && mqttClient.connected()) {
64
      led_effects::onBoardLEDOn();
65
66
67
      Serial.print(F("MQTT - Publishing message to '"));
      Serial.print(publish_topic);
      Serial.print(F("' ... "));
68
69

      char payload[75]; // Should be enough for json...
70
      snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity);
71
      // Topic is 'MQTT_TOPIC_PREFIX/ESP123456'
Eric Duminil's avatar
Eric Duminil committed
72
      if (mqttClient.publish(publish_topic, payload)) {
Eric Duminil's avatar
Eric Duminil committed
73
        Serial.println(F("OK"));
74
        ntp::getLocalTime(last_successful_publish);
75
      } else {
Eric Duminil's avatar
Eric Duminil committed
76
        Serial.println(F("Failed."));
77
      }
78
      led_effects::onBoardLEDOff();
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
    }
  }

  /**
   * 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;
    }
94
    led_effects::onBoardLEDOn();
95
    Serial.print(F("Message arrived on topic: "));
Eric Duminil's avatar
Eric Duminil committed
96
97
    Serial.println(sub_topic);
    char command[length + 1];
98
    for (unsigned int i = 0; i < length; i++) {
Eric Duminil's avatar
Eric Duminil committed
99
      command[i] = message[i];
100
    }
Eric Duminil's avatar
Eric Duminil committed
101
    command[length] = 0;
102
    sensor_console::execute(command);
Eric Duminil's avatar
Eric Duminil committed
103
    led_effects::onBoardLEDOff();
104
105
106
  }

  void reconnect() {
Eric Duminil's avatar
Eric Duminil committed
107
    if (last_failed_at > 0 && (seconds() - last_failed_at < config::wait_after_fail)) {
108
109
110
      // It failed less than wait_after_fail ago. Not even trying.
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
111
    if (!wifi::connected()) { //NOTE: Sadly, WiFi.status is sometimes WL_CONNECTED even though it's really not
112
113
114
      // No WIFI
      return;
    }
115

Eric Duminil's avatar
Eric Duminil committed
116
    Serial.print(F("MQTT - Attempting connection to "));
Eric Duminil's avatar
Eric Duminil committed
117
    Serial.print(config::mqtt_server);
Eric Duminil's avatar
Eric Duminil committed
118
    Serial.print(config::mqtt_encryption ? F(" (Encrypted") : F(" (Unencrypted"));
119
    Serial.print(F(", port "));
Eric Duminil's avatar
Eric Duminil committed
120
121
122
123
124
    Serial.print(config::mqtt_port);
    Serial.print(F(") "));
    Serial.print(F("User:'"));
    Serial.print(config::mqtt_user);
    Serial.print(F("' ..."));
125

126
    led_effects::onBoardLEDOn();
127
    // Wait for connection, at most 15s (default)
Eric Duminil's avatar
Eric Duminil committed
128
    mqttClient.connect(publish_topic, config::mqtt_user, config::mqtt_password);
129
    led_effects::onBoardLEDOff();
130

131
132
133
    connected = mqttClient.connected();

    if (connected) {
134
      if (config::allow_mqtt_commands) {
135
        char control_topic[50]; // Should be enough for "MQTT_TOPIC_PREFIX/ESPd03cc5/control\0"
Eric Duminil's avatar
Eric Duminil committed
136
        snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic);
137
138
139
140
141
142
        mqttClient.subscribe(control_topic);
        mqttClient.setCallback(controlSensorCallback);
      }
      Serial.println(F(" Connected."));
      last_failed_at = 0;
    } else {
Eric Duminil's avatar
Eric Duminil committed
143
144
145
146
      // As defined in PubSubClient, between -4 and 5
      const __FlashStringHelper *mqtt_statuses[] = { F("Connection timeout"), F("Connection lost"), F(
          "Connection failed"), F("Disconnected"), F("Connected"), F("Bad protocol"), F("Bad client ID"), F(
          "Unavailable"), F("Bad credentials"), F("Unauthorized") };
147
      last_failed_at = seconds();
Eric Duminil's avatar
Eric Duminil committed
148
      Serial.print(mqtt_statuses[mqttClient.state() + 4]);
Eric Duminil's avatar
Info    
Eric Duminil committed
149
      Serial.print("! (Code=");
150
      Serial.print(mqttClient.state());
Eric Duminil's avatar
Eric Duminil committed
151
      Serial.print(F("). Will try again in "));
152
153
154
155
156
      Serial.print(config::wait_after_fail);
      Serial.println("s.");
    }
  }

157
  void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum) {
158
159
    // Send message via MQTT according to sending interval
    unsigned long now = seconds();
Eric Duminil's avatar
Eric Duminil committed
160
    if (now - last_sent_at > config::mqtt_sending_interval) {
161
      last_sent_at = now;
162
      publish(timestamp, co2, temp, hum);
163
164
165
166
167
168
169
170
171
172
173
    }
  }

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

174
175
176
177
  /*****************************************************************
   * Callbacks for sensor commands                                 *
   *****************************************************************/
  void setMQTTinterval(int32_t sending_interval) {
Eric Duminil's avatar
Eric Duminil committed
178
    config::mqtt_sending_interval = sending_interval;
Eric Duminil's avatar
Eric Duminil committed
179
    Serial.print(F("Setting MQTT sending interval to : "));
Eric Duminil's avatar
Eric Duminil committed
180
    Serial.print(config::mqtt_sending_interval);
Eric Duminil's avatar
Eric Duminil committed
181
    Serial.println(F("s."));
182
183
    led_effects::showKITTWheel(color::green, 1);
  }
Eric Duminil's avatar
Eric Duminil committed
184

Eric Duminil's avatar
Doc    
Eric Duminil committed
185
186
187
  // 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".
Eric Duminil's avatar
Eric Duminil committed
188
  void sendInfoAboutLocalNetwork() {
189
    char info_topic[50]; // Should be enough for "MQTT_TOPIC_PREFIX/ESP123456/info"
Eric Duminil's avatar
Eric Duminil committed
190
    snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic);
Eric Duminil's avatar
Eric Duminil committed
191
192
193

    char payload[75]; // Should be enough for info json...
    const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}");
Eric Duminil's avatar
Rename    
Eric Duminil committed
194
    snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, config::selected_ssid());
Eric Duminil's avatar
Eric Duminil committed
195
196
197

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