Configure shades via MQTT messages
This commit is contained in:
62
lib/configurator/Configurator.cpp
Normal file
62
lib/configurator/Configurator.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "Configurator.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
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<void (Shade)> 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;
|
||||||
|
}
|
||||||
30
lib/configurator/Configurator.h
Normal file
30
lib/configurator/Configurator.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef CONFIGURATOR_H
|
||||||
|
#define CONFIGURATOR_H
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <functional>
|
||||||
|
#include "Shade.h"
|
||||||
|
|
||||||
|
// TODO: Allow shades to be removed
|
||||||
|
|
||||||
|
class Configurator
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<std::function<void (Shade)>> shadeConfiguredCallbacks;
|
||||||
|
std::map<std::string, uint16_t> discoveredIds;
|
||||||
|
std::map<std::string, std::string> discoveredFriendlyNames;
|
||||||
|
|
||||||
|
std::string extractKey(std::string topic);
|
||||||
|
bool tryBuild(std::string topic);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Configurator();
|
||||||
|
|
||||||
|
void addShadeConfiguredCallback(std::function<void (Shade)> callback);
|
||||||
|
|
||||||
|
void processIdMessage(std::string topic, std::string id);
|
||||||
|
void processFriendlyNameMessage(std::string topic, std::string friendlyName);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CONFIGURATOR_H
|
||||||
23
src/main.cpp
23
src/main.cpp
@@ -6,6 +6,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "Shade.h"
|
#include "Shade.h"
|
||||||
|
#include "Configurator.h"
|
||||||
#include "ShadeRepository.h"
|
#include "ShadeRepository.h"
|
||||||
#include "HADiscovery.h"
|
#include "HADiscovery.h"
|
||||||
#include "secrets.h"
|
#include "secrets.h"
|
||||||
@@ -48,6 +49,8 @@ EspMQTTClient client(
|
|||||||
1883
|
1883
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Configurator configurator = Configurator();
|
||||||
|
|
||||||
ShadeRepository shadeRepository = ShadeRepository();
|
ShadeRepository shadeRepository = ShadeRepository();
|
||||||
|
|
||||||
HADiscovery haDiscovery(topic_prefix.c_str(), [] (const char* topic, const char* message) {
|
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");
|
Serial.println("Failed to set max packet size");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurator.addShadeConfiguredCallback([] (Shade shade) {
|
||||||
|
shadeRepository.upsert(shade);
|
||||||
|
});
|
||||||
|
|
||||||
shadeRepository.addShadeAddedCallback([] (Shade& shade) {
|
shadeRepository.addShadeAddedCallback([] (Shade& shade) {
|
||||||
// Only add shade if client is already connected
|
// Only add shade if client is already connected
|
||||||
if (client.isConnected()) {
|
if (client.isConnected()) {
|
||||||
@@ -109,22 +116,6 @@ void setup() {
|
|||||||
|
|
||||||
delay(100);
|
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");
|
Serial.println("Ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
162
test/test_configurator/test_configurator.cpp
Normal file
162
test/test_configurator/test_configurator.cpp
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user