Compare commits
51 Commits
597a05fbc5
...
82fdc29f2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 82fdc29f2a | |||
| e8003772ef | |||
| 0458c70b47 | |||
| 7c24d6b411 | |||
| 244af38c2b | |||
| a71eeb1dec | |||
| e60fa052df | |||
| 59b6ed281c | |||
| a9dccae331 | |||
| eee6cbef46 | |||
| 0a9e3c640c | |||
| 39b61997d2 | |||
| 7adbff6890 | |||
| 97d450ab3e | |||
| 55d114afd6 | |||
| bbc54947b1 | |||
| 01f2e267a9 | |||
| 42713c1bb1 | |||
| 0eba1a6f1b | |||
| be9de4a2f6 | |||
| a5c5a7ac19 | |||
| fcd4f81798 | |||
| 3c190897ee | |||
| dbed260fd1 | |||
| e02add87ff | |||
| ccdbea4283 | |||
| 8b835b6af0 | |||
| 62474c3911 | |||
| a1cd3656d4 | |||
| 9fb9c19a27 | |||
| 1838de5b35 | |||
| abd850399a | |||
| 4d0af9e98d | |||
| c2c55d6764 | |||
| 2bc7835dce | |||
| bdebcf83ff | |||
| 194d1e96f3 | |||
| 18639220b8 | |||
| f41e0edf56 | |||
| cffae2bfe5 | |||
| 80ea823fae | |||
| 0ec231cb0d | |||
| 7dfe523c52 | |||
| f80cc9a6b9 | |||
| 981c28f72e | |||
| 3051246cb2 | |||
| d6a72c5811 | |||
| 041f09896a | |||
| 33551d7e01 | |||
| 207ffe9cc9 | |||
| df13de502d |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
39
include/README
Normal file
39
include/README
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
lib/README
Normal file
46
lib/README
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
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
|
||||
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
|
||||
18
lib/shade/Shade.h
Normal file
18
lib/shade/Shade.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef SHADE_H
|
||||
#define SHADE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
66
lib/shade_repository/ShadeRepository.cpp
Normal file
66
lib/shade_repository/ShadeRepository.cpp
Normal file
@@ -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<Shade>::iterator ShadeRepository::begin()
|
||||
{
|
||||
return shades.begin();
|
||||
}
|
||||
|
||||
std::vector<Shade>::iterator ShadeRepository::end()
|
||||
{
|
||||
return shades.end();
|
||||
}
|
||||
|
||||
void ShadeRepository::addShadeAddedCallback(std::function<void(Shade&)> callback) {
|
||||
shadeAddedCallbacks.push_back(callback);
|
||||
}
|
||||
|
||||
void ShadeRepository::addShadeChangedCallback(std::function<void(Shade&)> callback) {
|
||||
shadeChangedCallbacks.push_back(callback);
|
||||
}
|
||||
25
lib/shade_repository/ShadeRepository.h
Normal file
25
lib/shade_repository/ShadeRepository.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef SHADE_REPOSITORY_H
|
||||
#define SHADE_REPOSITORY_H
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include "Shade.h"
|
||||
|
||||
class ShadeRepository {
|
||||
private:
|
||||
std::vector<Shade> shades;
|
||||
std::vector<std::function<void(Shade&)>> shadeAddedCallbacks;
|
||||
std::vector<std::function<void(Shade&)>> shadeChangedCallbacks;
|
||||
|
||||
public:
|
||||
void upsert(Shade shade);
|
||||
std::vector<Shade>::iterator begin();
|
||||
std::vector<Shade>::iterator end();
|
||||
Shade* findById(const uint16_t id);
|
||||
Shade* findByKey(const std::string& key);
|
||||
|
||||
void addShadeAddedCallback(std::function<void(Shade&)> callback);
|
||||
void addShadeChangedCallback(std::function<void(Shade&)> callback);
|
||||
};
|
||||
|
||||
#endif // SHADE_REPOSITORY_H
|
||||
49
platformio.ini
Normal file
49
platformio.ini
Normal file
@@ -0,0 +1,49 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:release]
|
||||
platform = espressif8266
|
||||
board = nodemcuv2
|
||||
framework = arduino
|
||||
build_flags =
|
||||
-D RF_CE_PIN=5
|
||||
-D RF_CS_PIN=15
|
||||
-D RF_IRQ_PIN=4
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
|
||||
nrf24/RF24@^1.4.8
|
||||
|
||||
plapointe6/EspMQTTClient@^1.13.3
|
||||
|
||||
contrem/arduino-timer@^3.0.1
|
||||
|
||||
RFPowerView=https://git.mattway.com.au/matt/RFPowerView.git#v0.0.3
|
||||
|
||||
[env:test_embedded]
|
||||
platform = espressif8266
|
||||
board = nodemcuv2
|
||||
framework = arduino
|
||||
build_type = test
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
|
||||
[env:test_desktop]
|
||||
platform = native
|
||||
build_type = test
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ArduinoFake
|
||||
|
||||
[env]
|
||||
monitor_speed = 115200
|
||||
test_framework = unity
|
||||
lib_deps =
|
||||
ArduinoJson=https://github.com/bblanchon/ArduinoJson#v7.0.0
|
||||
1
src/.gitignore
vendored
Normal file
1
src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
secrets.h
|
||||
400
src/main.cpp
Normal file
400
src/main.cpp
Normal file
@@ -0,0 +1,400 @@
|
||||
#include <Arduino.h>
|
||||
#include <EspMqttClient.h>
|
||||
#include <arduino-timer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RFPowerView.h>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include "Shade.h"
|
||||
#include "Configurator.h"
|
||||
#include "ShadeRepository.h"
|
||||
#include "HADiscovery.h"
|
||||
#include "secrets.h"
|
||||
|
||||
#define SER_BAUDRATE (115200)
|
||||
|
||||
// Copied from Powerview Hub userdata API
|
||||
// eg: http://POWERVIEW_HUB_IP_ADDRESS/api/userdata/ and find the field labeled "rfID"
|
||||
#define RF_ID (0x2EC8)
|
||||
|
||||
RFPowerView powerView(RF_CE_PIN, RF_CS_PIN, RF_IRQ_PIN, RF_ID);
|
||||
|
||||
uint8_t lastRollingCode1 = 0x3D;
|
||||
uint8_t lastRollingCode2 = 0x96;
|
||||
|
||||
static std::mt19937 generator(std::chrono::system_clock::now().time_since_epoch().count());
|
||||
|
||||
std::string generate_client_id_suffix(int length) {
|
||||
std::uniform_int_distribution<char> distribution('0', '9');
|
||||
|
||||
std::string random_string;
|
||||
for (int i = 0; i < length; ++i) {
|
||||
random_string += distribution(generator);
|
||||
}
|
||||
return random_string;
|
||||
}
|
||||
|
||||
std::string clientID = "hotdog_" + generate_client_id_suffix(6);
|
||||
|
||||
std::string topic_prefix = "hotdog/";
|
||||
std::string last_will_topic = std::string(topic_prefix) + "availability";
|
||||
|
||||
EspMQTTClient client(
|
||||
SECRET_WIFI_SSID, // Wifi SSID
|
||||
SECRET_WIFI_PASSWORD, // Wifi Password
|
||||
SECRET_MQTT_SERVER_IP, // MQTT Broker server IP
|
||||
SECRET_MQTT_USERNAME, // MQTT Username
|
||||
SECRET_MQTT_PASSWORD, // MQTT Password
|
||||
clientID.c_str(), // MQTT Client ID
|
||||
1883
|
||||
);
|
||||
|
||||
Configurator configurator = Configurator();
|
||||
|
||||
ShadeRepository shadeRepository = ShadeRepository();
|
||||
|
||||
HADiscovery haDiscovery(topic_prefix.c_str(), [] (const char* topic, const char* message) {
|
||||
client.publish(topic, message);
|
||||
});
|
||||
|
||||
#define MAX_FETCH_COUNT (20)
|
||||
auto timer = Timer<10, millis, uint16_t>();
|
||||
|
||||
void processPacket(const Packet*);
|
||||
void processCommandMessage(const String& topic, const String &payload);
|
||||
void processSetPositionMessage(const String& topic, const String &payload);
|
||||
|
||||
bool sendOpenPacket(uint16_t destination);
|
||||
bool sendClosePacket(uint16_t destination);
|
||||
bool sendStopPacket(uint16_t destination);
|
||||
bool sendSetPosition(uint16_t destination, float percentage);
|
||||
bool sendFetchPosition(uint16_t destination);
|
||||
bool sendPacket(Packet *packet);
|
||||
|
||||
bool checkPosition(uint16_t shadeID);
|
||||
void startFetchingPosition(uint16_t shadeID, int8_t targetPosition);
|
||||
|
||||
void publishPosition(const std::string& shadeKey, const uint8_t position);
|
||||
void publishState(const std::string& shadeKey, const String& state);
|
||||
void publishBattery(const std::string& shadeKey, const uint8_t battery);
|
||||
|
||||
void publishDiscoveryTopics();
|
||||
|
||||
void setup() {
|
||||
Serial.begin(SER_BAUDRATE);
|
||||
|
||||
Serial.println("Starting up");
|
||||
|
||||
powerView.setPacketReceivedCallback(processPacket);
|
||||
if (!powerView.begin()) {
|
||||
Serial.println("Failed to start RFPowerView");
|
||||
}
|
||||
|
||||
client.setKeepAlive(10);
|
||||
client.enableLastWillMessage(last_will_topic.c_str(), "offline", true);
|
||||
client.enableDebuggingMessages();
|
||||
if (!client.setMaxPacketSize(1024)) {
|
||||
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()) {
|
||||
client.subscribe((topic_prefix + shade.key + "/command").c_str(), processCommandMessage);
|
||||
client.subscribe((topic_prefix + shade.key + "/set_position").c_str(), processSetPositionMessage);
|
||||
}
|
||||
publishDiscoveryTopics();
|
||||
});
|
||||
|
||||
shadeRepository.addShadeChangedCallback([] (Shade& shade) {
|
||||
publishDiscoveryTopics();
|
||||
});
|
||||
|
||||
delay(100);
|
||||
|
||||
Serial.println("Ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
powerView.loop();
|
||||
client.loop();
|
||||
timer.tick();
|
||||
}
|
||||
|
||||
void processPacket(const Packet *packet) {
|
||||
Serial.println("Got a packet");
|
||||
|
||||
uint16_t source = -1;
|
||||
|
||||
if (std::holds_alternative<BroadcastHeader>(packet->header)) {
|
||||
auto header = std::get<BroadcastHeader>(packet->header);
|
||||
source = header.source;
|
||||
} else if (std::holds_alternative<UnicastHeader>(packet->header)) {
|
||||
auto header = std::get<UnicastHeader>(packet->header);
|
||||
source = header.source;
|
||||
} else if (std::holds_alternative<GroupsHeader>(packet->header)) {
|
||||
auto header = std::get<GroupsHeader>(packet->header);
|
||||
source = header.source;
|
||||
} else {
|
||||
Serial.print("Cannot process packet, unknown header");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update last rolling codes each time a packet from a real hub is detected
|
||||
if (source == 0x0000) {
|
||||
Serial.println("Updating rolling codes");
|
||||
lastRollingCode1 = packet->rollingCode1;
|
||||
lastRollingCode2 = packet->rollingCode2;
|
||||
}
|
||||
|
||||
if (packet->type == PacketType::FIELDS) {
|
||||
FieldsParameters parameters = std::get<FieldsParameters>(packet->parameters);
|
||||
for (size_t i = 0; i < parameters.fields.size(); i++) {
|
||||
Field field = parameters.fields[i];
|
||||
if (field.identifier == 0x50) {
|
||||
auto shade = shadeRepository.findById(source);
|
||||
if (shade != nullptr) {
|
||||
uint16_t value = std::get<uint16_t>(field.value);
|
||||
uint8_t position = (uint8_t)std::round(((float)value / 0xFFFF) * 100);
|
||||
|
||||
if (shade->lastPosition == position) {
|
||||
shade->samePositionCount++;
|
||||
} else {
|
||||
shade->samePositionCount = 1;
|
||||
}
|
||||
shade->lastPosition = position;
|
||||
|
||||
publishPosition(shade->key, position);
|
||||
}
|
||||
} else if (field.identifier == 0x42) {
|
||||
auto shade = shadeRepository.findById(source);
|
||||
if (shade != nullptr) {
|
||||
uint8_t value = std::get<uint8_t>(field.value);
|
||||
uint8_t battery = uint8_t(((float)value / 200) * 100);
|
||||
|
||||
publishBattery(shade->key, battery);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onConnectionEstablished() {
|
||||
Serial.println("Connection established");
|
||||
|
||||
for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) {
|
||||
client.subscribe((topic_prefix + shade->key + "/command").c_str(), processCommandMessage);
|
||||
client.subscribe((topic_prefix + shade->key + "/set_position").c_str(), processSetPositionMessage);
|
||||
}
|
||||
|
||||
client.publish((topic_prefix + "availability").c_str(), "online", true);
|
||||
|
||||
publishDiscoveryTopics();
|
||||
|
||||
client.subscribe("homeassistant/status", [] (const String& topic, const String& message) {
|
||||
if (message == "online") {
|
||||
publishDiscoveryTopics();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool sendOpenPacket(uint16_t destination) {
|
||||
Packet packet;
|
||||
auto header = UnicastHeader {};
|
||||
header.destination = destination;
|
||||
header.source = 0x0000;
|
||||
packet.header = header;
|
||||
packet.type = PacketType::OPEN;
|
||||
|
||||
return sendPacket(&packet);
|
||||
}
|
||||
|
||||
bool sendClosePacket(uint16_t destination) {
|
||||
Packet packet;
|
||||
auto header = UnicastHeader {};
|
||||
header.destination = destination;
|
||||
header.source = 0x0000;
|
||||
packet.header = header;
|
||||
packet.type = PacketType::CLOSE;
|
||||
|
||||
return sendPacket(&packet);
|
||||
}
|
||||
|
||||
bool sendStopPacket(uint16_t destination) {
|
||||
Packet packet;
|
||||
auto header = UnicastHeader {};
|
||||
header.destination = destination;
|
||||
header.source = 0x0000;
|
||||
packet.header = header;
|
||||
packet.type = PacketType::STOP;
|
||||
|
||||
return sendPacket(&packet);
|
||||
}
|
||||
|
||||
bool sendSetPosition(uint16_t destination, float percentage) {
|
||||
Packet packet;
|
||||
auto header = UnicastHeader {};
|
||||
header.destination = destination;
|
||||
header.source = 0x0000;
|
||||
packet.header = header;
|
||||
packet.type = PacketType::FIELD_COMMAND;
|
||||
|
||||
std::vector<Field> fields;
|
||||
|
||||
uint8_t identifier = 0x50; // position
|
||||
FieldType type = FieldType::SET;
|
||||
uint16_t position = (uint16_t)(0xFFFF * percentage);
|
||||
fields.push_back(Field{identifier, type, true, position});
|
||||
|
||||
packet.parameters = FieldsParameters {fields};
|
||||
|
||||
return sendPacket(&packet);
|
||||
}
|
||||
|
||||
bool sendFetchPosition(uint16_t destination) {
|
||||
Packet packet;
|
||||
auto header = UnicastHeader {};
|
||||
header.destination = destination;
|
||||
header.source = 0x0000;
|
||||
packet.header = header;
|
||||
packet.type = PacketType::FIELD_COMMAND;
|
||||
|
||||
std::vector<Field> fields;
|
||||
|
||||
// position
|
||||
fields.push_back(Field{0x50, FieldType::FETCH, false, std::monostate{}});
|
||||
|
||||
// battery
|
||||
fields.push_back(Field{0x42, FieldType::FETCH, false, std::monostate{}});
|
||||
|
||||
packet.parameters = FieldsParameters {fields};
|
||||
|
||||
return sendPacket(&packet);
|
||||
}
|
||||
|
||||
bool sendPacket(Packet *packet) {
|
||||
Serial.println("Attempting to send a packet");
|
||||
packet->rollingCode1 = lastRollingCode1 + 1;
|
||||
packet->rollingCode2 = lastRollingCode2 + 1;
|
||||
|
||||
bool didSend = powerView.sendPacket(packet);
|
||||
if (!didSend) {
|
||||
Serial.println("Failed to send");
|
||||
return false;
|
||||
} else {
|
||||
lastRollingCode1++;
|
||||
lastRollingCode2++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void processCommandMessage(const String &topic, const String &payload) {
|
||||
int startIndex = topic.indexOf("/") + 1;
|
||||
int endIndex = topic.indexOf("/", startIndex);
|
||||
auto key = topic.substring(startIndex, endIndex).c_str();
|
||||
|
||||
auto shade = shadeRepository.findByKey(key);
|
||||
if (shade != nullptr) {
|
||||
if (payload == "OPEN") {
|
||||
sendOpenPacket(shade->ID);
|
||||
|
||||
startFetchingPosition(shade->ID, 100);
|
||||
|
||||
publishState(shade->key, "opening");
|
||||
} else if (payload == "CLOSE") {
|
||||
sendClosePacket(shade->ID);
|
||||
|
||||
startFetchingPosition(shade->ID, 0);
|
||||
|
||||
publishState(shade->key, "closing");
|
||||
} else if (payload == "STOP") {
|
||||
sendStopPacket(shade->ID);
|
||||
|
||||
startFetchingPosition(shade->ID, -1);
|
||||
timer.in(100, sendFetchPosition, shade->ID);
|
||||
} else if (payload == "REFRESH") {
|
||||
startFetchingPosition(shade->ID, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void processSetPositionMessage(const String& topic, const String &payload) {
|
||||
int startIndex = topic.indexOf("/") + 1;
|
||||
int endIndex = topic.indexOf("/", startIndex);
|
||||
auto key = topic.substring(startIndex, endIndex).c_str();
|
||||
|
||||
auto shade = shadeRepository.findByKey(key);
|
||||
if (shade != nullptr) {
|
||||
float percentage = payload.toInt() / 100.0f;
|
||||
sendSetPosition(shade->ID, percentage);
|
||||
|
||||
if (payload.toInt() > shade->lastPosition) {
|
||||
publishState(shade->key, "opening");
|
||||
} else if (payload.toInt() < shade->lastPosition) {
|
||||
publishState(shade->key, "closing");
|
||||
}
|
||||
|
||||
startFetchingPosition(shade->ID, payload.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
bool checkPosition(uint16_t shadeID) {
|
||||
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(shade->key, shade->lastPosition > 0 ? "open" : "closed");
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void startFetchingPosition(uint16_t shadeID, int8_t targetPosition) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void publishState(const std::string& shadeKey, const String& state) {
|
||||
client.publish((topic_prefix + shadeKey + "/state").c_str(), state, true);
|
||||
}
|
||||
|
||||
void publishPosition(const std::string& shadeKey, const uint8_t position) {
|
||||
client.publish((topic_prefix + shadeKey + "/position").c_str(), String(position), true);
|
||||
}
|
||||
|
||||
void publishBattery(const std::string& shadeKey, const uint8_t battery) {
|
||||
client.publish((topic_prefix + shadeKey + "/battery").c_str(), String(battery), true);
|
||||
}
|
||||
|
||||
void publishDiscoveryTopics() {
|
||||
for (auto shade = shadeRepository.begin(); shade != shadeRepository.end(); shade++) {
|
||||
haDiscovery.publish(*shade);
|
||||
}
|
||||
}
|
||||
11
test/README
Normal file
11
test/README
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
158
test/test_shade_repository/test_shade_repository.cpp
Normal file
158
test/test_shade_repository/test_shade_repository.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#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();
|
||||
}
|
||||
Reference in New Issue
Block a user