mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
test: Home Assistant discovery config on the PC
The config a shade publishes for MQTT discovery moves to MqttDisco, with 11 unit tests: the topics, the device, which way each kind of shade opens, tilt, tilt only and dry contacts. A deleted group's retained sunSensor topic was never cleared: unpublish named it SunSensor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8c59cf4903
commit
64f166490f
4 changed files with 326 additions and 128 deletions
91
src/MqttDisco.cpp
Normal file
91
src/MqttDisco.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#include "MqttDisco.h"
|
||||||
|
|
||||||
|
bool isDiscoSwitch(shade_types type) { return type == shade_types::drycontact || type == shade_types::drycontact2; }
|
||||||
|
void shadeDiscoTopic(const char *discoTopic, uint8_t shadeId, bool isSwitch, char *topic, size_t len) {
|
||||||
|
snprintf(topic, len, "%s/%s/%d/config", discoTopic, isSwitch ? "switch" : "cover", shadeId);
|
||||||
|
}
|
||||||
|
static const char *deviceClass(shade_types type) {
|
||||||
|
switch(type) {
|
||||||
|
case shade_types::blind:
|
||||||
|
return "blind";
|
||||||
|
case shade_types::lgate:
|
||||||
|
case shade_types::cgate:
|
||||||
|
case shade_types::rgate:
|
||||||
|
case shade_types::lgate1:
|
||||||
|
case shade_types::cgate1:
|
||||||
|
case shade_types::rgate1:
|
||||||
|
case shade_types::ldrapery:
|
||||||
|
case shade_types::rdrapery:
|
||||||
|
case shade_types::cdrapery:
|
||||||
|
return "curtain";
|
||||||
|
case shade_types::garage1:
|
||||||
|
case shade_types::garage3:
|
||||||
|
return "garage";
|
||||||
|
case shade_types::awning:
|
||||||
|
return "awning";
|
||||||
|
case shade_types::shutter:
|
||||||
|
return "shutter";
|
||||||
|
default:
|
||||||
|
return "shade";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void shadeDisco(const shade_disco_t &shade, JsonObject obj, char *topic, size_t len) {
|
||||||
|
snprintf(topic, len, "%s/shades/%d", shade.rootTopic, shade.shadeId);
|
||||||
|
obj["~"] = topic;
|
||||||
|
JsonObject dobj = obj["device"].to<JsonObject>();
|
||||||
|
dobj["hw_version"] = shade.fwVersion;
|
||||||
|
dobj["name"] = shade.hostname;
|
||||||
|
dobj["mf"] = "rstrouse";
|
||||||
|
JsonArray arrids = dobj["identifiers"].to<JsonArray>();
|
||||||
|
snprintf(topic, len, "mqtt_espsomfyrts_%s", shade.serverId);
|
||||||
|
arrids.add(topic);
|
||||||
|
dobj["model"] = "ESPSomfy-RTS MQTT";
|
||||||
|
snprintf(topic, len, "%s/status", shade.rootTopic);
|
||||||
|
obj["availability_topic"] = topic;
|
||||||
|
obj["payload_available"] = "online";
|
||||||
|
obj["payload_not_available"] = "offline";
|
||||||
|
obj["name"] = shade.name;
|
||||||
|
snprintf(topic, len, "mqtt_%s_shade%d", shade.serverId, shade.shadeId);
|
||||||
|
obj["unique_id"] = topic;
|
||||||
|
const bool isSwitch = isDiscoSwitch(shade.shadeType);
|
||||||
|
if(!isSwitch) {
|
||||||
|
obj["device_class"] = deviceClass(shade.shadeType);
|
||||||
|
// Positions count down from open: 0 is open and 100 closed, so an
|
||||||
|
// upward move (-1) opens. An awning, which opens downward, and a
|
||||||
|
// flipped position turn that around.
|
||||||
|
const bool reversed = shade.flipPosition != (shade.shadeType == shade_types::awning);
|
||||||
|
obj["payload_close"] = reversed ? "-1" : "1";
|
||||||
|
obj["payload_open"] = reversed ? "1" : "-1";
|
||||||
|
obj["position_open"] = reversed ? 100 : 0;
|
||||||
|
obj["position_closed"] = reversed ? 0 : 100;
|
||||||
|
obj["state_closing"] = reversed ? "-1" : "1";
|
||||||
|
obj["state_opening"] = reversed ? "1" : "-1";
|
||||||
|
if(shade.tiltType != tilt_types::tiltonly) {
|
||||||
|
obj["command_topic"] = "~/direction/set";
|
||||||
|
obj["position_topic"] = "~/position";
|
||||||
|
obj["set_position_topic"] = "~/target/set";
|
||||||
|
obj["state_topic"] = "~/direction";
|
||||||
|
obj["payload_stop"] = "0";
|
||||||
|
obj["state_stopped"] = "0";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
obj["payload_close"] = nullptr;
|
||||||
|
obj["payload_open"] = nullptr;
|
||||||
|
obj["payload_stop"] = nullptr;
|
||||||
|
}
|
||||||
|
if(shade.tiltType != tilt_types::none) {
|
||||||
|
obj["tilt_command_topic"] = "~/tiltTarget/set";
|
||||||
|
obj["tilt_status_topic"] = "~/tiltPosition";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
obj["payload_on"] = 100;
|
||||||
|
obj["payload_off"] = 0;
|
||||||
|
obj["state_off"] = 0;
|
||||||
|
obj["state_on"] = 100;
|
||||||
|
obj["state_topic"] = "~/position";
|
||||||
|
obj["command_topic"] = "~/target/set";
|
||||||
|
}
|
||||||
|
obj["enabled_by_default"] = true;
|
||||||
|
shadeDiscoTopic(shade.discoTopic, shade.shadeId, isSwitch, topic, len);
|
||||||
|
}
|
||||||
29
src/MqttDisco.h
Normal file
29
src/MqttDisco.h
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef MQTT_DISCO_H
|
||||||
|
#define MQTT_DISCO_H
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "ShadeTypes.h"
|
||||||
|
|
||||||
|
// What Home Assistant's MQTT discovery needs to know about a shade. Kept
|
||||||
|
// free of the shade model and MQTT so the config it gets can be unit tested
|
||||||
|
// on the PC (pio test -e native).
|
||||||
|
struct shade_disco_t {
|
||||||
|
uint8_t shadeId = 0;
|
||||||
|
const char *name = "";
|
||||||
|
shade_types shadeType = shade_types::roller;
|
||||||
|
tilt_types tiltType = tilt_types::none;
|
||||||
|
bool flipPosition = false;
|
||||||
|
// From the MQTT and general settings.
|
||||||
|
const char *rootTopic = "";
|
||||||
|
const char *discoTopic = "";
|
||||||
|
const char *serverId = "";
|
||||||
|
const char *hostname = "";
|
||||||
|
const char *fwVersion = "";
|
||||||
|
};
|
||||||
|
// Dry contacts are switches, the rest covers.
|
||||||
|
bool isDiscoSwitch(shade_types type);
|
||||||
|
// Where the shade's config goes: <discoTopic>/cover/<id>/config or
|
||||||
|
// <discoTopic>/switch/<id>/config.
|
||||||
|
void shadeDiscoTopic(const char *discoTopic, uint8_t shadeId, bool isSwitch, char *topic, size_t len);
|
||||||
|
// Fills obj with the shade's config and topic with where it goes.
|
||||||
|
void shadeDisco(const shade_disco_t &shade, JsonObject obj, char *topic, size_t len);
|
||||||
|
#endif
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
#include "SomfySnapshot.h"
|
#include "SomfySnapshot.h"
|
||||||
#include "MqttConnection.h"
|
#include "MqttConnection.h"
|
||||||
|
#include "MqttDisco.h"
|
||||||
|
|
||||||
extern SomfyShadeController somfy;
|
extern SomfyShadeController somfy;
|
||||||
extern ConfigSettings settings;
|
extern ConfigSettings settings;
|
||||||
|
|
@ -48,140 +49,30 @@ void SomfyShade::publishState() {
|
||||||
snapshotShade(*this, s);
|
snapshotShade(*this, s);
|
||||||
publishShadeTopics(shadeStateTopics(s), s, publishShadeTopic, this);
|
publishShadeTopics(shadeStateTopics(s), s, publishShadeTopic, this);
|
||||||
}
|
}
|
||||||
|
// The shade's config for Home Assistant's MQTT discovery.
|
||||||
void SomfyShade::publishDisco() {
|
void SomfyShade::publishDisco() {
|
||||||
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
||||||
|
shade_disco_t disco;
|
||||||
|
disco.shadeId = this->shadeId;
|
||||||
|
disco.name = this->name;
|
||||||
|
disco.shadeType = this->shadeType;
|
||||||
|
disco.tiltType = this->tiltType;
|
||||||
|
disco.flipPosition = this->flipPosition;
|
||||||
|
disco.rootTopic = settings.MQTT.rootTopic;
|
||||||
|
disco.discoTopic = settings.MQTT.discoTopic;
|
||||||
|
disco.serverId = settings.serverId;
|
||||||
|
disco.hostname = settings.hostname;
|
||||||
|
disco.fwVersion = settings.fwVersion.name;
|
||||||
char topic[128] = "";
|
char topic[128] = "";
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
JsonObject obj = doc.to<JsonObject>();
|
JsonObject obj = doc.to<JsonObject>();
|
||||||
snprintf(topic, sizeof(topic), "%s/shades/%d", settings.MQTT.rootTopic, this->shadeId);
|
shadeDisco(disco, obj, topic, sizeof(topic));
|
||||||
obj["~"] = topic;
|
|
||||||
JsonObject dobj = obj.createNestedObject("device");
|
|
||||||
dobj["hw_version"] = settings.fwVersion.name;
|
|
||||||
dobj["name"] = settings.hostname;
|
|
||||||
dobj["mf"] = "rstrouse";
|
|
||||||
JsonArray arrids = dobj.createNestedArray("identifiers");
|
|
||||||
//snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s_shade%d", settings.serverId, this->shadeId);
|
|
||||||
snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s", settings.serverId);
|
|
||||||
arrids.add(topic);
|
|
||||||
//snprintf(topic, sizeof(topic), "ESPSomfy-RTS_%s", settings.serverId);
|
|
||||||
dobj["model"] = "ESPSomfy-RTS MQTT";
|
|
||||||
snprintf(topic, sizeof(topic), "%s/status", settings.MQTT.rootTopic);
|
|
||||||
obj["availability_topic"] = topic;
|
|
||||||
obj["payload_available"] = "online";
|
|
||||||
obj["payload_not_available"] = "offline";
|
|
||||||
obj["name"] = this->name;
|
|
||||||
snprintf(topic, sizeof(topic), "mqtt_%s_shade%d", settings.serverId, this->shadeId);
|
|
||||||
obj["unique_id"] = topic;
|
|
||||||
switch(this->shadeType) {
|
|
||||||
case shade_types::blind:
|
|
||||||
obj["device_class"] = "blind";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
|
||||||
break;
|
|
||||||
case shade_types::lgate:
|
|
||||||
case shade_types::cgate:
|
|
||||||
case shade_types::rgate:
|
|
||||||
case shade_types::lgate1:
|
|
||||||
case shade_types::cgate1:
|
|
||||||
case shade_types::rgate1:
|
|
||||||
case shade_types::ldrapery:
|
|
||||||
case shade_types::rdrapery:
|
|
||||||
case shade_types::cdrapery:
|
|
||||||
obj["device_class"] = "curtain";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
|
||||||
break;
|
|
||||||
case shade_types::garage1:
|
|
||||||
case shade_types::garage3:
|
|
||||||
obj["device_class"] = "garage";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
|
||||||
break;
|
|
||||||
case shade_types::awning:
|
|
||||||
obj["device_class"] = "awning";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "-1" : "1";
|
|
||||||
break;
|
|
||||||
case shade_types::shutter:
|
|
||||||
obj["device_class"] = "shutter";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
|
||||||
break;
|
|
||||||
case shade_types::drycontact2:
|
|
||||||
case shade_types::drycontact:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
obj["device_class"] = "shade";
|
|
||||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
|
||||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
|
||||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
|
||||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
|
||||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::drycontact2) {
|
|
||||||
if(this->tiltType != tilt_types::tiltonly) {
|
|
||||||
obj["command_topic"] = "~/direction/set";
|
|
||||||
obj["position_topic"] = "~/position";
|
|
||||||
obj["set_position_topic"] = "~/target/set";
|
|
||||||
obj["state_topic"] = "~/direction";
|
|
||||||
obj["payload_stop"] = "0";
|
|
||||||
obj["state_stopped"] = "0";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
obj["payload_close"] = nullptr;
|
|
||||||
obj["payload_open"] = nullptr;
|
|
||||||
obj["payload_stop"] = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this->tiltType != tilt_types::none) {
|
|
||||||
obj["tilt_command_topic"] = "~/tiltTarget/set";
|
|
||||||
obj["tilt_status_topic"] = "~/tiltPosition";
|
|
||||||
}
|
|
||||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
obj["payload_on"] = 100;
|
|
||||||
obj["payload_off"] = 0;
|
|
||||||
obj["state_off"] = 0;
|
|
||||||
obj["state_on"] = 100;
|
|
||||||
obj["state_topic"] = "~/position";
|
|
||||||
obj["command_topic"] = "~/target/set";
|
|
||||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
obj["enabled_by_default"] = true;
|
|
||||||
mqtt.publishDisco(topic, obj, true);
|
mqtt.publishDisco(topic, obj, true);
|
||||||
}
|
}
|
||||||
void SomfyShade::unpublishDisco() {
|
void SomfyShade::unpublishDisco() {
|
||||||
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
||||||
char topic[128] = "";
|
char topic[128] = "";
|
||||||
if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::drycontact2) {
|
shadeDiscoTopic(settings.MQTT.discoTopic, this->shadeId, isDiscoSwitch(this->shadeType), topic, sizeof(topic));
|
||||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
|
||||||
mqtt.unpublish(topic);
|
mqtt.unpublish(topic);
|
||||||
}
|
}
|
||||||
void SomfyShade::publish() {
|
void SomfyShade::publish() {
|
||||||
|
|
@ -248,9 +139,9 @@ void SomfyShade::unpublish(uint8_t id) {
|
||||||
SomfyShade::unpublish(id, "sunny");
|
SomfyShade::unpublish(id, "sunny");
|
||||||
if(settings.MQTT.pubDisco) {
|
if(settings.MQTT.pubDisco) {
|
||||||
char topic[128] = "";
|
char topic[128] = "";
|
||||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, id);
|
shadeDiscoTopic(settings.MQTT.discoTopic, id, false, topic, sizeof(topic));
|
||||||
mqtt.unpublish(topic);
|
mqtt.unpublish(topic);
|
||||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, id);
|
shadeDiscoTopic(settings.MQTT.discoTopic, id, true, topic, sizeof(topic));
|
||||||
mqtt.unpublish(topic);
|
mqtt.unpublish(topic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +155,7 @@ void SomfyGroup::unpublish(uint8_t id) {
|
||||||
SomfyGroup::unpublish(id, "direction");
|
SomfyGroup::unpublish(id, "direction");
|
||||||
SomfyGroup::unpublish(id, "lastRollingCode");
|
SomfyGroup::unpublish(id, "lastRollingCode");
|
||||||
SomfyGroup::unpublish(id, "flags");
|
SomfyGroup::unpublish(id, "flags");
|
||||||
SomfyGroup::unpublish(id, "SunSensor");
|
SomfyGroup::unpublish(id, "sunSensor");
|
||||||
SomfyGroup::unpublish(id, "flipCommands");
|
SomfyGroup::unpublish(id, "flipCommands");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
187
test/test_mqttdisco/test_mqttdisco.cpp
Normal file
187
test/test_mqttdisco/test_mqttdisco.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#include <unity.h>
|
||||||
|
#include "MqttDisco.h"
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
static shade_disco_t shade(shade_types type = shade_types::roller) {
|
||||||
|
shade_disco_t s;
|
||||||
|
s.shadeId = 3;
|
||||||
|
s.name = "Kitchen";
|
||||||
|
s.shadeType = type;
|
||||||
|
s.rootTopic = "somfy";
|
||||||
|
s.discoTopic = "homeassistant";
|
||||||
|
s.serverId = "abc123";
|
||||||
|
s.hostname = "ESPSomfyRTS";
|
||||||
|
s.fwVersion = "v4.0.0";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// The config Home Assistant gets and the topic it goes to.
|
||||||
|
struct disco_t {
|
||||||
|
JsonDocument doc;
|
||||||
|
char topic[128] = "";
|
||||||
|
};
|
||||||
|
static void build(const shade_disco_t &s, disco_t &d) {
|
||||||
|
shadeDisco(s, d.doc.to<JsonObject>(), d.topic, sizeof(d.topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_a_roller_is_a_cover_under_the_discovery_topic() {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("homeassistant/cover/3/config", d.topic);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("somfy/shades/3", d.doc["~"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Kitchen", d.doc["name"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("mqtt_abc123_shade3", d.doc["unique_id"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("shade", d.doc["device_class"]);
|
||||||
|
TEST_ASSERT_TRUE(d.doc["enabled_by_default"].as<bool>());
|
||||||
|
}
|
||||||
|
// Every shade of a device is grouped under it in Home Assistant.
|
||||||
|
static void test_the_device_is_the_esp() {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(), d);
|
||||||
|
JsonObject device = d.doc["device"];
|
||||||
|
TEST_ASSERT_EQUAL_STRING("mqtt_espsomfyrts_abc123", device["identifiers"][0]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ESPSomfyRTS", device["name"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("v4.0.0", device["hw_version"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ESPSomfy-RTS MQTT", device["model"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("somfy/status", d.doc["availability_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("online", d.doc["payload_available"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("offline", d.doc["payload_not_available"]);
|
||||||
|
}
|
||||||
|
// The topics under ~ are the ones MqttCommands listens on and the shade
|
||||||
|
// publishes.
|
||||||
|
static void test_a_cover_moves_through_the_shade_topics() {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/direction/set", d.doc["command_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/target/set", d.doc["set_position_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/position", d.doc["position_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/direction", d.doc["state_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("0", d.doc["payload_stop"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("0", d.doc["state_stopped"]);
|
||||||
|
TEST_ASSERT_FALSE(d.doc["tilt_command_topic"].is<const char *>());
|
||||||
|
}
|
||||||
|
// 0 is open: an upward move opens.
|
||||||
|
static void test_a_roller_opens_up() {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", d.doc["payload_open"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_close"]);
|
||||||
|
TEST_ASSERT_EQUAL(0, d.doc["position_open"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL(100, d.doc["position_closed"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", d.doc["state_opening"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", d.doc["state_closing"]);
|
||||||
|
}
|
||||||
|
static void test_a_flipped_position_opens_at_100() {
|
||||||
|
shade_disco_t s = shade();
|
||||||
|
s.flipPosition = true;
|
||||||
|
disco_t d;
|
||||||
|
build(s, d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_open"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", d.doc["payload_close"]);
|
||||||
|
TEST_ASSERT_EQUAL(100, d.doc["position_open"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL(0, d.doc["position_closed"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", d.doc["state_opening"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", d.doc["state_closing"]);
|
||||||
|
}
|
||||||
|
// An awning opens downward, and flipped opens up again.
|
||||||
|
static void test_an_awning_opens_down() {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(shade_types::awning), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("awning", d.doc["device_class"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_open"]);
|
||||||
|
TEST_ASSERT_EQUAL(100, d.doc["position_open"].as<int>());
|
||||||
|
shade_disco_t s = shade(shade_types::awning);
|
||||||
|
s.flipPosition = true;
|
||||||
|
disco_t flipped;
|
||||||
|
build(s, flipped);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", flipped.doc["payload_open"]);
|
||||||
|
TEST_ASSERT_EQUAL(0, flipped.doc["position_open"].as<int>());
|
||||||
|
}
|
||||||
|
static void test_each_kind_of_shade_has_its_device_class() {
|
||||||
|
const struct { shade_types type; const char *deviceClass; } kinds[] = {
|
||||||
|
{shade_types::roller, "shade"},
|
||||||
|
{shade_types::blind, "blind"},
|
||||||
|
{shade_types::ldrapery, "curtain"},
|
||||||
|
{shade_types::rdrapery, "curtain"},
|
||||||
|
{shade_types::cdrapery, "curtain"},
|
||||||
|
{shade_types::lgate, "curtain"},
|
||||||
|
{shade_types::cgate1, "curtain"},
|
||||||
|
{shade_types::garage1, "garage"},
|
||||||
|
{shade_types::garage3, "garage"},
|
||||||
|
{shade_types::shutter, "shutter"},
|
||||||
|
{shade_types::awning, "awning"},
|
||||||
|
};
|
||||||
|
for(const auto &kind : kinds) {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(kind.type), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(kind.deviceClass, d.doc["device_class"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void test_a_tilting_blind_has_the_tilt_topics() {
|
||||||
|
shade_disco_t s = shade(shade_types::blind);
|
||||||
|
s.tiltType = tilt_types::tiltmotor;
|
||||||
|
disco_t d;
|
||||||
|
build(s, d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/tiltTarget/set", d.doc["tilt_command_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/tiltPosition", d.doc["tilt_status_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/direction/set", d.doc["command_topic"]);
|
||||||
|
}
|
||||||
|
// A tilt only blind doesn't open or close, so it has only the tilt.
|
||||||
|
static void test_a_tilt_only_blind_has_only_the_tilt() {
|
||||||
|
shade_disco_t s = shade(shade_types::blind);
|
||||||
|
s.tiltType = tilt_types::tiltonly;
|
||||||
|
disco_t d;
|
||||||
|
build(s, d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/tiltTarget/set", d.doc["tilt_command_topic"]);
|
||||||
|
TEST_ASSERT_FALSE(d.doc["command_topic"].is<const char *>());
|
||||||
|
TEST_ASSERT_FALSE(d.doc["set_position_topic"].is<const char *>());
|
||||||
|
// Sent as null so Home Assistant drops its defaults.
|
||||||
|
std::string json;
|
||||||
|
serializeJson(d.doc, json);
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_open\":null"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_close\":null"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_stop\":null"));
|
||||||
|
}
|
||||||
|
// A dry contact is on (100) or off (0).
|
||||||
|
static void test_a_dry_contact_is_a_switch() {
|
||||||
|
for(shade_types type : {shade_types::drycontact, shade_types::drycontact2}) {
|
||||||
|
disco_t d;
|
||||||
|
build(shade(type), d);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("homeassistant/switch/3/config", d.topic);
|
||||||
|
TEST_ASSERT_FALSE(d.doc["device_class"].is<const char *>());
|
||||||
|
TEST_ASSERT_EQUAL(100, d.doc["payload_on"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL(0, d.doc["payload_off"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL(100, d.doc["state_on"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL(0, d.doc["state_off"].as<int>());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/position", d.doc["state_topic"]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("~/target/set", d.doc["command_topic"]);
|
||||||
|
TEST_ASSERT_FALSE(d.doc["payload_open"].is<const char *>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deleting a shade clears both, as its kind may have changed.
|
||||||
|
static void test_the_config_topic_of_each_kind() {
|
||||||
|
char topic[64];
|
||||||
|
shadeDiscoTopic("ha", 12, false, topic, sizeof(topic));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ha/cover/12/config", topic);
|
||||||
|
shadeDiscoTopic("ha", 12, true, topic, sizeof(topic));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ha/switch/12/config", topic);
|
||||||
|
TEST_ASSERT_TRUE(isDiscoSwitch(shade_types::drycontact2));
|
||||||
|
TEST_ASSERT_FALSE(isDiscoSwitch(shade_types::garage1));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_a_roller_is_a_cover_under_the_discovery_topic);
|
||||||
|
RUN_TEST(test_the_device_is_the_esp);
|
||||||
|
RUN_TEST(test_a_cover_moves_through_the_shade_topics);
|
||||||
|
RUN_TEST(test_a_roller_opens_up);
|
||||||
|
RUN_TEST(test_a_flipped_position_opens_at_100);
|
||||||
|
RUN_TEST(test_an_awning_opens_down);
|
||||||
|
RUN_TEST(test_each_kind_of_shade_has_its_device_class);
|
||||||
|
RUN_TEST(test_a_tilting_blind_has_the_tilt_topics);
|
||||||
|
RUN_TEST(test_a_tilt_only_blind_has_only_the_tilt);
|
||||||
|
RUN_TEST(test_a_dry_contact_is_a_switch);
|
||||||
|
RUN_TEST(test_the_config_topic_of_each_kind);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue