From 4693969aff010553bf6296d849fbb5625ad28811 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 22 May 2024 00:04:21 +0200 Subject: [PATCH 1/7] reorganize platformio.ini configuration --- gateway/include/mqtt.h | 4 ++ gateway/platformio.ini | 64 ++++++---------------- gateway/test/embedded/test_dummy/dummy.cpp | 18 ------ 3 files changed, 21 insertions(+), 65 deletions(-) delete mode 100644 gateway/test/embedded/test_dummy/dummy.cpp diff --git a/gateway/include/mqtt.h b/gateway/include/mqtt.h index 23ee529..840372e 100644 --- a/gateway/include/mqtt.h +++ b/gateway/include/mqtt.h @@ -31,14 +31,18 @@ namespace Mqtt { } void publishInit() { +#if MQTT_CLEANUP Ha::publisher = publish; Component::components.forEach([](Component* c) { c->publishConfig(); }); AbstractBuilder::deleteAll(); +#endif ts.deleteTask(tPublishInit); } 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) { diff --git a/gateway/platformio.ini b/gateway/platformio.ini index 8f659c4..067b8f3 100644 --- a/gateway/platformio.ini +++ b/gateway/platformio.ini @@ -9,12 +9,9 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = pro-mini +default_envs = huzzah -[env:huzzah] -platform = espressif8266 -board = huzzah -framework = arduino +[env] lib_extra_dirs = ../libraries lib_deps = @@ -22,65 +19,38 @@ lib_deps = bblanchon/ArduinoJson@6.21.5 adafruit/Adafruit Unified Sensor@^1.1.4 adafruit/DHT sensor library@1.3.2 - arkhipenko/TaskScheduler@^3.7.0 - marvinroger/AsyncMqttClient@^0.9.0 https://git.hodos.ro/arduino/lib_serial-reader.git@^1.0.0 build_flags = -D DHT_SENSOR=0 -D DEBUG_RAW=0 check_tool = cppcheck check_flags = --enable=all check_skip_packages = yes check_severity = medium, high + +[env:huzzah] +platform = espressif8266 +board = huzzah +framework = arduino +lib_deps = + ${env.lib_deps} + arkhipenko/TaskScheduler@^3.7.0 + marvinroger/AsyncMqttClient@^0.9.0 upload_port = 192.168.6.161 upload_protocol = espota upload_flags = --host_port=10000 +[env:huzzah_dev] +extends = env:huzzah +build_flags = ${env.build_flags} -D MQTT_CLEANUP=1 + [env:pro-mini] platform = atmelavr board = pro16MHzatmega328 framework = arduino -lib_extra_dirs = - ../libraries -lib_deps = - sui77/rc-switch@^2.6.3 - bblanchon/ArduinoJson@6.21.5 - adafruit/Adafruit Unified Sensor@^1.1.4 - adafruit/DHT sensor library@1.3.2 - https://git.hodos.ro/arduino/lib_serial-reader.git@^1.0.0 -build_flags = -D DHT_SENSOR=0 -D DEBUG_RAW=0 -upload_port = /dev/ttyUSB0 -check_tool = cppcheck -check_flags = --enable=all -check_skip_packages = yes -check_severity = medium, high [env:native] platform = native test_filter = native/* -lib_extra_dirs = - ../libraries lib_deps = - bblanchon/ArduinoJson@6.19.4 -build_flags = -std=c++11 - -[env:embedded] -platform = atmelavr -framework = arduino -board = pro16MHzatmega328 -lib_extra_dirs = - ../libraries -lib_deps = - sui77/rc-switch@^2.6.3 - bblanchon/ArduinoJson@6.19.4 - -platform_packages = - platformio/tool-simavr -test_speed = 9600 -test_testing_command = - ${platformio.packages_dir}/tool-simavr/bin/simavr - -m - atmega328p - -f - 16000000L - ${platformio.build_dir}/${this.__env__}/firmware.elf -test_filter = embedded/* + bblanchon/ArduinoJson@6.21.5 +debug_build_flags = -std=c++11 diff --git a/gateway/test/embedded/test_dummy/dummy.cpp b/gateway/test/embedded/test_dummy/dummy.cpp deleted file mode 100644 index 352dc1e..0000000 --- a/gateway/test/embedded/test_dummy/dummy.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include -#include - -void setUp(void) { - // set stuff up here -} - -void tearDown(void) { - // clean stuff up here -} - -void setup() { - UNITY_BEGIN(); - UNITY_END(); -} - -void loop() { -} From c03c3d03fd4a6d7046afade360e4e11be712da4e Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 22 May 2024 13:42:52 +0200 Subject: [PATCH 2/7] decouple mqtt: - register onConnected and onDisconnected callbacks - allow optional Scheduler - publishInit is independent on Scheduler --- gateway/include/huzzah.h | 19 +++++++++++++----- gateway/include/mqtt.h | 43 ++++++++++++++++++---------------------- gateway/include/wifi.h | 5 +---- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/gateway/include/huzzah.h b/gateway/include/huzzah.h index 69c82aa..5f9c3d9 100644 --- a/gateway/include/huzzah.h +++ b/gateway/include/huzzah.h @@ -1,13 +1,15 @@ #include +#define MQTT_HOST IPAddress(192, 168, 5, 11) +#define MQTT_PORT 1883 + using namespace std; Scheduler ts; -void turnLed(uint8_t led, bool on = true) { - on ? digitalWrite(led, LOW) : digitalWrite(led, HIGH); -} - +#include "devices.h" +#include "mqtt.h" +#include "ota.h" #include "wifi.h" namespace Board { @@ -19,6 +21,10 @@ namespace Board { } }, &ts); + void turnLed(uint8_t led, bool on = true) { + on ? digitalWrite(led, LOW) : digitalWrite(led, HIGH); + } + void setup() { // Serial.begin(9600, SERIAL_8N1, SERIAL_TX_ONLY); @@ -27,9 +33,12 @@ namespace Board { turnLed(RED_LED, false); turnLed(BLUE_LED); + Mqtt::setup(&ts, + [] {turnLed(BLUE_LED, false);}, + [] {turnLed(BLUE_LED);} + ); Wifi::setup(); Ota::setup(); - Mqtt::setup(); tReadCommand.enable(); } diff --git a/gateway/include/mqtt.h b/gateway/include/mqtt.h index 840372e..93df1c9 100644 --- a/gateway/include/mqtt.h +++ b/gateway/include/mqtt.h @@ -2,27 +2,20 @@ #include #include -#include "devices.h" - -#define MQTT_HOST IPAddress(192, 168, 5, 11) -#define MQTT_PORT 1883 +#define MAIN_TOPIC "homeassistant/+/" MAIN_DEVICE_ID "/#" namespace Mqtt { AsyncMqttClient client; - Task tReConnect(5 * TASK_MINUTE, TASK_FOREVER, []{ + Task tReConnect(TASK_MINUTE, TASK_FOREVER, []{ Serial.println("Connecting to MQTT..."); client.connect(); - }, &ts); - void publishInit(); - Task tPublishInit(TASK_IMMEDIATE, TASK_ONCE, publishInit, &ts); - - const char* mainTopic = "homeassistant/+/rc-gateway/#"; + }); void disconnect() { - client.unsubscribe(mainTopic); + client.unsubscribe(MAIN_TOPIC); client.disconnect(); } @@ -31,12 +24,12 @@ namespace Mqtt { } void publishInit() { -#if MQTT_CLEANUP - Ha::publisher = publish; - Component::components.forEach([](Component* c) { c->publishConfig(); }); - AbstractBuilder::deleteAll(); -#endif - ts.deleteTask(tPublishInit); + static bool firstTime = true; + if (firstTime) { + Component::components.forEach([](Component* c) { c->publishConfig(); }); + AbstractBuilder::deleteAll(); + firstTime = false; + } } void publishCleanupConfig() { @@ -53,18 +46,20 @@ namespace Mqtt { if (cmd) cmd->onCommand(msg); } - void setup() { - client.onConnect([](bool sessionPresent) { - tPublishInit.enable(); - client.subscribe(mainTopic, 0); + 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"); - turnLed(BLUE_LED, false); + if (onConnected) onConnected(); }); - client.onDisconnect([](AsyncMqttClientDisconnectReason reason) { + client.onDisconnect([onDisconnected](AsyncMqttClientDisconnectReason reason) { tReConnect.enableDelayed(); Serial.println("Disconnected from MQTT"); - turnLed(BLUE_LED); + if (onDisconnected) onDisconnected(); }); client.onMessage(onMessage); client.setServer(MQTT_HOST, MQTT_PORT); diff --git a/gateway/include/wifi.h b/gateway/include/wifi.h index 2f0c877..d09a1cf 100644 --- a/gateway/include/wifi.h +++ b/gateway/include/wifi.h @@ -1,8 +1,6 @@ #include #include #include -#include "mqtt.h" -#include "ota.h" #include "credentials.h" namespace Wifi { @@ -30,12 +28,11 @@ namespace Wifi { printStatus(); tReconnect.cancel(); Ota::tLoop.enable(); - Mqtt::tReConnect.restart(); + Mqtt::tReConnect.enable(); }); stationDisconnectedHandler = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& e) { Serial.println("Disconnected from network."); - tReconnect.restartDelayed(); }); From ce21dedcd53d30f41627ce56d638970e63b39805 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 29 May 2024 16:01:00 +0200 Subject: [PATCH 3/7] generic support for using device's name --- gateway/include/devices.h | 4 ++-- gateway/include/ha.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gateway/include/devices.h b/gateway/include/devices.h index bc90c58..bb686c5 100644 --- a/gateway/include/devices.h +++ b/gateway/include/devices.h @@ -59,7 +59,7 @@ struct PollinSwitch : Switch { unsigned char channel; PollinSwitch(const char* name, const char* group, const unsigned char channel, const char* area = nullptr) - : Switch(name, [group, channel]{ + : Switch(nullptr, [group, channel]{ // copy id from string into a new pointer, to avoid memory leaks string s = Protocol_1::buildId(group, channel); char* uId = new char[s.length() + 1]; @@ -83,7 +83,7 @@ struct EasyHomeSwitch : Switch { unsigned long off[8] = { 4483146, 4626810, 4661562, 4819642 }; EasyHomeSwitch(const char* name, const char* id, unsigned long on[4], unsigned long off[4], const char* area = nullptr) - : Switch(name, id) { + : Switch(nullptr, id) { memcpy(&this->on[4], on, 4 * sizeof(unsigned long)); memcpy(&this->off[4], off, 4 * sizeof(unsigned long)); mainDevice = &DeviceConfig::create(id).withName(name).withManufacturer("Intertek").withModel("Easy Home").withArea(area).withParent(gatewayDevice); diff --git a/gateway/include/ha.h b/gateway/include/ha.h index 6a56af9..e37bbaa 100644 --- a/gateway/include/ha.h +++ b/gateway/include/ha.h @@ -219,7 +219,6 @@ namespace Ha { void buildConfig(JsonDocument& jsonDoc) override { Command::buildConfig(jsonDoc); - jsonDoc["name"] = nullptr; // jsonDoc["retain"] = true; if (stateTopic[0]) jsonDoc["state_topic"] = stateTopic; } From b4daba4cb29740440c1eee32077364ad1d2fb071 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 29 May 2024 16:08:05 +0200 Subject: [PATCH 4/7] rename secondary entities and add support for configuration type entity --- gateway/include/devices.h | 10 +++++----- gateway/include/ha.h | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/gateway/include/devices.h b/gateway/include/devices.h index bb686c5..e38b2cf 100644 --- a/gateway/include/devices.h +++ b/gateway/include/devices.h @@ -25,8 +25,8 @@ namespace OilTank { return Builder::instance(id) .asDevice(device) .withValueTemplate("{{ value_json.sensor.temperature }}") - .asDiagnostic(new VoltageSensor{id, "Battery voltage", "{{ value_json.sensor.diagnostic.voltage }}"}) - .asDiagnostic(new BatterySensor{id, "Battery level", "{{ ((states('sensor.servers_room_battery_voltage')|float-2.5)|round(2)*100/2)|int }}"}) + .addDiagnostic(new VoltageSensor{id, "Battery voltage", "{{ value_json.sensor.diagnostic.voltage }}"}) + .addDiagnostic(new BatterySensor{id, "Battery level", "{{ ((states('sensor.servers_room_battery_voltage')|float-2.5)|round(2)*100/2)|int }}"}) .build(); } @@ -42,14 +42,14 @@ namespace OilTank { .withDeviceClass("distance") .withUnitMseasure("cm") .withValueTemplate("{{ value_json.sensor.value }}") - .asSecondary( + .addSecondary( Builder::instance(new Sensor{ "Level", id }) .withUnitMseasure("%") .withValueTemplate("{{ 100 - ((value_json.sensor.value-7)|float*100/110)|round(2) }}") .build() ) - .asDiagnostic(new VoltageSensor{id, "Battery voltage", "{{ value_json.sensor.diagnostic.voltage }}"}) - .asDiagnostic(new BatterySensor{id, "Battery level", "{{ ((states('sensor.oil_tank_battery_voltage')|float-3.6)|round(2)*100/1.6)|int }}"}) + .addDiagnostic(new VoltageSensor{id, "Battery voltage", "{{ value_json.sensor.diagnostic.voltage }}"}) + .addDiagnostic(new BatterySensor{id, "Battery level", "{{ ((states('sensor.oil_tank_battery_voltage')|float-3.6)|round(2)*100/1.6)|int }}"}) .build(); } } diff --git a/gateway/include/ha.h b/gateway/include/ha.h index e37bbaa..c280d02 100644 --- a/gateway/include/ha.h +++ b/gateway/include/ha.h @@ -160,14 +160,19 @@ namespace Ha { return *this; } - Builder& asSecondary(Component* c) { - c->mainDevice = &DeviceConfig::create(cmp->id); + Builder& addSecondary(Component* c) { + if (cmp->mainDevice) c->mainDevice = &DeviceConfig::create(cmp->mainDevice->id); return *this; } - Builder& asDiagnostic(Component* c) { + Builder& addDiagnostic(Component* c) { c->entityCategory = "diagnostic"; - return asSecondary(c); + return addSecondary(c); + } + + Builder& addConfiguration(Component* c) { + c->entityCategory = "config"; + return addSecondary(c); } Builder& asDevice(DeviceConfig* deviceConfig) { From 1fe24a5b7fddb3c22295631c12c2d045d151a50a Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 29 May 2024 16:09:54 +0200 Subject: [PATCH 5/7] add support for setting retain --- gateway/include/ha.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gateway/include/ha.h b/gateway/include/ha.h index c280d02..4a16eb8 100644 --- a/gateway/include/ha.h +++ b/gateway/include/ha.h @@ -179,9 +179,16 @@ namespace Ha { cmp->mainDevice = deviceConfig; return *this; } + + Builder& withRetain(bool retain = true) { + cmp->retain = retain; + return *this; + } + }; struct Command : Component { + bool retain = false; char commandTopic[TOPIC_SIZE]; onMessage f; static unordered_map mapCommands; @@ -198,6 +205,7 @@ namespace Ha { void buildConfig(JsonDocument& jsonDoc) override { Component::buildConfig(jsonDoc); jsonDoc["command_topic"] = commandTopic; + if (retain) jsonDoc["retain"] = true; } }; From 510a378a5ec19fa72150661590c08ee46f30e865 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Wed, 29 May 2024 16:12:17 +0200 Subject: [PATCH 6/7] add Number, refactor StateConfig and add support for restoring state at startup --- gateway/include/devices.h | 14 +++--- gateway/include/ha.h | 91 +++++++++++++++++++++++++++++---------- gateway/include/mqtt.h | 9 +++- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/gateway/include/devices.h b/gateway/include/devices.h index e38b2cf..589f7fb 100644 --- a/gateway/include/devices.h +++ b/gateway/include/devices.h @@ -67,6 +67,7 @@ struct PollinSwitch : Switch { return uId; }()), group(group), channel(channel) { mainDevice = &DeviceConfig::create(id).withName(name).withManufacturer("Pollin").withArea(area).withParent(gatewayDevice); + withStateTopic(); deviceClass = "outlet"; p1Switches.insert({ string(id), this }); } @@ -87,6 +88,7 @@ struct EasyHomeSwitch : Switch { memcpy(&this->on[4], on, 4 * sizeof(unsigned long)); memcpy(&this->off[4], off, 4 * sizeof(unsigned long)); mainDevice = &DeviceConfig::create(id).withName(name).withManufacturer("Intertek").withModel("Easy Home").withArea(area).withParent(gatewayDevice); + withStateTopic(); deviceClass = "outlet"; for (int i = 0; i < 8; i++) { onSwitches.insert({ this->on[i], this }); @@ -120,12 +122,12 @@ Command* commands[] = { .withParent(gatewayDevice) ) .build(), - (new EasyHomeSwitch{"FritzBox", "easy_home_a", (unsigned long[4]) { 4483136, 4626800, 4661552, 4819632 }, (unsigned long[4]) { 4326544, 4537104, 4767520, 4972704 }, "Basement"})->withStateTopic(), - (new EasyHomeSwitch{"Outside", "easy_home_b", (unsigned long[4]) { 4483140, 4626804, 4661556, 4819636 }, (unsigned long[4]) { 4326548, 4537108, 4767524, 4972708 }, "Basement"})->withStateTopic(), - (new PollinSwitch{"Meeting sensor", "00001", 1, "Dining room"})->withStateTopic(), - (new PollinSwitch{"Fire Tv", "00001", 2, "Living room"})->withStateTopic(), - (new PollinSwitch{"Diningroom player", "00001", 3, "Dining room"})->withStateTopic(), - (new PollinSwitch{"Train", "11111", 4, "Playroom"})->withStateTopic() + new EasyHomeSwitch{"FritzBox", "easy_home_a", (unsigned long[4]) { 4483136, 4626800, 4661552, 4819632 }, (unsigned long[4]) { 4326544, 4537104, 4767520, 4972704 }, "Basement"}, + new EasyHomeSwitch{"Outside", "easy_home_b", (unsigned long[4]) { 4483140, 4626804, 4661556, 4819636 }, (unsigned long[4]) { 4326548, 4537108, 4767524, 4972708 }, "Basement"}, + new PollinSwitch{"Meeting sensor", "00001", 1, "Dining room"}, + new PollinSwitch{"Fire Tv", "00001", 2, "Living room"}, + new PollinSwitch{"Diningroom player", "00001", 3, "Dining room"}, + new PollinSwitch{"Train", "11111", 4, "Playroom"} }; Sensor* sensors[] = { diff --git a/gateway/include/ha.h b/gateway/include/ha.h index 4a16eb8..714a3f8 100644 --- a/gateway/include/ha.h +++ b/gateway/include/ha.h @@ -3,6 +3,8 @@ #include #include "utils.h" +using namespace std; + #define JSON_SIZE 512 #define TOPIC_SIZE 255 @@ -180,11 +182,20 @@ namespace Ha { 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 { @@ -216,37 +227,76 @@ namespace Ha { }; - template struct StateConfig { char stateTopic[TOPIC_SIZE]; + static unordered_map mapStateTopics; + Component* cmp; - T* withStateTopic() { - sprintf(stateTopic, "homeassistant/%s/%s/%s/state", ((T*)this)->type, MAIN_DEVICE_ID, ((T*)this)->id); - return static_cast(this); + StateConfig(Component* cmp) : cmp(cmp) {} + + void withStateTopic() { + sprintf(stateTopic, "homeassistant/%s/%s/%s/state", cmp->type, MAIN_DEVICE_ID, cmp->id); } - }; - struct Switch : Command, StateConfig { - - Switch(const char* name, const char* id, onMessage f = nullptr) : Command(name, id, "switch", f) {} - - void buildConfig(JsonDocument& jsonDoc) override { - Command::buildConfig(jsonDoc); - // jsonDoc["retain"] = true; + void buildConfig(JsonDocument& jsonDoc) { if (stateTopic[0]) jsonDoc["state_topic"] = stateTopic; } - void updateState(bool state) { - publisher(stateTopic, state ? "ON" : "OFF"); + void updateState(const char* message) { + if (stateTopic[0]) publisher(stateTopic, message); } }; - struct Sensor : Component, StateConfig { + 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") { + Sensor(const char* name, const char* id) : Component(name, id, "sensor"), StateConfig(this) { withStateTopic(); mapSensors.insert({ id, this }); } @@ -269,15 +319,11 @@ namespace Ha { 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["state_topic"] = stateTopic; jsonDoc["suggested_display_precision"] = 2; } - - void updateState(const char* message) { - publisher(stateTopic, message); - } }; struct TemperatureSensor : Sensor { @@ -307,7 +353,6 @@ namespace Ha { HumiditySensor(const char* id, const char* name = "Humidity") : Sensor(name, id) { deviceClass = "humidity"; unitMeasure = "%"; - // valueTemplate = "{{ value_json.sensor.humidity }}"; } }; @@ -315,7 +360,6 @@ namespace Ha { PressureSensor(const char* id, const char* name = "Pressure") : Sensor(name, id) { deviceClass = "pressure"; unitMeasure = "hPa"; - // valueTemplate = "{{ value_json.sensor.pressure }}"; } }; @@ -323,4 +367,5 @@ namespace Ha { 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 index 93df1c9..66e8d46 100644 --- a/gateway/include/mqtt.h +++ b/gateway/include/mqtt.h @@ -42,8 +42,15 @@ namespace Mqtt { char msg[len + 1]; memcpy(msg, payload, len); msg[len] = 0; - Command* cmd = Command::mapCommands[string{ topic }]; + 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) { From 3a19576b51040fcfc391d683ea31d298db0d8bc9 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Thu, 30 May 2024 08:30:23 +0200 Subject: [PATCH 7/7] turn into ha-mqtt library --- README.md | 10 ++ gateway/include/ha.h | 371 ---------------------------------------- gateway/include/mqtt.h | 74 -------- gateway/include/utils.h | 35 ---- gateway/platformio.ini | 3 +- 5 files changed, 11 insertions(+), 482 deletions(-) create mode 100644 README.md delete mode 100644 gateway/include/ha.h delete mode 100644 gateway/include/mqtt.h delete mode 100644 gateway/include/utils.h 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 =