diff --git a/lib/ha_discovery/HADiscovery.cpp b/lib/ha_discovery/HADiscovery.cpp new file mode 100644 index 0000000..5717f6b --- /dev/null +++ b/lib/ha_discovery/HADiscovery.cpp @@ -0,0 +1,101 @@ +#include "HADiscovery.h" +#include + +HADiscovery::HADiscovery(const char *topic_prefix, std::function publish_callback) : topic_prefix(topic_prefix), publish_callback(publish_callback) +{ +} + +void HADiscovery::publish(const Shade &shade) +{ + publishCoverDiscoveryTopic(shade); + publishBatteryDiscoveryTopic(shade); + publishRefreshButtonDiscoveryTopic(shade); +} + +void HADiscovery::addDeviceObject(JsonDocument &doc, const Shade &shade) +{ + std::stringstream ss; + ss << "hotdog-" << std::hex << shade.ID; + std::string deviceID = ss.str(); + + JsonObject device = doc["device"].to(); + device["name"] = shade.friendlyName; + JsonArray identifiers = device["identifiers"].to(); + identifiers.add(deviceID); + device["manufacturer"] = "Hunter Douglas"; + // TODO: Add fields like sw_version and model +} + +void HADiscovery::publishCoverDiscoveryTopic(const Shade &shade) +{ + std::stringstream ss; + ss << std::hex << shade.ID; + std::string objectID = ss.str(); + + std::string entityID = "cover-" + objectID; + + JsonDocument doc; + + doc["name"] = nullptr; + doc["unique_id"] = entityID; + doc["availability_topic"] = std::string(topic_prefix) + "availability"; + doc["state_topic"] = std::string(topic_prefix) + shade.key + "/state"; + doc["command_topic"] = std::string(topic_prefix) + shade.key + "/command"; + doc["position_topic"] = std::string(topic_prefix) + shade.key + "/position"; + doc["set_position_topic"] = std::string(topic_prefix) + shade.key + "/set_position"; + doc["position_open"] = 100; + doc["position_closed"] = 0; + doc["optimistic"] = false; + + addDeviceObject(doc, shade); + + serializeJson(doc, jsonBuffer); + + publish_callback(("homeassistant/cover/" + objectID + "/config").c_str(), jsonBuffer); +} + +void HADiscovery::publishBatteryDiscoveryTopic(const Shade &shade) +{ + std::stringstream ss; + ss << std::hex << shade.ID; + std::string objectID = ss.str(); + + std::string entityID = "battery-" + objectID; + + JsonDocument doc; + + doc["name"] = "Battery"; + doc["unique_id"] = entityID; + doc["availability_topic"] = std::string(topic_prefix) + "availability"; + doc["device_class"] = "battery"; + doc["state_topic"] = std::string(topic_prefix) + shade.key + "/battery"; + + addDeviceObject(doc, shade); + + serializeJson(doc, jsonBuffer); + + publish_callback(("homeassistant/sensor/" + objectID + "/config").c_str(), jsonBuffer); +} + +void HADiscovery::publishRefreshButtonDiscoveryTopic(const Shade &shade) +{ + std::stringstream ss; + ss << std::hex << shade.ID; + std::string objectID = ss.str(); + + std::string entityID = "refresh-button-" + objectID; + + JsonDocument doc; + + doc["name"] = "Refresh"; + doc["unique_id"] = entityID; + doc["availability_topic"] = std::string(topic_prefix) + "availability"; + doc["command_topic"] = std::string(topic_prefix) + shade.key + "/command"; + doc["payload_press"] = "REFRESH"; + + addDeviceObject(doc, shade); + + serializeJson(doc, jsonBuffer); + + publish_callback(("homeassistant/button/" + objectID + "/config").c_str(), jsonBuffer); +} \ No newline at end of file diff --git a/lib/ha_discovery/HADiscovery.h b/lib/ha_discovery/HADiscovery.h new file mode 100644 index 0000000..24f6fae --- /dev/null +++ b/lib/ha_discovery/HADiscovery.h @@ -0,0 +1,27 @@ +#ifndef HA_DISCOVERY_H +#define HA_DISCOVERY_H + +#include "Shade.h" +#include +#include + +class HADiscovery +{ +private: + char jsonBuffer[1024]; + + const char *topic_prefix; + const std::function publish_callback; + + void addDeviceObject(JsonDocument &doc, const Shade &shade); + void publishCoverDiscoveryTopic(const Shade &shade); + void publishBatteryDiscoveryTopic(const Shade &shade); + void publishRefreshButtonDiscoveryTopic(const Shade &shade); + +public: + HADiscovery(const char *topic_prefix, std::function publish_callback); + + void publish(const Shade &shade); +}; + +#endif // HA_DISCOVERY_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 3f79531..17889f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include "Shade.h" #include "ShadeRepository.h" +#include "HADiscovery.h" #include "secrets.h" #define SER_BAUDRATE (115200) @@ -30,6 +31,10 @@ EspMQTTClient client( ShadeRepository shadeRepository = ShadeRepository(); +HADiscovery haDiscovery("hotdog/", [] (const char* topic, const char* message) { + client.publish(topic, message); +}); + #define MAX_FETCH_COUNT (20) auto timer = Timer<10, millis, uint16_t>(); @@ -377,85 +382,8 @@ void publishBattery(const String& shadeName, const uint8_t battery) { client.publish("hotdog/" + shadeName + "/battery", String(battery), true); } -char jsonBuffer[1024]; - -void addDeviceObject(JsonDocument &doc, const Shade& shade) { - String deviceID = "hotdog-" + String(shade.ID, HEX); - - JsonObject device = doc["device"].to(); - device["name"] = shade.friendlyName; - JsonArray identifiers = device["identifiers"].to(); - identifiers.add(deviceID); - device["manufacturer"] = "Hunter Douglas"; - // TODO: Add fields like sw_version and model -} - -void publishCoverDiscoveryTopic(const Shade& shade) { - String objectID = String(shade.ID, HEX); - String entityID = "cover-" + objectID; - - JsonDocument doc; - - doc["name"] = nullptr; - doc["unique_id"] = entityID; - doc["availability_topic"] = "hotdog/availability"; - 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; - - addDeviceObject(doc, shade); - - serializeJson(doc, jsonBuffer); - - client.publish("homeassistant/cover/" + objectID + "/config", jsonBuffer); -} - -void publishBatteryDiscoveryTopic(const Shade& shade) { - String objectID = String(shade.ID, HEX); - String entityID = "battery-" + objectID; - - JsonDocument doc; - - doc["name"] = "Battery"; - doc["unique_id"] = entityID; - doc["availability_topic"] = "hotdog/availability"; - doc["device_class"] = "battery"; - doc["state_topic"] = "hotdog/" + shade.key + "/battery"; - - addDeviceObject(doc, shade); - - serializeJson(doc, jsonBuffer); - - client.publish("homeassistant/sensor/" + objectID + "/config", jsonBuffer); -} - -void publishRefreshButtonDiscoveryTopic(const Shade& shade) { - String objectID = String(shade.ID, HEX); - String entityID = "refresh-button-" + objectID; - - JsonDocument doc; - - doc["name"] = "Refresh"; - doc["unique_id"] = entityID; - doc["availability_topic"] = "hotdog/availability"; - doc["command_topic"] = "hotdog/" + shade.key + "/command"; - doc["payload_press"] = "REFRESH"; - - addDeviceObject(doc, shade); - - serializeJson(doc, jsonBuffer); - - client.publish("homeassistant/button/" + objectID + "/config", jsonBuffer); -} - void publishDiscoveryTopics() { for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) { - publishCoverDiscoveryTopic(*shade); - publishBatteryDiscoveryTopic(*shade); - publishRefreshButtonDiscoveryTopic(*shade); + haDiscovery.publish(*shade); } } \ No newline at end of file diff --git a/test/test_ha_discovery/test_ha_discovery.cpp b/test/test_ha_discovery/test_ha_discovery.cpp new file mode 100644 index 0000000..ba80b5b --- /dev/null +++ b/test/test_ha_discovery/test_ha_discovery.cpp @@ -0,0 +1,130 @@ +#include +#include +#include "HADiscovery.h" + +void setUp() +{ +} + +void tearDown() +{ +} + +void test_publish_message_count() +{ + int callbackInvokedCount = 0; + + auto callback = [&](const char *topic, const char *message) + { + callbackInvokedCount++; + // TEST_ASSERT_EQUAL_STRING("my_prefix/shade_key", topic); + // TEST_ASSERT_EQUAL_STRING("{}", message); + }; + + HADiscovery haDiscovery = HADiscovery("my_prefix/", callback); + + haDiscovery.publish(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(3, callbackInvokedCount); +} + +void test_publish_cover_message() +{ + int messageAssertedCount = 0; + + auto callback = [&](const char *topic, const char *message) + { + if (strstr(topic, "homeassistant/cover") != nullptr) + { + messageAssertedCount++; + TEST_ASSERT_EQUAL_STRING("homeassistant/cover/abcd/config", topic); + + auto expected_message = "{\"name\":null,\"unique_id\":\"cover-abcd\",\"availability_topic\":\"my_prefix/availability\",\"state_topic\":\"my_prefix/test_shade/state\",\"command_topic\":\"my_prefix/test_shade/command\",\"position_topic\":\"my_prefix/test_shade/position\",\"set_position_topic\":\"my_prefix/test_shade/set_position\",\"position_open\":100,\"position_closed\":0,\"optimistic\":false,\"device\":{\"name\":\"Test Shade\",\"identifiers\":[\"hotdog-abcd\"],\"manufacturer\":\"Hunter Douglas\"}}"; + + TEST_ASSERT_EQUAL_STRING(expected_message, message); + } + }; + + HADiscovery haDiscovery = HADiscovery("my_prefix/", callback); + + haDiscovery.publish(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(1, messageAssertedCount); +} + +void test_publish_battery_message() +{ + int messageAssertedCount = 0; + + auto callback = [&](const char *topic, const char *message) + { + if (strstr(topic, "homeassistant/sensor") != nullptr) + { + messageAssertedCount++; + TEST_ASSERT_EQUAL_STRING("homeassistant/sensor/abcd/config", topic); + + auto expected_message = "{\"name\":\"Battery\",\"unique_id\":\"battery-abcd\",\"availability_topic\":\"my_prefix/availability\",\"device_class\":\"battery\",\"state_topic\":\"my_prefix/test_shade/battery\",\"device\":{\"name\":\"Test Shade\",\"identifiers\":[\"hotdog-abcd\"],\"manufacturer\":\"Hunter Douglas\"}}"; + + TEST_ASSERT_EQUAL_STRING(expected_message, message); + } + }; + + HADiscovery haDiscovery = HADiscovery("my_prefix/", callback); + + haDiscovery.publish(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(1, messageAssertedCount); +} + +void test_publish_button_message() +{ + int messageAssertedCount = 0; + + auto callback = [&](const char *topic, const char *message) + { + if (strstr(topic, "homeassistant/button") != nullptr) + { + messageAssertedCount++; + TEST_ASSERT_EQUAL_STRING("homeassistant/button/abcd/config", topic); + + auto expected_message = "{\"name\":\"Refresh\",\"unique_id\":\"refresh-button-abcd\",\"availability_topic\":\"my_prefix/availability\",\"command_topic\":\"my_prefix/test_shade/command\",\"payload_press\":\"REFRESH\",\"device\":{\"name\":\"Test Shade\",\"identifiers\":[\"hotdog-abcd\"],\"manufacturer\":\"Hunter Douglas\"}}"; + + TEST_ASSERT_EQUAL_STRING(expected_message, message); + } + }; + + HADiscovery haDiscovery = HADiscovery("my_prefix/", callback); + + haDiscovery.publish(Shade{0xABCD, "test_shade", "Test Shade", -1, -1, 0, 0, nullptr}); + + TEST_ASSERT_EQUAL_INT(1, messageAssertedCount); +} + +int runUnityTests(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_publish_message_count); + RUN_TEST(test_publish_cover_message); + RUN_TEST(test_publish_battery_message); + RUN_TEST(test_publish_button_message); + 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