From 4873c26384231f8571e4485c456341ffec5c5cd6 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Mon, 28 Oct 2024 17:51:29 +0100 Subject: [PATCH 1/5] move native tests in dedicated folder --- platformio.ini | 4 +++- test/{ => native/test_utils}/utils.cpp | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename test/{ => native/test_utils}/utils.cpp (100%) diff --git a/platformio.ini b/platformio.ini index ad67c12..fc08087 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,4 +13,6 @@ default_envs = native [env:native] platform = native -debug_build_flags = -std=c++11 +test_filter = native/* +lib_deps = + bblanchon/ArduinoJson@6.21.5 diff --git a/test/utils.cpp b/test/native/test_utils/utils.cpp similarity index 100% rename from test/utils.cpp rename to test/native/test_utils/utils.cpp From 803d969de6f066101719ba1cf5005e75691b8729 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Mon, 28 Oct 2024 21:10:53 +0100 Subject: [PATCH 2/5] fixes: - use HA's default values for min, max & step - initialize topic arrays with nullptr - re-order code --- src/ha.h | 352 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 177 insertions(+), 175 deletions(-) diff --git a/src/ha.h b/src/ha.h index 111ebc1..471604d 100644 --- a/src/ha.h +++ b/src/ha.h @@ -89,7 +89,7 @@ namespace Ha { StaticJsonDocument jsonDoc; buildConfig(jsonDoc); - char message[JSON_SIZE]; + char message[JSON_SIZE] = {}; serializeJson(jsonDoc, message); publisher(configTopic, message); @@ -110,7 +110,7 @@ namespace Ha { buildConfigTopic(); } private: - char configTopic[TOPIC_SIZE]; + char configTopic[TOPIC_SIZE] = {}; void buildUniqueId(JsonDocument& jsonDoc) { char uniqueId[50]; @@ -131,6 +131,181 @@ namespace Ha { } }; + struct Command : Config { + bool retain = false; + 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; + + 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; + } + } + } + private: + Component* cmp; + }; + + struct StatefulCommand : Command, State { + static unordered_map mapRestoreStateTopics; + + StatefulCommand(Component* cmp, onMessage f) : Command(cmp, f), State(cmp) {} + + void restoreFromState() { + 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 Number : Component, StatefulCommand { + unsigned int min = 1; + unsigned int max = 100; + unsigned int step = 1; + + 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; + } + }; + + struct Sensor : Component, State { + const char* unitMeasure = nullptr; + const char* valueTemplate = nullptr; + unsigned int precision = 2; + 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 (valueTemplate) jsonDoc["value_template"] = valueTemplate; + if (isNumericSensor()) jsonDoc["suggested_display_precision"] = precision; + } + + private: + bool isNumericSensor() { + return deviceClass || unitMeasure; + } + }; + + struct TemperatureSensor : Sensor { + TemperatureSensor(const char* id, const char* name = "Temperature") : Sensor(name, id) { + deviceClass = "temperature"; + unitMeasure = "°C"; + } + }; + + 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"; + } + }; + + 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 AbstractBuilder { static List builders; @@ -248,179 +423,6 @@ namespace Ha { } }; - struct Command : Config { - bool retain = false; - 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 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 State : Config { - char stateTopic[TOPIC_SIZE]; - const char* jsonAttributesTemplate = 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; - } - } - } - private: - Component* cmp; - }; - - struct StatefulCommand : Command, State { - static unordered_map mapRestoreStateTopics; - - StatefulCommand(Component* cmp, onMessage f) : Command(cmp, f), State(cmp) {} - - void restoreFromState() { - mapRestoreStateTopics.insert({stateTopic, this}); - } - - protected: - void buildConfig(JsonDocument& jsonDoc) override { - Command::buildConfig(jsonDoc); - State::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 Number : Component, StatefulCommand { - unsigned int min, max, step; - - 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; - } - }; - - struct Sensor : Component, State { - const char* unitMeasure = nullptr; - const char* valueTemplate = nullptr; - unsigned int precision = 2; - 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 (valueTemplate) jsonDoc["value_template"] = valueTemplate; - if (isNumericSensor()) jsonDoc["suggested_display_precision"] = precision; - } - - private: - bool isNumericSensor() { - return deviceClass || unitMeasure; - } - }; - - 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::mapCommandTopics; From 9e7f2dc065ea063993dcd4ba00d572d43551e122 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Mon, 28 Oct 2024 21:11:17 +0100 Subject: [PATCH 3/5] add tests for all components --- test/native/test_ha/components.cpp | 210 +++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 test/native/test_ha/components.cpp diff --git a/test/native/test_ha/components.cpp b/test/native/test_ha/components.cpp new file mode 100644 index 0000000..1fe6f05 --- /dev/null +++ b/test/native/test_ha/components.cpp @@ -0,0 +1,210 @@ +#include +#include + +#define MAIN_DEVICE_ID "test" + +#include "ha.h" + +using namespace Ha; + +void setUp(void) { + // set stuff up here +} + +void tearDown(void) { + // clean stuff up here +} + +void testDevice(void) { + auto d = DeviceConfig::create("1").withName("name").withModel("model").withArea("area").withManufacturer("man"); + + StaticJsonDocument<256> doc; + d.buildConfig(doc); + + JsonObject dev = doc["device"]; + TEST_ASSERT_TRUE(doc.containsKey("device")); + TEST_ASSERT_EQUAL_STRING("name", dev["name"]); + TEST_ASSERT_EQUAL_STRING("model", dev["model"]); + TEST_ASSERT_EQUAL_STRING("area", dev["suggested_area"]); + TEST_ASSERT_EQUAL_STRING("man", dev["manufacturer"]); + TEST_ASSERT_TRUE(dev.containsKey("identifiers")); + TEST_ASSERT_EQUAL_STRING("1", dev["identifiers"][0]); +} + +void testButton(void) { + Button b("a_name", "id"); + + StaticJsonDocument<256> doc; + b.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING(MAIN_DEVICE_ID"_id", doc["unique_id"]); + TEST_ASSERT_EQUAL_STRING("homeassistant/button/" MAIN_DEVICE_ID "/id/set", doc["command_topic"]); + TEST_ASSERT_FALSE(doc["retain"]); + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["device_class"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["entity_category"]); + TEST_ASSERT_NOT_NULL(Command::mapCommandTopics[doc["command_topic"]]); +} + +void testSensor(void) { + Sensor s("a_name", "id"); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING(MAIN_DEVICE_ID"_id", doc["unique_id"]); + TEST_ASSERT_EQUAL_STRING("homeassistant/sensor/" MAIN_DEVICE_ID "/id/state", doc["state_topic"]); + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["device_class"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["entity_category"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["value_template"]); + TEST_ASSERT_EQUAL_INT(0, doc["suggested_display_precision"]); + TEST_ASSERT_NOT_NULL(Sensor::mapSensors["id"]); +} + +void testNumericSensor1(void) { + Sensor s("a_name", "id"); + s.deviceClass = "class"; + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testNumericSensor2(void) { + Sensor s("a_name", "id"); + s.unitMeasure = "%"; + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testSwitch(void) { + Switch s("a_name", "id"); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING(MAIN_DEVICE_ID"_id", doc["unique_id"]); + TEST_ASSERT_EQUAL_STRING("homeassistant/switch/" MAIN_DEVICE_ID "/id/set", doc["command_topic"]); + TEST_ASSERT_FALSE(doc["retain"]); + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["device_class"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["entity_category"]); + TEST_ASSERT_NOT_NULL(Command::mapCommandTopics[doc["command_topic"]]); + + TEST_ASSERT_EQUAL_STRING(NULL, doc["state_topic"]); +} + +void testSwitchWithState(void) { + Switch s("a_name", "id"); + s.withStateTopic(); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("homeassistant/switch/" MAIN_DEVICE_ID "/id/state", doc["state_topic"]); +} + +void testNumber(void) { + Number n("a_name", "id", nullptr); + + StaticJsonDocument<256> doc; + n.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING(MAIN_DEVICE_ID"_id", doc["unique_id"]); + TEST_ASSERT_EQUAL_INT16(1, doc["min"]); + TEST_ASSERT_EQUAL_INT16(100, doc["max"]); + TEST_ASSERT_EQUAL_INT16(1, doc["step"]); + + TEST_ASSERT_EQUAL_STRING("homeassistant/number/" MAIN_DEVICE_ID "/id/set", doc["command_topic"]); + TEST_ASSERT_FALSE(doc["retain"]); + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["device_class"]); + TEST_ASSERT_EQUAL_STRING(NULL, doc["entity_category"]); + TEST_ASSERT_NOT_NULL(Command::mapCommandTopics[doc["command_topic"]]); + + TEST_ASSERT_EQUAL_STRING(NULL, doc["state_topic"]); +} + +void testTemperatureSensor(void) { + TemperatureSensor s("id"); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("Temperature", doc["name"]); + TEST_ASSERT_EQUAL_STRING("temperature", doc["device_class"]); + TEST_ASSERT_EQUAL_STRING("°C", doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testHumiditySensor(void) { + HumiditySensor s("id"); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("Humidity", doc["name"]); + TEST_ASSERT_EQUAL_STRING("humidity", doc["device_class"]); + TEST_ASSERT_EQUAL_STRING("%", doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testPressureSensor(void) { + PressureSensor s("id"); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("Pressure", doc["name"]); + TEST_ASSERT_EQUAL_STRING("pressure", doc["device_class"]); + TEST_ASSERT_EQUAL_STRING("hPa", doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testVoltageSensor(void) { + VoltageSensor s("id", "a_name", nullptr); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING("voltage", doc["device_class"]); + TEST_ASSERT_EQUAL_STRING("V", doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +void testBatterySensor(void) { + BatterySensor s("id", "a_name", nullptr); + + StaticJsonDocument<256> doc; + s.buildConfig(doc); + + TEST_ASSERT_EQUAL_STRING("a_name", doc["name"]); + TEST_ASSERT_EQUAL_STRING("battery", doc["device_class"]); + TEST_ASSERT_EQUAL_STRING("%", doc["unit_of_measurement"]); + TEST_ASSERT_EQUAL_INT(2, doc["suggested_display_precision"]); +} + +int main(int argc, char **argv) { + UNITY_BEGIN(); + RUN_TEST(testDevice); + RUN_TEST(testButton); + RUN_TEST(testSensor); + RUN_TEST(testTemperatureSensor); + RUN_TEST(testHumiditySensor); + RUN_TEST(testPressureSensor); + RUN_TEST(testVoltageSensor); + RUN_TEST(testBatterySensor); + RUN_TEST(testNumericSensor1); + RUN_TEST(testNumericSensor2); + RUN_TEST(testSwitch); + RUN_TEST(testSwitchWithState); + RUN_TEST(testNumber); + return UNITY_END(); +} From 5c3e0c2236a277eef72f35ad3641dc2255130c01 Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Tue, 29 Oct 2024 10:04:45 +0100 Subject: [PATCH 4/5] rename utils.h to list.h --- src/ha.h | 2 +- src/{utils.h => list.h} | 0 test/native/{test_utils/utils.cpp => test_list/main.cpp} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{utils.h => list.h} (100%) rename test/native/{test_utils/utils.cpp => test_list/main.cpp} (97%) diff --git a/src/ha.h b/src/ha.h index 471604d..ae43d0a 100644 --- a/src/ha.h +++ b/src/ha.h @@ -1,7 +1,7 @@ #pragma once #include -#include "utils.h" +#include "list.h" using namespace std; diff --git a/src/utils.h b/src/list.h similarity index 100% rename from src/utils.h rename to src/list.h diff --git a/test/native/test_utils/utils.cpp b/test/native/test_list/main.cpp similarity index 97% rename from test/native/test_utils/utils.cpp rename to test/native/test_list/main.cpp index 966d90c..d80b4b1 100644 --- a/test/native/test_utils/utils.cpp +++ b/test/native/test_list/main.cpp @@ -1,5 +1,5 @@ #include -#include "utils.h" +#include "list.h" void setUp(void) { // set stuff up here From 5eb3e6f0a5ddaa39679cd09db7051ceef505510e Mon Sep 17 00:00:00 2001 From: Nicu Hodos Date: Tue, 29 Oct 2024 10:05:37 +0100 Subject: [PATCH 5/5] reorganize components tests - prepare for future HA tests, like builder --- .../{test_ha/components.cpp => ha/test_components/main.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/native/{test_ha/components.cpp => ha/test_components/main.cpp} (100%) diff --git a/test/native/test_ha/components.cpp b/test/native/ha/test_components/main.cpp similarity index 100% rename from test/native/test_ha/components.cpp rename to test/native/ha/test_components/main.cpp