Extract Home Assistant discovery logic
This commit is contained in:
101
lib/ha_discovery/HADiscovery.cpp
Normal file
101
lib/ha_discovery/HADiscovery.cpp
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#include "HADiscovery.h"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
HADiscovery::HADiscovery(const char *topic_prefix, std::function<void(const char*, const char*)> 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<JsonObject>();
|
||||||
|
device["name"] = shade.friendlyName;
|
||||||
|
JsonArray identifiers = device["identifiers"].to<JsonArray>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
27
lib/ha_discovery/HADiscovery.h
Normal file
27
lib/ha_discovery/HADiscovery.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef HA_DISCOVERY_H
|
||||||
|
#define HA_DISCOVERY_H
|
||||||
|
|
||||||
|
#include "Shade.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class HADiscovery
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
char jsonBuffer[1024];
|
||||||
|
|
||||||
|
const char *topic_prefix;
|
||||||
|
const std::function<void(const char*, const char*)> 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<void(const char* topic, const char* message)> publish_callback);
|
||||||
|
|
||||||
|
void publish(const Shade &shade);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // HA_DISCOVERY_H
|
||||||
84
src/main.cpp
84
src/main.cpp
@@ -5,6 +5,7 @@
|
|||||||
#include <RFPowerView.h>
|
#include <RFPowerView.h>
|
||||||
#include "Shade.h"
|
#include "Shade.h"
|
||||||
#include "ShadeRepository.h"
|
#include "ShadeRepository.h"
|
||||||
|
#include "HADiscovery.h"
|
||||||
#include "secrets.h"
|
#include "secrets.h"
|
||||||
|
|
||||||
#define SER_BAUDRATE (115200)
|
#define SER_BAUDRATE (115200)
|
||||||
@@ -30,6 +31,10 @@ EspMQTTClient client(
|
|||||||
|
|
||||||
ShadeRepository shadeRepository = ShadeRepository();
|
ShadeRepository shadeRepository = ShadeRepository();
|
||||||
|
|
||||||
|
HADiscovery haDiscovery("hotdog/", [] (const char* topic, const char* message) {
|
||||||
|
client.publish(topic, message);
|
||||||
|
});
|
||||||
|
|
||||||
#define MAX_FETCH_COUNT (20)
|
#define MAX_FETCH_COUNT (20)
|
||||||
auto timer = Timer<10, millis, uint16_t>();
|
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);
|
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<JsonObject>();
|
|
||||||
device["name"] = shade.friendlyName;
|
|
||||||
JsonArray identifiers = device["identifiers"].to<JsonArray>();
|
|
||||||
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() {
|
void publishDiscoveryTopics() {
|
||||||
for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) {
|
for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) {
|
||||||
publishCoverDiscoveryTopic(*shade);
|
haDiscovery.publish(*shade);
|
||||||
publishBatteryDiscoveryTopic(*shade);
|
|
||||||
publishRefreshButtonDiscoveryTopic(*shade);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
130
test/test_ha_discovery/test_ha_discovery.cpp
Normal file
130
test/test_ha_discovery/test_ha_discovery.cpp
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user