Created
June 12, 2025 21:05
-
-
Save pardeike/5b06dd0c3cd044901d85b39b86292695 to your computer and use it in GitHub Desktop.
Dynamically maintaining Home Assistant MQTT topics for a presence sensor from an ESP32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Arduino.h> | |
#include <vector> | |
#include <WiFi.h> | |
#include <PubSubClient.h> | |
#include <ArduinoJson.h> | |
#include "led.h" | |
#include "secrets.h" | |
const char *DEVICE_ID = "esp32_presence"; | |
const char *DEVICE_NAME = "ESP32 Presence"; | |
#define MAX_ZONES 3 | |
#define NAME_LEN 16 // "Zone123" fits | |
#define ID_LEN 12 // "zone123" fits | |
WiFiClient wifiClient; | |
PubSubClient mqtt(wifiClient); | |
struct Zone | |
{ | |
bool active; | |
char id[ID_LEN]; | |
char name[NAME_LEN]; | |
bool state; | |
}; | |
Zone zones[MAX_ZONES] = {}; // all zero-initialised | |
uint8_t activeCnt = 0; | |
uint16_t nameCounter = 0; | |
void connectWiFi() | |
{ | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(WIFI_SSID, WIFI_PASS); | |
while (WiFi.status() != WL_CONNECTED) | |
delay(250); | |
} | |
void connectMQTT() | |
{ | |
mqtt.setServer(MQTT_HOST, MQTT_PORT); | |
mqtt.setBufferSize(1024); | |
while (!mqtt.connected()) | |
mqtt.connect(DEVICE_ID, MQTT_USER, MQTT_PASS, "homeassistant/status", 0, true, "offline"); | |
} | |
void topicConfig(char *out, size_t len, const Zone &z) | |
{ | |
snprintf(out, len, "homeassistant/binary_sensor/%s_%s/config", DEVICE_ID, z.id); | |
} | |
void topicState(char *out, size_t len, const Zone &z) | |
{ | |
snprintf(out, len, "esp32/%s/%s", DEVICE_ID, z.id); | |
} | |
void publishDiscovery(const Zone &z) | |
{ | |
JsonDocument doc; // ← heap-backed, auto-grows | |
doc["name"] = z.name; | |
doc["unique_id"] = String(DEVICE_ID) + "_" + z.id; | |
char st[64]; | |
snprintf(st, sizeof(st), "esp32/%s/%s", DEVICE_ID, z.id); | |
doc["state_topic"] = st; | |
doc["device_class"] = "occupancy"; | |
doc["payload_on"] = "ON"; | |
doc["payload_off"] = "OFF"; | |
JsonObject dev = doc["device"].to<JsonObject>(); | |
dev["name"] = DEVICE_NAME; | |
JsonArray ids = dev["identifiers"].to<JsonArray>(); | |
ids.add(DEVICE_ID); | |
char topic[128]; | |
snprintf(topic, sizeof(topic), "homeassistant/binary_sensor/%s_%s/config", DEVICE_ID, z.id); | |
size_t len = measureJson(doc); // you can reserve() if you like | |
uint8_t *buf = new uint8_t[len + 1]; | |
serializeJson(doc, buf, len + 1); | |
mqtt.publish(topic, buf, len, true); | |
delete[] buf; | |
} | |
void unpublishDiscovery(const Zone &z) | |
{ | |
char topic[128], st[64]; | |
topicConfig(topic, sizeof(topic), z); | |
topicState(st, sizeof(st), z); | |
mqtt.publish(topic, nullptr, 0, true); // delete retained config | |
mqtt.publish(st, nullptr, 0, true); // delete last state | |
} | |
void publishState(const Zone &z) | |
{ | |
char st[64]; | |
topicState(st, sizeof(st), z); | |
mqtt.publish(st, z.state ? "ON" : "OFF", true); | |
} | |
Zone *randomActiveZone() | |
{ | |
if (!activeCnt) return nullptr; | |
uint8_t pick = random(activeCnt); | |
for (uint8_t i = 0; i < MAX_ZONES; ++i) | |
if (zones[i].active) | |
if (!pick--) return &zones[i]; | |
return nullptr; // should never hit | |
} | |
void addZone() | |
{ | |
if (activeCnt >= MAX_ZONES) return; | |
for (Zone &z : zones) | |
if (!z.active) | |
{ | |
z.active = true; | |
++nameCounter; | |
snprintf(z.id, ID_LEN, "zone%u", nameCounter); | |
snprintf(z.name, NAME_LEN, "Zone%u", nameCounter); | |
z.state = false; | |
++activeCnt; | |
publishDiscovery(z); | |
publishState(z); | |
Serial.printf("[Add] %s='%s' created\n", z.id, z.name); | |
setLED(0, 8, 0); | |
return; | |
} | |
} | |
void removeZone() | |
{ | |
Zone *z = randomActiveZone(); | |
if (!z) return; | |
unpublishDiscovery(*z); | |
z->active = false; | |
--activeCnt; | |
Serial.printf("[Remove] %s='%s' removed\n", z->id, z->name); | |
setLED(8, 0, 0); | |
} | |
void renameZone() | |
{ | |
Zone *z = randomActiveZone(); | |
if (!z) return; | |
unpublishDiscovery(*z); | |
++nameCounter; | |
snprintf(z->name, NAME_LEN, "Zone%u", nameCounter); | |
publishDiscovery(*z); | |
publishState(*z); | |
Serial.printf("[Rename] %s='%s'\n", z->id, z->name); | |
setLED(0, 0, 8); | |
} | |
void toggleZone() | |
{ | |
Zone *z = randomActiveZone(); | |
if (!z) return; | |
z->state = !z->state; | |
publishState(*z); | |
Serial.printf("[Toggle] %s='%s' ➜ %s\n", z->id, z->name, z->state ? "ON" : "OFF"); | |
setLED(8, 0, 8); | |
} | |
void randomAction() | |
{ | |
bool canAdd = activeCnt < MAX_ZONES; | |
bool canChange = activeCnt > 0; | |
bool canRemove = activeCnt > 1; | |
uint8_t choices[4]; | |
uint8_t n = 0; | |
if (canAdd) | |
choices[n++] = 0; // add | |
if (canRemove) | |
choices[n++] = 1; // remove | |
if (canChange) | |
{ | |
choices[n++] = 2; // rename | |
choices[n++] = 3; // toggle | |
} | |
if (!n) return; | |
switch (choices[random(n)]) | |
{ | |
case 0: | |
addZone(); | |
break; | |
case 1: | |
removeZone(); | |
break; | |
case 2: | |
renameZone(); | |
break; | |
case 3: | |
toggleZone(); | |
break; | |
} | |
} | |
void setup() | |
{ | |
Serial.begin(115200); | |
setLED(0, 0, 0); | |
delay(1000); | |
randomSeed(esp_random()); | |
setLED(0, 8, 0); | |
connectWiFi(); | |
setLED(8, 0, 8); | |
connectMQTT(); | |
} | |
unsigned long lastTick = 0; | |
void loop() | |
{ | |
if (!mqtt.connected()) connectMQTT(); | |
mqtt.loop(); | |
if (millis() - lastTick > PUBLISH_INTERVAL) | |
{ | |
randomAction(); | |
lastTick = millis(); | |
} | |
} |
And for an M5Stack Atom3, this platformio.ini:
[env:atom-s3]
platform = espressif32
board = m5stack-atoms3
framework = arduino
monitor_speed = 115200
build_flags =
-DARDUINO_USB_CDC_ON_BOOT=1
-DMQTT_MAX_PACKET_SIZE=1024
lib_deps =
m5stack/M5Unified@^0.1.17
knolleary/PubSubClient@^2.8
bblanchon/ArduinoJson@^7.4.1
adafruit/Adafruit NeoPixel@^1.12.4
Not to forget this secrets.h
file template:
#pragma once
// ---------- Wi-Fi ----------
#define WIFI_SSID ""
#define WIFI_PASS ""
// ---------- MQTT ----------
#define MQTT_HOST "hass.local" // IP or mDNS name of broker
#define MQTT_PORT 1883
#define MQTT_USER ""
#define MQTT_PASS ""
// How often (ms) to publish simulated states
#define PUBLISH_INTERVAL 8000
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also needs this
LED.h
file: