From fcb1f4d5cf5c098b192416b603ba142b3bca6dc5 Mon Sep 17 00:00:00 2001 From: Mariete Date: Wed, 10 Jan 2024 10:34:49 +0100 Subject: [PATCH 1/2] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 91e3166e..42f08acd 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This repository is mainly addressed at developers. If you are an end user willin - WIFI connection - Sending of data via MQTT - Receiving remote commands via MQTT +- MQTT Discovery protocol for Home Assistant (and others supporting it as HomeSeer with mcsMQTT) - ESP-NOW communications protocol from Espressif for long range and low power consuption ([more info here](https://emariete.com/en/gateway-esp-now-mqtt/)) - Over the air updates OTA - Support for Neopixel (WS2812B) addressable LEDs (RGB, GBR and RGBW) From ae05ae83e19068db0627494ad529b1ba53888077 Mon Sep 17 00:00:00 2001 From: Mariete Date: Wed, 10 Jan 2024 12:46:37 +0100 Subject: [PATCH 2/2] Add Home Assistant (and others) MQTT Discovery (#103) * 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 <11509521+melkati@users.noreply.github.com> --- CO2_Gadget.ino | 30 ++++++-- CO2_Gadget_BLE.h | 5 ++ CO2_Gadget_Battery.h | 7 +- CO2_Gadget_Buttons.h | 5 ++ CO2_Gadget_MQTT.h | 157 +++++++++++++++++++++++++++++++++++++-- CO2_Gadget_Menu.h | 7 +- CO2_Gadget_Preferences.h | 7 +- CO2_Gadget_Sensors.h | 5 ++ CO2_Gadget_WIFI.h | 37 +++++---- credentials.h | 7 +- platformio.ini | 9 ++- 11 files changed, 234 insertions(+), 42 deletions(-) diff --git a/CO2_Gadget.ino b/CO2_Gadget.ino index 24344855..1aff7456 100644 --- a/CO2_Gadget.ino +++ b/CO2_Gadget.ino @@ -18,6 +18,7 @@ // 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 = ""; @@ -25,6 +26,7 @@ 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; @@ -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; @@ -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 @@ -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" /*****************************************************************************************************/ @@ -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(); @@ -404,10 +418,10 @@ void setup() { } void loop() { + batteryLoop(); wifiClientLoop(); mqttClientLoop(); sensorsLoop(); - readBatteryVoltage(); outputsLoop(); processPendingCommands(); readingsLoop(); @@ -418,4 +432,4 @@ void loop() { #ifdef SUPPORT_BLE BLELoop(); #endif -} +} \ No newline at end of file diff --git a/CO2_Gadget_BLE.h b/CO2_Gadget_BLE.h index 243e8b57..6717554b 100644 --- a/CO2_Gadget_BLE.h +++ b/CO2_Gadget_BLE.h @@ -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); @@ -37,3 +40,5 @@ void BLELoop() { delay(3); } } + +#endif // CO2_Gadget_BLE_h \ No newline at end of file diff --git a/CO2_Gadget_Battery.h b/CO2_Gadget_Battery.h index 55a99752..64bb45b9 100644 --- a/CO2_Gadget_Battery.h +++ b/CO2_Gadget_Battery.h @@ -1,3 +1,5 @@ +#ifndef CO2_Gadget_Battery_h +#define CO2_Gadget_Battery_h // clang-format off /*****************************************************************************************************/ @@ -39,6 +41,7 @@ float readBatteryVoltage() { } uint8_t getBatteryPercentage() { - Serial.printf("-->[BATT] Battery Level: %d%%\n", battery.level()); return battery.level(); -} \ No newline at end of file +} + +#endif // CO2_Gadget_Battery_h \ No newline at end of file diff --git a/CO2_Gadget_Buttons.h b/CO2_Gadget_Buttons.h index ce14dd85..5cb6e11e 100644 --- a/CO2_Gadget_Buttons.h +++ b/CO2_Gadget_Buttons.h @@ -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 @@ -79,3 +82,5 @@ void buttonsLoop() { btnUp.loop(); btnDwn.loop(); } + +#endif // CO2_Gadget_Buttons_h \ No newline at end of file diff --git a/CO2_Gadget_MQTT.h b/CO2_Gadget_MQTT.h index 8350ecc9..e0193062 100644 --- a/CO2_Gadget_MQTT.h +++ b/CO2_Gadget_MQTT.h @@ -1,3 +1,6 @@ +#ifndef CO2_Gadget_MQTT_h +#define CO2_Gadget_MQTT_h + // clang-format off /*****************************************************************************************************/ /********* *********/ @@ -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) { @@ -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 @@ -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: "); @@ -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 -} \ No newline at end of file +} + +#endif // CO2_Gadget_MQTT_h \ No newline at end of file diff --git a/CO2_Gadget_Menu.h b/CO2_Gadget_Menu.h index b773ae49..d9e2756d 100644 --- a/CO2_Gadget_Menu.h +++ b/CO2_Gadget_Menu.h @@ -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 @@ -1086,4 +1089,6 @@ void menu_init() { Serial.println("-->[MENU] Use keys + - * /"); Serial.println("-->[MENU] to control the menu navigation"); Serial.println(""); -} \ No newline at end of file +} + +#endif // CO2_Gadget_Menu_h \ No newline at end of file diff --git a/CO2_Gadget_Preferences.h b/CO2_Gadget_Preferences.h index e947747a..8b625569 100644 --- a/CO2_Gadget_Preferences.h +++ b/CO2_Gadget_Preferences.h @@ -1,3 +1,6 @@ +#ifndef CO2_Gadget_Preferences_h +#define CO2_Gadget_Preferences_h + #include Preferences preferences; @@ -203,4 +206,6 @@ void putPreferences() { preferences.putBool("showPM25", displayShowPM25); preferences.end(); -} \ No newline at end of file +} + +#endif // CO2_Gadget_Preferences_h \ No newline at end of file diff --git a/CO2_Gadget_Sensors.h b/CO2_Gadget_Sensors.h index eaa7be00..68876c4a 100644 --- a/CO2_Gadget_Sensors.h +++ b/CO2_Gadget_Sensors.h @@ -1,3 +1,6 @@ +#ifndef CO2_Gadget_Sensors_h +#define CO2_Gadget_Sensors_h + #include bool firstCO2SensorInit = true; @@ -116,3 +119,5 @@ void initSensors() { void sensorsLoop() { sensors.loop(); } + +#endif // CO2_Gadget_Sensors_h \ No newline at end of file diff --git a/CO2_Gadget_WIFI.h b/CO2_Gadget_WIFI.h index 5ac4f445..39c99ffe 100644 --- a/CO2_Gadget_WIFI.h +++ b/CO2_Gadget_WIFI.h @@ -25,6 +25,20 @@ void onWifiSettingsChanged(std::string ssid, std::string password) { WiFi.begin(ssid.c_str(), password.c_str()); } +String getMACAddressAsString() { + byte mac[6]; + WiFi.macAddress(mac); + + String macAddress = String(mac[5], HEX) + ":" + + String(mac[4], HEX) + ":" + + String(mac[3], HEX) + ":" + + String(mac[2], HEX) + ":" + + String(mac[1], HEX) + ":" + + String(mac[0], HEX); + + return macAddress; +} + void printWiFiStatus() { // Print wifi status on serial monitor // Get current status @@ -78,7 +92,8 @@ void printWiFiStatus() { // Print wifi status on serial monitor // Print your WiFi shield's MAC address: Serial.print("-->[WiFi] MAC Address: "); - Serial.println(WiFi.macAddress()); + MACAddress = getMACAddressAsString(); + Serial.println(MACAddress); // Print the received signal strength: Serial.print("-->[WiFi] Signal strength (RSSI):"); @@ -234,23 +249,6 @@ String processor(const String &var) { return String(); } -void serialPrintMACAddress() { - byte mac[6]; - WiFi.macAddress(mac); - Serial.print("-->[WiFi] MAC: "); - Serial.print(mac[5], HEX); - Serial.print(":"); - Serial.print(mac[4], HEX); - Serial.print(":"); - Serial.print(mac[3], HEX); - Serial.print(":"); - Serial.print(mac[2], HEX); - Serial.print(":"); - Serial.print(mac[1], HEX); - Serial.print(":"); - Serial.println(mac[0], HEX); -} - bool checkStringIsNumerical(String myString) { uint16_t Numbers = 0; @@ -375,7 +373,8 @@ void initWifi() { } } Serial.println(""); - serialPrintMACAddress(); + Serial.print("-->[WiFi] MAC: "); + Serial.println(MACAddress); Serial.print("-->[WiFi] WiFi connected - IP = "); Serial.println(WiFi.localIP()); #ifdef SUPPORT_MDNS diff --git a/credentials.h b/credentials.h index 38a6129b..d34a85a0 100644 --- a/credentials.h +++ b/credentials.h @@ -1,3 +1,6 @@ +#ifndef Credentials_h +#define Credentials_h + #ifndef CREDENTIALS_H #define CREDENTIALS_H @@ -6,4 +9,6 @@ #define MQTT_USER_CREDENTIAL "MyUser" #define MQTT_PW_CREDENTIAL "another_secret_password" -#endif \ No newline at end of file +#endif + +#endif // Credentials_h \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 4ea3da32..0fcc0fed 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,8 +50,8 @@ build_flags = -D MQTT_BROKER_SERVER="\"192.168.1.145"\" -D CO2_GADGET_VERSION="\"0.7."\" - -D CO2_GADGET_REV="\"006"\" - -D CORE_DEBUG_LEVEL=0 + -D CO2_GADGET_REV="\"010"\" + -D CORE_DEBUG_LEVEL=0 -DNEOPIXEL_PIN=26 ; Pinnumber for button for down/next and back / exit actions -DNEOPIXEL_COUNT=16 ; How many neopixels to control -DENABLE_PIN=27 ; Reserved for the future to enable the sensor @@ -72,6 +72,9 @@ build_flags = ; -DSUPPORT_OTA ; Needs SUPPORT_WIFI ; -DSUPPORT_MDNS ; Needs SUPPORT_WIFI -DSUPPORT_MQTT ; Needs SUPPORT_WIFI + -DSUPPORT_MQTT_DISCOVERY + -DMQTT_DISCOVERY_PREFIX="\"homeassistant/\"" + -DSUPPORT_ESPNOW -DESPNOW_PEER_MAC_ADDRESS="{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}" ; MAC Address of the ESP-NOW receiver (STA MAC). For unicast use peer address, as: {0xE8, 0x68, 0xE7, 0x0F, 0x08, 0x90} -DESPNOW_WIFI_CH=1 ; ESP-NOW WiFi Channel. Must be same as gateway @@ -260,4 +263,4 @@ build_flags = -DFLAVOUR="\"ESP32 OLED OTA"\" -USUPPORT_BLE -DSUPPORT_OTA - -DSUPPORT_MDNS \ No newline at end of file + -DSUPPORT_MDNS