diff --git a/docs/LOOP_QUEUE_PLAN.md b/docs/LOOP_QUEUE_PLAN.md index d69dc0d..8ae17c2 100644 --- a/docs/LOOP_QUEUE_PLAN.md +++ b/docs/LOOP_QUEUE_PLAN.md @@ -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. diff --git a/platformio.ini b/platformio.ini index 9954e42..6f9e89a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -113,7 +113,7 @@ lib_deps = platform = native test_framework = unity test_build_src = yes -build_src_filter = -<*> + + + + + + + + + + + + +build_src_filter = -<*> + + + + + + + + + + + + + build_flags = -std=gnu++17 -Isrc diff --git a/src/ShadeChanges.cpp b/src/ShadeChanges.cpp new file mode 100644 index 0000000..4e65f7f --- /dev/null +++ b/src/ShadeChanges.cpp @@ -0,0 +1,28 @@ +#include "ShadeChanges.h" + +static bool flag(const shade_snapshot_t &shade, somfy_flags_t f) { return (shade.flags & static_cast(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(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; +} diff --git a/src/ShadeChanges.h b/src/ShadeChanges.h new file mode 100644 index 0000000..9adacd7 --- /dev/null +++ b/src/ShadeChanges.h @@ -0,0 +1,44 @@ +#ifndef SHADE_CHANGES_H +#define SHADE_CHANGES_H +#include +#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//, 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 diff --git a/src/ShadeTypes.h b/src/ShadeTypes.h index bdbe7aa..7e2921d 100644 --- a/src/ShadeTypes.h +++ b/src/ShadeTypes.h @@ -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, diff --git a/src/Somfy.h b/src/Somfy.h index 1d1670c..6417efc 100644 --- a/src/Somfy.h +++ b/src/Somfy.h @@ -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(); diff --git a/src/SomfyController.ino b/src/SomfyController.ino index 5cb9b40..cb2fdf9 100644 --- a/src/SomfyController.ino +++ b/src/SomfyController.ino @@ -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(); diff --git a/src/SomfyGroup.cpp b/src/SomfyGroup.cpp index f09b09a..d3cfba4 100644 --- a/src/SomfyGroup.cpp +++ b/src/SomfyGroup.cpp @@ -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; } diff --git a/src/SomfyShade.cpp b/src/SomfyShade.cpp index 3e1607b..f19e587 100644 --- a/src/SomfyShade.cpp +++ b/src/SomfyShade.cpp @@ -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(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(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(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(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; } diff --git a/src/outputs/SomfyEvents.cpp b/src/outputs/SomfyEvents.cpp index 16917ad..116f196 100644 --- a/src/outputs/SomfyEvents.cpp +++ b/src/outputs/SomfyEvents.cpp @@ -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]; diff --git a/src/outputs/SomfyMqtt.cpp b/src/outputs/SomfyMqtt.cpp index 4b07edf..7fe7cf3 100644 --- a/src/outputs/SomfyMqtt.cpp +++ b/src/outputs/SomfyMqtt.cpp @@ -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//. +static void publishShadeTopic(const char *topic, int32_t value, bool retain, void *ctx) { + SomfyShade *shade = static_cast(ctx); + if(value < 0) shade->publish(topic, static_cast(value), retain); + else shade->publish(topic, static_cast(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(somfy_flags_t::SunFlag)); - const uint8_t isSunny = !!(this->flags & static_cast(somfy_flags_t::Sunny)); - const uint8_t isWindy = !!(this->flags & static_cast(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; diff --git a/test/test_shadechanges/test_shadechanges.cpp b/test/test_shadechanges/test_shadechanges.cpp new file mode 100644 index 0000000..9b15f48 --- /dev/null +++ b/test/test_shadechanges/test_shadechanges.cpp @@ -0,0 +1,91 @@ +#include +#include +#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(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(somfy_flags_t::SunFlag) | static_cast(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(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(); +}