Add Home Assistant controls over MQTT
Upgrade ArduinoHA from 1.3.0 to 2.1.0 and rewrite mqtt.ino for it. The clock now shows up in Home Assistant (discovery prefix "homeassistant") with: - switches Tubes (blank the display, runtime only) and Auto dim - numbers Day/Night brightness - light Backlight (on/off, brightness, RGB mapped to the colour wheel) - select LED effect, button Cathode protect - sensors Light (LDR) and WiFi signal, plus the upstream sensors when the clock has them; master-clock subscriptions are kept Settings changed from HA are saved to EEPROM 3 s after the last change. Fixes the unbounded memcpy in the MQTT message handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d3e878051
commit
9a7061383c
108 files changed
+11482
-6567
No files matched your search
@@ -2,127 +2,107 @@
|
||||
//=========================================================================================================================
|
||||
// mqtt.ino - Home Assistant / MQTT integration (compiled only if USE_MQTT is defined for the active CLOCK_NN in clocks.h)
|
||||
//
|
||||
// Library: ArduinoHA (https://github.com/dawidchyrzynski/arduino-home-assistant), which wraps PubSubClient
|
||||
// (https://pubsubclient.knolleary.net/) and adds Home Assistant MQTT Discovery on top of it. Because ArduinoHA
|
||||
// is used, this file never hand-builds HA discovery JSON payloads itself - HADevice/HAMqtt/HASensor below
|
||||
// publish the discovery config topics automatically the first time mqtt.begin()/mqtt.loop() connects.
|
||||
// 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.)
|
||||
//
|
||||
// MASTER / SLAVE sensor-sharing architecture (the distinctive part of this subsystem):
|
||||
// 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 (temperature/humidity/pressure/lux/radar, whichever are physically present - see
|
||||
// HASensor objects below) under a "<MQTT_PREFIX>/sensor/<mac>/..." style topic, discovered via HA MQTT Discovery.
|
||||
// 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:
|
||||
// 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
|
||||
// This lets one "master" clock's physical sensor (e.g. a dedicated sensor box, or a clock with a real radar/PIR
|
||||
// sensor wired to RADAR_PIN) feed several "slave" clocks that have no sensor of their own, purely over MQTT.
|
||||
// IMPORTANT: if a "slave" clock DOES also have its own local RADAR_PIN wired, the local pin always wins - the radar
|
||||
// topic subscription is skipped entirely in onMqttConnected() (see RADAR_PIN>=0 check there), and the core loop's
|
||||
// checkTubePowerOnOff() also prefers digitalRead(RADAR_PIN) over the MQTT-derived mqttRadarON when both exist.
|
||||
// As of the current clocks.h: CLOCK_99 ("Dummy clock, sensor box with MQTT connector") is a pure MASTER/publisher
|
||||
// (USE_MQTT on, USE_MASTER_CLOCK off, has its own RADAR_PIN). CLOCK_26 has USE_MQTT + USE_MASTER_CLOCK +
|
||||
// USE_MASTER_RADAR all on, so it both publishes its own temperature/humidity sensors AND subscribes to another
|
||||
// clock's radar topic. CLOCK_23/CLOCK_23A define the USE_MASTER_* macros too but currently have USE_MQTT commented
|
||||
// out in clocks.h, so for those configs this whole file is compiled out and the master settings are presently inert.
|
||||
// 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.
|
||||
//
|
||||
// Call pattern from the core .ino: setupMqtt() is called once from setup() when USE_MQTT is defined; mqttSend() is
|
||||
// called every loop() iteration (but only while WiFi is connected and prm.mqttEnable is set) - it internally rate-limits
|
||||
// actual publishes to once every prm.mqttBrokerRefresh seconds, while still pumping mqtt.loop() (which also drives
|
||||
// PubSubClient's/ArduinoHA's own reconnect handling) on every call.
|
||||
// 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 "UniClock32"
|
||||
//#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 from MASTER CLOCK
|
||||
//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
|
||||
//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
|
||||
//#define MASTER_RADAR_TOPIC "homeassistant/sensor/10521c5e14c4/radar/state"
|
||||
//#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 "..."
|
||||
//_______________________________________________________________________________________________________________________
|
||||
// (the block above is documentation only, shown here as a template of what to copy into a CLOCK_NN section of clocks.h -
|
||||
// none of these #define lines are active in this file; the real values come from clocks.h at compile time.)
|
||||
|
||||
//To subscribe to my sensors: You can check it with Chrome browser + MQTTlens addon) MAC_ADDRESS: for example: 3e090b3e090b
|
||||
// homeassistant/sensor/3e090b3e090b/temperature/value
|
||||
// homeassistant/sensor/3e090b3e090b/humidity/value
|
||||
// homeassistant/sensor/3e090b3e090b/pressure/value
|
||||
// homeassistant/sensor/3e090b3e090b/lux/value
|
||||
|
||||
// old solution, now moved to prm.mqttBrokerAddr, available on clock's web page
|
||||
//#define BROKER_ADDR IPAddress(192,168,1,241)
|
||||
//#define BROKER_USERNAME "" //moved to prm.mqttBrokerUser
|
||||
//#define BROKER_PASSWORD "" //moved to mqttBrokerPsw
|
||||
//_______________________________________________________________________________________________________________________
|
||||
// Credential handling: broker IP/user/password are NOT compile-time constants (see the commented-out BROKER_* macros
|
||||
// above, kept only as historical documentation) - they live in prm.mqttBrokerAddr/mqttBrokerUser/mqttBrokerPsw, which
|
||||
// are persisted settings editable from the clock's own web UI (and read here in setupMqtt()/mqttSend()). This lets a
|
||||
// user point any already-flashed clock at a different broker without recompiling.
|
||||
// SECURITY: factoryReset() in the core .ino seeds these with a hardcoded default
|
||||
// (strcpy(prm.mqttBrokerAddr,"10.10.0.202"); strcpy(prm.mqttBrokerUser,"mqtt"); strcpy(prm.mqttBrokerPsw,"mqtt")) -
|
||||
// i.e. a real broker IP and a trivial user/password baked into the firmware as the out-of-the-box default. Not this
|
||||
// file's code, but directly relevant here since it is exactly the credential pair mqttSend()/setupMqtt() use below.
|
||||
|
||||
#include <ArduinoHA.h> //https://github.com/dawidchyrzynski/arduino-home-assistant
|
||||
//https://pubsubclient.knolleary.net/ https://github.com/knolleary/pubsubclient
|
||||
|
||||
#if !defined(MQTT_PREFIX)
|
||||
#define MQTT_PREFIX "UniClock32" //used both as the HA discovery prefix and as the first path segment of this device's own published topics
|
||||
#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 a per-device unique ID/topic segment (see setupMqtt())
|
||||
char masterTemperatureTopic[80] = ""; //resolved from MASTER_TEMPERATURE_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_TEMP is used - topic this clock SUBSCRIBES to
|
||||
char masterHumidityTopic[80] = ""; //resolved from MASTER_HUMIDITY_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_HUMID is used - topic this clock SUBSCRIBES to
|
||||
char masterRadarTopic[80] = ""; //resolved from MASTER_RADAR_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_RADAR is used - topic this clock SUBSCRIBES to
|
||||
|
||||
// Index into the shared temperature[]/humid[] arrays (declared in the core .ino, up to 6 slots) reserved for the
|
||||
// MASTER clock's mirrored reading, so the rest of the firmware (display rotation, web UI, etc.) can treat the
|
||||
// MQTT-sourced master reading exactly like one more local sensor. Assigned in setupMqtt() as "next free slot"
|
||||
// (useTemp/useHumid BEFORE incrementing) - despite the doc comment above saying "this will be the sensor#0", it is
|
||||
// only actually sensor#0 if this clock has no other local temperature/humidity sensor already counted.
|
||||
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); //ArduinoHA wrapper around PubSubClient; mqtt.loop() also drives PubSubClient's own reconnect/keepalive handling
|
||||
//Sensors of local clock, to send data (each one auto-publishes its HA MQTT Discovery config the first time it connects):
|
||||
HASensor mqttTemp("temperature");
|
||||
HASensor mqttHumid("humidity");
|
||||
HASensor mqttPress("pressure");
|
||||
HASensor mqttLux("lux");
|
||||
HASensor mqttRadar("radar");
|
||||
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;
|
||||
}
|
||||
|
||||
// This is the SLAVE side of the master/slave architecture: called by ArduinoHA/PubSubClient whenever a message
|
||||
// arrives on any topic this clock is subscribed to (see onMqttConnected() below for what gets subscribed - only
|
||||
// the MASTER_* topics selected via USE_MASTER_TEMP/USE_MASTER_HUMID/USE_MASTER_RADAR in clocks.h). The payload is
|
||||
// the raw value published by the MASTER clock's own mqttSend()/HASensor.setValue() (a plain ASCII number for
|
||||
// temperature/humidity, "0"/"1" for radar - matching HASensor's default text encoding).
|
||||
void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
|
||||
float tempTMP,humidTMP;
|
||||
char tmp[100];
|
||||
// This callback is called when message from MQTT broker is received.
|
||||
// Please note that you should always verify if the message's topic is the one you expect.
|
||||
// For example: if (memcmp(topic, "myCustomTopic") == 0) { ... }
|
||||
char tmp[32];
|
||||
|
||||
payloadToString(payload, length, tmp, sizeof(tmp));
|
||||
DPRINT("New message on topic: "); DPRINT(topic);
|
||||
|
||||
// POSSIBLE BUG: `length` (the broker-reported payload size) is copied into the fixed 100-byte `tmp` buffer with
|
||||
// no bounds check - a payload of 100 bytes or more (e.g. a misconfigured/malicious publisher on the same topic)
|
||||
// overflows `tmp` on the stack. Also, memcpy() does NOT null-terminate `tmp`: it is used afterwards as a C string
|
||||
// (DPRINTLN(tmp), atof(tmp), tmp[0]) with whatever byte happens to follow the copied data in memory, which is
|
||||
// undefined behavior if that byte isn't already '\0'. A `if (length >= sizeof(tmp)) length = sizeof(tmp)-1;`
|
||||
// guard plus `tmp[length] = '\0';` after the memcpy would fix both issues.
|
||||
memcpy(tmp,payload,length);
|
||||
DPRINT(" Data: "); DPRINTLN(tmp);
|
||||
// Topic match: compares up to sizeof(masterTemperatureTopic) (80, the fixed buffer size) bytes rather than the
|
||||
// actual topic length. This still behaves like strcmp() in practice because strncmp() stops as soon as it hits
|
||||
// the '\0' terminator on either side, but it is fragile - if MASTER_TEMPERATURE_TOPIC in clocks.h were ever 80
|
||||
// characters or longer, strncpy() in setupMqtt() would not null-terminate masterTemperatureTopic, and this
|
||||
// comparison could silently misbehave. Using strcmp(topic, masterTemperatureTopic) would be more robust.
|
||||
|
||||
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)
|
||||
@@ -134,12 +114,8 @@ void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
|
||||
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)) {
|
||||
// Master clock's radar/PIR presence state, mirrored into mqttRadarON (and radarON) so the rest of the
|
||||
// firmware (night animation, tube power on/off, etc.) reacts to it exactly like a local sensor reading.
|
||||
// We only ever get here if this clock did NOT subscribe to the radar topic due to having its own local
|
||||
// RADAR_PIN (see the RADAR_PIN>=0 guard in onMqttConnected() below) - local hardware always wins.
|
||||
DPRINT("MASTER radar:"); DPRINTLN(tmp[0]);
|
||||
if (tmp[0] == '0') {
|
||||
if ((tmp[0] == '0') || (strcmp(tmp, "OFF") == 0)) {
|
||||
mqttRadarON = false;
|
||||
radarON = mqttRadarON;
|
||||
}
|
||||
@@ -149,17 +125,11 @@ void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
|
||||
radarLastOn = millis();
|
||||
}
|
||||
DPRINT("mqttRadarON:"); DPRINTLN(mqttRadarON);
|
||||
|
||||
}
|
||||
//mqtt.publish("myPublishTopic", "hello");
|
||||
}
|
||||
|
||||
// Called by ArduinoHA once the broker connection (re)establishes - this is where the SLAVE side actually subscribes
|
||||
// to the MASTER clock's topics. Runs again on every reconnect (not just the first connect), so subscriptions survive
|
||||
// broker restarts/WiFi drops without any extra logic needed in mqttSend().
|
||||
void onMqttConnected() {
|
||||
DPRINTLN("Connected to the broker!");
|
||||
// You can subscribe to custom topic if you need
|
||||
#ifdef USE_MASTER_CLOCK
|
||||
DPRINTLN(F("MASTER_CLOCK enabled! Subscribe to following topics:"));
|
||||
#ifdef USE_MASTER_TEMP
|
||||
@@ -181,10 +151,6 @@ void onMqttConnected() {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MASTER_RADAR
|
||||
// WHY: if this clock has its own physical radar/PIR sensor wired to RADAR_PIN, that local reading always
|
||||
// takes priority (see checkTubePowerOnOff() in the core .ino, which prefers digitalRead(RADAR_PIN) over
|
||||
// mqttRadarON when both exist). So there is no point subscribing to the master's radar topic at all in that
|
||||
// case - onMqttMessage()'s radar branch would just be mirrored into a value nothing ever reads.
|
||||
if (RADAR_PIN>=0) {
|
||||
DPRINTLN(F("Local radar enabled! MQTT MASTER radar disabled!"));
|
||||
}
|
||||
@@ -201,65 +167,213 @@ void onMqttConnected() {
|
||||
#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
|
||||
sprintf(mactmp,"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
|
||||
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]);
|
||||
|
||||
#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
|
||||
#endif
|
||||
#if defined(MASTER_RADAR_TOPIC)
|
||||
strncpy(masterRadarTopic,MASTER_RADAR_TOPIC,sizeof(masterRadarTopic));
|
||||
#endif
|
||||
DPRINT("Starting MQTT CLient: "); DPRINT(MQTT_PREFIX); DPRINT("/sensor/"); DPRINTLN(mactmp);
|
||||
|
||||
|
||||
DPRINT("Starting MQTT Client, discovery prefix: "); DPRINT(MQTT_PREFIX); DPRINT(" device: "); DPRINTLN(mactmp);
|
||||
mqtt.setDiscoveryPrefix(MQTT_PREFIX);
|
||||
device.setName(FW);
|
||||
device.setSoftwareVersion("3.0");
|
||||
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.setUnitOfMeasurement("°C");
|
||||
mqttTemp.setDeviceClass("sensor");
|
||||
mqttTemp.setName("Inside temperature");
|
||||
mqttTemp = new HASensorNumber("temperature", HASensorNumber::PrecisionP1);
|
||||
mqttTemp->setName("Temperature");
|
||||
mqttTemp->setDeviceClass("temperature");
|
||||
mqttTemp->setUnitOfMeasurement("°C");
|
||||
}
|
||||
if (useHumid>0) {
|
||||
mqttHumid.setUnitOfMeasurement("%");
|
||||
mqttHumid.setDeviceClass("sensor");
|
||||
mqttHumid.setName("Inside humidity");
|
||||
mqttHumid = new HASensorNumber("humidity", HASensorNumber::PrecisionP1);
|
||||
mqttHumid->setName("Humidity");
|
||||
mqttHumid->setDeviceClass("humidity");
|
||||
mqttHumid->setUnitOfMeasurement("%");
|
||||
}
|
||||
if (usePress>0) {
|
||||
mqttPress.setUnitOfMeasurement("hPa");
|
||||
mqttPress.setDeviceClass("sensor");
|
||||
mqttPress.setName("Air pressure");
|
||||
mqttPress = new HASensorNumber("pressure", HASensorNumber::PrecisionP1);
|
||||
mqttPress->setName("Air pressure");
|
||||
mqttPress->setDeviceClass("pressure");
|
||||
mqttPress->setUnitOfMeasurement("hPa");
|
||||
}
|
||||
if (useLux>0) {
|
||||
mqttLux.setUnitOfMeasurement("lux");
|
||||
mqttLux.setDeviceClass("sensor");
|
||||
mqttLux.setName("Inside lux");
|
||||
mqttLux = new HASensorNumber("lux");
|
||||
mqttLux->setName("Light");
|
||||
mqttLux->setDeviceClass("illuminance");
|
||||
mqttLux->setUnitOfMeasurement("lx");
|
||||
}
|
||||
#if RADAR_PIN>=0
|
||||
mqttRadar.setUnitOfMeasurement("on/off");
|
||||
mqttRadar.setDeviceClass("sensor");
|
||||
mqttRadar.setName("Radar sensor");
|
||||
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
|
||||
else
|
||||
mqtt.begin(tmp, prm.mqttBrokerUser, prm.mqttBrokerPsw);
|
||||
|
||||
//mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD);
|
||||
|
||||
device.enableLastWill();
|
||||
#ifdef USE_MASTER_CLOCK
|
||||
DPRINTLN("MQTT MASTER_CLOCK enabled!");
|
||||
DPRINT("MASTER_TEMPERATURE_TOPIC:"); DPRINTLN(masterTemperatureTopic);
|
||||
@@ -270,37 +384,69 @@ void setupMqtt() {
|
||||
MASTERtempPtr = useTemp; //remember my ID-s
|
||||
useTemp++; //increase the number of sensors
|
||||
#endif
|
||||
#ifdef USE_MASTER_HUMID
|
||||
#ifdef USE_MASTER_HUMID
|
||||
DPRINTLN(" - MQTT MASTER_HUMID sensor added!");
|
||||
MASTERhumidPtr = useHumid;
|
||||
useHumid++;
|
||||
#endif
|
||||
#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 = millis();
|
||||
//if (!mqtt.isConnected()) return;
|
||||
static unsigned long lastRun = 0;
|
||||
static unsigned long lastReport = 0;
|
||||
static bool wasConnected = false;
|
||||
|
||||
mqtt.loop();
|
||||
|
||||
if ((millis() - lastRun) < prm.mqttBrokerRefresh*1000) return;
|
||||
lastRun = millis();
|
||||
if (mqtt.isConnected()) DPRINT("MQTT Connected.");
|
||||
else {
|
||||
DPRINTLN("MQTT Failed to connect.");
|
||||
|
||||
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 (useTemp>0) {mqttTemp.setValue(temperature[0]+ prm.corrT0); DPRINT(temperature[0]+ prm.corrT0); DPRINT(", ");}
|
||||
if (useHumid>0) {mqttHumid.setValue(humid[0]+ prm.corrH0); DPRINT(humid[0]+ prm.corrH0); DPRINT(", ");}
|
||||
if (usePress>0) {mqttPress.setValue(pressur[0]); DPRINT(pressur[0]); DPRINT(", ");}
|
||||
if (useLux>0) {mqttLux.setValue(lx); DPRINT(lx); DPRINT(", ");}
|
||||
#if RADAR_PIN>=0
|
||||
mqttRadar.setValue(radarON); DPRINT(radarON);
|
||||
#endif
|
||||
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
|
||||
Reference in new issue
Block a user