#ifdef USE_MQTT //========================================================================================================================= // mqtt.ino - Home Assistant / MQTT integration (compiled only if USE_MQTT is defined for the active CLOCK_NN in clocks.h) // // Library: ArduinoHA 2.x (https://github.com/dawidchyrzynski/arduino-home-assistant), which wraps PubSubClient // (https://pubsubclient.knolleary.net/) and adds Home Assistant MQTT Discovery on top of it. The HA* objects // below publish their discovery config automatically whenever the broker connection is (re)established. // (Rewritten for ArduinoHA 2.1: the bundled 1.3 had no light/number/select/button entities.) // // Entities (all grouped under one HA device, unique IDs prefixed with the MAC address): // Sensors: temperature/humidity/pressure/lux/radar (whichever exist on this clock), WiFi signal // Controls: Tubes (switch, runtime only), Day/Night brightness (numbers), Auto dim (switch), // Cathode protect (button); with USE_NEOPIXEL also Backlight (light: on/off, brightness, colour) // and LED effect (select). // Changes made on the clock's own web page are reported back to HA by mqttSend(). // Settings changed from HA are saved to EEPROM a few seconds after the last change (one flash write per burst). // // MASTER / SLAVE sensor-sharing architecture: // Every clock that defines USE_MQTT connects to the same broker (prm.mqttBrokerAddr/User/Psw, set on the web UI) and // PUBLISHES its own local sensors. A clock that does NOT have its own sensor (or wants to reuse another clock's // reading) can instead be built with USE_MASTER_CLOCK, and SUBSCRIBE to another clock's already-published topics: // USE_MASTER_TEMP + MASTER_TEMPERATURE_TOPIC -> mirrors the master's temperature into local temperature[] array // USE_MASTER_HUMID + MASTER_HUMIDITY_TOPIC -> mirrors the master's humidity into local humid[] array // USE_MASTER_RADAR + MASTER_RADAR_TOPIC -> mirrors the master's PIR/radar presence state into mqttRadarON // If a "slave" clock DOES also have its own local RADAR_PIN wired, the local pin always wins - the radar topic // subscription is skipped in onMqttConnected(), and checkTubePowerOnOff() prefers digitalRead(RADAR_PIN). // NOTE: the topic layout changed with ArduinoHA 2.x: sensor values are now published on // "///stat_t" (data prefix "aha" by default), so MASTER_*_TOPIC settings // written for the old library must be updated. // // Call pattern from the core .ino: setupMqtt() is called once from setup() when USE_MQTT is defined (the broker // address/user/password are read then, so a change on the web UI needs a reboot); mqttSend() is called every loop() // iteration while WiFi is connected and prm.mqttEnable is set. It pumps mqtt.loop() on every call and refreshes // sensor values every prm.mqttBrokerRefresh seconds (at least every MQTT_MIN_REFRESH seconds). //_______________________________________________________________________________________________________________________ //To use MQTT services the following settings should be defined in clocks.h //#define USE_MQTT //#define MQTT_PREFIX "homeassistant" //HA discovery prefix (Home Assistant's default is "homeassistant") //#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0 //#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK //#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK //#define USE_MASTER_RADAR //enable it, if you want to get radar state from MASTER CLOCK //#define MASTER_TEMPERATURE_TOPIC "..." //#define MASTER_HUMIDITY_TOPIC "..." //#define MASTER_RADAR_TOPIC "..." //_______________________________________________________________________________________________________________________ #include //https://github.com/dawidchyrzynski/arduino-home-assistant #if !defined(MQTT_PREFIX) #define MQTT_PREFIX "UniClock32" //HA discovery prefix (upstream default; Home Assistant itself listens on "homeassistant") #endif #define MQTT_MIN_REFRESH 10 //seconds between sensor refreshes if prm.mqttBrokerRefresh is lower #define MQTT_SAVE_DELAY 3000 //ms after the last change from HA before the settings are written to EEPROM #define MQTT_WHITE_INDEX 192 //same as WHITE_INDEX in z_neopixel.ino (that file is compiled after this one) byte mac[6]; //this device's WiFi MAC, used as the HA device ID char masterTemperatureTopic[80] = ""; //resolved from MASTER_TEMPERATURE_TOPIC (clocks.h) - topic this clock SUBSCRIBES to char masterHumidityTopic[80] = ""; //resolved from MASTER_HUMIDITY_TOPIC (clocks.h) - topic this clock SUBSCRIBES to char masterRadarTopic[80] = ""; //resolved from MASTER_RADAR_TOPIC (clocks.h) - topic this clock SUBSCRIBES to byte MASTERtempPtr=0; byte MASTERhumidPtr=0; char mqttConfigUrl[40]; unsigned long mqttSaveRequested = 0; //millis() of the last change from HA that still has to be saved, 0 = none WiFiClient client; HADevice device; HAMqtt mqtt(client, device, 20); //up to 20 entities //Sensors that only some clocks have are created in setupMqtt(): every constructed entity registers itself with //mqtt and is published to HA, so unused ones would show up as empty entities. HASensorNumber* mqttTemp = nullptr; HASensorNumber* mqttHumid = nullptr; HASensorNumber* mqttPress = nullptr; HASensorNumber* mqttLux = nullptr; HABinarySensor* mqttRadar = nullptr; HASensorNumber mqttRssi("rssi"); HASwitch mqttTubes("tubes"); HANumber mqttDayBright("day_brightness"); HANumber mqttNightBright("night_brightness"); HASwitch mqttAutoDim("auto_dim"); HAButton mqttCathodeProtect("cathode_protect"); #ifdef USE_NEOPIXEL HALight mqttBacklight("backlight", HALight::BrightnessFeature | HALight::RGBFeature); HASelect mqttLedEffect("led_effect"); byte mqttLastEffect = 2; //effect restored when the backlight is switched on from HA (rainbow flow by default) #endif //Bounded copy of an MQTT payload into a zero-terminated buffer. // FIXED: the old onMqttMessage() memcpy'd the payload into a 100-byte stack buffer without a length check or a // terminator (possible stack overflow with a long payload). static void payloadToString(const uint8_t* payload, uint16_t length, char* buf, size_t bufSize) { size_t n = min((size_t)length, bufSize - 1); memcpy(buf, payload, n); buf[n] = 0; } void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { float tempTMP,humidTMP; char tmp[32]; payloadToString(payload, length, tmp, sizeof(tmp)); DPRINT("New message on topic: "); DPRINT(topic); DPRINT(" Data: "); DPRINTLN(tmp); if ((strlen(masterTemperatureTopic)>0) && (strncmp(topic,masterTemperatureTopic,sizeof(masterTemperatureTopic)) ==0)) { tempTMP = atof(tmp); temperature[MASTERtempPtr] = tempTMP; //overwrite the reserved "master" slot in the shared temperature[] array (see MASTERtempPtr above) DPRINT("MASTER clock's temperature:"); DPRINTLN(tempTMP); } if ((strlen(masterHumidityTopic)>0) && (strncmp(topic,masterHumidityTopic,sizeof(masterHumidityTopic)) ==0)) { humidTMP = atof(tmp); DPRINT("MASTER clock's humidity:"); DPRINTLN(humidTMP); humid[MASTERhumidPtr] = humidTMP; //overwrite the reserved "master" slot in the shared humid[] array (see MASTERhumidPtr above) } if ((strlen(masterRadarTopic)>0) && (strncmp(topic,masterRadarTopic,sizeof(masterRadarTopic)) ==0)) { DPRINT("MASTER radar:"); DPRINTLN(tmp[0]); if ((tmp[0] == '0') || (strcmp(tmp, "OFF") == 0)) { mqttRadarON = false; radarON = mqttRadarON; } else { mqttRadarON = true; radarON = mqttRadarON; radarLastOn = millis(); } DPRINT("mqttRadarON:"); DPRINTLN(mqttRadarON); } } void onMqttConnected() { DPRINTLN("Connected to the broker!"); #ifdef USE_MASTER_CLOCK DPRINTLN(F("MASTER_CLOCK enabled! Subscribe to following topics:")); #ifdef USE_MASTER_TEMP if (strlen(masterTemperatureTopic)==0) { DPRINTLN("Please define MASTER_TEMPERATURE_TOPIC in clocks.h'"); } else { DPRINT(F("Master temperature topic:")); DPRINTLN(masterTemperatureTopic); mqtt.subscribe(masterTemperatureTopic); } #endif #ifdef USE_MASTER_HUMID if (strlen(masterHumidityTopic)==0) { DPRINTLN("Please define MASTER_HUMIDITY_TOPIC in clocks.h'"); } else { DPRINT(F("Master humidity topic:")); DPRINTLN(masterHumidityTopic); mqtt.subscribe(masterHumidityTopic); } #endif #ifdef USE_MASTER_RADAR if (RADAR_PIN>=0) { DPRINTLN(F("Local radar enabled! MQTT MASTER radar disabled!")); } else { if (strlen(masterRadarTopic)==0) { DPRINTLN("Please define MASTER_RADAR_TOPIC in clocks.h'"); } else { DPRINT(F("Master radar topic:")); DPRINTLN(masterRadarTopic); mqtt.subscribe(masterRadarTopic); } } #endif #endif } //--- commands from Home Assistant --------------------------------------------------------------------------------------- void mqttRequestSave() { mqttSaveRequested = millis(); if (mqttSaveRequested == 0) mqttSaveRequested = 1; } void onTubesCommand(bool state, HASwitch* sender) { tubesOn = state; DPRINT("MQTT: tubes "); DPRINTLN(state ? "on" : "off"); sender->setState(state); } void onBrightnessCommand(HANumeric number, HANumber* sender) { if (!number.isSet()) return; int v = constrain((int)number.toInt16(), 0, MAXBRIGHTNESS); if (sender == &mqttDayBright) prm.dayBright = v; else prm.nightBright = v; sender->setState(v); mqttRequestSave(); } void onAutoDimCommand(bool state, HASwitch* sender) { prm.enableAutoDim = state; autoBrightness = state; //same as the web UI handler: takes effect at once sender->setState(state); mqttRequestSave(); } void onCathodeProtectCommand(HAButton* sender) { makeCathodeProtect = true; //executed from loop(), like the web UI button } #ifdef USE_NEOPIXEL //Inverse of Wheel() in z_neopixel.ino: the wheel position whose hue is closest to an RGB colour (saturation and //brightness are ignored; grey/white maps to the white position). byte rgbToWheel(byte r, byte g, byte b) { byte lo = min(r, min(g, b)); int q; if ((max(r, max(g, b)) - lo) < 16) return MQTT_WHITE_INDEX; r -= lo; g -= lo; b -= lo; if (g == 0) q = (85 * b) / (r + b); //Wheel q 0..84: (255-3k, 0, 3k) else if (r == 0) q = 85 + (85 * g) / (g + b); //Wheel q 85..169: (0, 3k, 255-3k) else q = 170 + (85 * r) / (r + g); //Wheel q 170..254: (3k, 255-3k, 0) q = min(q, 254); byte pos = 255 - q; //Wheel() inverts its argument if (pos == MQTT_WHITE_INDEX) pos++; //192 is reserved for white return pos; } HALight::RGBColor wheelToRgb(byte pos) { RgbColor c = Wheel(pos); HALight::RGBColor color; color.red = c.R; color.green = c.G; color.blue = c.B; color.isSet = true; return color; } void onBacklightStateCommand(bool state, HALight* sender) { if (state) { if (prm.rgbEffect == 0) prm.rgbEffect = (mqttLastEffect > 0) ? mqttLastEffect : 1; } else { if (prm.rgbEffect > 0) mqttLastEffect = prm.rgbEffect; prm.rgbEffect = 0; } sender->setState(state); mqttLedEffect.setState(prm.rgbEffect); mqttRequestSave(); } void onBacklightBrightnessCommand(uint8_t brightness, HALight* sender) { prm.rgbBrightness = brightness; sender->setBrightness(brightness); mqttRequestSave(); } void onBacklightColorCommand(HALight::RGBColor color, HALight* sender) { if (!color.isSet) return; prm.rgbFixColor = rgbToWheel(color.red, color.green, color.blue); prm.rgbEffect = 1; //a colour from HA means "fixed colour" sender->setRGBColor(wheelToRgb(prm.rgbFixColor)); mqttLedEffect.setState(prm.rgbEffect); mqttRequestSave(); } void onLedEffectCommand(int8_t index, HASelect* sender) { if ((index < 0) || (index > 10)) return; prm.rgbEffect = index; if (index > 0) mqttLastEffect = index; sender->setState(index); mqttBacklight.setState(index > 0); mqttRequestSave(); } #endif void setupMqtt() { char mactmp[20]; WiFi.macAddress(mac); device.setUniqueId(mac, sizeof(mac)); //set device MAC, as unique ID device.enableExtendedUniqueIds(); //entity unique IDs = _, so several clocks don't clash in HA sprintf(mactmp,"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]); #if defined(MASTER_TEMPERATURE_TOPIC) strncpy(masterTemperatureTopic,MASTER_TEMPERATURE_TOPIC,sizeof(masterTemperatureTopic)); #endif #if defined(MASTER_HUMIDITY_TOPIC) strncpy(masterHumidityTopic,MASTER_HUMIDITY_TOPIC,sizeof(masterHumidityTopic)); #endif #if defined(MASTER_RADAR_TOPIC) strncpy(masterRadarTopic,MASTER_RADAR_TOPIC,sizeof(masterRadarTopic)); #endif DPRINT("Starting MQTT Client, discovery prefix: "); DPRINT(MQTT_PREFIX); DPRINT(" device: "); DPRINTLN(mactmp); mqtt.setDiscoveryPrefix(MQTT_PREFIX); device.setName(webName); device.setManufacturer("UniClock"); device.setModel(tubeDriver); device.setSoftwareVersion(FW); snprintf(mqttConfigUrl, sizeof(mqttConfigUrl), "http://%s/", WiFi.localIP().toString().c_str()); device.setConfigurationUrl(mqttConfigUrl); device.enableSharedAvailability(); device.enableLastWill(); //useTemp/useHumid count the master clock's sensors too, which are added at the end of this function - //so as before, a master-only sensor is not republished by this clock if (useTemp>0) { mqttTemp = new HASensorNumber("temperature", HASensorNumber::PrecisionP1); mqttTemp->setName("Temperature"); mqttTemp->setDeviceClass("temperature"); mqttTemp->setUnitOfMeasurement("°C"); } if (useHumid>0) { mqttHumid = new HASensorNumber("humidity", HASensorNumber::PrecisionP1); mqttHumid->setName("Humidity"); mqttHumid->setDeviceClass("humidity"); mqttHumid->setUnitOfMeasurement("%"); } if (usePress>0) { mqttPress = new HASensorNumber("pressure", HASensorNumber::PrecisionP1); mqttPress->setName("Air pressure"); mqttPress->setDeviceClass("pressure"); mqttPress->setUnitOfMeasurement("hPa"); } if (useLux>0) { mqttLux = new HASensorNumber("lux"); mqttLux->setName("Light"); mqttLux->setDeviceClass("illuminance"); mqttLux->setUnitOfMeasurement("lx"); } #if RADAR_PIN>=0 mqttRadar = new HABinarySensor("radar"); mqttRadar->setName("Radar"); mqttRadar->setDeviceClass("occupancy"); #endif mqttRssi.setName("WiFi signal"); mqttRssi.setDeviceClass("signal_strength"); mqttRssi.setUnitOfMeasurement("dBm"); mqttTubes.setName("Tubes"); mqttTubes.setIcon("mdi:numeric"); mqttTubes.onCommand(onTubesCommand); mqttDayBright.setName("Day brightness"); mqttNightBright.setName("Night brightness"); for (HANumber* n : {&mqttDayBright, &mqttNightBright}) { n->setIcon("mdi:brightness-6"); n->setMin(0); n->setMax(MAXBRIGHTNESS); n->setMode(HANumber::ModeSlider); n->onCommand(onBrightnessCommand); } mqttAutoDim.setName("Auto dim"); mqttAutoDim.setIcon("mdi:brightness-auto"); mqttAutoDim.onCommand(onAutoDimCommand); mqttCathodeProtect.setName("Cathode protect"); mqttCathodeProtect.setIcon("mdi:autorenew"); mqttCathodeProtect.onCommand(onCathodeProtectCommand); #ifdef USE_NEOPIXEL mqttBacklight.setName("Backlight"); mqttBacklight.onStateCommand(onBacklightStateCommand); mqttBacklight.onBrightnessCommand(onBacklightBrightnessCommand); mqttBacklight.onRGBColorCommand(onBacklightColorCommand); if (prm.rgbEffect > 0) mqttLastEffect = prm.rgbEffect; mqttLedEffect.setName("LED effect"); mqttLedEffect.setIcon("mdi:led-strip-variant"); //order = prm.rgbEffect values, see doAnimationMakuna() in z_neopixel.ino mqttLedEffect.setOptions("Off;Fixed colour;Rainbow flow;Rainbow step;Colour dimmer;Colour stepper;" "Stepflow table;Stepflow random;Stepflow random pixels;Stepflow random all;KITT"); mqttLedEffect.onCommand(onLedEffectCommand); #endif mqtt.onMessage(onMqttMessage); mqtt.onConnected(onMqttConnected); IPAddress tmp; tmp.fromString(String(prm.mqttBrokerAddr)); if (strlen(prm.mqttBrokerUser)==0) mqtt.begin(tmp); else mqtt.begin(tmp, prm.mqttBrokerUser, prm.mqttBrokerPsw); #ifdef USE_MASTER_CLOCK DPRINTLN("MQTT MASTER_CLOCK enabled!"); DPRINT("MASTER_TEMPERATURE_TOPIC:"); DPRINTLN(masterTemperatureTopic); DPRINT("MASTER_HUMIDITY_TOPIC:"); DPRINTLN(masterHumidityTopic); DPRINT("MASTER_RADAR_TOPIC:"); DPRINTLN(masterRadarTopic); #ifdef USE_MASTER_TEMP DPRINTLN(" - MQTT MASTER_TEMP sensor added!"); MASTERtempPtr = useTemp; //remember my ID-s useTemp++; //increase the number of sensors #endif #ifdef USE_MASTER_HUMID DPRINTLN(" - MQTT MASTER_HUMID sensor added!"); MASTERhumidPtr = useHumid; useHumid++; #endif #endif } //Report the current settings to HA. The library only publishes values that changed, so this is cheap; it keeps //HA in sync with changes made on the clock's web page. void mqttReportState() { mqttTubes.setState(tubesOn); mqttDayBright.setState((int16_t)prm.dayBright); mqttNightBright.setState((int16_t)prm.nightBright); mqttAutoDim.setState(prm.enableAutoDim); #ifdef USE_NEOPIXEL mqttBacklight.setState(prm.rgbEffect > 0); mqttBacklight.setBrightness(prm.rgbBrightness); mqttBacklight.setRGBColor(wheelToRgb(prm.rgbFixColor)); mqttLedEffect.setState(prm.rgbEffect); #endif } void mqttSend() { static unsigned long lastRun = 0; static unsigned long lastReport = 0; static bool wasConnected = false; mqtt.loop(); if (mqttSaveRequested && ((millis() - mqttSaveRequested) > MQTT_SAVE_DELAY)) { mqttSaveRequested = 0; DPRINTLN("MQTT: saving settings"); saveEEPROM(); } if (!mqtt.isConnected()) { wasConnected = false; return; } if (!wasConnected) { //(re)connected: force the first sensor refresh now wasConnected = true; lastRun = millis() - 1000ul * max((int)prm.mqttBrokerRefresh, MQTT_MIN_REFRESH); } if ((millis() - lastReport) > 1000) { lastReport = millis(); mqttReportState(); } if ((millis() - lastRun) < 1000ul * max((int)prm.mqttBrokerRefresh, MQTT_MIN_REFRESH)) return; lastRun = millis(); DPRINT("MQTT send:"); if (mqttTemp) {mqttTemp->setValue(temperature[0]+ prm.corrT0); DPRINT(temperature[0]+ prm.corrT0); DPRINT(", ");} if (mqttHumid) {mqttHumid->setValue(humid[0]+ prm.corrH0); DPRINT(humid[0]+ prm.corrH0); DPRINT(", ");} if (mqttPress) {mqttPress->setValue(pressur[0]); DPRINT(pressur[0]); DPRINT(", ");} if (mqttLux) {mqttLux->setValue((int32_t)lx); DPRINT(lx); DPRINT(", ");} if (mqttRadar) {mqttRadar->setState(radarON); DPRINT(radarON);} mqttRssi.setValue((int32_t)WiFi.RSSI()); DPRINTLN(" "); } #else void mqttSend() {} #endif