diff --git a/README.md b/README.md index 98fbb8b..7e26cf3 100644 --- a/README.md +++ b/README.md @@ -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 + diff --git a/src/HaMqttConfigBuilder.h b/src/HaMqttConfigBuilder.h index 0240c19..b106c0e 100644 --- a/src/HaMqttConfigBuilder.h +++ b/src/HaMqttConfigBuilder.h @@ -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 +#include // 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 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; + } + } } -}; \ No newline at end of file +};