Merge pull request #43 from cjkas/refactor/change-fanout

refactor: send shade and group changes once per loop() pass
This commit is contained in:
cjkas 2026-10-16 14:40:19 +02:00 • committed by GitHub
commit e516a4138c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 268 additions and 51 deletions

View file

@ -1,6 +1,6 @@
# Plan: domain modules behind the LoopQueue
**Status:** steps 0–3 built (PRs #39, #40, #41 and #42); steps 4 and 5 planned.
**Status:** steps 0–4 built (PRs #39 to #43); step 5 planned.
**Written:** 2026-10-15, against commit `e624a41` on `refactor/entry-points-outputs`. Terms are defined in [GLOSSARY.md](../GLOSSARY.md); the threading rule is [ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
## Why
@ -158,6 +158,16 @@ Flash: `esp32devdbg` 1,547,381 → 1,547,821 bytes, `esp32dev` 1,398,337 → 1,4
Flash: `esp32devdbg` 1,547,821 → 1,536,337 bytes, `esp32dev` 1,400,401 → 1,393,221 bytes. The 19 handler bodies became one path.
### Step 4: what changed in behaviour
- **MQTT gets the shade's real `position` and `target`.** Both were a constant 50 since `c94819e`, and `direction` was a constant 0 when MQTT connected.
- **Each pass sends a changed shade or group once.** The `p_*` setters and `emitState()` only mark the shade (`SHADE_CHANGED_*` bits in `ShadeChanges.h`). At the end of each `loop()` pass, `SomfyShadeController::sendChanges()` publishes the changed MQTT topics and sends one `shadeState` or `groupState`.
- A group whose flags change now sends one `groupState`, not two.
- `emitState(evt)` with an event name (`shadeAdded`, ...) and `emitState(num)` for one client still go out at once.
- **`position` and `tiltPosition` are retained by the broker** when they change, as they already were when MQTT connected. A client that subscribes later gets the current position.
- **Topic values come from one tested place.** `publishShadeTopics()` writes the topics from a shade snapshot, and `publishState()` (all topics, on connect) uses it too.
8 new native tests, 327 in all. Flash: `esp32devdbg` 1,536,337 → 1,536,533 bytes, `esp32dev` 1,393,221 → 1,393,449 bytes.
### Step 0 on a device
NVS stays at `0x9000`, so the rolling codes survive. The file system partition shrinks from `0xE0000` to `0xD0000`, and the existing LittleFS won't mount, because `LittleFS.begin()` doesn't format. So:
1. Take a backup through `/backup`.
@ -174,7 +184,6 @@ NVS stays at `0x9000`, so the rolling codes survive. The file system partition s
## Found during the review, not part of this plan
Still open, and each can be fixed on its own:
- MQTT `position`/`target` published as a constant 50 ([SomfyShade.cpp:305](../src/SomfyShade.cpp#L305), [SomfyMqtt.cpp:36-38](../src/outputs/SomfyMqtt.cpp#L36-L38)). It's a one-line fix that needn't wait for step 4.
- A restore without shades skips the shade and group records but not the room records, so it reads the rest of the file from the wrong place ([ConfigFile.cpp:578-580](../src/ConfigFile.cpp#L578-L580)).
- `settingPos` is never cleared when the lift arrives, while `settingTiltPos` is. A later arrival may send an extra stop. Not confirmed on a device.
- The Home Assistant integration sends no `apikey` header on shade, group and tilt commands.
@ -187,3 +196,7 @@ Fixed by step 3:
- `/linkRemote` and `/unlinkRemote` sent a 500 and then a second response.
- The web UI's `procRoomRemoved` kept the removed room and dropped the others, and `deleteShade` never closed its prompt (`prompt.remove;`).
- The web UI called `/repeaters`, which didn't exist.
Fixed by step 4:
- MQTT `position`/`target` were published as a constant 50, and `direction` as 0 on connect.
- A group whose flags changed sent `groupState` twice.

View file

@ -113,7 +113,7 @@ lib_deps =
platform = native
test_framework = unity
test_build_src = yes
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ModelSnapshot.cpp> +<ModelEditor.cpp>
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ModelSnapshot.cpp> +<ModelEditor.cpp> +<ShadeChanges.cpp>
build_flags =
-std=gnu++17
-Isrc

28
src/ShadeChanges.cpp Normal file
View file

@ -0,0 +1,28 @@
#include "ShadeChanges.h"
static bool flag(const shade_snapshot_t &shade, somfy_flags_t f) { return (shade.flags & static_cast<uint8_t>(f)) != 0; }
void publishShadeTopics(uint16_t changed, const shade_snapshot_t &shade, topic_sink_t sink, void *ctx) {
// Where the shade is and is going are kept by the broker so a client that
// connects later gets them; the rest are passing news.
if(changed & SHADE_CHANGED_POSITION) sink("position", shade.position, true, ctx);
if(changed & SHADE_CHANGED_DIRECTION) sink("direction", shade.direction, true, ctx);
if(changed & SHADE_CHANGED_TARGET) sink("target", shade.target, true, ctx);
if(changed & SHADE_CHANGED_ROLLING_CODE) sink("lastRollingCode", shade.lastRollingCode, false, ctx);
if(changed & SHADE_CHANGED_MY_POS) sink("mypos", shade.myPos, true, ctx);
if(changed & SHADE_CHANGED_MY_TILT_POS) sink("myTiltPos", shade.myTiltPos, true, ctx);
if(changed & SHADE_CHANGED_TILT_DIRECTION) sink("tiltDirection", shade.tiltDirection, true, ctx);
if(changed & SHADE_CHANGED_TILT_POSITION) sink("tiltPosition", shade.tiltPosition, true, ctx);
if(changed & SHADE_CHANGED_TILT_TARGET) sink("tiltTarget", shade.tiltTarget, true, ctx);
if(changed & SHADE_CHANGED_SUN_FLAG) sink("sunFlag", flag(shade, somfy_flags_t::SunFlag), false, ctx);
if(changed & SHADE_CHANGED_SUNNY) sink("sunny", flag(shade, somfy_flags_t::Sunny), false, ctx);
if(changed & SHADE_CHANGED_WINDY) sink("windy", flag(shade, somfy_flags_t::Windy), false, ctx);
}
uint16_t shadeStateTopics(const shade_snapshot_t &shade) {
uint16_t topics = SHADE_CHANGED_POSITION | SHADE_CHANGED_DIRECTION | SHADE_CHANGED_TARGET | SHADE_CHANGED_ROLLING_CODE |
SHADE_CHANGED_MY_POS | SHADE_CHANGED_MY_TILT_POS | SHADE_CHANGED_WINDY;
if(shade.tiltType != static_cast<uint8_t>(tilt_types::none))
topics |= SHADE_CHANGED_TILT_DIRECTION | SHADE_CHANGED_TILT_POSITION | SHADE_CHANGED_TILT_TARGET;
if(shade.sunSensor) topics |= SHADE_CHANGED_SUN_FLAG | SHADE_CHANGED_SUNNY;
return topics;
}

44
src/ShadeChanges.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef SHADE_CHANGES_H
#define SHADE_CHANGES_H
#include <stdint.h>
#include "ModelSnapshot.h"
// What changed about a shade since the change fan-out last sent it out, as
// bits. The shade's setters and emitState() set them; once per loop() pass the
// fan-out sends one shadeState for the shade, publishes the MQTT topics whose
// bits are set, and clears them. Kept free of the shade model and MQTT so it
// can be unit tested on the PC (pio test -e native).
enum : uint16_t {
SHADE_CHANGED_POSITION = 0x0001,
SHADE_CHANGED_TILT_POSITION = 0x0002,
SHADE_CHANGED_DIRECTION = 0x0004,
SHADE_CHANGED_TILT_DIRECTION = 0x0008,
SHADE_CHANGED_TARGET = 0x0010,
SHADE_CHANGED_TILT_TARGET = 0x0020,
SHADE_CHANGED_MY_POS = 0x0040,
SHADE_CHANGED_MY_TILT_POS = 0x0080,
SHADE_CHANGED_ROLLING_CODE = 0x0100,
SHADE_CHANGED_SUN_FLAG = 0x0200,
SHADE_CHANGED_SUNNY = 0x0400,
SHADE_CHANGED_WINDY = 0x0800,
// Something only the socket's shadeState carries changed.
SHADE_CHANGED_STATE = 0x1000
};
// The bits that have an MQTT topic.
#define SHADE_CHANGED_TOPICS 0x0FFF
// The same for a group: its direction topic, and groupState for the socket.
enum : uint8_t {
GROUP_CHANGED_DIRECTION = 0x01,
GROUP_CHANGED_STATE = 0x02
};
// Publishes one MQTT topic: its name under shades/<id>/, its value and whether
// the broker keeps it.
typedef void (*topic_sink_t)(const char *topic, int32_t value, bool retain, void *ctx);
// Publishes the topics of the changed bits with the shade's values, positions
// as clients see them.
void publishShadeTopics(uint16_t changed, const shade_snapshot_t &shade, topic_sink_t sink, void *ctx);
// The topics a shade publishes in full when MQTT connects: the tilt ones only
// with a tilt, the sun ones only with a sun sensor.
uint16_t shadeStateTopics(const shade_snapshot_t &shade);
#endif

View file

@ -35,6 +35,17 @@ enum class shade_types : byte {
cgate1 = 0x0F,
rgate1 = 0x10
};
// The bits of a shade's or group's flags.
enum class somfy_flags_t : byte {
SunFlag = 0x01,
SunSensor = 0x02,
DemoMode = 0x04,
Light = 0x08,
Windy = 0x10,
Sunny = 0x20,
Lighted = 0x40,
SimMy = 0x80
};
enum class tilt_types : byte {
none = 0x00,
tiltmotor = 0x01,

View file

@ -10,18 +10,9 @@
#include "ShadeTypes.h"
#include "ShadeMotion.h"
#include "ShadeSensors.h"
#include "ShadeChanges.h"
#include "Transceiver.h"
enum class somfy_flags_t : byte {
SunFlag = 0x01,
SunSensor = 0x02,
DemoMode = 0x04,
Light = 0x08,
Windy = 0x10,
Sunny = 0x20,
Lighted = 0x40,
SimMy = 0x80
};
enum class gpio_flags_t : byte {
LowLevelTrigger = 0x01
};
@ -180,8 +171,14 @@ class SomfyShade : public SomfyRemote {
bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0);
bool unlinkRemote(uint32_t remoteAddress);
void emitState(const char *evt = "shadeState");
// Marks the shade as changed; the change fan-out sends shadeState once per
// loop() pass.
void emitState();
// Sends evt about the shade to every client now.
void emitState(const char *evt);
void emitState(uint8_t num, const char *evt = "shadeState");
// What changed since the fan-out last sent the shade (SHADE_CHANGED_* bits).
uint16_t changed = 0;
void emitCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
void emitCommand(uint8_t num, somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
// False when the radio refused a command it sent.
@ -193,6 +190,8 @@ class SomfyShade : public SomfyRemote {
static void unpublish(uint8_t id);
static void unpublish(uint8_t id, const char *topic);
void publishState();
// Publishes the MQTT topics of the SHADE_CHANGED_* bits in changed.
void publishTopics(uint16_t changed);
void commit();
void commitShadePosition();
void commitTiltPosition();
@ -250,8 +249,14 @@ class SomfyGroup : public SomfyRemote {
static void unpublish(uint8_t id, const char *topic);
void publishState();
void updateFlags();
void emitState(const char *evt = "groupState");
// Marks the group as changed; the change fan-out sends groupState once per
// loop() pass.
void emitState();
// Sends evt about the group to every client now.
void emitState(const char *evt);
void emitState(uint8_t num, const char *evt = "groupState");
// What changed since the fan-out last sent the group (GROUP_CHANGED_* bits).
uint8_t changed = 0;
bool sendCommand(somfy_commands cmd);
bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
int8_t p_direction(int8_t dir);
@ -307,6 +312,9 @@ class SomfyShadeController {
void markTxPending(uint32_t remoteAddress, uint32_t ticket);
void processTxEvent(const somfy_tx_event_t &evt);
void emitState(uint8_t num = 255);
// The change fan-out: sends every shade and group that changed since the
// last pass, its changed MQTT topics and one state event. loop() only.
void sendChanges();
void publish();
void processWaitingFrame();
void commit();

View file

@ -158,6 +158,10 @@ void loop() {
loopQueue.drain();
logIfSlow(TAG, "Loop queue", timing.lap(), LOOP_SLOW_MS);
esp_task_wdt_reset();
// What changed in this pass goes out once, to MQTT and the sockets.
somfy.sendChanges();
logIfSlow(TAG, "Loop changes", timing.lap(), LOOP_SLOW_MS);
esp_task_wdt_reset();
if(net.connected() || net.softAPOpened) {
if(!rebootDelay.reboot && net.connected() && !net.softAPOpened) {
git.loop();

View file

@ -43,7 +43,7 @@ int8_t SomfyGroup::p_direction(int8_t dir) {
int8_t old = this->direction;
if(old != dir) {
this->direction = dir;
this->publish("direction", this->direction);
this->changed |= GROUP_CHANGED_DIRECTION;
}
return old;
}

View file

@ -298,22 +298,23 @@ void SomfyShade::checkMovement() {
}
// State Setters
// State Setters. Each marks what clients see as changed when it does change;
// the change fan-out publishes it once per loop() pass.
float SomfyShade::p_currentPos(float pos) {
float old = this->currentPos;
this->currentPos = pos;
if(floor(old) != floor(pos)) this->publish("position", (uint8_t)50);
if(floor(old) != floor(pos)) this->changed |= SHADE_CHANGED_POSITION;
return old;
}
float SomfyShade::p_currentTiltPos(float pos) {
float old = this->currentTiltPos;
this->currentTiltPos = pos;
if(floor(old) != floor(pos)) this->publish("tiltPosition", this->transformPosition(static_cast<uint8_t>(floor(this->currentTiltPos))));
if(floor(old) != floor(pos)) this->changed |= SHADE_CHANGED_TILT_POSITION;
return old;
}
uint16_t SomfyShade::p_lastRollingCode(uint16_t code) {
uint16_t old = SomfyRemote::p_lastRollingCode(code);
if(old != code) this->publish("lastRollingCode", code);
if(old != code) this->changed |= SHADE_CHANGED_ROLLING_CODE;
return old;
}
bool SomfyShade::p_flag(somfy_flags_t flag, bool val) {
@ -326,24 +327,24 @@ bool SomfyShade::p_flag(somfy_flags_t flag, bool val) {
}
bool SomfyShade::p_sunFlag(bool val) {
bool old = this->p_flag(somfy_flags_t::SunFlag, val);
if(old != val) this->publish("sunFlag", static_cast<uint8_t>(val));
if(old != val) this->changed |= SHADE_CHANGED_SUN_FLAG;
return old;
}
bool SomfyShade::p_windy(bool val) {
bool old = this->p_flag(somfy_flags_t::Windy, val);
if(old != val) this->publish("windy", static_cast<uint8_t>(val));
if(old != val) this->changed |= SHADE_CHANGED_WINDY;
return old;
}
bool SomfyShade::p_sunny(bool val) {
bool old = this->p_flag(somfy_flags_t::Sunny, val);
if(old != val) this->publish("sunny", static_cast<uint8_t>(val));
if(old != val) this->changed |= SHADE_CHANGED_SUNNY;
return old;
}
int8_t SomfyShade::p_direction(int8_t dir) {
int8_t old = this->direction;
if(old != dir) {
this->direction = dir;
this->publish("direction", this->direction, true);
this->changed |= SHADE_CHANGED_DIRECTION;
}
return old;
}
@ -351,7 +352,7 @@ int8_t SomfyShade::p_tiltDirection(int8_t dir) {
int8_t old = this->tiltDirection;
if(old != dir) {
this->tiltDirection = dir;
this->publish("tiltDirection", this->tiltDirection, true);
this->changed |= SHADE_CHANGED_TILT_DIRECTION;
}
return old;
}
@ -360,7 +361,7 @@ float SomfyShade::p_target(float target) {
if(old != target) {
this->target = target;
if(this->transformPosition(old) != this->transformPosition(target))
this->publish("target", this->transformPosition(this->target), true);
this->changed |= SHADE_CHANGED_TARGET;
}
return old;
}
@ -369,17 +370,16 @@ float SomfyShade::p_tiltTarget(float target) {
if(old != target) {
this->tiltTarget = target;
if(this->transformPosition(old) != this->transformPosition(target))
this->publish("tiltTarget", this->transformPosition(this->tiltTarget), true);
this->changed |= SHADE_CHANGED_TILT_TARGET;
}
return old;
}
float SomfyShade::p_myPos(float pos) {
float old = this->myPos;
if(old != pos) {
//if(this->transformPosition(pos) == 0) Serial.println("MyPos = %.2f", pos);
this->myPos = pos;
if(this->transformPosition(old) != this->transformPosition(pos))
this->publish("mypos", this->transformPosition(this->myPos), true);
this->changed |= SHADE_CHANGED_MY_POS;
}
return old;
}
@ -388,7 +388,7 @@ float SomfyShade::p_myTiltPos(float pos) {
if(old != pos) {
this->myTiltPos = pos;
if(this->transformPosition(old) != this->transformPosition(pos))
this->publish("myTiltPos", this->transformPosition(this->myTiltPos), true);
this->changed |= SHADE_CHANGED_MY_TILT_POS;
}
return old;
}

View file

@ -8,6 +8,7 @@ extern SomfyShadeController somfy;
extern SocketEmitter sockEmit;
extern MQTTClass mqtt;
void SomfyShade::emitState() { this->changed |= SHADE_CHANGED_STATE; }
void SomfyShade::emitState(const char *evt) { this->emitState(255, evt); }
void SomfyShade::emitState(uint8_t num, const char *evt) {
JsonSockEvent *json = sockEmit.beginEmit(evt);
@ -65,6 +66,7 @@ void SomfyRoom::emitState(uint8_t num, const char *evt) {
sockEmit.endEmit(num);
this->publish();
}
void SomfyGroup::emitState() { this->changed |= GROUP_CHANGED_STATE; }
void SomfyGroup::emitState(const char *evt) { this->emitState(255, evt); }
void SomfyGroup::emitState(uint8_t num, const char *evt) {
uint8_t flags = 0;
@ -102,6 +104,26 @@ void emitRequestResult(uint32_t requestId, const edit_result_t &result) {
json->endObject();
sockEmit.endEmit();
}
// The change fan-out. Everything that changed about a shade or group in this
// loop() pass goes out once: its MQTT topics, then one state event.
void SomfyShadeController::sendChanges() {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade &shade = this->shades[i];
const uint16_t changed = shade.changed;
shade.changed = 0;
if(!changed || shade.getShadeId() == 255) continue;
if(changed & SHADE_CHANGED_TOPICS) shade.publishTopics(changed);
shade.emitState(255, "shadeState");
}
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
SomfyGroup &group = this->groups[i];
const uint8_t changed = group.changed;
group.changed = 0;
if(!changed || group.getGroupId() == 255) continue;
if(changed & GROUP_CHANGED_DIRECTION) group.publish("direction", group.direction);
group.emitState(255, "groupState");
}
}
void SomfyShadeController::emitState(uint8_t num) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &this->shades[i];

View file

@ -1,6 +1,7 @@
#include "Utils.h"
#include "ConfigSettings.h"
#include "Somfy.h"
#include "SomfySnapshot.h"
#include "Sockets.h"
#include "entryPoints/MQTT.h"
@ -31,28 +32,23 @@ void SomfyRoom::unpublish() {
mqtt.unpublish(topic);
}
}
// Publishes one topic of a shade: shades/<id>/<topic>.
static void publishShadeTopic(const char *topic, int32_t value, bool retain, void *ctx) {
SomfyShade *shade = static_cast<SomfyShade *>(ctx);
if(value < 0) shade->publish(topic, static_cast<int8_t>(value), retain);
else shade->publish(topic, static_cast<uint32_t>(value), retain);
}
void SomfyShade::publishTopics(uint16_t changed) {
if(!mqtt.connected()) return;
shade_snapshot_t s;
snapshotShade(*this, s);
publishShadeTopics(changed, s, publishShadeTopic, this);
}
void SomfyShade::publishState() {
if(mqtt.connected()) {
this->publish("position", (uint8_t)50, true);
this->publish("direction", (int8_t)0, true);
this->publish("target", (uint8_t)50, true);
this->publish("lastRollingCode", this->lastRollingCode);
this->publish("mypos", this->transformPosition(this->myPos), true);
this->publish("myTiltPos", this->transformPosition(this->myTiltPos), true);
if(this->tiltType != tilt_types::none) {
this->publish("tiltDirection", this->tiltDirection, true);
this->publish("tiltPosition", this->transformPosition(this->currentTiltPos), true);
this->publish("tiltTarget", this->transformPosition(this->tiltTarget), true);
}
const uint8_t sunFlag = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
const uint8_t isSunny = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny));
const uint8_t isWindy = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Windy));
if(this->hasSunSensor()) {
this->publish("sunFlag", sunFlag);
this->publish("sunny", isSunny);
}
this->publish("windy", isWindy);
}
if(!mqtt.connected()) return;
shade_snapshot_t s;
snapshotShade(*this, s);
publishShadeTopics(shadeStateTopics(s), s, publishShadeTopic, this);
}
void SomfyShade::publishDisco() {
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;

View file

@ -0,0 +1,91 @@
#include <unity.h>
#include <string>
#include "ShadeChanges.h"
static std::string published;
static void record(const char *topic, int32_t value, bool retain, void *ctx) {
(void)ctx;
if(!published.empty()) published += " ";
published += std::string(topic) + "=" + std::to_string(value) + (retain ? "R" : "");
}
static std::string topics(uint16_t changed, const shade_snapshot_t &shade) {
published.clear();
publishShadeTopics(changed, shade, record, nullptr);
return published;
}
// A shade as the snapshot shows it to clients.
static shade_snapshot_t shade() {
shade_snapshot_t s{};
s.shadeId = 4;
s.position = 30;
s.target = 70;
s.direction = 1;
s.myPos = -1;
s.myTiltPos = -1;
s.tiltType = static_cast<uint8_t>(tilt_types::none);
s.tiltPosition = 40;
s.tiltTarget = 45;
s.tiltDirection = -1;
s.lastRollingCode = 1234;
return s;
}
void setUp() {}
void tearDown() {}
static void test_nothing_changed_publishes_nothing() {
TEST_ASSERT_EQUAL_STRING("", topics(0, shade()).c_str());
TEST_ASSERT_EQUAL_STRING("", topics(SHADE_CHANGED_STATE, shade()).c_str());
}
static void test_position_and_target_are_the_shades_own() {
// Both used to go out as a constant 50.
TEST_ASSERT_EQUAL_STRING("position=30R", topics(SHADE_CHANGED_POSITION, shade()).c_str());
TEST_ASSERT_EQUAL_STRING("target=70R", topics(SHADE_CHANGED_TARGET, shade()).c_str());
}
static void test_only_the_changed_topics_go_out() {
TEST_ASSERT_EQUAL_STRING("direction=1R lastRollingCode=1234",
topics(SHADE_CHANGED_DIRECTION | SHADE_CHANGED_ROLLING_CODE, shade()).c_str());
}
static void test_an_unset_favorite_is_minus_1() {
TEST_ASSERT_EQUAL_STRING("mypos=-1R myTiltPos=-1R", topics(SHADE_CHANGED_MY_POS | SHADE_CHANGED_MY_TILT_POS, shade()).c_str());
}
static void test_tilt_topics() {
TEST_ASSERT_EQUAL_STRING("tiltDirection=-1R tiltPosition=40R tiltTarget=45R",
topics(SHADE_CHANGED_TILT_DIRECTION | SHADE_CHANGED_TILT_POSITION | SHADE_CHANGED_TILT_TARGET, shade()).c_str());
}
static void test_sun_and_wind_are_0_or_1() {
shade_snapshot_t s = shade();
s.flags = static_cast<uint8_t>(somfy_flags_t::SunFlag) | static_cast<uint8_t>(somfy_flags_t::Windy);
TEST_ASSERT_EQUAL_STRING("sunFlag=1 sunny=0 windy=1",
topics(SHADE_CHANGED_SUN_FLAG | SHADE_CHANGED_SUNNY | SHADE_CHANGED_WINDY, s).c_str());
}
static void test_full_state_of_a_plain_shade() {
// Direction used to go out as a constant 0 when MQTT connected.
TEST_ASSERT_EQUAL_STRING("position=30R direction=1R target=70R lastRollingCode=1234 mypos=-1R myTiltPos=-1R windy=0",
topics(shadeStateTopics(shade()), shade()).c_str());
}
static void test_full_state_adds_tilt_and_sun_when_the_shade_has_them() {
shade_snapshot_t s = shade();
s.tiltType = static_cast<uint8_t>(tilt_types::integrated);
s.sunSensor = true;
const uint16_t state = shadeStateTopics(s);
TEST_ASSERT_TRUE(state & SHADE_CHANGED_TILT_POSITION);
TEST_ASSERT_TRUE(state & SHADE_CHANGED_SUNNY);
TEST_ASSERT_FALSE(state & SHADE_CHANGED_STATE);
TEST_ASSERT_EQUAL_UINT16(SHADE_CHANGED_TOPICS, state);
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_nothing_changed_publishes_nothing);
RUN_TEST(test_position_and_target_are_the_shades_own);
RUN_TEST(test_only_the_changed_topics_go_out);
RUN_TEST(test_an_unset_favorite_is_minus_1);
RUN_TEST(test_tilt_topics);
RUN_TEST(test_sun_and_wind_are_0_or_1);
RUN_TEST(test_full_state_of_a_plain_shade);
RUN_TEST(test_full_state_adds_tilt_and_sun_when_the_shade_has_them);
return UNITY_END();
}