diff --git a/lib/configurator/Configurator.cpp b/lib/configurator/Configurator.cpp new file mode 100644 index 0000000..de68a6d --- /dev/null +++ b/lib/configurator/Configurator.cpp @@ -0,0 +1,62 @@ +#include "Configurator.h" +#include + +Configurator::Configurator() { + // TODO: Should be using MQTT directly here (and methods for handling methods should be private) +} + +std::string Configurator::extractKey(std::string topic) { + int startIndex = topic.find("/") + 1; + int endIndex = topic.find("/", startIndex); + // TODO: need to verify that startIndex and endIndex are valid before substr() + return topic.substr(startIndex, endIndex - startIndex); +} + +void Configurator::processIdMessage(std::string topic, std::string payload) { + auto key = extractKey(topic); + + if (payload.length() != 4) { + // Ignore payloads that aren't exactly 4 characters + return; + } + + uint16_t id = 0; + for (char c : payload) { + // Check if valid hex digit (0-9, A-F, a-f) + if (!std::isxdigit(c)) { + return; // Invalid character, not a hex digit + } + + // Convert hex digit to numerical value (0-9, A-B=10-11, ...) + int digit = std::toupper(c) - (c <= '9' ? '0' : 'A' - 10); + + id = (id << 4) | digit; + } + + discoveredIds[key] = id; + tryBuild(key); +} + +void Configurator::processFriendlyNameMessage(std::string topic, std::string friendlyName) { + auto key = extractKey(topic); + + discoveredFriendlyNames[key] = friendlyName; + tryBuild(key); +} + +void Configurator::addShadeConfiguredCallback(std::function callback) { + shadeConfiguredCallbacks.push_back(callback); +} + +bool Configurator::tryBuild(std::string key) { + if (auto id = discoveredIds.find(key); id != discoveredIds.end()) { + if (auto friendlyName = discoveredFriendlyNames.find(key); friendlyName != discoveredFriendlyNames.end()) { + auto shade = Shade{id->second, key, friendlyName->second, -1, -1, 0, 0, nullptr}; + for (size_t i = 0; i < shadeConfiguredCallbacks.size(); i++) { + shadeConfiguredCallbacks[i](shade); + } + return true; + } + } + return false; +} \ No newline at end of file diff --git a/lib/configurator/Configurator.h b/lib/configurator/Configurator.h new file mode 100644 index 0000000..d404b5c --- /dev/null +++ b/lib/configurator/Configurator.h @@ -0,0 +1,30 @@ +#ifndef CONFIGURATOR_H +#define CONFIGURATOR_H + +#include +#include +#include +#include "Shade.h" + +// TODO: Allow shades to be removed + +class Configurator +{ +private: + std::vector> shadeConfiguredCallbacks; + std::map discoveredIds; + std::map discoveredFriendlyNames; + + std::string extractKey(std::string topic); + bool tryBuild(std::string topic); + +public: + Configurator(); + + void addShadeConfiguredCallback(std::function callback); + + void processIdMessage(std::string topic, std::string id); + void processFriendlyNameMessage(std::string topic, std::string friendlyName); +}; + +#endif // CONFIGURATOR_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 9c79917..78a782f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include "Shade.h" +#include "Configurator.h" #include "ShadeRepository.h" #include "HADiscovery.h" #include "secrets.h" @@ -48,6 +49,8 @@ EspMQTTClient client( 1883 ); +Configurator configurator = Configurator(); + ShadeRepository shadeRepository = ShadeRepository(); HADiscovery haDiscovery(topic_prefix.c_str(), [] (const char* topic, const char* message) { @@ -94,6 +97,10 @@ void setup() { Serial.println("Failed to set max packet size"); } + configurator.addShadeConfiguredCallback([] (Shade shade) { + shadeRepository.upsert(shade); + }); + shadeRepository.addShadeAddedCallback([] (Shade& shade) { // Only add shade if client is already connected if (client.isConnected()) { @@ -109,22 +116,6 @@ void setup() { delay(100); - shadeRepository.upsert(Shade{0x4EF1, "study_blind", "Study Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0xA51F, "studio_blockout_left_blind", "Studio Blockout Left Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0xDAEF, "studio_blockout_right_blind", "Studio Blockout Right Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x7687, "studio_left_blind", "Studio Left Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0xB0DE, "studio_right_blind", "Studio Right Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0xB451, "bedroom_door_blockout_blind", "Bedroom Door Blockout Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x48A6, "bedroom_window_blockout_blind", "Bedroom Window Blockout Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x9E14, "bedroom_door_blind", "Bedroom Door Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x061C, "bedroom_window_blind", "Bedroom Window Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x2959, "bathroom_blind", "Bathroom Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x6FAD, "kitchen_door_blind", "Kitchen Door Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0xFB21, "kitchen_window_blind", "Kitchen Window Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x8B10, "living_room_big_window_blind", "Living Room Big Window Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x3EB8, "living_room_door_blind", "Living Room Door Blind", -1, -1, 0, 0, nullptr}); - shadeRepository.upsert(Shade{0x5463, "living_room_window_blind", "Living Room Window Blind", -1, -1, 0, 0, nullptr}); - Serial.println("Ready"); } diff --git a/test/test_configurator/test_configurator.cpp b/test/test_configurator/test_configurator.cpp new file mode 100644 index 0000000..7d28d7d --- /dev/null +++ b/test/test_configurator/test_configurator.cpp @@ -0,0 +1,162 @@ +#include +#include +#include "Configurator.h" + +void setUp() +{ +} + +void tearDown() +{ +} + +void test_shade_is_configured_with_id_and_friendly_name() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + + TEST_ASSERT_EQUAL_STRING("Test Shade", shade.friendlyName.c_str()); + TEST_ASSERT_EQUAL_HEX16(0xABCD, shade.ID); + TEST_ASSERT_EQUAL_STRING("test_shade", shade.key.c_str()); + }); + + configurator.processIdMessage("hotdog/test_shade/id", "ABCD"); + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Test Shade"); + + TEST_ASSERT_EQUAL_INT(1, callbackInvokedCount); +} + +void test_shade_is_configured_again_with_new_friendly_name() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + + if (callbackInvokedCount == 1) { + TEST_ASSERT_EQUAL_STRING("Test Shade", shade.friendlyName.c_str()); + } else if (callbackInvokedCount == 2) { + TEST_ASSERT_EQUAL_STRING("Updated Test Shade", shade.friendlyName.c_str()); + } else { + TEST_ABORT(); + } + TEST_ASSERT_EQUAL_HEX16(0xABCD, shade.ID); + TEST_ASSERT_EQUAL_STRING("test_shade", shade.key.c_str()); + }); + + configurator.processIdMessage("hotdog/test_shade/id", "ABCD"); + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Test Shade"); + + TEST_ASSERT_EQUAL_INT(1, callbackInvokedCount); + + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Updated Test Shade"); + + TEST_ASSERT_EQUAL_INT(2, callbackInvokedCount); +} + +void test_shade_is_configured_again_with_new_id() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + + TEST_ASSERT_EQUAL_STRING("Test Shade", shade.friendlyName.c_str()); + if (callbackInvokedCount == 1) { + TEST_ASSERT_EQUAL_HEX16(0xABCD, shade.ID); + } else if (callbackInvokedCount == 2) { + TEST_ASSERT_EQUAL_HEX16(0x9876, shade.ID); + } else { + TEST_ABORT(); + } + TEST_ASSERT_EQUAL_STRING("test_shade", shade.key.c_str()); + }); + + configurator.processIdMessage("hotdog/test_shade/id", "ABCD"); + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Test Shade"); + + TEST_ASSERT_EQUAL_INT(1, callbackInvokedCount); + + configurator.processIdMessage("hotdog/test_shade/id", "9876"); + + TEST_ASSERT_EQUAL_INT(2, callbackInvokedCount); +} + +void test_shade_is_not_configured_without_friendly_name() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + }); + + configurator.processIdMessage("hotdog/test_shade/id", "ABCD"); + + TEST_ASSERT_EQUAL_INT(0, callbackInvokedCount); +} + +void test_shade_is_not_configured_without_id() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + }); + + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Test Shade"); + + TEST_ASSERT_EQUAL_INT(0, callbackInvokedCount); +} + +void test_shade_is_not_configured_with_invalid_id() +{ + int callbackInvokedCount = 0; + + Configurator configurator = Configurator(); + configurator.addShadeConfiguredCallback([&] (Shade shade) { + callbackInvokedCount++; + }); + + configurator.processIdMessage("hotdog/test_shade/id", "ZZZZ"); + configurator.processFriendlyNameMessage("hotdog/test_shade/friendly_name", "Test Shade"); + + TEST_ASSERT_EQUAL_INT(0, callbackInvokedCount); +} + +int runUnityTests(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_shade_is_configured_with_id_and_friendly_name); + RUN_TEST(test_shade_is_configured_again_with_new_friendly_name); + RUN_TEST(test_shade_is_configured_again_with_new_id); + RUN_TEST(test_shade_is_not_configured_without_friendly_name); + RUN_TEST(test_shade_is_not_configured_without_id); + RUN_TEST(test_shade_is_not_configured_with_invalid_id); + return UNITY_END(); +} + +/** + * For Arduino framework + */ + +void setup() +{ + // Wait ~2 seconds before the Unity test runner + // establishes connection with a board Serial interface + delay(2000); + + runUnityTests(); +} +void loop() {} + +int main(void) +{ + return runUnityTests(); +} \ No newline at end of file