diff --git a/README.md b/README.md new file mode 100644 index 0000000..810d518 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Gateway & sensors + +## Branches +Each sensor has a dedicated branch. E.g.: +* temp_sensor +* oil_sensor + +The gateway uses `master` as the main branch. Other sensors' branches get merged once they are ready for production. + +## Release flow diff --git a/gateway/include/ha.h b/gateway/include/ha.h deleted file mode 100644 index 714a3f8..0000000 --- a/gateway/include/ha.h +++ /dev/null @@ -1,371 +0,0 @@ -#pragma once - -#include -#include "utils.h" - -using namespace std; - -#define JSON_SIZE 512 -#define TOPIC_SIZE 255 - -namespace Ha { - uint16_t(*publisher)(const char* topic, const char* message); - typedef void (*onMessage)(const char* msg); - - struct DeviceConfig { - 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 }); - } - - void buildConfig(JsonDocument& jsonDoc) { - 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); - } - - 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; - } - - protected: - DeviceConfig(const char* id) : id(id) {} - }; - - struct Component { - const char* entityCategory = nullptr; - const char* deviceClass = nullptr; - const char* name = nullptr; - char* id = nullptr; - const char* type = nullptr; - char configTopic[TOPIC_SIZE]; - DeviceConfig* mainDevice = nullptr; - static List components; - - Component(const char* name, const char* id, const char* type) : name(name), id((char*)id), type(type) { - components.add(this); - } - - virtual void buildUniqueId(char* uniqueId) { - sprintf(uniqueId, "%s_%s", MAIN_DEVICE_ID, id); - } - - virtual void buildConfigTopic() { - sprintf(configTopic, "homeassistant/%s/%s/%s/config", type, MAIN_DEVICE_ID, id); - } - - virtual void buildConfig(JsonDocument& jsonDoc) { - if (mainDevice) mainDevice->buildConfig(jsonDoc); - if (entityCategory) jsonDoc["entity_category"] = entityCategory; - if (deviceClass) jsonDoc["device_class"] = deviceClass; - jsonDoc["name"] = name; - char uniqueId[50]; - buildUniqueId(uniqueId); - jsonDoc["unique_id"] = uniqueId; - buildConfigTopic(); - } - - void publishConfig() { - StaticJsonDocument jsonDoc; - buildConfig(jsonDoc); - - char message[JSON_SIZE]; - serializeJson(jsonDoc, message); - - publisher(configTopic, message); - } - - void publishCleanupConfig() { - publisher(configTopic, ""); - } - }; - - struct AbstractBuilder { - static List builders; - - AbstractBuilder() { - builders.add(this); - } - - static void deleteAll() { - builders.forEach([](AbstractBuilder* builder) { delete builder; }); - builders.empty(); - } - }; - - 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& withUnitMseasure(const char* value) { - cmp->unitMeasure = value; - return *this; - } - - Builder& withValueTemplate(const char* value) { - cmp->valueTemplate = value; - return *this; - } - - Builder& addSecondary(Component* c) { - if (cmp->mainDevice) c->mainDevice = &DeviceConfig::create(cmp->mainDevice->id); - return *this; - } - - Builder& addDiagnostic(Component* c) { - c->entityCategory = "diagnostic"; - return addSecondary(c); - } - - 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; - } - - Builder& restoreFromState() { - cmp->restoreFromState(); - return *this; - } - }; - - struct Command : Component { - bool retain = false; - char commandTopic[TOPIC_SIZE]; - onMessage f; - static unordered_map mapCommands; - - Command(const char* name, const char* id, const char* type, onMessage f) : Component(name, id, type), f(f) { - sprintf(commandTopic, "homeassistant/%s/%s/%s/set", type, MAIN_DEVICE_ID, id); - mapCommands.insert({ string(commandTopic), this }); - } - - virtual void onCommand(const char* msg) { - if (f) f(msg); - } - - void buildConfig(JsonDocument& jsonDoc) override { - Component::buildConfig(jsonDoc); - jsonDoc["command_topic"] = commandTopic; - if (retain) jsonDoc["retain"] = true; - } - - }; - - struct Button : Command { - - Button(const char* name, const char* id, onMessage f = nullptr) : Command(name, id, "button", f) {} - - }; - - struct StateConfig { - char stateTopic[TOPIC_SIZE]; - static unordered_map mapStateTopics; - Component* cmp; - - StateConfig(Component* cmp) : cmp(cmp) {} - - void withStateTopic() { - sprintf(stateTopic, "homeassistant/%s/%s/%s/state", cmp->type, MAIN_DEVICE_ID, cmp->id); - } - - void buildConfig(JsonDocument& jsonDoc) { - if (stateTopic[0]) jsonDoc["state_topic"] = stateTopic; - } - - void updateState(const char* message) { - if (stateTopic[0]) publisher(stateTopic, message); - } - }; - - struct Switch : Command, StateConfig { - - Switch(const char* name, const char* id, onMessage f = nullptr) : Command(name, id, "switch", f), StateConfig(this) {} - - void buildConfig(JsonDocument& jsonDoc) override { - Command::buildConfig(jsonDoc); - StateConfig::buildConfig(jsonDoc); - } - - void updateState(bool state) { - StateConfig::updateState(state ? "ON" : "OFF"); - } - - void restoreFromState() { - mapStateTopics.insert({stateTopic, this}); - } - }; - - struct Number : Command, StateConfig { - unsigned int min, max, step; - - Number(const char* name, const char* id, unsigned int min, unsigned int max, unsigned int step, onMessage f) - : Command(name, id, "number", f), StateConfig(this), min(min), max(max), step(step) {} - - void buildConfig(JsonDocument& jsonDoc) override { - Command::buildConfig(jsonDoc); - StateConfig::buildConfig(jsonDoc); - jsonDoc["min"] = min; - jsonDoc["max"] = max; - jsonDoc["step"] = step; - } - - void updateState(unsigned int value) { - char message[32]; - sprintf(message, "%u", value); - StateConfig::updateState(message); - } - - void restoreFromState() { - mapStateTopics.insert({stateTopic, this}); - } - - }; - - struct Sensor : Component, StateConfig { - const char* unitMeasure = nullptr; - const char* valueTemplate = nullptr; - static unordered_map mapSensors; - - Sensor(const char* name, const char* id) : Component(name, id, "sensor"), StateConfig(this) { - withStateTopic(); - mapSensors.insert({ id, this }); - } - - void buildUniqueId(char* uniqueId) override { - if (deviceClass) { - sprintf(uniqueId, "%s_%s_%s", MAIN_DEVICE_ID, deviceClass, id); - } else { - Component::buildUniqueId(uniqueId); - } - } - - void buildConfigTopic() override { - if (deviceClass) { - sprintf(configTopic, "homeassistant/%s/%s/%s_%s/config", type, MAIN_DEVICE_ID, deviceClass, id); - } else { - Component::buildConfigTopic(); - } - } - - void buildConfig(JsonDocument& jsonDoc) override { - Component::buildConfig(jsonDoc); - StateConfig::buildConfig(jsonDoc); - if (unitMeasure) jsonDoc["unit_of_measurement"] = unitMeasure; - if (valueTemplate) jsonDoc["value_template"] = valueTemplate; - jsonDoc["suggested_display_precision"] = 2; - } - }; - - struct TemperatureSensor : Sensor { - TemperatureSensor(const char* id, const char* name = "Temperature") : Sensor(name, id) { - deviceClass = "temperature"; - unitMeasure = "°C"; - } - }; - - 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 = "%"; - } - }; - - struct HumiditySensor : Sensor { - HumiditySensor(const char* id, const char* name = "Humidity") : Sensor(name, id) { - deviceClass = "humidity"; - unitMeasure = "%"; - } - }; - - struct PressureSensor : Sensor { - PressureSensor(const char* id, const char* name = "Pressure") : Sensor(name, id) { - deviceClass = "pressure"; - unitMeasure = "hPa"; - } - }; - - List Component::components; - List AbstractBuilder::builders; - unordered_map Command::mapCommands; - unordered_map Sensor::mapSensors; - unordered_map StateConfig::mapStateTopics; -} diff --git a/gateway/include/mqtt.h b/gateway/include/mqtt.h deleted file mode 100644 index 66e8d46..0000000 --- a/gateway/include/mqtt.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include - -#define MAIN_TOPIC "homeassistant/+/" MAIN_DEVICE_ID "/#" - -namespace Mqtt { - - AsyncMqttClient client; - - Task tReConnect(TASK_MINUTE, TASK_FOREVER, []{ - Serial.println("Connecting to MQTT..."); - client.connect(); - }); - - void disconnect() { - client.unsubscribe(MAIN_TOPIC); - client.disconnect(); - } - - uint16_t publish(const char* topic, const char* message) { - return client.publish(topic, 0, true, message); - } - - void publishInit() { - static bool firstTime = true; - if (firstTime) { - Component::components.forEach([](Component* c) { c->publishConfig(); }); - AbstractBuilder::deleteAll(); - firstTime = false; - } - } - - void publishCleanupConfig() { -#if MQTT_CLEANUP - Component::components.forEach([](Component* c) { c->publishCleanupConfig(); }); -#endif - } - - void onMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - char msg[len + 1]; - memcpy(msg, payload, len); - msg[len] = 0; - auto strTopic = string{ topic }; - auto cmd = Command::mapCommands[strTopic]; - if (cmd) cmd->onCommand(msg); - - cmd = StateConfig::mapStateTopics[strTopic]; - if (cmd) { - cmd->onCommand(msg); - StateConfig::mapStateTopics.erase(strTopic); - } - } - - void setup(Scheduler* ts = nullptr, void(*onConnected)() = nullptr, void(*onDisconnected)() = nullptr) { - if (ts) ts->addTask(tReConnect); - Ha::publisher = publish; - client.onConnect([onConnected](bool sessionPresent) { - publishInit(); - client.subscribe(MAIN_TOPIC, 0); - tReConnect.disable(); - Serial.println("Connected to MQTT"); - if (onConnected) onConnected(); - }); - client.onDisconnect([onDisconnected](AsyncMqttClientDisconnectReason reason) { - tReConnect.enableDelayed(); - Serial.println("Disconnected from MQTT"); - if (onDisconnected) onDisconnected(); - }); - client.onMessage(onMessage); - client.setServer(MQTT_HOST, MQTT_PORT); - } -} diff --git a/gateway/include/utils.h b/gateway/include/utils.h deleted file mode 100644 index 4f63959..0000000 --- a/gateway/include/utils.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -template -struct List { - struct Container { - T* t; - Container* next = nullptr; - Container(T* t) : t(t) {} - }; - - Container* first = nullptr; - Container* last = nullptr; - - void add(T* t) { - Container* c = new Container{t}; - first == nullptr ? first = c : last->next = c; - last = c; - } - - void forEach(void(*f)(T*)) { - for (Container *c = first; c; c = c->next) { - f(c->t); - } - } - - void empty() { - Container *c = first; - while (c) { - auto n = c->next; - delete c; - c = n; - } - } - -}; diff --git a/gateway/platformio.ini b/gateway/platformio.ini index 067b8f3..de2bedf 100644 --- a/gateway/platformio.ini +++ b/gateway/platformio.ini @@ -32,8 +32,7 @@ board = huzzah framework = arduino lib_deps = ${env.lib_deps} - arkhipenko/TaskScheduler@^3.7.0 - marvinroger/AsyncMqttClient@^0.9.0 + https://git.hodos.ro/arduino/ha-mqtt.git@^1.0.0 upload_port = 192.168.6.161 upload_protocol = espota upload_flags =