Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 84 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,97 @@
# Config payloads generation for Home Assistant MQTT discovery
# HaMqttConfigBuilderLite

A tiny library that will let you generate config payloads for Home Assistant MQTT discovery. These payload ar JSON key/value pairs.
A lightweight, zero-dependency JSON builder for Home Assistant MQTT Discovery payloads.

This is intended to replace [plapointe6/HAMqttDevice](https://github.com/plapointe6/HAMqttDevice) in my future iot devices.
**Features:**

- No `ArduinoJson` or STL containers
- Single pre-allocated buffer (`String`)
- Very low RAM usage (no heap fragmentation)
- Support for nested objects (device, availability, etc.)
- Efficient reuse of the `device` block for multiple sensor entities

## Exemple
---

```c++
#include "Arduino.h"
## 🔧 Basic Usage

```cpp
#include "HaMqttConfigBuilder.h"

String generateConfig() {
return HaMqttConfigBuilder()
.add("~", "homeassistant/light/my_light")
.add("unique_id", "my_light")
.add("name", "My test light")
.add("stat_t", "~/state")
.add("cmd_t", "~/cmd")
.generatePayload();
}
HaMqttConfigBuilder cfg;

void setup() {
Serial.begin(115200);
Serial.println(generateConfig());
}
// Step 1: Define the device block once
cfg.beginDevice()
.add("name", "Plant Sensor")
.add("model", "v1.0")
.add("sw", "0.1")
.endDevice();

void loop() {
}
// Step 2: Add the first sensor
cfg.add("name", "Soil Moisture")
.add("state_topic", "plant/sensor/soil")
.add("unit_of_meas", "%")
.add("device_class", "humidity");

Serial.println(cfg.generate());

// Step 3: Reuse device block, add a second sensor
cfg.nextSensor();
cfg.add("name", "Plant Battery")
.add("state_topic", "plant/sensor/batt")
.add("unit_of_meas", "%")
.add("device_class", "battery");

Serial.println(cfg.generate());
```

Output:
---

## 📦 Output Example

```json
{
"device": {
"name": "Plant Sensor",
"model": "v1.0",
"sw": "0.1"
},
"name": "Soil Moisture",
"state_topic": "plant/sensor/soil",
"unit_of_meas": "%",
"device_class": "humidity"
}
```
{"~":"homeassistant/light/my_light","unique_id":"my_light","name":"My test light","stat_t":"~/state","cmd_t":"~/cmd"}

---

## 🔍 Extract Field Value

You can extract values from the generated JSON using `getString()`:

```cpp
String topic;
if (cfg.getString("state_topic", topic)) {
Serial.println("Topic is: " + topic);
}
```

---

## 🧠 Memory Efficiency

- Internally uses a single `String` buffer
- Nested objects supported up to configurable max depth (default 6)
- Device block is cached via position pointer to avoid duplication

---

## 💡 Tip

Use `cfg.clear()` if you want to start over completely and redefine the device block.

---

## License

MIT

213 changes: 173 additions & 40 deletions src/HaMqttConfigBuilder.h
Original file line number Diff line number Diff line change
@@ -1,54 +1,187 @@
#include "Arduino.h"
#pragma once
/**
* HaMqttConfigBuilder
* -----------------------------------------------------------------
* Ultra-small Home Assistant MQTT Discovery JSON Builder
* • Header-only, no STL containers, no ArduinoJson
* • Single preallocated string as buffer → minimal RAM usage
* • Nested objects (Default maxDepth = 6)
* • Integer, Float, Bool, String fields without temporary objects
* • Simple top-level lookup via getString(key, result)
* • **Device caching**: `beginDevice()` / `endDevice()` + `nextSensor()`
* → Device block is retained, sensor part is overwritten
*/

#include <Arduino.h>
#include <stdlib.h> // dtostrf

// An helper class to generate a json Key/Value config payload for Home Assistant
class HaMqttConfigBuilder {
public:
static constexpr uint8_t kStaticMaxDepth = 6; ///< Compile-time depth limit

private:
struct Elem {
String key;
String str;
double num;
};
std::vector<Elem> elements;
explicit HaMqttConfigBuilder(size_t reserveBytes = 256, uint8_t maxDepth = 4)
: maxDepth_(maxDepth > kStaticMaxDepth ? kStaticMaxDepth : maxDepth) {
payload_.reserve(reserveBytes);
payload_ = '{';
depth_ = 0;
memset(first_, 1, sizeof(first_));
}

public:
// Add an element to the config paylaod
HaMqttConfigBuilder& add(const String& key, const String& str) {
elements.push_back({key, str, 0});
/** Clear everything (including device block). */
void clear() {
payload_.remove(0);
payload_ = '{';
depth_ = 0;
memset(first_, 1, sizeof(first_));
deviceSet_ = false;
deviceEndPos_ = 0;
}

/* ------------------ Append fields ------------------ */

HaMqttConfigBuilder &add(const char *key, const char *val) {
beginField(key);
payload_ += '"';
escapeAndAppend(val);
payload_ += '"';
return *this;
}
HaMqttConfigBuilder &add(const char *key, const String &val) { return add(key, val.c_str()); }

HaMqttConfigBuilder &add(const char *key, long val) {
beginField(key);
payload_.concat(val);
return *this;
}

HaMqttConfigBuilder &add(const char *key, float val, uint8_t decimals = 2) {
beginField(key);
char buf[32];
dtostrf(val, 0, decimals, buf);
payload_ += buf;
return *this;
}
HaMqttConfigBuilder& add(const String& key, const double num) {
elements.push_back({key, "", num});

HaMqttConfigBuilder &add(const char *key, bool val) {
beginField(key);
payload_ += (val ? F("true") : F("false"));
return *this;
}

// Clear all elements
void clear() { elements.clear(); };
/* -------------- Nested objects -------------- */

// Generate the json key/value payload for home assistant
String generatePayload() {
String str = "{";
HaMqttConfigBuilder &beginObject(const char *key) {
if (depth_ >= maxDepth_) return *this;
beginField(key);
payload_ += '{';
++depth_;
first_[depth_] = true;
return *this;
}

for(Elem elem : elements) {
str.concat('"');
str.concat(elem.key);
str.concat("\":");

if (elem.str.length() > 0) {
str.concat('"');
str.concat(elem.str);
str.concat('"');
} else {
str.concat(elem.num);
}
str.concat(',');
}
HaMqttConfigBuilder &endObject() {
if (depth_ == 0) return *this;
payload_ += '}';
--depth_;
return *this;
}

/* -------------- Device block shortcuts -------------- */

HaMqttConfigBuilder &beginDevice() {
// Device can only be created once per clear
if (deviceSet_) return *this; // already set
beginObject("device");
return *this;
}

HaMqttConfigBuilder &endDevice() {
if (deviceSet_) return *this; // already finished
endObject(); // closes "device"
deviceSet_ = true;
deviceEndPos_ = payload_.length(); // remember position after '}'
first_[0] = false; // root already has entry
return *this;
}

/**
* Removes the last appended sensor fields, but keeps the device block.
* Afterwards, new fields can be appended via add().
*/
void nextSensor() {
if (!deviceSet_) return; // no device → nothing to do
// remove everything after deviceEndPos_ (including previous sensor fields)
payload_.remove(deviceEndPos_);
depth_ = 0; // back to root level
// root already has an entry (device), so not first
first_[0] = false;
}

/* ----------------- Generate JSON ----------------- */

String generate() const {
String out = payload_;
for (int8_t d = depth_; d >= 0; --d) out += '}';
return out;
}

/* ----------- Simple top-level lookup ----------- */

bool getString(const char *key, String &result) const {
String json = generate();
String needle = '"' + String(key) + '"';

int pos = json.indexOf(needle);
if (pos < 0) return false;

if (str.endsWith(","))
str.setCharAt(str.length()-1, '}');
else
str.concat('}');
pos = json.indexOf(':', pos + needle.length());
if (pos < 0) return false;
++pos;

return str;
while (pos < (int)json.length() && isspace(json[(size_t)pos])) ++pos;
if (pos >= (int)json.length() || json[(size_t)pos] != '"') return false;
++pos;

int end = pos;
while (end < (int)json.length() && (json[(size_t)end] != '"' || json[(size_t)end - 1] == '\\')) ++end;
if (end >= (int)json.length()) return false;

result = json.substring(pos, end);
return true;
}

private:
String payload_;
uint8_t depth_ = 0;
const uint8_t maxDepth_;
bool first_[kStaticMaxDepth + 1];

bool deviceSet_ = false; ///< Device block has been finished
size_t deviceEndPos_ = 0; ///< Index after '}' of device object

/* ----------- Internal helper for key entry ----------- */
inline void beginField(const char *key) {
if (!first_[depth_]) payload_ += ',';
else first_[depth_] = false;

payload_ += '"';
escapeAndAppend(key);
payload_ += "\":\"";
}

/* ---------------- JSON escaping ---------------- */
void escapeAndAppend(const char *s) {
for (; *s; ++s) {
switch (*s) {
case '"': payload_ += F("\\\""); break;
case '\\': payload_ += F("\\\\"); break;
case '\b': payload_ += F("\\b"); break;
case '\f': payload_ += F("\\f"); break;
case '\n': payload_ += F("\\n"); break;
case '\r': payload_ += F("\\r"); break;
case '\t': payload_ += F("\\t"); break;
default: payload_ += *s; break;
}
}
}
};
};