Files
nixie-clock/firmware/uniclock/ESP32_UniClock2/mqtt.ino
T

452 lines
19 KiB
Arduino
Raw Normal View History

2026-10-25 20:48:04 +01:00
#ifdef USE_MQTT
//=========================================================================================================================
// mqtt.ino - Home Assistant / MQTT integration (compiled only if USE_MQTT is defined for the active CLOCK_NN in clocks.h)
//
2026-10-25 21:37:25 +01:00
// 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.)
2026-10-25 20:48:04 +01:00
//
2026-10-25 21:37:25 +01:00
// 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:
2026-10-25 20:48:04 +01:00
// Every clock that defines USE_MQTT connects to the same broker (prm.mqttBrokerAddr/User/Psw, set on the web UI) and
2026-10-25 21:37:25 +01:00
// 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:
2026-10-25 20:48:04 +01:00
// 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
2026-10-25 21:37:25 +01:00
// 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
// "<data prefix>/<device id>/<entity id>/stat_t" (data prefix "aha" by default), so MASTER_*_TOPIC settings
// written for the old library must be updated.
2026-10-25 20:48:04 +01:00
//
2026-10-25 21:37:25 +01:00
// 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).
2026-10-25 20:48:04 +01:00
//_______________________________________________________________________________________________________________________
//To use MQTT services the following settings should be defined in clocks.h
//#define USE_MQTT
2026-10-25 21:37:25 +01:00
//#define MQTT_PREFIX "homeassistant" //HA discovery prefix (Home Assistant's default is "homeassistant")
2026-10-25 20:48:04 +01:00
//#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
2026-10-25 21:37:25 +01:00
//#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 "..."
2026-10-25 20:48:04 +01:00
//_______________________________________________________________________________________________________________________
#include <ArduinoHA.h> //https://github.com/dawidchyrzynski/arduino-home-assistant
#if !defined(MQTT_PREFIX)
2026-10-25 21:37:25 +01:00
#define MQTT_PREFIX "UniClock32" //HA discovery prefix (upstream default; Home Assistant itself listens on "homeassistant")
2026-10-25 20:48:04 +01:00
#endif
2026-10-25 21:37:25 +01:00
#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)
2026-10-25 20:48:04 +01:00
2026-10-25 21:37:25 +01:00
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
2026-10-25 20:48:04 +01:00
byte MASTERtempPtr=0;
byte MASTERhumidPtr=0;
2026-10-25 21:37:25 +01:00
char mqttConfigUrl[40];
unsigned long mqttSaveRequested = 0; //millis() of the last change from HA that still has to be saved, 0 = none
2026-10-25 20:48:04 +01:00
WiFiClient client;
HADevice device;
2026-10-25 21:37:25 +01:00
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;
}
2026-10-25 20:48:04 +01:00
void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
float tempTMP,humidTMP;
2026-10-25 21:37:25 +01:00
char tmp[32];
2026-10-25 20:48:04 +01:00
2026-10-25 21:37:25 +01:00
payloadToString(payload, length, tmp, sizeof(tmp));
2026-10-25 20:48:04 +01:00
DPRINT("New message on topic: "); DPRINT(topic);
DPRINT(" Data: "); DPRINTLN(tmp);
2026-10-25 21:37:25 +01:00
2026-10-25 20:48:04 +01:00
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]);
2026-10-25 21:37:25 +01:00
if ((tmp[0] == '0') || (strcmp(tmp, "OFF") == 0)) {
2026-10-25 20:48:04 +01:00
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
}
2026-10-25 21:37:25 +01:00
//--- 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
2026-10-25 20:48:04 +01:00
void setupMqtt() {
char mactmp[20];
WiFi.macAddress(mac);
device.setUniqueId(mac, sizeof(mac)); //set device MAC, as unique ID
2026-10-25 21:37:25 +01:00
device.enableExtendedUniqueIds(); //entity unique IDs = <mac>_<entity>, 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]);
2026-10-25 20:48:04 +01:00
#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));
2026-10-25 21:37:25 +01:00
#endif
2026-10-25 20:48:04 +01:00
#if defined(MASTER_RADAR_TOPIC)
strncpy(masterRadarTopic,MASTER_RADAR_TOPIC,sizeof(masterRadarTopic));
#endif
2026-10-25 21:37:25 +01:00
DPRINT("Starting MQTT Client, discovery prefix: "); DPRINT(MQTT_PREFIX); DPRINT(" device: "); DPRINTLN(mactmp);
2026-10-25 20:48:04 +01:00
mqtt.setDiscoveryPrefix(MQTT_PREFIX);
2026-10-25 21:37:25 +01:00
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
2026-10-25 20:48:04 +01:00
if (useTemp>0) {
2026-10-25 21:37:25 +01:00
mqttTemp = new HASensorNumber("temperature", HASensorNumber::PrecisionP1);
mqttTemp->setName("Temperature");
mqttTemp->setDeviceClass("temperature");
mqttTemp->setUnitOfMeasurement("°C");
2026-10-25 20:48:04 +01:00
}
if (useHumid>0) {
2026-10-25 21:37:25 +01:00
mqttHumid = new HASensorNumber("humidity", HASensorNumber::PrecisionP1);
mqttHumid->setName("Humidity");
mqttHumid->setDeviceClass("humidity");
mqttHumid->setUnitOfMeasurement("%");
2026-10-25 20:48:04 +01:00
}
if (usePress>0) {
2026-10-25 21:37:25 +01:00
mqttPress = new HASensorNumber("pressure", HASensorNumber::PrecisionP1);
mqttPress->setName("Air pressure");
mqttPress->setDeviceClass("pressure");
mqttPress->setUnitOfMeasurement("hPa");
2026-10-25 20:48:04 +01:00
}
if (useLux>0) {
2026-10-25 21:37:25 +01:00
mqttLux = new HASensorNumber("lux");
mqttLux->setName("Light");
mqttLux->setDeviceClass("illuminance");
mqttLux->setUnitOfMeasurement("lx");
2026-10-25 20:48:04 +01:00
}
#if RADAR_PIN>=0
2026-10-25 21:37:25 +01:00
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);
2026-10-25 20:48:04 +01:00
#endif
mqtt.onMessage(onMqttMessage);
mqtt.onConnected(onMqttConnected);
2026-10-25 21:37:25 +01:00
2026-10-25 20:48:04 +01:00
IPAddress tmp;
tmp.fromString(String(prm.mqttBrokerAddr));
if (strlen(prm.mqttBrokerUser)==0)
mqtt.begin(tmp);
2026-10-25 21:37:25 +01:00
else
2026-10-25 20:48:04 +01:00
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
2026-10-25 21:37:25 +01:00
#ifdef USE_MASTER_HUMID
2026-10-25 20:48:04 +01:00
DPRINTLN(" - MQTT MASTER_HUMID sensor added!");
MASTERhumidPtr = useHumid;
useHumid++;
2026-10-25 21:37:25 +01:00
#endif
2026-10-25 20:48:04 +01:00
#endif
}
2026-10-25 21:37:25 +01:00
//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
}
2026-10-25 20:48:04 +01:00
void mqttSend() {
2026-10-25 21:37:25 +01:00
static unsigned long lastRun = 0;
static unsigned long lastReport = 0;
static bool wasConnected = false;
2026-10-25 20:48:04 +01:00
mqtt.loop();
2026-10-25 21:37:25 +01:00
if (mqttSaveRequested && ((millis() - mqttSaveRequested) > MQTT_SAVE_DELAY)) {
mqttSaveRequested = 0;
DPRINTLN("MQTT: saving settings");
saveEEPROM();
}
if (!mqtt.isConnected()) {
wasConnected = false;
2026-10-25 20:48:04 +01:00
return;
}
2026-10-25 21:37:25 +01:00
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();
2026-10-25 20:48:04 +01:00
DPRINT("MQTT send:");
2026-10-25 21:37:25 +01:00
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());
2026-10-25 20:48:04 +01:00
DPRINTLN(" ");
}
2026-10-25 21:37:25 +01:00
2026-10-25 20:48:04 +01:00
#else
void mqttSend() {}
#endif