#pragma once #include #include "list.h" #include "enums.h" using namespace std; #define JSON_SIZE 512 #define TOPIC_SIZE 255 #define BASE_TOPIC "homeassistant/%s/%s/%s" namespace Ha { uint16(*publisher)(const char*, const char*); typedef void (*onMessage)(const char*); struct Config { virtual void buildConfig(JsonDocument& jsonDoc) = 0; }; struct DeviceConfig : Config { const char* id = nullptr; const char* name = nullptr; const char* model = nullptr; const char* manufacturer = nullptr; const char* area = nullptr; const DeviceConfig* parent = nullptr; static DeviceConfig& create(const char* id) { return *(new DeviceConfig{ id }); } DeviceConfig& withName(const char* value) { name = value; return *this; } DeviceConfig& withModel(const char* value) { model = value; return *this; } DeviceConfig& withManufacturer(const char* value) { manufacturer = value; return *this; } DeviceConfig& withArea(const char* value) { area = value; return *this; } DeviceConfig& withParent(const DeviceConfig* deviceConfig) { parent = deviceConfig; return *this; } void buildConfig(JsonDocument& jsonDoc) override { JsonObject device = jsonDoc.createNestedObject("device"); if (name) device["name"] = name; if (model) device["model"] = model; if (manufacturer) device["manufacturer"] = manufacturer; if (area) device["suggested_area"] = area; if (parent) device["via_device"] = parent->id; JsonArray identifiers = device.createNestedArray("identifiers"); identifiers.add(id); } private: DeviceConfig(const char* id) : id(id) {} }; struct Component : Config { const char* id = nullptr; const char* type = nullptr; const char* name = nullptr; const char* entityCategory = nullptr; const char* deviceClass = nullptr; const char* icon = nullptr; DeviceConfig* mainDevice = nullptr; inline static List components; bool multiValueComponent = false; Component(const char* id, const char* name, const char* type) : id(id), type(type), name(name) { components.add(this); } void publishConfig() { StaticJsonDocument jsonDoc; buildConfig(jsonDoc); char message[JSON_SIZE] = {}; serializeJson(jsonDoc, message); publisher(configTopic, message); } void publishCleanupConfig() { publisher(configTopic, ""); } protected: void buildConfig(JsonDocument& jsonDoc) override { if (mainDevice) mainDevice->buildConfig(jsonDoc); if (entityCategory) jsonDoc["entity_category"] = entityCategory; if (deviceClass) jsonDoc["device_class"] = deviceClass; if (icon) jsonDoc["icon"] = icon; jsonDoc["name"] = name; buildUniqueId(jsonDoc); buildConfigTopic(); } private: char configTopic[TOPIC_SIZE] = {}; void buildUniqueId(JsonDocument& jsonDoc) { char uniqueId[50]; if (multiValueComponent && deviceClass) { snprintf(uniqueId, sizeof(uniqueId), "%s_%s_%s", MAIN_DEVICE_ID, deviceClass, id); } else { snprintf(uniqueId, sizeof(uniqueId), "%s_%s", MAIN_DEVICE_ID, id); } jsonDoc["unique_id"] = uniqueId; } void buildConfigTopic() { if (multiValueComponent && deviceClass) { snprintf(configTopic, sizeof(configTopic), BASE_TOPIC"_%s""/config", type, MAIN_DEVICE_ID, deviceClass, id); } else { snprintf(configTopic, sizeof(configTopic), BASE_TOPIC"/config", type, MAIN_DEVICE_ID, id); } } }; struct Command : Config { bool retain = false; inline static unordered_map mapCommandTopics; Command(Component* cmp, onMessage f) : f(f), cmp(cmp) { snprintf(commandTopic, sizeof(commandTopic), BASE_TOPIC"/set", cmp->type, MAIN_DEVICE_ID, cmp->id); mapCommandTopics.insert({ string(commandTopic), this }); } virtual void onCommand(const char* msg) { if (f) f(msg); } protected: char commandTopic[TOPIC_SIZE] = {}; onMessage f; void buildConfig(JsonDocument& jsonDoc) override { jsonDoc["command_topic"] = (const char*)commandTopic; if (retain) jsonDoc["retain"] = true; } private: Component* cmp; }; struct State : Config { char stateTopic[TOPIC_SIZE] = {}; const char* jsonAttributesTemplate = nullptr; const char* valueTemplate = nullptr; State(Component* cmp) : cmp(cmp) {} void withStateTopic() { snprintf(stateTopic, sizeof(stateTopic), BASE_TOPIC"/state", cmp->type, MAIN_DEVICE_ID, cmp->id); } void updateState(const char* message) { if (stateTopic[0]) publisher(stateTopic, message); } protected: void buildConfig(JsonDocument& jsonDoc) override { if (stateTopic[0]) { jsonDoc["state_topic"] = (const char*)stateTopic; if (jsonAttributesTemplate) { jsonDoc["json_attributes_template"] = jsonAttributesTemplate; jsonDoc["json_attributes_topic"] = (const char*)stateTopic; } if (valueTemplate) jsonDoc["value_template"] = valueTemplate; } } private: Component* cmp; }; struct StatefulCommand : Command, State { inline static unordered_map mapRestoreStateTopics; StatefulCommand(Component* cmp, onMessage f) : Command(cmp, f), State(cmp) {} void restoreStateFromCommand() { withStateTopic(); mapRestoreStateTopics.insert({stateTopic, this}); } protected: void buildConfig(JsonDocument& jsonDoc) override { Command::buildConfig(jsonDoc); State::buildConfig(jsonDoc); } }; struct Button : Component, Command { Button(const char* name, const char* id, onMessage f = nullptr) : Component(id, name, "button"), Command(this, f) {} void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); Command::buildConfig(jsonDoc); } }; struct Switch : Component, StatefulCommand { Switch(const char* name, const char* id, onMessage f = nullptr) : Component(id, name, "switch"), StatefulCommand(this, f) {} void updateState(bool state) { State::updateState(state ? "ON" : "OFF"); } void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); StatefulCommand::buildConfig(jsonDoc); } }; struct Light : Component, StatefulCommand { Light(const char* name, const char* id, onMessage f = nullptr) : Component(id, name, "light"), StatefulCommand(this, f) {} void updateState(bool state) { State::updateState(state ? "ON" : "OFF"); } void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); StatefulCommand::buildConfig(jsonDoc); } }; struct Number : Component, StatefulCommand { unsigned int min = 1; unsigned int max = 100; unsigned int step = 1; const char* unitMeasure = nullptr; Number(const char* name, const char* id, onMessage f) : Component(id, name, "number"), StatefulCommand(this, f) {} void updateState(unsigned int value) { State::updateState(to_string(value).c_str()); } void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); StatefulCommand::buildConfig(jsonDoc); jsonDoc["min"] = min; jsonDoc["max"] = max; jsonDoc["step"] = step; if (unitMeasure) jsonDoc["unit_of_measurement"] = unitMeasure; } }; struct Text : Component, StatefulCommand { unsigned int min = 1; unsigned int max = 100; const char* pattern = nullptr; Text(const char* name, const char* id, onMessage f) : Component(id, name, "text"), StatefulCommand(this, f) {} void updateState(unsigned int value) { State::updateState(to_string(value).c_str()); } void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); StatefulCommand::buildConfig(jsonDoc); jsonDoc["min"] = min; jsonDoc["max"] = max; jsonDoc["platform"] = "text"; if (pattern) jsonDoc["pattern"] = pattern; } }; struct Sensor : Component, State { const char* unitMeasure = nullptr; unsigned int precision = 2; SensorStateClass sensorStateClass; inline static unordered_map mapSensors; Sensor(const char* name, const char* id) : Component(id, name, "sensor"), State(this) { withStateTopic(); mapSensors.insert({ string(id), this }); } void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); State::buildConfig(jsonDoc); if (unitMeasure) jsonDoc["unit_of_measurement"] = unitMeasure; if (isNumericSensor()) jsonDoc["suggested_display_precision"] = precision; if (sensorStateClass) jsonDoc["state_class"] = static_cast(sensorStateClass); } private: bool isNumericSensor() { return deviceClass || unitMeasure; } }; struct TemperatureSensor : Sensor { TemperatureSensor(const char* id, const char* name = "Temperature") : Sensor(name, id) { deviceClass = "temperature"; unitMeasure = "°C"; sensorStateClass = MEASUREMENT; } }; struct HumiditySensor : Sensor { HumiditySensor(const char* id, const char* name = "Humidity") : Sensor(name, id) { deviceClass = "humidity"; unitMeasure = "%"; sensorStateClass = MEASUREMENT; } }; struct PressureSensor : Sensor { PressureSensor(const char* id, const char* name = "Pressure") : Sensor(name, id) { deviceClass = "pressure"; unitMeasure = "hPa"; } }; struct VoltageSensor : Sensor { VoltageSensor(const char* id, const char* name, const char* valueTemplate) : Sensor(name, id) { this->valueTemplate = valueTemplate; deviceClass = "voltage"; unitMeasure = "V"; } }; struct BatterySensor : Sensor { BatterySensor(const char* id, const char* name, const char* valueTemplate) : Sensor(name, id) { this->valueTemplate = valueTemplate; deviceClass = "battery"; unitMeasure = "%"; sensorStateClass = MEASUREMENT; } }; struct AbstractBuilder { inline static List builders; static void deleteAll() { builders.forEach([](AbstractBuilder* builder) { delete builder; }); builders.empty(); } protected: AbstractBuilder() { builders.add(this); } }; template struct Builder : AbstractBuilder { T* cmp; Builder(T* cmp) : AbstractBuilder(), cmp(cmp) {} Builder(const char* id) : AbstractBuilder() { cmp = new T{ id }; } static Builder& instance(T* c) { return *(new Builder(c)); } static Builder& instance(const char* id) { return *(new Builder(id)); } T* build() { return static_cast(cmp); } Builder& withDeviceClass(const char* value) { cmp->deviceClass = value; return *this; } Builder& withUnitMeasure(const char* value) { cmp->unitMeasure = value; return *this; } Builder& withIcon(const char* value) { cmp->icon = value; return *this; } Builder& withValueTemplate(const char* value) { cmp->valueTemplate = value; return *this; } Builder& withPrecision(unsigned int value) { cmp->precision = value; return *this; } Builder& withSensorStateClass(SensorStateClass value) { cmp->sensorStateClass = value; return *this; } Builder& withMin(unsigned int value) { cmp->min = value; return *this; } Builder& withMax(unsigned int value) { cmp->max = value; return *this; } Builder& withStep(unsigned int value) { cmp->step = value; return *this; } Builder& withPattern(const char* value) { cmp->pattern = value; return *this; } Builder& withJsonAttributes(const char* value) { cmp->jsonAttributesTemplate = value; return *this; } Builder& addSecondary(Component* c) { if (cmp->mainDevice) c->mainDevice = &DeviceConfig::create(cmp->mainDevice->id).withName(cmp->mainDevice->name); if (!strcmp(cmp->id, c->id)) c->multiValueComponent = true; return *this; } Builder& addDiagnostic(Component* c) { c->entityCategory = "diagnostic"; return addSecondary(c); } Builder& addPreconfigured(Builder& (*factoryBuilder)(Builder& builder)) { return factoryBuilder(*this); } Builder& addConfiguration(Component* c) { c->entityCategory = "config"; return addSecondary(c); } Builder& asDevice(DeviceConfig* deviceConfig) { cmp->mainDevice = deviceConfig; return *this; } Builder& withStateTopic() { cmp->withStateTopic(); return *this; } Builder& withRetain(bool retain = true) { cmp->retain = retain; return *this; } [[deprecated("Use restoreStateFromCommand() instead")]] Builder& restoreFromState() { cmp->restoreStateFromCommand(); return *this; } Builder& restoreStateFromCommand() { cmp->restoreStateFromCommand(); return *this; } }; }