diff --git a/lib/shade/Shade.h b/lib/shade/Shade.h new file mode 100644 index 0000000..af9eec2 --- /dev/null +++ b/lib/shade/Shade.h @@ -0,0 +1,18 @@ +#ifndef SHADE_H +#define SHADE_H + +#include +#include + +struct Shade { + uint16_t ID; + std::string key; + std::string friendlyName; + int8_t lastTargetPosition; + int8_t lastPosition; + uint8_t samePositionCount; + uint8_t positionFetchCount; + void* timer; +}; + +#endif // SHADE_H \ No newline at end of file diff --git a/lib/shade_repository/ShadeRepository.cpp b/lib/shade_repository/ShadeRepository.cpp new file mode 100644 index 0000000..02ec2ac --- /dev/null +++ b/lib/shade_repository/ShadeRepository.cpp @@ -0,0 +1,66 @@ +#include "ShadeRepository.h" +#include "Shade.h" + +void ShadeRepository::upsert(Shade shade) +{ + for (Shade &existing_shade : shades) + { + if (existing_shade.key == shade.key) + { + existing_shade.friendlyName = shade.friendlyName; + existing_shade.ID = shade.ID; + for (auto &callback : shadeChangedCallbacks) { + callback(shade); + } + return; + } + } + + // Didn't find it in the repository, add it + shades.push_back(shade); + for (auto &callback : shadeAddedCallbacks) { + callback(shade); + } +} + +Shade* ShadeRepository::findById(const uint16_t id) +{ + for (Shade &shade : shades) + { + if (shade.ID == id) + { + return &shade; + } + } + return nullptr; +} + +Shade* ShadeRepository::findByKey(const std::string &key) +{ + for (Shade &shade : shades) + { + if (shade.key == key) + { + return &shade; + } + } + return nullptr; +} + +std::vector::iterator ShadeRepository::begin() +{ + return shades.begin(); +} + +std::vector::iterator ShadeRepository::end() +{ + return shades.end(); +} + +void ShadeRepository::addShadeAddedCallback(std::function callback) { + shadeAddedCallbacks.push_back(callback); +} + +void ShadeRepository::addShadeChangedCallback(std::function callback) { + shadeChangedCallbacks.push_back(callback); +} \ No newline at end of file diff --git a/lib/shade_repository/ShadeRepository.h b/lib/shade_repository/ShadeRepository.h new file mode 100644 index 0000000..d78a4f5 --- /dev/null +++ b/lib/shade_repository/ShadeRepository.h @@ -0,0 +1,25 @@ +#ifndef SHADE_REPOSITORY_H +#define SHADE_REPOSITORY_H + +#include +#include +#include "Shade.h" + +class ShadeRepository { + private: + std::vector shades; + std::vector> shadeAddedCallbacks; + std::vector> shadeChangedCallbacks; + + public: + void upsert(Shade shade); + std::vector::iterator begin(); + std::vector::iterator end(); + Shade* findById(const uint16_t id); + Shade* findByKey(const std::string& key); + + void addShadeAddedCallback(std::function callback); + void addShadeChangedCallback(std::function callback); +}; + +#endif // SHADE_REPOSITORY_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 37d21f7..3f79531 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,8 @@ #include #include #include +#include "Shade.h" +#include "ShadeRepository.h" #include "secrets.h" #define SER_BAUDRATE (115200) @@ -26,6 +28,8 @@ EspMQTTClient client( 1883 ); +ShadeRepository shadeRepository = ShadeRepository(); + #define MAX_FETCH_COUNT (20) auto timer = Timer<10, millis, uint16_t>(); @@ -49,40 +53,11 @@ void publishBattery(const String& shadeName, const uint8_t battery); void publishDiscoveryTopics(); -struct Shade { - uint16_t ID; - String name; - String friendlyName; - int8_t lastTargetPosition; - int8_t lastPosition; - uint8_t samePositionCount; - uint8_t positionFetchCount; - void* timer; -}; - -std::vector shades; - void setup() { Serial.begin(SER_BAUDRATE); Serial.println("Starting up"); - shades.push_back(Shade{0x4EF1, "study_blind", "Study Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0xA51F, "studio_blockout_left_blind", "Studio Blockout Left Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0xDAEF, "studio_blockout_right_blind", "Studio Blockout Right Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x7687, "studio_left_blind", "Studio Left Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0xB0DE, "studio_right_blind", "Studio Right Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0xB451, "bedroom_door_blockout_blind", "Bedroom Door Blockout Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x48A6, "bedroom_window_blockout_blind", "Bedroom Window Blockout Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x9E14, "bedroom_door_blind", "Bedroom Door Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x061C, "bedroom_window_blind", "Bedroom Window Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x2959, "bathroom_blind", "Bathroom Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x6FAD, "kitchen_door_blind", "Kitchen Door Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0xFB21, "kitchen_window_blind", "Kitchen Window Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x8B10, "living_room_big_window_blind", "Living Room Big Window Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x3EB8, "living_room_door_blind", "Living Room Door Blind", -1, -1, 0, 0, nullptr}); - shades.push_back(Shade{0x5463, "living_room_window_blind", "Living Room Window Blind", -1, -1, 0, 0, nullptr}); - powerView.setPacketReceivedCallback(processPacket); if (!powerView.begin()) { Serial.println("Failed to start RFPowerView"); @@ -94,7 +69,37 @@ void setup() { client.enableDebuggingMessages(); client.setMaxPacketSize(2048); + shadeRepository.addShadeAddedCallback([] (Shade& shade) { + // Only add shade if client is already connected + if (client.isConnected()) { + client.subscribe(("hotdog/" + shade.key + "/command").c_str(), processCommandMessage); + client.subscribe(("hotdog/" + shade.key + "/set_position").c_str(), processSetPositionMessage); + } + publishDiscoveryTopics(); + }); + + shadeRepository.addShadeChangedCallback([] (Shade& shade) { + publishDiscoveryTopics(); + }); + 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"); } @@ -135,29 +140,27 @@ void processPacket(const Packet *packet) { for (size_t i = 0; i < parameters.fields.size(); i++) { Field field = parameters.fields[i]; if (field.identifier == 0x50) { - for (size_t i = 0; i < shades.size(); i++) { - if (source == shades[i].ID) { - uint16_t value = std::get(field.value); - uint8_t position = (uint8_t)std::round(((float)value / 0xFFFF) * 100); + auto shade = shadeRepository.findById(source); + if (shade != nullptr) { + uint16_t value = std::get(field.value); + uint8_t position = (uint8_t)std::round(((float)value / 0xFFFF) * 100); - if (shades[i].lastPosition == position) { - shades[i].samePositionCount++; - } else { - shades[i].samePositionCount = 1; - } - shades[i].lastPosition = position; - - publishPosition(shades[i].name, position); + if (shade->lastPosition == position) { + shade->samePositionCount++; + } else { + shade->samePositionCount = 1; } + shade->lastPosition = position; + + publishPosition(shade->key.c_str(), position); } } else if (field.identifier == 0x42) { - for (size_t i = 0; i < shades.size(); i++) { - if (source == shades[i].ID) { - uint8_t value = std::get(field.value); - uint8_t battery = uint8_t(((float)value / 200) * 100); - - publishBattery(shades[i].name, battery); - } + auto shade = shadeRepository.findById(source); + if (shade != nullptr) { + uint8_t value = std::get(field.value); + uint8_t battery = uint8_t(((float)value / 200) * 100); + + publishBattery(shade->key.c_str(), battery); } } } @@ -167,9 +170,9 @@ void processPacket(const Packet *packet) { void onConnectionEstablished() { Serial.println("Connection established"); - for (size_t i = 0; i < shades.size(); i++) { - client.subscribe("hotdog/" + shades[i].name + "/command", processCommandMessage); - client.subscribe("hotdog/" + shades[i].name + "/set_position", processSetPositionMessage); + for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) { + client.subscribe(("hotdog/" + shade->key + "/command").c_str(), processCommandMessage); + client.subscribe(("hotdog/" + shade->key + "/set_position").c_str(), processSetPositionMessage); } client.publish("hotdog/availability", "online", true); @@ -276,30 +279,29 @@ bool sendPacket(Packet *packet) { void processCommandMessage(const String &topic, const String &payload) { int startIndex = topic.indexOf("/") + 1; int endIndex = topic.indexOf("/", startIndex); - String shadeName = topic.substring(startIndex, endIndex); + auto key = topic.substring(startIndex, endIndex).c_str(); - for (size_t i = 0; i < shades.size(); i++) { - if (shades[i].name == shadeName) { - if (payload == "OPEN") { - sendOpenPacket(shades[i].ID); + auto shade = shadeRepository.findByKey(key); + if (shade != nullptr) { + if (payload == "OPEN") { + sendOpenPacket(shade->ID); - startFetchingPosition(shades[i].ID, 100); + startFetchingPosition(shade->ID, 100); - publishState(shades[i].name, "opening"); - } else if (payload == "CLOSE") { - sendClosePacket(shades[i].ID); + publishState(shade->key.c_str(), "opening"); + } else if (payload == "CLOSE") { + sendClosePacket(shade->ID); - startFetchingPosition(shades[i].ID, 0); + startFetchingPosition(shade->ID, 0); - publishState(shades[i].name, "closing"); - } else if (payload == "STOP") { - sendStopPacket(shades[i].ID); + publishState(shade->key.c_str(), "closing"); + } else if (payload == "STOP") { + sendStopPacket(shade->ID); - startFetchingPosition(shades[i].ID, -1); - timer.in(100, sendFetchPosition, shades[i].ID); - } else if (payload == "REFRESH") { - startFetchingPosition(shades[i].ID, -1); - } + startFetchingPosition(shade->ID, -1); + timer.in(100, sendFetchPosition, shade->ID); + } else if (payload == "REFRESH") { + startFetchingPosition(shade->ID, -1); } } } @@ -307,62 +309,59 @@ void processCommandMessage(const String &topic, const String &payload) { void processSetPositionMessage(const String& topic, const String &payload) { int startIndex = topic.indexOf("/") + 1; int endIndex = topic.indexOf("/", startIndex); - String shadeName = topic.substring(startIndex, endIndex); + auto key = topic.substring(startIndex, endIndex).c_str(); - for (size_t i = 0; i < shades.size(); i++) { - if (shades[i].name == shadeName) { - float percentage = payload.toInt() / 100.0f; - sendSetPosition(shades[i].ID, percentage); + auto shade = shadeRepository.findByKey(key); + if (shade != nullptr) { + float percentage = payload.toInt() / 100.0f; + sendSetPosition(shade->ID, percentage); - if (payload.toInt() > shades[i].lastPosition) { - publishState(shades[i].name, "opening"); - } else if (payload.toInt() < shades[i].lastPosition) { - publishState(shades[i].name, "closing"); - } - - startFetchingPosition(shades[i].ID, payload.toInt()); + if (payload.toInt() > shade->lastPosition) { + publishState(shade->key.c_str(), "opening"); + } else if (payload.toInt() < shade->lastPosition) { + publishState(shade->key.c_str(), "closing"); } + + startFetchingPosition(shade->ID, payload.toInt()); } } bool checkPosition(uint16_t shadeID) { - for (size_t i = 0; i < shades.size(); i++) { - if (shades[i].ID == shadeID) { - // Keep fetching position if: - // - the last reported position doesn't match the target position - // - a position hasn't been reported yet - // - there is no target position - if (shades[i].lastTargetPosition != shades[i].lastPosition || shades[i].lastPosition == -1 || shades[i].lastTargetPosition == -1) { - // Keep fetching position if the count limits have not been reached - if (shades[i].positionFetchCount < MAX_FETCH_COUNT && shades[i].samePositionCount < 2) { - shades[i].positionFetchCount++; - sendFetchPosition(shadeID); - return true; - } + auto shade = shadeRepository.findById(shadeID); + if (shade != nullptr) { + // Keep fetching position if: + // - the last reported position doesn't match the target position + // - a position hasn't been reported yet + // - there is no target position + if (shade->lastTargetPosition != shade->lastPosition || shade->lastPosition == -1 || shade->lastTargetPosition == -1) { + // Keep fetching position if the count limits have not been reached + if (shade->positionFetchCount < MAX_FETCH_COUNT && shade->samePositionCount < 2) { + shade->positionFetchCount++; + sendFetchPosition(shadeID); + return true; } - - publishState(shades[i].name, shades[i].lastPosition > 0 ? "open" : "closed"); - return false; } + + publishState(shade->key.c_str(), shade->lastPosition > 0 ? "open" : "closed"); + return false; } return false; } void startFetchingPosition(uint16_t shadeID, int8_t targetPosition) { - for (size_t i = 0; i < shades.size(); i++) { - if (shades[i].ID == shadeID) { - // Cancel any existing timer - if (shades[i].timer != nullptr) { - timer.cancel(shades[i].timer); - shades[i].timer = nullptr; - } - - shades[i].lastTargetPosition = targetPosition; - shades[i].positionFetchCount = 0; - shades[i].samePositionCount = 1; - - shades[i].timer = timer.every(2000, checkPosition, shades[i].ID); + auto shade = shadeRepository.findById(shadeID); + if (shade != nullptr) { + // Cancel any existing timer + if (shade->timer != nullptr) { + timer.cancel(shade->timer); + shade->timer = nullptr; } + + shade->lastTargetPosition = targetPosition; + shade->positionFetchCount = 0; + shade->samePositionCount = 1; + + shade->timer = timer.every(2000, checkPosition, shade->ID); } } @@ -400,10 +399,10 @@ void publishCoverDiscoveryTopic(const Shade& shade) { doc["name"] = nullptr; doc["unique_id"] = entityID; doc["availability_topic"] = "hotdog/availability"; - doc["state_topic"] = "hotdog/" + shade.name + "/state"; - doc["command_topic"] = "hotdog/" + shade.name + "/command"; - doc["position_topic"] = "hotdog/" + shade.name + "/position"; - doc["set_position_topic"] = "hotdog/" + shade.name + "/set_position"; + doc["state_topic"] = "hotdog/" + shade.key + "/state"; + doc["command_topic"] = "hotdog/" + shade.key + "/command"; + doc["position_topic"] = "hotdog/" + shade.key + "/position"; + doc["set_position_topic"] = "hotdog/" + shade.key + "/set_position"; doc["position_open"] = 100; doc["position_closed"] = 0; doc["optimistic"] = false; @@ -425,7 +424,7 @@ void publishBatteryDiscoveryTopic(const Shade& shade) { doc["unique_id"] = entityID; doc["availability_topic"] = "hotdog/availability"; doc["device_class"] = "battery"; - doc["state_topic"] = "hotdog/" + shade.name + "/battery"; + doc["state_topic"] = "hotdog/" + shade.key + "/battery"; addDeviceObject(doc, shade); @@ -443,7 +442,7 @@ void publishRefreshButtonDiscoveryTopic(const Shade& shade) { doc["name"] = "Refresh"; doc["unique_id"] = entityID; doc["availability_topic"] = "hotdog/availability"; - doc["command_topic"] = "hotdog/" + shade.name + "/command"; + doc["command_topic"] = "hotdog/" + shade.key + "/command"; doc["payload_press"] = "REFRESH"; addDeviceObject(doc, shade); @@ -454,12 +453,9 @@ void publishRefreshButtonDiscoveryTopic(const Shade& shade) { } void publishDiscoveryTopics() { - for (size_t i = 0; i < shades.size(); i++) { - Shade shade = shades[i]; - String objectID = String(shade.ID, HEX); - - publishCoverDiscoveryTopic(shade); - publishBatteryDiscoveryTopic(shade); - publishRefreshButtonDiscoveryTopic(shade); + for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) { + publishCoverDiscoveryTopic(*shade); + publishBatteryDiscoveryTopic(*shade); + publishRefreshButtonDiscoveryTopic(*shade); } } \ No newline at end of file diff --git a/test/test_shade_repository/test_shade_repository.cpp b/test/test_shade_repository/test_shade_repository.cpp new file mode 100644 index 0000000..78f6ef5 --- /dev/null +++ b/test/test_shade_repository/test_shade_repository.cpp @@ -0,0 +1,158 @@ +#include +#include +#include "ShadeRepository.h" + +void setUp() +{ +} + +void tearDown() +{ +} + +void test_shade_is_found_with_id() +{ + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade = shadeRepository.findById(0xABCD); + + TEST_ASSERT_NOT_NULL_MESSAGE(shade, "shade should not be null"); + 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()); +} + +void test_shade_is_found_with_key() +{ + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade = shadeRepository.findByKey("test_shade"); + + TEST_ASSERT_NOT_NULL_MESSAGE(shade, "shade should not be null"); + 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()); +} + +void test_adding_shade_twice_only_added_once() +{ + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + int count = 0; + + for (auto iter = shadeRepository.begin(); iter != shadeRepository.end(); iter++) { + count++; + } + + TEST_ASSERT_EQUAL_INT(1, count); +} + +void test_updating_shade_id() +{ + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade1 = shadeRepository.findById(0xABCD); + + TEST_ASSERT_EQUAL_HEX16(0xABCD, shade1->ID); + + shadeRepository.upsert(Shade{0x1234, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade2 = shadeRepository.findById(0x1234); + + auto shade3 = shadeRepository.findById(0xABCD); + + TEST_ASSERT_EQUAL_HEX16(0x1234, shade2->ID); + TEST_ASSERT_EQUAL_HEX16(0x1234, shade1->ID); + TEST_ASSERT_NULL(shade3); +} + +void test_updating_shade_friendly_name() +{ + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade1 = shadeRepository.findByKey("test_shade"); + + TEST_ASSERT_EQUAL_STRING("Test Shade", shade1->friendlyName.c_str()); + + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Updated Test Shade", -1, -1, 0, 0, nullptr}); + + auto shade2 = shadeRepository.findByKey("test_shade"); + + TEST_ASSERT_EQUAL_STRING("Updated Test Shade", shade2->friendlyName.c_str()); + TEST_ASSERT_EQUAL_STRING("Updated Test Shade", shade1->friendlyName.c_str()); +} + +void test_shade_added_callback() +{ + int callbackInvokedCount = 0; + + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.addShadeAddedCallback([&] (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()); + }); + + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(1, callbackInvokedCount); +} + +void test_shade_changed_callback() +{ + int callbackInvokedCount = 0; + + ShadeRepository shadeRepository = ShadeRepository(); + shadeRepository.addShadeChangedCallback([&] (Shade& shade) { + callbackInvokedCount++; + + TEST_ASSERT_EQUAL_STRING("Updated Test Shade", shade.friendlyName.c_str()); + TEST_ASSERT_EQUAL_HEX16(0x1234, shade.ID); + TEST_ASSERT_EQUAL_STRING("test_shade", shade.key.c_str()); + }); + + shadeRepository.upsert(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + shadeRepository.upsert(Shade{0x1234, "test_shade", "Updated Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(1, callbackInvokedCount); +} + +int runUnityTests(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_shade_is_found_with_id); + RUN_TEST(test_shade_is_found_with_key); + RUN_TEST(test_adding_shade_twice_only_added_once); + RUN_TEST(test_updating_shade_id); + RUN_TEST(test_updating_shade_friendly_name); + RUN_TEST(test_shade_added_callback); + RUN_TEST(test_shade_changed_callback); + 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