Skip to content

Commit

Permalink
Add Home Assistant (and others) MQTT Discovery (#103)
Browse files Browse the repository at this point in the history
* Add MQTT Discovery support

* Add header guards to all header files

* Update setup() function to print system information

* Send system data by MQTT

* Add battery level and voltage reporting to MQTT

* Add QoS parameter to publishStrDiscoveryMQTT function

* Temporary remove MAC Address, Hostname, IP and Status from discovery

* Update README.md (#105)

---------

Co-authored-by: Mario Mariete <[email protected]>
  • Loading branch information
melkati and melkati authored Jan 10, 2024
1 parent fcb1f4d commit ae05ae8
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 42 deletions.
30 changes: 22 additions & 8 deletions CO2_Gadget.ino
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
// Next data always defined to be able to configure in menu
String hostName = UNITHOSTNAME;
String rootTopic = UNITHOSTNAME;
String discoveryTopic = MQTT_DISCOVERY_PREFIX;
String mqttClientId = UNITHOSTNAME;
String mqttBroker = MQTT_BROKER_SERVER;
String mqttUser = "";
String mqttPass = "";
String wifiSSID = WIFI_SSID_CREDENTIALS;
String wifiPass = WIFI_PW_CREDENTIALS;
String mDNSName = "Unset";
String MACAddress = "Unset";
// String peerESPNow = ESPNOW_PEER_MAC_ADDRESS;
uint8_t peerESPNowAddress[] = ESPNOW_PEER_MAC_ADDRESS;

Expand All @@ -41,6 +43,7 @@ uint64_t timeToRetryTroubledWIFI = 300; // Time in seconds to retry WIFI connec
uint64_t timeToRetryTroubledMQTT = 900; // Time in seconds to retry MQTT connection after it is troubled (no need to retry so often as it retries automatically after WiFi is connected)
uint16_t WiFiConnectionRetries = 0;
uint16_t maxWiFiConnectionRetries = 5;
bool mqttDiscoverySent = false;

// Display and menu options
uint32_t DisplayBrightness = 100;
Expand All @@ -63,6 +66,7 @@ uint16_t boardIdESPNow = 0;

// Variables for Battery reading
float battery_voltage = 0;
uint8_t battery_level = 0;
uint16_t timeBetweenBatteryRead = 15;
uint64_t lastTimeBatteryRead = 0; // Time of last battery reading

Expand Down Expand Up @@ -229,6 +233,7 @@ uint16_t batteryFullyChargedMillivolts = 4200; // Voltage of battery when it is
/********* SETUP PUSH BUTTONS FUNCTIONALITY *********/
/********* *********/
/*****************************************************************************************************/
#include "Arduino.h"
#include "CO2_Gadget_Buttons.h"

/*****************************************************************************************************/
Expand Down Expand Up @@ -366,19 +371,28 @@ void displayLoop() {
}
}

void batteryLoop() {
const float lastBatteryVoltage = battery_voltage;
readBatteryVoltage();
if (abs(lastBatteryVoltage - battery_voltage) >= 0.1) { // If battery voltage changed by at least 0.1, update battery level
battery_level = getBatteryPercentage();
Serial.printf("-->[BATT] Battery Level: %d%%\n", battery.level());
}
}

// application entry point
void setup() {
uint32_t brown_reg_temp = READ_PERI_REG(RTC_CNTL_BROWN_OUT_REG); // save WatchDog register
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector
Serial.begin(115200);
delay(100);
// Serial.printf("Total heap: %d", ESP.getHeapSize());
// Serial.printf("Free heap: %d", ESP.getFreeHeap());
// Serial.printf("Total PSRAM: %d", ESP.getPsramSize());
// Serial.printf("Free PSRAM: %d", ESP.getFreePsram());
Serial.printf("\n-->[MAIN] CO2 Gadget Version: %s%s Flavour: %s\n", CO2_GADGET_VERSION, CO2_GADGET_REV, FLAVOUR);
Serial.printf("\n-->[STUP] CO2 Gadget Version: %s%s Flavour: %s\n", CO2_GADGET_VERSION, CO2_GADGET_REV, FLAVOUR);
Serial.printf("-->[STUP] Version compiled: %s at %s\n", __DATE__, __TIME__);
Serial.printf("-->[STUP] Total heap: %d", ESP.getHeapSize());
Serial.printf("-->[STUP] Free heap: %d", ESP.getFreeHeap());
Serial.printf("-->[STUP] Total PSRAM: %d", ESP.getPsramSize());
Serial.printf("-->[STUP] Free PSRAM: %d", ESP.getFreePsram());
Serial.printf("Starting up...\n");
Serial.printf("-->[MAIN] Version compiled: %s at %s\n", __DATE__, __TIME__);

setCpuFrequencyMhz(80); // Lower CPU frecuency to reduce power consumption
initPreferences();
Expand All @@ -404,10 +418,10 @@ void setup() {
}

void loop() {
batteryLoop();
wifiClientLoop();
mqttClientLoop();
sensorsLoop();
readBatteryVoltage();
outputsLoop();
processPendingCommands();
readingsLoop();
Expand All @@ -418,4 +432,4 @@ void loop() {
#ifdef SUPPORT_BLE
BLELoop();
#endif
}
}
5 changes: 5 additions & 0 deletions CO2_Gadget_BLE.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_BLE_h
#define CO2_Gadget_BLE_h

#include "Sensirion_GadgetBle_Lib.h"
GadgetBle gadgetBle = GadgetBle(GadgetBle::DataType::T_RH_CO2_ALT);

Expand Down Expand Up @@ -37,3 +40,5 @@ void BLELoop() {
delay(3);
}
}

#endif // CO2_Gadget_BLE_h
7 changes: 5 additions & 2 deletions CO2_Gadget_Battery.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#ifndef CO2_Gadget_Battery_h
#define CO2_Gadget_Battery_h

// clang-format off
/*****************************************************************************************************/
Expand Down Expand Up @@ -39,6 +41,7 @@ float readBatteryVoltage() {
}

uint8_t getBatteryPercentage() {
Serial.printf("-->[BATT] Battery Level: %d%%\n", battery.level());
return battery.level();
}
}

#endif // CO2_Gadget_Battery_h
5 changes: 5 additions & 0 deletions CO2_Gadget_Buttons.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_Buttons_h
#define CO2_Gadget_Buttons_h

#include "Button2.h"
#undef LONGCLICK_TIME_MS
#define LONGCLICK_TIME_MS 300 // https://github.com/LennartHennigs/Button2/issues/10
Expand Down Expand Up @@ -79,3 +82,5 @@ void buttonsLoop() {
btnUp.loop();
btnDwn.loop();
}

#endif // CO2_Gadget_Buttons_h
157 changes: 150 additions & 7 deletions CO2_Gadget_MQTT.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_MQTT_h
#define CO2_Gadget_MQTT_h

// clang-format off
/*****************************************************************************************************/
/********* *********/
Expand Down Expand Up @@ -110,16 +113,130 @@ void publishFloatMQTT(String topic, float payload) {

void publishStrMQTT(String topic, String payload) {
#ifdef SUPPORT_MQTT
payload.toCharArray(charPublish, payload.length());
topic = rootTopic + topic;
if (!inMenu) {
Serial.printf("-->[MQTT] Publishing %s to ", payload);
Serial.printf("-->[MQTT] Publishing %s to ", payload.c_str());
Serial.println("topic: " + topic);
mqttClient.publish((topic).c_str(), charPublish);
mqttClient.publish(topic.c_str(), payload.c_str());
}
#endif
}

void publishStrDiscoveryMQTT(String topic, String payload, int qos) {
#ifdef SUPPORT_MQTT
if (!inMenu) {
Serial.printf("-->[MQTT] Publishing %s to ", payload.c_str());
Serial.println("topic: " + topic);
mqttClient.publish(topic.c_str(), payload.c_str(), true);
}
#endif
}

bool sendMQTTDiscoveryTopic(String deviceClass, String stateClass, String entityCategory,
String group, String field, String name, String icon, String unit,
int qos) {
String version = String(CO2_GADGET_VERSION) + String(CO2_GADGET_REV) + " (" + String(FLAVOUR) + ")";
String hw_version = String(FLAVOUR);

String maintopic = String(rootTopic);

String topicFull;
String configTopic;
String payload;

configTopic = field;

if (field == "problem") { // Special binary sensor which is based on error topic
topicFull = discoveryTopic + "binary_sensor/" + maintopic + "/" + configTopic + "/config";
} else {
topicFull = discoveryTopic + "sensor/" + maintopic + "/" + configTopic + "/config";
}

/* See https://www.home-assistant.io/docs/mqtt/discovery/ */
payload = String("{") +
"\"~\": \"" + maintopic + "\"," +
"\"unique_id\": \"" + maintopic + "-" + configTopic + "\"," +
"\"object_id\": \"" + maintopic + "_" + configTopic + "\"," +
"\"name\": \"" + name + "\"," +
"\"icon\": \"mdi:" + icon + "\"," +
"\"unit_of_measurement\": \"" + unit + "\",";

if (field == "problem") { // Special binary sensor which is based on error topic
payload += "\"state_topic\": \"~/error\",";
payload += "\"value_template\": \"{{ 'OFF' if 'no error' in value else 'ON'}}\",";
} else {
payload += "\"state_topic\": \"~/" + field + "\",";
}

if (deviceClass != "") {
payload += "\"device_class\": \"" + deviceClass + "\",";
}

if (stateClass != "") {
payload += "\"state_class\": \"" + stateClass + "\",";
}

if (entityCategory != "") {
payload += "\"entity_category\": \"" + entityCategory + "\",";
}

payload += String("\"device\": {") +
"\"identifiers\": [\"" + maintopic + "\"]," +
"\"name\": \"" + maintopic + "\"," +
"\"model\": \"CO2 Gadget\"," +
"\"manufacturer\": \"emariete.com\"," +
"\"hw_version\": \"" + hw_version + "\"," +
"\"sw_version\": \"" + version + "\"," +
"\"configuration_url\": \"http://" + WiFi.localIP().toString() + "\"" +
"}" +
"}";

// Replace the following line with your MQTT publish function
// return MQTTPublish(topicFull, payload, qos, true);
Serial.print("MQTT Publish Topic: ");
Serial.println(topicFull);
Serial.print("MQTT Publish Payload: ");
Serial.println(payload);
// topicFull = "Test";
// payload = "Test";
publishStrDiscoveryMQTT(topicFull, payload, qos);
return true;
}

bool publishMQTTDiscovery(int qos) {
bool allSendsSuccessed = false;

if (!mqttClient.connected()) {
Serial.println("Unable to send MQTT Discovery Topics, we are not connected to the MQTT broker!");
return false;
}

// clang-format off
// TO-DO: Add MAC Address, Hostname, IP and Status to discovery. Don't know why they are not working (home assistant doesn't show them)
//
// Device Class | State Class | Entity Category | Group | Field | User Friendly Name | Icon | Unit
allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "uptime", "Uptime", "clock-time-eight-outline", "s", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "MAC", "MAC Address", "network-outline", "", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "hostname", "Hostname", "network-outline", "", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("", "measurement", "diagnostic", "", "freeMem", "Free Memory", "memory", "B", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "wifiRSSI", "Wi-Fi RSSI", "wifi", "dBm", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "IP", "IP", "network-outline", "", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "status", "Status", "list-status", "", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("", "measurement", "diagnostic", "", "battery", "Battery", "", "%", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("", "measurement", "diagnostic", "", "voltage", "Voltage", "", "V", qos);

allSendsSuccessed |= sendMQTTDiscoveryTopic("carbon_dioxide", "", "", "", "co2", "CO2", "molecule-co2", "ppm", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("temperature", "", "", "", "temp", "Temperature", "temperature-celsius", "°C", qos);
allSendsSuccessed |= sendMQTTDiscoveryTopic("humidity", "", "", "", "humi", "Humidity", "water-percent", "%", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "error", "Error", "alert-circle-outline", "", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "diagnostic", "", "json", "JSON", "code-json", "", qos);
// allSendsSuccessed |= sendMQTTDiscoveryTopic("", "", "", "", "problem", "Problem", "alert-outline", "", qos); // Special binary sensor which is based on error topic
// clang-format on

Serial.println("Successfully published all MQTT Discovery topics");
return allSendsSuccessed;
}

void initMQTT() {
#ifdef SUPPORT_MQTT
if (activeMQTT) {
Expand All @@ -133,6 +250,7 @@ void initMQTT() {
Serial.printf("-->[MQTT] Initializing MQTT to broker IP: %s\n", mqttBroker.c_str());
mqttClient.setServer(mqttBroker.c_str(), 1883);
mqttClient.setCallback(callbackMQTT);
mqttClient.setBufferSize(1024);
mqttReconnect();
}
#endif
Expand Down Expand Up @@ -167,15 +285,32 @@ void publishMQTTAlarms() {
}
}

void publishMQTTSystemData() {
publishIntMQTT("/uptime", millis() / 1000);
publishFloatMQTT("/voltage", battery_voltage);
publishIntMQTT("/battery", battery_level);
publishIntMQTT("/freeMem", ESP.getFreeHeap());
publishIntMQTT("/wifiRSSI", WiFi.RSSI());
publishStrMQTT("/IP", WiFi.localIP().toString());
publishStrMQTT("/MAC", WiFi.macAddress());
publishStrMQTT("/hostname", hostName);
publishStrMQTT("/status", "OK");
}

void publishMeasurementsMQTT() {
publishIntMQTT("/co2", co2);
publishFloatMQTT("/temp", temp);
publishFloatMQTT("/humi", hum);
}

void publishMQTT() {
#ifdef SUPPORT_MQTT
if (activeMQTT) {
if ((WiFi.status() == WL_CONNECTED) && (mqttClient.connected())) {
if (millis() - lastTimeMQTTPublished >= timeBetweenMQTTPublish * 1000) {
publishIntMQTT("/co2", co2);
publishFloatMQTT("/temp", temp);
publishFloatMQTT("/humi", hum);
publishMeasurementsMQTT();
publishMQTTAlarms();
publishMQTTSystemData();
lastTimeMQTTPublished = millis();
}
// Serial.print("-->[MQTT] Free heap: ");
Expand All @@ -198,5 +333,13 @@ void mqttClientLoop() {
mqttClient.loop();
}
}

if (!mqttDiscoverySent && mqttClient.connected()) {
Serial.printf("-->[MQTT] Connected to broker. Sending discovery...\n");
publishMQTTDiscovery(0);
mqttDiscoverySent = true;
}
#endif
}
}

#endif // CO2_Gadget_MQTT_h
7 changes: 6 additions & 1 deletion CO2_Gadget_Menu.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_Menu_h
#define CO2_Gadget_Menu_h

// Based on
// https://drive.google.com/file/d/1_qGqs3XpFQRoT-u5-GK8aJk6f0aI7EA3/view?usp=drive_web

Expand Down Expand Up @@ -1086,4 +1089,6 @@ void menu_init() {
Serial.println("-->[MENU] Use keys + - * /");
Serial.println("-->[MENU] to control the menu navigation");
Serial.println("");
}
}

#endif // CO2_Gadget_Menu_h
7 changes: 6 additions & 1 deletion CO2_Gadget_Preferences.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_Preferences_h
#define CO2_Gadget_Preferences_h

#include <Preferences.h>
Preferences preferences;

Expand Down Expand Up @@ -203,4 +206,6 @@ void putPreferences() {
preferences.putBool("showPM25", displayShowPM25);

preferences.end();
}
}

#endif // CO2_Gadget_Preferences_h
5 changes: 5 additions & 0 deletions CO2_Gadget_Sensors.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef CO2_Gadget_Sensors_h
#define CO2_Gadget_Sensors_h

#include <Sensors.hpp>

bool firstCO2SensorInit = true;
Expand Down Expand Up @@ -116,3 +119,5 @@ void initSensors() {
void sensorsLoop() {
sensors.loop();
}

#endif // CO2_Gadget_Sensors_h
Loading

0 comments on commit ae05ae8

Please sign in to comment.