From 4f8eac03acbfde8fc298c928179fed7abb97229b Mon Sep 17 00:00:00 2001 From: cjkas Date: Fri, 16 Oct 2026 21:21:38 +0200 Subject: [PATCH] refactor: move the shade's behaviour into ShadeBehaviour PR B of SHADE_BEHAVIOUR_PLAN.md. What a shade does with frames, commands, group commands and radio events moves unchanged from SomfyShade into ShadeBehaviour, which builds natively and is pinned by test_shadebehaviour. ShadeBehaviour reads ShadeSettings and RemoteSettings, gets the time on every input, and holds the motion state that other files now read through state(). It sends through MotorLink and reports through ShadeEvents (changes, commands, save wanted, sensor flags) as things happen. SomfyShade wraps it: its MotorLink is SomfyRemote::sendCommandAfter, which now leaves the sender out of the echo and radio hold because the module hears its own frame. lastFrame stays shared with the send path. ConfigFile writes through restore() and clearTilt(), SomfyShadeOps through setPosition(). The pinning tests label where the copies of a rule disagree "today:", for the reconcile phase. Co-Authored-By: Claude Opus 5.5 --- GLOSSARY.md | 10 + docs/SHADE_BEHAVIOUR_PLAN.md | 21 +- platformio.ini | 2 +- src/ConfigFile.cpp | 41 +- src/ShadeBehaviour.cpp | 1215 ++++++++++++++++ src/ShadeBehaviour.h | 167 +++ src/ShadeController.cpp | 6 +- src/ShadeGpio.cpp | 14 +- src/ShadeTypes.h | 13 + src/Somfy.h | 96 +- src/SomfyFrame.cpp | 14 + src/SomfyFrame.h | 2 + src/SomfyRemote.cpp | 23 +- src/SomfyShade.cpp | 1221 +---------------- src/SomfyShadeOps.cpp | 16 +- src/SomfySnapshot.cpp | 16 +- src/outputs/SomfyEvents.cpp | 16 +- .../test_shadebehaviour.cpp | 632 +++++++++ 18 files changed, 2182 insertions(+), 1343 deletions(-) create mode 100644 src/ShadeBehaviour.cpp create mode 100644 src/ShadeBehaviour.h create mode 100644 test/test_shadebehaviour/test_shadebehaviour.cpp diff --git a/GLOSSARY.md b/GLOSSARY.md index 70964f6..5736cb0 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -70,6 +70,16 @@ What shades and groups share about how their frames go on air: flip commands, bi **Flags** The byte a shade is saved and sent with. Its sun sensor, light and simulated My bits are settings; its sun flag, sunny, windy and demo mode bits are state. A group's flags are its shades' flags together. +**ShadeBehaviour** +What a shade does with frames, commands, group commands and radio events, and where that leaves it: positions, targets, directions, the favorite and the sensor state. +_Avoid_: shade logic, motion (`ShadeMotion` is the position maths it uses) + +**MotorLink** +The seam a shade's commands reach its motor through, radio or GPIO. The shade hears its own frame; every other shade hears it through the link. + +**ShadeEvents** +The seam through which a shade's behaviour reports what happened: changes for the change fan-out, commands for clients, a save wanted, and sensor flags for the groups. + ## Entry points and outputs **Entry point** diff --git a/docs/SHADE_BEHAVIOUR_PLAN.md b/docs/SHADE_BEHAVIOUR_PLAN.md index d4b7910..8a52f7d 100644 --- a/docs/SHADE_BEHAVIOUR_PLAN.md +++ b/docs/SHADE_BEHAVIOUR_PLAN.md @@ -1,6 +1,6 @@ # Plan: deepen the shade behaviour -**Status:** design settled (rounds 1–3, see "Decisions"), apart from the reconcile order, which waits for PR B's pinning tests. PR A (`refactor/shade-settings`) is built; PR B is next. +**Status:** design settled (see "Decisions"). PR A is #46. PR B (`refactor/shade-behaviour`) is built: the behaviour is in `ShadeBehaviour`, pinned by `test_shadebehaviour`. The reconcile order is the open question (below). **Written:** 2026-10-16, on top of `refactor/split-transports` (PR #44, the top of the LoopQueue stack #40–#44, which is not yet merged or run on a device). Line numbers refer to that branch. **Start here:** read [GLOSSARY.md](../GLOSSARY.md), [ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md) and [LOOP_QUEUE_PLAN.md](LOOP_QUEUE_PLAN.md) first. Their decisions are not reopened here. @@ -84,7 +84,15 @@ The decisions are the user's. Each question comes with a recommended answer, and ### Still open -- **Reconcile: what is decided first, and in what order.** Asked once PR B's pinning tests have listed every "today: …" disagreement. Known candidates: `settingPos` on arrival (may send extra stops), Up/Down/My on tilt-only shades, the self-echo (Q5), and merging group commands into the frame rule (Q8). +- **Reconcile: what is decided first, and in what order.** The candidates, each pinned by a "today:" test in `test_shadebehaviour` unless marked: + - `settingPos` is never cleared on a lift arrival, so a later step also sends a stop (`test_today_a_step_after_move_to_target_sends_a_stop`). + - Up and My on tilt-only shades differ between the web, a remote and a group (`test_today_up_on_tilt_only_*`, `test_today_my_while_tilt_only_moves`). + - An integrated tilt sent to a lift target without a tilt turns its slats back with an Up after the stop (`test_today_integrated_tilt_turns_back_after_a_lift_target`). + - A remote step uses the step size this shade last *sent*, because `copy()` never copies `stepSize` (`test_remote_step_uses_the_step_size_last_sent`). + - A group's SunFlag or Flag doesn't make other groups work their flags out again (`test_group_sun_flag_needs_a_sensor`). + - The self-echo (Q5) and merging group commands into the frame rule (Q8). + - `lastFrame` doing two jobs (the Q6 revisit). + - The 500 ms long-press deadline is compared as `now > await`, which isn't wrap-safe (not tested). ## Constraints @@ -135,3 +143,12 @@ The decisions are the user's. Each question comes with a recommended answer, and - A shade's byte is `getFlags()` (put together) and `setFlags()` (split), used by ConfigFile, JSON, MQTT, events and group flags. - Groups keep their own `flags` byte (the OR of their shades), and `SomfyRemote::getFlags()` is virtual for `sendSensorCommand`. - `test_shadesettings` checks that all 256 byte values come back unchanged. + +**During PR B (2026-10-16):** + +- **Q6 revisited: `lastFrame` is shared.** The module holds a reference to `SomfyRemote::lastFrame` instead of its own copy. A copy would have to mirror five writes exactly: each send overwrites it; the shade's own echo then counts as a repeat; `repeatFrame` adds the queued repeats; a sensor send overwrites it; and `copy()` never copies `stepSize` or `await`. The user chose sharing. Splitting the two jobs is a reconcile candidate. +- **Q12 refined.** `MotorLink::send` returns `{sent, ticket}`; the frame as sent is in the shared `lastFrame`. `SomfyRemote::sendCommandAfter(…, hearsItself = true)` leaves the sender out of the echo and the radio hold; `ShadeBehaviour::transmit` does both for itself. +- **Q7 and Q9 refined: one output seam that fires on the spot.** `ShadeEvents` carries `onChanged` (the change bits), `onCommand`, `onSaveWanted` and `onSensorFlags`. Nothing is collected and read back after each call, so the timing is today's. `onSensorFlags` fires exactly where `somfy.updateGroupFlags()` was called, not on every sensor-bit change. +- **`onTxEvent` takes no `now`:** the event carries its own time. +- **A known ordering change:** when a simMy shade's Favorite or My sends again from inside its own echo, a shade linked to it now hears the outer frame before the inner one. Before, it depended on the two shades' slots. Nothing else changes order. +- **Flash:** `esp32dev` `firmware.bin` grows 1,397,776 → 1,399,776 B (+2,000 B, 88.6%), mostly the virtual calls through the two seams. diff --git a/platformio.ini b/platformio.ini index 6f9e89a..542026f 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/ConfigFile.cpp b/src/ConfigFile.cpp index 05622c6..ea4cc32 100644 --- a/src/ConfigFile.cpp +++ b/src/ConfigFile.cpp @@ -835,27 +835,29 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { shade->lastRollingCode = max(rc, shade->lastRollingCode); if(rc < shade->lastRollingCode) pref.putUShort(shade->getRemotePrefId(), shade->lastRollingCode); } + float myPos = shade->state().myPos; + float myTiltPos = shade->state().myTiltPos; if(this->header.version < 4) - shade->myPos = static_cast(this->readUInt8(255)); + myPos = static_cast(this->readUInt8(255)); else { - shade->myPos = this->readFloat(-1); - shade->myTiltPos = this->readFloat(-1); + myPos = this->readFloat(-1); + myTiltPos = this->readFloat(-1); } - if(shade->myPos > 100 || shade->myPos < 0) shade->myPos = -1; - if(shade->myTiltPos > 100 || shade->myTiltPos < 0) shade->myTiltPos = -1; - shade->currentPos = this->readFloat(0); - shade->currentTiltPos = this->readFloat(0); + if(myPos > 100 || myPos < 0) myPos = -1; + if(myTiltPos > 100 || myTiltPos < 0) myTiltPos = -1; + float currentPos = this->readFloat(0); + float currentTiltPos = this->readFloat(0); if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { - shade->myTiltPos = -1; - shade->currentTiltPos = 0; + myTiltPos = -1; + currentTiltPos = 0; shade->tiltType = tilt_types::none; } if(this->header.version < 3) { - shade->currentPos = shade->currentPos * 100; - shade->currentTiltPos = shade->currentTiltPos * 100; + currentPos = currentPos * 100; + currentTiltPos = currentTiltPos * 100; } - shade->target = floor(shade->currentPos); - shade->tiltTarget = floor(shade->currentTiltPos); + // The targets are where the shade is. + shade->restore(currentPos, currentTiltPos, myPos, myTiltPos); if(this->header.version >= 9) shade->flipCommands = this->readBool(false); if(this->header.version >= 10) shade->flipPosition = this->readBool(false); if(this->header.version >= 12) shade->repeats = this->readUInt8(1); @@ -871,7 +873,7 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { shade->gpioFlags = this->readUInt8(shade->gpioFlags); if(shade->getShadeId() == 255) shade->clear(); else if(shade->tiltType == tilt_types::tiltonly) { - shade->myPos = shade->currentPos = shade->target = 100.0f; + shade->restore(100.0f, shade->state().currentTiltPos, 100.0f, shade->state().myTiltPos); } pref.end(); if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { @@ -973,8 +975,7 @@ bool ShadeConfigFile::writeRoomRecord(SomfyRoom *room) { } bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) { if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { - shade->myTiltPos = -1; - shade->currentTiltPos = 0; + shade->clearTilt(); shade->tiltType = tilt_types::none; } this->writeUInt8(shade->getShadeId()); @@ -996,10 +997,10 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) { this->writeUInt16(shade->lastRollingCode); if(shade->getShadeId() != 255) { this->writeUInt8(shade->getFlags()); - this->writeFloat(shade->myPos, 5); - this->writeFloat(shade->myTiltPos, 5); - this->writeFloat(shade->currentPos, 5); - this->writeFloat(shade->currentTiltPos, 5); + this->writeFloat(shade->state().myPos, 5); + this->writeFloat(shade->state().myTiltPos, 5); + this->writeFloat(shade->state().currentPos, 5); + this->writeFloat(shade->state().currentTiltPos, 5); } else { // Make sure that we write cleared values when the shade is deleted. diff --git a/src/ShadeBehaviour.cpp b/src/ShadeBehaviour.cpp new file mode 100644 index 0000000..fc8c461 --- /dev/null +++ b/src/ShadeBehaviour.cpp @@ -0,0 +1,1215 @@ +#include "esp_log.h" +#include "ShadeBehaviour.h" + +static const char *TAG = "Somfy"; + +#define SETMY_REPEATS 35 +#define TILT_REPEATS 15 + +// How long a shade's simulated movement waits for its command to go on air +// before it starts anyway in case the event was lost. +#define TX_PENDING_TIMEOUT 30000 + +ShadeBehaviour::ShadeBehaviour(const ShadeSettings &settings, const RemoteSettings &remote, somfy_frame_t &lastFrame, + MotorLink &link, ShadeEvents &events, const uint8_t &shadeId, const char *name) + : settings(settings), remote(remote), lastFrame(lastFrame), link(link), events(events), shadeId(shadeId), name(name) {} +void ShadeBehaviour::clear() { + this->moveStart = 0; + this->tiltStart = 0; + this->sensors.clear(); + this->startPos = 0.0f; + this->startTiltPos = 0.0f; + this->settingMyPos = false; + this->settingPos = false; + this->settingTiltPos = false; + this->awaitMy = 0; + this->s.currentPos = 0.0f; + this->s.currentTiltPos = 0.0f; + this->s.direction = 0; + this->s.tiltDirection = 0; + this->s.target = 0.0f; + this->s.tiltTarget = 0.0f; + this->s.myPos = -1.0f; + this->s.myTiltPos = -1.0f; +} +void ShadeBehaviour::setPosition(float pos, float tilt) { + // Through the setters, so MQTT hears about it too. + if(pos >= 0.0f) { + this->p_target(pos); + this->p_currentPos(pos); + } + if(tilt >= 0.0f) { + this->p_tiltTarget(tilt); + this->p_currentTiltPos(tilt); + } +} +void ShadeBehaviour::restore(float pos, float tiltPos, float myPos, float myTiltPos) { + this->s.currentPos = pos; + this->s.currentTiltPos = tiltPos; + this->s.myPos = myPos; + this->s.myTiltPos = myTiltPos; + this->s.target = floor(pos); + this->s.tiltTarget = floor(tiltPos); +} +void ShadeBehaviour::clearTilt() { + this->s.myTiltPos = -1; + this->s.currentTiltPos = 0; +} +somfy_commands ShadeBehaviour::transformCommand(somfy_commands cmd) const { return this->remote.flipCommands ? flipCommand(cmd) : cmd; } +send_result_t ShadeBehaviour::transmit(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority, uint32_t now) { + const send_result_t result = this->link.send(cmd, repeat, stepSize, delayMs, priority); + if(!result.sent) return result; + // The link has put the frame in lastFrame and every other shade has heard + // it. The shade hears it too, as if a remote had sent it. + this->onFrame(this->lastFrame, true, now); + // An idle shade this sets moving waits for the frame to go on air. Stops + // are sent ahead of everything else so they don't need to. + if(result.ticket != 0 && !priority && startsMovement(this->lastFrame.cmd)) this->onTxQueued(result.ticket, now); + return result; +} +bool ShadeBehaviour::isAtTarget() const { + float epsilon = .00001; + if(this->settings.tiltType == tilt_types::tiltonly) return fabs(this->s.currentTiltPos - this->s.tiltTarget) < epsilon; + else if(this->settings.tiltType == tilt_types::none) return fabs(this->s.currentPos - this->s.target) < epsilon; + return fabs(this->s.currentPos - this->s.target) < epsilon && fabs(this->s.currentTiltPos - this->s.tiltTarget) < epsilon; +} +void ShadeBehaviour::tick(uint32_t now) { + const uint32_t curTime = now; + const bool sunFlag = this->s.stateFlags & static_cast(somfy_flags_t::SunFlag); + const bool isSunny = this->s.stateFlags & static_cast(somfy_flags_t::Sunny); + const bool isWindy = this->s.stateFlags & static_cast(somfy_flags_t::Windy); + // We need to first evaluate the sensor flags as these could be triggering movement from previous sensor inputs. So + // we must check this before setting the directional items or it will not get processed until the next loop. + int32_t downTime = (int32_t)this->settings.downTime; + int32_t upTime = (int32_t)this->settings.upTime; + int32_t tiltTime = (int32_t)this->settings.tiltTime; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) downTime = upTime = tiltTime = 1; + + + // We are checking movement for essentially 3 types of motors. + // If this is an integrated tilt we need to first tilt in the direction we are moving then move. We know + // what needs to be done by the tilt type. Set a tilt first flag to indicate whether we should be tilting or + // moving. If this is only a tilt action then the regular tilt action should operate fine. + int8_t currDir = this->s.direction; + int8_t currTiltDir = this->s.tiltDirection; + this->p_direction(this->s.currentPos == this->s.target ? 0 : this->s.currentPos > this->s.target ? -1 : 1); + bool tilt_first = this->settings.tiltType == tilt_types::integrated && ((this->s.direction == -1 && this->s.currentTiltPos != 0.0f) || (this->s.direction == 1 && this->s.currentTiltPos != 100.0f)); + + this->p_tiltDirection(this->s.currentTiltPos == this->s.tiltTarget ? 0 : this->s.currentTiltPos > this->s.tiltTarget ? -1 : 1); + if(tilt_first) this->p_tiltDirection(this->s.direction); + else if(this->s.direction != 0) this->p_tiltDirection(0); + uint8_t currPos = floor(this->s.currentPos); + uint8_t currTiltPos = floor(this->s.currentTiltPos); + if(this->s.direction != 0) this->s.lastMovement = this->s.direction; + switch(this->tx.hold.check(this->s.direction != 0 || this->s.tiltDirection != 0, (uint32_t)curTime, TX_PENDING_TIMEOUT)) { + case TxHoldCheck::holding: + // The command that set us moving hasn't gone on air yet so the motor + // hasn't started. Hold the simulated position until it does. + this->moveStart = this->tiltStart = curTime; + if(currDir != this->s.direction || currTiltDir != this->s.tiltDirection) this->emitState(); + return; + case TxHoldCheck::expired: + ESP_LOGW(TAG, "[Shade %u] Timed out waiting for the radio, moving anyway", this->shadeId); + break; + case TxHoldCheck::none: + break; + } + this->applySensorMove(this->sensors.check(sunFlag, isSunny, isWindy, (uint32_t)curTime)); + + if(!tilt_first && this->s.direction > 0) { + this->p_currentPos(travelPosition(this->startPos, 1, (uint32_t)(curTime - this->moveStart), downTime)); + if(this->s.currentPos >= this->s.target) { + this->p_currentPos(this->s.target); + //if(this->settingMyPos) Serial.printf("IsAtTarget: %d %f=%f\n", this->isAtTarget(), this->s.currentPos, this->s.target); + // If we need to stop the shade do this before we indicate that we are + // not moving otherwise the my function will kick in. + if(this->settingPos) { + // At the end limit the motor stops by itself. If the stop doesn't go + // out the shade is now running on to the limit so leave the tilt be. + if(this->s.target == 100.0 || this->sendStop(1, false, now)) { + if(!isAtTarget()) { + ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->s.tiltTarget); + // We now need to move the tilt to the position we requested. + this->moveToTiltTarget(this->s.tiltTarget, now); + } + } + } + this->p_direction(0); + this->tiltStart = curTime; + this->startTiltPos = this->s.currentTiltPos; + if(this->isAtTarget()) this->commitShadePosition(); + } + } + else if(!tilt_first && this->s.direction < 0) { + // Shades often move slower up so this uses its own travel time. + this->p_currentPos(travelPosition(this->startPos, -1, (uint32_t)(curTime - this->moveStart), upTime)); + if(this->s.currentPos <= this->s.target) { + this->p_currentPos(this->s.target); + //if(this->settingMyPos) Serial.printf("IsAtTarget: %d %f=%f\n", this->isAtTarget(), this->s.currentPos, this->s.target); + + // If we need to stop the shade do this before we indicate that we are + // not moving otherwise the my function will kick in. + if(this->settingPos) { + // At the end limit the motor stops by itself. If the stop doesn't go + // out the shade is now running on to the limit so leave the tilt be. + if(this->s.target == 0.0 || this->sendStop(-1, false, now)) { + if(!isAtTarget()) { + ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->s.tiltTarget); + // We now need to move the tilt to the position we requested. + this->moveToTiltTarget(this->s.tiltTarget, now); + } + } + } + this->p_direction(0); + this->tiltStart = curTime; + this->startTiltPos = this->s.currentTiltPos; + if(this->isAtTarget()) this->commitShadePosition(); + } + } + if(this->s.tiltDirection > 0) { + if(tilt_first) this->moveStart = curTime; + this->p_currentTiltPos(travelPosition(this->startTiltPos, 1, (uint32_t)(curTime - this->tiltStart), tiltTime)); + if(tilt_first) { + if(this->s.currentTiltPos >= 100.0f) { + this->p_currentTiltPos(100.0f); + this->moveStart = curTime; + this->startPos = this->s.currentPos; + //this->p_tiltDirection(0); + //Serial.println("Setting tiltDirection to 0 (tilt_first)"); + } + } + else if(this->s.currentTiltPos >= this->s.tiltTarget) { + this->p_currentTiltPos(this->s.tiltTarget); + // If we need to stop the shade do this before we indicate that we are + // not moving otherwise the my function will kick in. + if(this->settingTiltPos) { + if(this->settings.tiltType == tilt_types::integrated) { + // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. + //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->s.tiltTarget, this->s.tiltDirection); + if(this->s.tiltTarget != 100.0f || this->s.currentPos != 100.0f) this->sendStop(1, true, now); + } + else { + // This is a tilt motor so let it complete if it is going to 100. + if(this->s.tiltTarget != 100.0f) this->sendStop(1, true, now); + } + } + this->p_tiltDirection(0); + this->settingTiltPos = false; + if(this->isAtTarget()) this->commitShadePosition(); + } + } + else if(this->s.tiltDirection < 0) { + if(tilt_first) this->moveStart = curTime; + if(tiltTime == 0) this->p_tiltDirection(0); + this->p_currentTiltPos(travelPosition(this->startTiltPos, -1, (uint32_t)(curTime - this->tiltStart), tiltTime)); + if(tilt_first) { + if(this->s.currentTiltPos <= 0.0f) { + this->p_currentTiltPos(0.0f); + this->moveStart = curTime; + this->startPos = this->s.currentPos; + //this->p_tiltDirection(0); + } + } + else if(this->s.currentTiltPos <= this->s.tiltTarget) { + this->p_currentTiltPos(this->s.tiltTarget); + // If we need to stop the shade do this before we indicate that we are + // not moving otherwise the my function will kick in. + if(this->settingTiltPos) { + if(this->settings.tiltType == tilt_types::integrated) { + // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. + //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->s.tiltTarget, this->s.tiltDirection); + if(this->s.tiltTarget != 0.0 || this->s.currentPos != 0.0) this->sendStop(-1, true, now); + } + else { + // This is a tilt motor so let it complete if it is going to 0. + if(this->s.tiltTarget != 0.0) this->sendStop(-1, true, now); + } + } + this->p_tiltDirection(0); + this->settingTiltPos = false; + ESP_LOGI(TAG, "Stopping at tilt position"); + if(this->isAtTarget()) this->commitShadePosition(); + } + } + if(this->settingMyPos && this->isAtTarget()) { + // Set this position before sending the command. If you don't the processFrame function + // will send the shade back to its original My position. + const float oldMyPos = this->s.myPos; + const float oldMyTiltPos = this->s.myTiltPos; + if(this->settings.tiltType != tilt_types::none) { + if(this->s.myTiltPos == this->s.currentTiltPos && this->s.myPos == this->s.currentPos) this->s.myPos = this->s.myTiltPos = -1; + else { + this->p_myPos(this->s.currentPos); + this->p_myTiltPos(this->s.currentTiltPos); + } + } + else { + this->p_myTiltPos(-1); + if(this->s.myPos == this->s.currentPos) this->p_myPos(-1); + else this->p_myPos(this->s.currentPos); + } + // Give the motor time to settle at the target before it is told to store it. + this->sendSetMy(200, oldMyPos, oldMyTiltPos, now); + this->settingMyPos = false; + this->emitState(); + } + else if(currDir != this->s.direction || currPos != floor(this->s.currentPos) || currTiltDir != this->s.tiltDirection || currTiltPos != floor(this->s.currentTiltPos)) { + // We need to emit on the socket that our state has changed. + this->emitState(); + } +} + + +// State Setters. Each marks what clients see as changed when it does change; +// the change fan-out publishes it once per loop() pass. +float ShadeBehaviour::p_currentPos(float pos) { + float old = this->s.currentPos; + this->s.currentPos = pos; + if(floor(old) != floor(pos)) this->events.onChanged(SHADE_CHANGED_POSITION); + return old; +} +float ShadeBehaviour::p_currentTiltPos(float pos) { + float old = this->s.currentTiltPos; + this->s.currentTiltPos = pos; + if(floor(old) != floor(pos)) this->events.onChanged(SHADE_CHANGED_TILT_POSITION); + return old; +} +bool ShadeBehaviour::p_flag(somfy_flags_t flag, bool val) { + bool old = !!(this->s.stateFlags & static_cast(flag)); + if(val) + this->s.stateFlags |= static_cast(flag); + else + this->s.stateFlags &= ~(static_cast(flag)); + return old; +} +bool ShadeBehaviour::p_sunFlag(bool val) { + bool old = this->p_flag(somfy_flags_t::SunFlag, val); + if(old != val) this->events.onChanged(SHADE_CHANGED_SUN_FLAG); + return old; +} +bool ShadeBehaviour::p_windy(bool val) { + bool old = this->p_flag(somfy_flags_t::Windy, val); + if(old != val) this->events.onChanged(SHADE_CHANGED_WINDY); + return old; +} +bool ShadeBehaviour::p_sunny(bool val) { + bool old = this->p_flag(somfy_flags_t::Sunny, val); + if(old != val) this->events.onChanged(SHADE_CHANGED_SUNNY); + return old; +} +int8_t ShadeBehaviour::p_direction(int8_t dir) { + int8_t old = this->s.direction; + if(old != dir) { + this->s.direction = dir; + this->events.onChanged(SHADE_CHANGED_DIRECTION); + } + return old; +} +int8_t ShadeBehaviour::p_tiltDirection(int8_t dir) { + int8_t old = this->s.tiltDirection; + if(old != dir) { + this->s.tiltDirection = dir; + this->events.onChanged(SHADE_CHANGED_TILT_DIRECTION); + } + return old; +} +float ShadeBehaviour::p_target(float target) { + float old = this->s.target; + if(old != target) { + this->s.target = target; + if(this->transformPosition(old) != this->transformPosition(target)) + this->events.onChanged(SHADE_CHANGED_TARGET); + } + return old; +} +float ShadeBehaviour::p_tiltTarget(float target) { + float old = this->s.tiltTarget; + if(old != target) { + this->s.tiltTarget = target; + if(this->transformPosition(old) != this->transformPosition(target)) + this->events.onChanged(SHADE_CHANGED_TILT_TARGET); + } + return old; +} +float ShadeBehaviour::p_myPos(float pos) { + float old = this->s.myPos; + if(old != pos) { + this->s.myPos = pos; + if(this->transformPosition(old) != this->transformPosition(pos)) + this->events.onChanged(SHADE_CHANGED_MY_POS); + } + return old; +} +float ShadeBehaviour::p_myTiltPos(float pos) { + float old = this->s.myTiltPos; + if(old != pos) { + this->s.myTiltPos = pos; + if(this->transformPosition(old) != this->transformPosition(pos)) + this->events.onChanged(SHADE_CHANGED_MY_TILT_POS); + } + return old; +} +bool ShadeBehaviour::isIdle() const { + return this->isAtTarget() && this->s.direction == 0 && this->s.tiltDirection == 0; +} +void ShadeBehaviour::onWaitingFrame(uint32_t now) { + if(this->shadeId == 255) { + this->lastFrame.await = 0; + return; + } + if(this->lastFrame.processed) return; + if(this->lastFrame.await > 0 && (now > this->lastFrame.await)) { + somfy_commands cmd = this->transformCommand(this->lastFrame.cmd); + switch(cmd) { + case somfy_commands::StepUp: + this->lastFrame.processed = true; + // Simply move the shade up by 1%. + if(this->s.currentPos > 0) { + this->p_target(floor(this->s.currentPos) - 1); + this->setMovement(-1, now); + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + break; + case somfy_commands::StepDown: + this->lastFrame.processed = true; + // Simply move the shade down by 1%. + if(this->s.currentPos < 100) { + this->p_target(floor(this->s.currentPos) + 1); + this->setMovement(1, now); + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + break; + case somfy_commands::Down: + case somfy_commands::Up: + if(this->settings.tiltType == tilt_types::tiltmotor) { // Theoretically this should get here unless it does have a tilt motor. + if(this->lastFrame.repeats >= TILT_REPEATS) { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->p_tiltTarget(dir > 0 ? 100.0f : 0.0f); + this->setTiltMovement(dir, now); + this->lastFrame.processed = true; + ESP_LOGD(TAG, "%s Processing tilt %s after %d repeats", + this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + else { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->p_target(dir > 0 ? 100 : 0); + this->setMovement(dir, now); + this->lastFrame.processed = true; + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + if(this->lastFrame.repeats > TILT_REPEATS + 2) { + this->lastFrame.processed = true; + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + } + else if(this->settings.tiltType == tilt_types::euromode) { + if(this->lastFrame.repeats >= TILT_REPEATS) { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->p_target(dir > 0 ? 100.0f : 0.0f); + this->setMovement(dir, now); + this->lastFrame.processed = true; + ESP_LOGD(TAG, "%s Processing %s after %d repeats", + this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + else { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->p_tiltTarget(dir > 0 ? 100 : 0); + this->setTiltMovement(dir, now); + this->lastFrame.processed = true; + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + if(this->lastFrame.repeats > TILT_REPEATS + 2) { + this->lastFrame.processed = true; + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + } + break; + case somfy_commands::My: + if(this->lastFrame.repeats >= SETMY_REPEATS && this->isIdle()) { + if(floor(this->s.myPos) == floor(this->s.currentPos)) { + // We are clearing it. + this->p_myPos(-1); + this->p_myTiltPos(-1); + } + else { + this->p_myPos(this->s.currentPos); + this->p_myTiltPos(this->s.currentTiltPos); + } + this->commitMyPosition(); + this->lastFrame.processed = true; + this->emitState(); + } + else if(this->isIdle()) { + if(this->settings.simMy) + this->moveToMyPosition(now); // Call out like this (instead of move to target) so that we don't get some of the goofy tilt only problems. + else { + if(this->s.myPos >= 0.0f && this->s.myPos <= 100.0f) this->p_target(this->s.myPos); + if(this->s.myTiltPos >= 0.0f && this->s.myTiltPos <= 100.0f) this->p_tiltTarget(this->s.myTiltPos); + } + this->setMovement(0, now); + this->lastFrame.processed = true; + this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); + } + else { + this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + } + if(this->lastFrame.repeats > SETMY_REPEATS + 2) this->lastFrame.processed = true; + if(this->lastFrame.processed) { + ESP_LOGD(TAG, "%s Processing MY after %d repeats", this->name, this->lastFrame.repeats); + } + break; + default: + break; + } + } +} +void ShadeBehaviour::onFrame(somfy_frame_t &frame, bool internal, uint32_t now) { + // The reason why we are processing all frames here is so + // any linked remotes that may happen to be on the same ESPSomfy RTS + // device can trigger the appropriate actions. + if(this->shadeId == 255) return; + const uint32_t curTime = now; + this->lastFrame.copy(frame); + int8_t dir = 0; + this->moveStart = this->tiltStart = curTime; + this->startPos = this->s.currentPos; + this->startTiltPos = this->s.currentTiltPos; + // If the command is coming from a remote then we are aborting all these positioning operations. + if(!internal) this->settingMyPos = this->settingPos = this->settingTiltPos = false; + somfy_commands cmd = this->transformCommand(frame.cmd); + // At this point we are not processing the combo buttons + // will need to see what the shade does when you press both. + switch(cmd) { + case somfy_commands::Sensor: + this->lastFrame.processed = true; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + { + const uint8_t prevFlags = this->s.stateFlags; + const bool wasSunny = prevFlags & static_cast(somfy_flags_t::Sunny); + const bool wasWindy = prevFlags & static_cast(somfy_flags_t::Windy); + const uint16_t status = frame.rollingCode << 4; + if (status & static_cast(somfy_flags_t::Sunny)) + this->p_sunny(true); + //this->flags |= static_cast(somfy_flags_t::Sunny); + else + this->p_sunny(false); + //this->flags &= ~(static_cast(somfy_flags_t::Sunny)); + if (status & static_cast(somfy_flags_t::Windy)) + this->p_windy(true); + //this->flags |= static_cast(somfy_flags_t::Windy); + else + this->p_windy(false); + //this->flags &= ~(static_cast(somfy_flags_t::Windy)); + if(frame.rollingCode & static_cast(somfy_flags_t::DemoMode)) + this->s.stateFlags |= static_cast(somfy_flags_t::DemoMode); + else + this->s.stateFlags &= ~(static_cast(somfy_flags_t::DemoMode)); + const bool isSunny = this->s.stateFlags & static_cast(somfy_flags_t::Sunny); + const bool isWindy = this->s.stateFlags & static_cast(somfy_flags_t::Windy); + this->sensors.onSensorFrame(wasSunny, wasWindy, isSunny, isWindy, (uint32_t)curTime); + this->emitState(); + this->events.onSensorFlags(); + } + break; + case somfy_commands::Prog: + case somfy_commands::MyUp: + case somfy_commands::MyDown: + case somfy_commands::MyUpDown: + case somfy_commands::UpDown: + this->lastFrame.processed = true; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + break; + + case somfy_commands::Flag: + this->lastFrame.processed = true; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state. + this->p_sunFlag(false); + //this->flags &= ~(static_cast(somfy_flags_t::SunFlag)); + this->events.onSaveWanted(); + this->emitState(); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + this->events.onSensorFlags(); + break; + case somfy_commands::SunFlag: + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state. + { + const bool isWindy = this->s.stateFlags & static_cast(somfy_flags_t::Windy); + const bool isSunny = this->s.stateFlags & static_cast(somfy_flags_t::Sunny); + //this->flags |= static_cast(somfy_flags_t::SunFlag); + this->p_sunFlag(true); + this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy)); + this->events.onSaveWanted(); + this->emitState(); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + this->events.onSensorFlags(); + } + break; + case somfy_commands::Up: + if(this->settings.shadeType == shade_types::drycontact) { + this->lastFrame.processed = true; + return; + } + else if(this->settings.shadeType == shade_types::drycontact2) { + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->s.currentPos != 0.0f) this->p_target(0); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + return; + } + if(this->settings.tiltType == tilt_types::tiltmotor || this->settings.tiltType == tilt_types::euromode) { + // Wait another half second just in case we are potentially processing a tilt. + if(!internal) this->lastFrame.await = curTime + 500; + else this->lastFrame.processed = true; + } + else { + // If from a remote we will simply be going up. + if(this->settings.tiltType == tilt_types::tiltonly && !internal) this->p_tiltTarget(0.0f); + else if(!internal) { + if(this->settings.tiltType != tilt_types::tiltonly) this->p_target(0.0f); + this->p_tiltTarget(0.0f); + } + this->lastFrame.processed = true; + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + } + break; + case somfy_commands::Down: + if(this->settings.shadeType == shade_types::drycontact) { + this->lastFrame.processed = true; + return; + } + else if(this->settings.shadeType == shade_types::drycontact2) { + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->s.currentPos != 100.0f) this->p_target(100); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + return; + } + if (!this->sensors.windBlocksDown((uint32_t)curTime)) { + if(this->settings.tiltType == tilt_types::tiltmotor || this->settings.tiltType == tilt_types::euromode) { + // Wait another half seccond just in case we are potentially processing a tilt. + if(!internal) this->lastFrame.await = curTime + 500; + else this->lastFrame.processed = true; + } + else { + this->lastFrame.processed = true; + if(!internal) { + if(this->settings.tiltType != tilt_types::tiltonly) this->p_target(100.0f); + if(this->settings.tiltType != tilt_types::none) this->p_tiltTarget(100.0f); + } + } + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + } + break; + case somfy_commands::My: + if(this->settings.shadeType == shade_types::drycontact2) return; + if(this->isToggle()) { // This is a one button device + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(!this->isIdle()) this->p_target(this->s.currentPos); + else if(this->s.currentPos == 100.0f) this->p_target(0.0f); + else if(this->s.currentPos == 0.0f) this->p_target(100.0f); + else this->p_target(this->s.lastMovement == -1 ? 100 : 0); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + return; + } + else if(this->settings.shadeType == shade_types::drycontact) { + // In this case we need to toggle the contact but we only should do this if + // this is not a repeat. + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->s.currentPos == 100.0f) this->p_target(0); + else if(this->s.currentPos == 0.0f) this->p_target(100); + else this->p_target(this->s.lastMovement == -1 ? 100 : 0); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + return; + } + if(this->isIdle()) { + if(!internal) { + // This frame is coming from a remote. We are potentially setting + // the my position. + this->lastFrame.await = curTime + 500; + } + else { + if(this->lastFrame.processed) return; + ESP_LOGI(TAG, "Moving to My target"); + this->lastFrame.processed = true; + if(this->s.myTiltPos >= 0.0f && this->s.myTiltPos <= 100.0f) this->p_tiltTarget(this->s.myTiltPos); + if(this->s.myPos >= 0.0f && this->s.myPos <= 100.0f && this->settings.tiltType != tilt_types::tiltonly) this->p_target(this->s.myPos); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + } + } + else { + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(!internal) { + if(this->settings.tiltType != tilt_types::tiltonly) this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + } + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + } + break; + case somfy_commands::StepUp: + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + dir = 0; + if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1; + if(!this->applyStep(-1, this->lastFrame.stepSize)) return; + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + break; + case somfy_commands::StepDown: + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->settings.shadeType == shade_types::drycontact || this->settings.shadeType == shade_types::drycontact2) return; + dir = 1; + if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1; + if(!this->applyStep(1, this->lastFrame.stepSize)) return; + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + break; + case somfy_commands::Toggle: + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(!this->isIdle()) this->p_target(this->s.currentPos); + else if(this->s.currentPos == 100.0f) this->p_target(0); + else if(this->s.currentPos == 0.0f) this->p_target(100); + else this->p_target(this->s.lastMovement == -1 ? 100 : 0); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + break; + case somfy_commands::Stop: + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + break; + case somfy_commands::Favorite: + if(this->lastFrame.processed) return; + this->lastFrame.processed = true; + if(this->settings.simMy) { + this->moveToMyPosition(now); + } + else { + if(this->s.myTiltPos >= 0.0f && this->s.myTiltPos <= 100.0f) this->p_tiltTarget(this->s.myTiltPos); + if(this->s.myPos >= 0.0f && this->s.myPos <= 100.0f && this->settings.tiltType != tilt_types::tiltonly) this->p_target(this->s.myPos); + this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); + } + break; + default: + dir = 0; + break; + } + //if(dir == 0 && this->settings.tiltType == tilt_types::tiltmotor && this->s.tiltDirection != 0) this->setTiltMovement(0, now); + this->setMovement(dir, now); +} +void ShadeBehaviour::onGroupCommand(somfy_commands cmd, uint8_t repeat, uint32_t now) { + // The reason why we are processing all frames here is so + // any linked remotes that may happen to be on the same ESPSomfy RTS + // device can trigger the appropriate actions. + if(this->shadeId == 255) return; + const uint32_t curTime = now; + int8_t dir = 0; + this->moveStart = this->tiltStart = curTime; + this->startPos = this->s.currentPos; + this->startTiltPos = this->s.currentTiltPos; + // If the command is coming from a remote then we are aborting all these positioning operations. + switch(cmd) { + case somfy_commands::Up: + if(this->settings.tiltType == tilt_types::tiltmotor) { + if(repeat >= TILT_REPEATS) + this->p_tiltTarget(0.0f); + else + this->p_target(0.0f); + } + else if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_target(100.0f); + this->p_currentPos(100.0f); + this->p_tiltTarget(0.0f); + } + else { + this->p_target(0.0f); + this->p_tiltTarget(0.0f); + } + break; + case somfy_commands::Down: + if (!this->sensors.windBlocksDown((uint32_t)curTime)) { + if(this->settings.tiltType == tilt_types::tiltmotor) { + if(repeat >= TILT_REPEATS) + this->p_tiltTarget(100.0f); + else + this->p_target(100.0f); + } + else if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_target(100.0f); + this->p_currentPos(100.0f); + this->p_tiltTarget(100.0f); + } + else { + this->p_target(100.0f); + if(this->settings.tiltType != tilt_types::none) this->p_tiltTarget(100.0f); + } + } + break; + case somfy_commands::My: + if(this->isIdle()) { + ESP_LOGD(TAG, "Shade #%d is idle", this->shadeId); + if(this->settings.simMy) { + this->moveToMyPosition(now); + } + else { + if(this->s.myTiltPos >= 0.0f && this->s.myTiltPos <= 100.0f) this->p_tiltTarget(this->s.myTiltPos); + if(this->s.myPos >= 0.0f && this->s.myPos <= 100.0f && this->settings.tiltType != tilt_types::tiltonly) this->p_target(this->s.myPos); + } + } + else { + if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_target(100.0f); + } + else this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + } + break; + case somfy_commands::StepUp: + // A group step is always one step. + if(!this->applyStep(-1, 1)) return; + break; + case somfy_commands::StepDown: + dir = 1; + if(!this->applyStep(1, 1)) return; + break; + case somfy_commands::Flag: + this->p_sunFlag(false); + if(this->settings.sunSensor) { + this->events.onSaveWanted(); + this->emitState(); + } + else { + ESP_LOGW(TAG, "Shade does not have sensor %d", joinShadeFlags(this->settings, false, this->s.stateFlags)); + } + break; + case somfy_commands::SunFlag: + if(this->settings.sunSensor) { + const bool isWindy = this->s.stateFlags & static_cast(somfy_flags_t::Windy); + const bool isSunny = this->s.stateFlags & static_cast(somfy_flags_t::Sunny); + this->p_sunFlag(true); + //this->flags |= static_cast(somfy_flags_t::SunFlag); + this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy)); + this->events.onSaveWanted(); + this->emitState(); + } + else + ESP_LOGW(TAG, "Shade does not have sensor %d", joinShadeFlags(this->settings, false, this->s.stateFlags)); + break; + default: + dir = 0; + break; + } + this->setMovement(dir, now); +} +void ShadeBehaviour::setTiltMovement(int8_t dir, uint32_t now) { + int8_t currDir = this->s.tiltDirection; + if(dir == 0) { + // The shade tilt is stopped. + this->startTiltPos = this->s.currentTiltPos; + this->tiltStart = 0; + this->p_tiltDirection(dir); + if(currDir != dir) { + this->commitTiltPosition(); + } + } + else if(this->s.tiltDirection != dir) { + this->tiltStart = now; + this->startTiltPos = this->s.currentTiltPos; + this->p_tiltDirection(dir); + } + if(this->s.tiltDirection != currDir) { + this->emitState(); + } +} +void ShadeBehaviour::onTxQueued(uint32_t ticket, uint32_t now) { + // This runs before the caller sets the new target so direction still says + // whether the shade was moving. A reversal is simulated as soon as it is + // queued even though the motor turns once it is on air. See TxHold::mark + // for which of several queued commands is held. + this->tx.hold.mark(ticket, this->s.direction != 0 || this->s.tiltDirection != 0, now); +} +void ShadeBehaviour::onTxEvent(const somfy_tx_event_t &evt) { + const ShadeTxActions act = this->tx.onEvent(evt, this->s.direction != 0 || this->s.tiltDirection != 0); + if(act.startMovement) { + // The motor starts now so the simulated movement does too. + this->moveStart = this->tiltStart = act.startAt; + } + if(act.cancelMovement) { + // The motor never got the command so it hasn't moved. + ESP_LOGW(TAG, "[Shade %u] Command dropped by the radio, staying at %.2f%%", this->shadeId, this->s.currentPos); + this->settingPos = this->settingTiltPos = this->settingMyPos = false; + this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + this->emitState(); + } + if(act.commitMy) this->commitMyPosition(); + if(act.restoreMy) { + ESP_LOGW(TAG, "[Shade %u] Favorite not sent in full, keeping the previous one", this->shadeId); + this->p_myPos(act.myOldPos); + this->p_myTiltPos(act.myOldTiltPos); + this->emitState(); + } + if(act.runToLimit) { + ESP_LOGW(TAG, "[Shade %u] Stop dropped by the radio, running on to the limit", this->shadeId); + this->runToLimit(act.limitDir, act.limitTilt, act.limitSince); + this->emitState(); + } +} +bool ShadeBehaviour::sendStop(int8_t dir, bool tilt, uint32_t now) { + // Stops jump the radio queue so the motor stops as close to the target as it can. + if(const send_result_t sent = this->transmit(somfy_commands::My, this->remote.repeats, 0, 0, true, now)) { + this->tx.stopSent(sent.ticket, now, dir, tilt); + return true; + } + ESP_LOGW(TAG, "[Shade %u] Stop not sent, running on to the limit", this->shadeId); + this->runToLimit(dir, tilt, now); + return false; +} +bool ShadeBehaviour::sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos, uint32_t now) { + // The new favorite is already in myPos/myTiltPos. It is committed once the + // whole long press is on air, see processTxEvent. + if(const send_result_t sent = this->transmit(somfy_commands::My, SETMY_REPEATS, 0, delayMs, false, now)) { + this->tx.setMySent(sent.ticket, oldPos, oldTiltPos); + // Without the radio (GPIO remotes) there is nothing to wait for. + if(sent.ticket == 0) this->commitMyPosition(); + return true; + } + // The motor never learned it so keep the favorite it has. + this->p_myPos(oldPos); + this->p_myTiltPos(oldTiltPos); + return false; +} +void ShadeBehaviour::runToLimit(int8_t dir, bool tilt, uint32_t since) { + // The motor didn't get a stop at the target so it keeps going until its end + // limit. Carry on simulating that from where and when it should have stopped. + if(tilt) { + this->startTiltPos = this->s.currentTiltPos; + this->tiltStart = since; + this->settingTiltPos = false; + this->p_tiltTarget(limitTarget(dir)); + } + else { + this->startPos = this->s.currentPos; + this->moveStart = since; + this->settingPos = false; + this->p_target(limitTarget(dir)); + } + // The favorite can't be set where the shade won't stop. + this->settingMyPos = false; +} +void ShadeBehaviour::applySensorMove(SensorMove move) { + const SensorTargets t = sensorTargets(move, this->settings.tiltType, this->s.myPos, this->s.myTiltPos); + if(t.setLift) this->p_target(t.lift); + if(t.setTilt) this->p_tiltTarget(t.tilt); +} +bool ShadeBehaviour::applyStep(int8_t dir, uint8_t frameStepSize) { + const StepTargets t = stepTargets(this->settings.tiltType, dir, this->s.currentPos, this->s.currentTiltPos, + this->settings.upTime, this->settings.downTime, this->settings.tiltTime, this->settings.stepSize, frameStepSize); + if(t.action == StepAction::ignore) return false; + if(t.setLift) this->p_target(t.lift); + if(t.setTilt) this->p_tiltTarget(t.tilt); + return true; +} +void ShadeBehaviour::setMovement(int8_t dir, uint32_t now) { + int8_t currDir = this->s.direction; + int8_t currTiltDir = this->s.tiltDirection; + if(dir == 0) { + if(currDir != dir || currTiltDir != dir) this->commitShadePosition(); + } + else { + this->tiltStart = this->moveStart = now; + this->startPos = this->s.currentPos; + this->startTiltPos = this->s.currentTiltPos; + } + if(this->s.direction != currDir || currTiltDir != this->s.tiltDirection) { + this->emitState(); + } +} +bool ShadeBehaviour::setMyPosition(int8_t pos, int8_t tilt, uint32_t now) { + if(!this->isIdle()) return true; // Don't do this if it is moving. + bool sent = true; + if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_myPos(-1.0f); + if(tilt != floor(this->s.currentTiltPos)) { + // The favorite is set once we get there, if the move went out. + if(tilt == floor(this->s.myTiltPos)) + sent = this->settingMyPos = this->moveToMyPosition(now); + else + sent = this->settingMyPos = this->moveToTarget(100, tilt, now); + } + else if(tilt == floor(this->s.myTiltPos)) { + // Of so we need to clear the my position. These motors are finicky so send + // a my command to ensure we are actually at the my position then send the clear + // command. There really is no other way to do this. + if(this->s.currentTiltPos != this->s.myTiltPos) { + sent = this->settingMyPos = this->moveToMyPosition(now); + } + else if((sent = this->transmit(somfy_commands::My, this->remote.repeats, 0, 0, false, now).sent)) { + this->settingPos = false; + this->settingMyPos = true; + } + } + else return this->storeMyPosition(-1.0f, this->s.currentTiltPos, now); + this->commitMyPosition(); + this->emitState(); + } + else if(this->settings.tiltType != tilt_types::none) { + if(tilt < 0) tilt = 0; + if(pos != floor(this->s.currentPos) || tilt != floor(this->s.currentTiltPos)) { + // The favorite is set once we get there, if the move went out. + if(pos == floor(this->s.myPos) && tilt == floor(this->s.myTiltPos)) + sent = this->settingMyPos = this->moveToMyPosition(now); + else + sent = this->settingMyPos = this->moveToTarget(pos, tilt, now); + } + else if(pos == floor(this->s.myPos) && tilt == floor(this->s.myTiltPos)) { + // Of so we need to clear the my position. These motors are finicky so send + // a my command to ensure we are actually at the my position then send the clear + // command. There really is no other way to do this. + if(this->s.currentPos != this->s.myPos || this->s.currentTiltPos != this->s.myTiltPos) { + sent = this->settingMyPos = this->moveToMyPosition(now); + } + else if((sent = this->transmit(somfy_commands::My, this->remote.repeats, 0, 0, false, now).sent)) { + this->settingPos = false; + this->settingMyPos = true; + } + } + else return this->storeMyPosition(this->s.currentPos, this->s.currentTiltPos, now); + this->commitMyPosition(); + this->emitState(); + } + else { + if(pos != floor(this->s.currentPos)) { + // The favorite is set once we get there, if the move went out. + if(pos == floor(this->s.myPos)) + sent = this->settingMyPos = this->moveToMyPosition(now); + else + sent = this->settingMyPos = this->moveToTarget(pos, -1.0f, now); + } + else if(pos == floor(this->s.myPos)) { + // Of so we need to clear the my position. These motors are finicky so send + // a my command to ensure we are actually at the my position then send the clear + // command. There really is no other way to do this. + if(this->s.myPos != this->s.currentPos) { + sent = this->settingMyPos = this->moveToMyPosition(now); + } + else if((sent = this->transmit(somfy_commands::My, this->remote.repeats, 0, 0, false, now).sent)) { + this->settingPos = false; + this->settingMyPos = true; + } + } + else sent = this->storeMyPosition(this->s.currentPos, -1.0f, now); + } + return sent; +} +bool ShadeBehaviour::storeMyPosition(float pos, float tiltPos, uint32_t now) { + // Set it before sending so processFrame doesn't head for the old one. It is + // only committed once the motor was told about it. + const float oldPos = this->s.myPos; + const float oldTiltPos = this->s.myTiltPos; + this->p_myPos(pos); + this->p_myTiltPos(tiltPos); + const bool sent = this->sendSetMy(0, oldPos, oldTiltPos, now); + this->emitState(); + return sent; +} +bool ShadeBehaviour::moveToMyPosition(uint32_t now) { + if(!this->isIdle()) return true; + ESP_LOGI(TAG, "Moving to My Position"); + if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_currentPos(100.0f); + this->p_myPos(-1.0f); + } + if(this->s.currentPos == this->s.myPos) { + if(this->settings.tiltType != tilt_types::none) { + if(this->s.currentTiltPos == this->s.myTiltPos) return true; // Nothing to see here since we are already here. + } + else + return true; + } + if(this->s.myPos == -1 && (this->settings.tiltType == tilt_types::none || this->s.myTiltPos == -1)) return true; + this->settingPos = false; + if(this->settings.simMy) { + ESP_LOGI(TAG, "Moving to simulated favorite"); + if(!this->moveToTarget(this->s.myPos, this->s.myTiltPos, now)) return false; + } + else if(!this->transmit(somfy_commands::My, this->remote.repeats, 0, 0, false, now).sent) return false; + if(this->settings.tiltType != tilt_types::tiltonly && this->s.myPos >= 0.0f && this->s.myPos <= 100.0f) this->p_target(this->s.myPos); + if(this->s.myTiltPos >= 0.0f && this->s.myTiltPos <= 100.0f) this->p_tiltTarget(this->s.myTiltPos); + return true; +} +bool ShadeBehaviour::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint32_t now) { + ESP_LOGD(TAG, "Send command start"); + // This sendCommand function will always be called externally. sendCommand at the remote level + // is expected to be called internally when the motor needs commanded. + // If same direction command sent while already moving, stop the state tracking. + // The real motor stops on its own when it receives the same direction again. + if((cmd == somfy_commands::Up && this->s.direction == -1) || + (cmd == somfy_commands::Down && this->s.direction == 1)) { + ESP_LOGD(TAG, "Same command as dir"); + if(!this->transmit(cmd, repeat, 0, 0, false, now).sent) return false; + this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + this->setMovement(0, now); + return true; + } + // The simulated state is only moved for commands the radio took. + if(cmd == somfy_commands::Up) { + if(this->settings.tiltType == tilt_types::euromode) { + // In euromode we need to long press for 2 seconds on the + // up command. + if(!this->transmit(cmd, TILT_REPEATS, 0, 0, false, now).sent) return false; + this->p_target(0.0f); + } + else { + if(!this->transmit(cmd, repeat, 0, 0, false, now).sent) return false; + if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_target(100.0f); + this->p_tiltTarget(0.0f); + this->p_currentPos(100.0f); + } + else this->p_target(0.0f); + if(this->settings.tiltType == tilt_types::integrated) this->p_tiltTarget(0.0f); + } + } + else if(cmd == somfy_commands::Down) { + if(this->settings.tiltType == tilt_types::euromode) { + // In euromode we need to long press for 2 seconds on the + // down command. + if(!this->transmit(cmd, TILT_REPEATS, 0, 0, false, now).sent) return false; + this->p_target(100.0f); + } + else { + if(!this->transmit(cmd, repeat, 0, 0, false, now).sent) return false; + if(this->settings.tiltType == tilt_types::tiltonly) { + this->p_target(100.0f); + this->p_tiltTarget(100.0f); + this->p_currentPos(100.0f); + } + else this->p_target(100.0f); + if(this->settings.tiltType == tilt_types::integrated) this->p_tiltTarget(100.0f); + } + } + else if(cmd == somfy_commands::My) { + if(this->isToggle() || this->settings.shadeType == shade_types::drycontact) { + if(!this->transmit(cmd, repeat, 0, 0, false, now).sent) return false; + } + else if(this->settings.shadeType == shade_types::drycontact2){ + ESP_LOGD(TAG, "Send command start 1"); + return true; + } + else if(this->isIdle()) { + bool sent = this->moveToMyPosition(now); + ESP_LOGD(TAG, "Send command end 2"); + return sent; + } + else { + if(!this->transmit(cmd, repeat, 0, 0, false, now).sent) return false; + if(this->settings.tiltType != tilt_types::tiltonly) this->p_target(this->s.currentPos); + this->p_tiltTarget(this->s.currentTiltPos); + } + } + else if(cmd == somfy_commands::Toggle) { + if(this->remote.bitLength != 80) { if(!this->transmit(somfy_commands::My, repeat, stepSize, 0, false, now).sent) return false; } + else if(!this->transmit(somfy_commands::Toggle, repeat, 0, 0, false, now).sent) return false; + } + else if(this->isToggle() && cmd == somfy_commands::Prog) { + if(!this->transmit(somfy_commands::Toggle, repeat, stepSize, 0, false, now).sent) return false; + } + else { + if(!this->transmit(cmd, repeat, stepSize, 0, false, now).sent) return false; + } + ESP_LOGD(TAG, "Send command end"); + return true; +} +uint8_t ShadeBehaviour::tiltRepeats() const { return this->settings.tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->remote.repeats; } +bool ShadeBehaviour::sendTiltCommand(somfy_commands cmd, uint32_t now) { + float target; + if(cmd == somfy_commands::Up) target = 0.0f; + else if(cmd == somfy_commands::Down) target = 100.0f; + else if(cmd == somfy_commands::My) target = this->s.currentTiltPos; + else return true; + if(!this->transmit(cmd, this->tiltRepeats(), 0, 0, false, now).sent) return false; + this->p_tiltTarget(target); + return true; +} +bool ShadeBehaviour::moveToTiltTarget(float target, uint32_t now) { + somfy_commands cmd = somfy_commands::My; + if(target < this->s.currentTiltPos) + cmd = somfy_commands::Up; + else if(target > this->s.currentTiltPos) + cmd = somfy_commands::Down; + if(target >= 0.0f && target <= 100.0f) { + // Only send a command if the lift is not moving. + if(this->s.currentPos == this->s.target || this->settings.tiltType == tilt_types::tiltmotor) { + if(cmd != somfy_commands::My) { + ESP_LOGI(TAG, "Moving Tilt to %.2f%% from %.2f%% using %s", + target, this->s.currentTiltPos, translateSomfyCommand(cmd).c_str()); + if(!this->transmit(cmd, this->tiltRepeats(), 0, 0, false, now).sent) return false; + } + // If the blind is currently moving then the command to stop it + // will occur on its own when the tilt target is set. + } + this->p_tiltTarget(target); + } + if(cmd != somfy_commands::My) this->settingTiltPos = true; + return true; +} +bool ShadeBehaviour::moveToTarget(float pos, float tilt, uint32_t now) { + ESP_LOGI(TAG, "[Shade %u] moveToTarget(pos=%.2f, tilt=%.2f) settingPos=%d direction=%d currentTarget=%.2f currentPos=%.2f", + this->shadeId, pos, tilt, this->settingPos, this->s.direction, this->s.target, this->s.currentPos); + somfy_commands cmd = somfy_commands::My; + if(this->isToggle()) { + // Overload this as we cannot seek a position on a garage door or single button device. + this->p_target(pos); + this->p_currentPos(pos); + this->emitState(); + return true; + } + if(this->settings.tiltType == tilt_types::tiltonly) { + // A tilt only shade is always at 100%. + this->p_target(100.0f); + this->p_myPos(-1.0f); + this->p_currentPos(100.0f); + pos = 100; + if(tilt < this->s.currentTiltPos) cmd = somfy_commands::Up; + else if(tilt > this->s.currentTiltPos) cmd = somfy_commands::Down; + } + else { + if(pos < this->s.currentPos) + cmd = somfy_commands::Up; + else if(pos > this->s.currentPos) + cmd = somfy_commands::Down; + else if(tilt >= 0 && tilt < this->s.currentTiltPos) + cmd = somfy_commands::Up; + else if(tilt >= 0 && tilt > this->s.currentTiltPos) + cmd = somfy_commands::Down; + } + if(cmd != somfy_commands::My) { + if(tilt >= 0) { + ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% tilt %.2f%% from %.2f%% using %s", + pos, this->s.currentPos, tilt, this->s.currentTiltPos, translateSomfyCommand(cmd).c_str()); + } else { + ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% using %s", + pos, this->s.currentPos, translateSomfyCommand(cmd).c_str()); + } + if(!this->transmit(cmd, this->settings.tiltType == tilt_types::euromode ? TILT_REPEATS : this->remote.repeats, 0, 0, false, now).sent) return false; + this->settingPos = true; + this->p_target(pos); + if(tilt >= 0) { + this->p_tiltTarget(tilt); + this->settingTiltPos = true; + } + } + return true; +} diff --git a/src/ShadeBehaviour.h b/src/ShadeBehaviour.h new file mode 100644 index 0000000..7af0d21 --- /dev/null +++ b/src/ShadeBehaviour.h @@ -0,0 +1,167 @@ +#ifndef SHADE_BEHAVIOUR_H +#define SHADE_BEHAVIOUR_H +#include +#include "SomfyFrame.h" +#include "ShadeSettings.h" +#include "ShadeMotion.h" +#include "ShadeSensors.h" +#include "ShadeChanges.h" + +// What a shade does with frames, commands and radio events, and where that +// leaves it. Kept free of the shade model, radio and network so it can be +// unit tested on the PC (pio test -e native). Only loop() calls it. + +// What sending a command did. ticket is the radio job carrying it, 0 when the +// command went out over GPIO instead. +struct send_result_t { + bool sent = false; + uint32_t ticket = 0; + explicit operator bool() const { return this->sent; } +}; + +// How a shade's commands reach its motor, over the radio or GPIO. When it +// sends, the frame as sent is in the shade's lastFrame and every other shade +// has heard it; the shade hears its own frame itself. +class MotorLink { + public: + // sent is false when the radio could not take the command. + virtual send_result_t send(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) = 0; +}; + +// What a shade's behaviour tells the rest of the firmware, as it happens. +class ShadeEvents { + public: + // SHADE_CHANGED_* bits for the change fan-out. + virtual void onChanged(uint16_t bits) = 0; + // A command reached the shade. source is "remote", "internal" and so on. + virtual void onCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress) = 0; + // A position, the favorite or the sun flag changed and is worth saving. + virtual void onSaveWanted() = 0; + // A sensor, sun flag or flag frame arrived, so the groups work their + // flags out again. + virtual void onSensorFlags() = 0; +}; + +class ShadeBehaviour { + public: + // Where the shade is and where it is going. Positions run 0-100, the + // favorite is -1 when there is none. + struct State { + float currentPos = 0.0f; + float currentTiltPos = 0.0f; + float target = 0.0f; + float tiltTarget = 0.0f; + int8_t direction = 0; // 0 = stopped, 1 = down, -1 = up. + int8_t tiltDirection = 0; // 0 = stopped, 1 = clockwise, -1 = counter clockwise. + int8_t lastMovement = 0; + float myPos = -1.0f; + float myTiltPos = -1.0f; + // The flags bits that are state: sun flag, sunny, windy, demo mode and + // the unused lighted. The setting bits are in ShadeSettings. + uint8_t stateFlags = 0; + }; + // lastFrame is the shade's last frame, shared with the send path that + // builds the next one from it. shadeId and name are for the log. + ShadeBehaviour(const ShadeSettings &settings, const RemoteSettings &remote, somfy_frame_t &lastFrame, + MotorLink &link, ShadeEvents &events, const uint8_t &shadeId, const char *name); + ShadeBehaviour(const ShadeBehaviour &) = delete; + ShadeBehaviour &operator=(const ShadeBehaviour &) = delete; + const State &state() const { return this->s; } + bool isIdle() const; + bool isAtTarget() const; + + // A frame from a remote, sensor or linked remote, or one the shade or + // another shade sent (internal). + void onFrame(somfy_frame_t &frame, bool internal, uint32_t now); + // A remote button may still be held: acts on lastFrame once it is clear + // whether the press was short or long. + void onWaitingFrame(uint32_t now); + // A group the shade belongs to sent cmd. + void onGroupCommand(somfy_commands cmd, uint8_t repeat, uint32_t now); + // Every loop() pass: moves the simulated position, stops at the target and + // stores the favorite. + void tick(uint32_t now); + // A command that may set the shade moving was queued as radio job ticket. + void onTxQueued(uint32_t ticket, uint32_t now); + // A radio job started, finished, was aborted or dropped. + void onTxEvent(const somfy_tx_event_t &evt); + + // Commands from the web, MQTT and Home Assistant. False when the radio + // refused a command they sent. + bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint32_t now); + bool sendTiltCommand(somfy_commands cmd, uint32_t now); + bool moveToTarget(float pos, float tilt, uint32_t now); + bool moveToTiltTarget(float target, uint32_t now); + bool setMyPosition(int8_t pos, int8_t tilt, uint32_t now); + bool moveToMyPosition(uint32_t now); + + // Puts the shade at pos and tilt (-1 leaves one alone) without moving it. + void setPosition(float pos, float tilt); + // Where the config file says the shade was. The targets are where it is. + void restore(float pos, float tiltPos, float myPos, float myTiltPos); + // Forgets the tilt of a shade that has none. + void clearTilt(); + void setStateFlags(uint8_t flags) { this->s.stateFlags = flags; } + // Back to a stopped shade at 0 with no favorite, for an empty slot. + void clear(); + + private: + const ShadeSettings &settings; + const RemoteSettings &remote; + somfy_frame_t &lastFrame; + MotorLink &link; + ShadeEvents &events; + const uint8_t &shadeId; + const char *name; + State s; + uint32_t moveStart = 0; + uint32_t tiltStart = 0; + ShadeSensors sensors; + float startPos = 0.0f; + float startTiltPos = 0.0f; + bool settingMyPos = false; + bool settingPos = false; + bool settingTiltPos = false; + uint32_t awaitMy = 0; + // Radio jobs whose outcome decides what the motor really did. + ShadeTxState tx; + + // Sends through the link, then hears the frame as every other shade has + // and waits for it to go on air if it sets the shade moving. + send_result_t transmit(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority, uint32_t now); + bool sendStop(int8_t dir, bool tilt, uint32_t now); + bool sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos, uint32_t now); + // Makes pos/tiltPos the favorite and tells the motor to store it. + bool storeMyPosition(float pos, float tiltPos, uint32_t now); + // Repeats for a tilt command, more for a separate tilt motor. + uint8_t tiltRepeats() const; + void runToLimit(int8_t dir, bool tilt, uint32_t since); + // Sets the targets the sun or wind sends the shade to. + void applySensorMove(SensorMove move); + // Sets the targets for a step in dir. False when the step is dropped. + bool applyStep(int8_t dir, uint8_t frameStepSize); + void setTiltMovement(int8_t dir, uint32_t now); + void setMovement(int8_t dir, uint32_t now); + bool isToggle() const { return isToggleShade(this->settings.shadeType); } + somfy_commands transformCommand(somfy_commands cmd) const; + int8_t transformPosition(float fpos) const { return displayPosition(fpos, this->settings.flipPosition); } + void emitState() { this->events.onChanged(SHADE_CHANGED_STATE); } + void emitCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress) { this->events.onCommand(cmd, source, sourceAddress); } + void commitShadePosition() { this->events.onSaveWanted(); } + void commitTiltPosition() { this->events.onSaveWanted(); } + void commitMyPosition() { this->events.onSaveWanted(); } + // State setters. Each marks what clients see as changed when it does change. + float p_currentPos(float pos); + float p_currentTiltPos(float pos); + int8_t p_direction(int8_t dir); + int8_t p_tiltDirection(int8_t dir); + float p_target(float target); + float p_tiltTarget(float target); + float p_myPos(float pos); + float p_myTiltPos(float pos); + bool p_flag(somfy_flags_t flag, bool val); + bool p_sunFlag(bool val); + bool p_sunny(bool val); + bool p_windy(bool val); +}; +#endif diff --git a/src/ShadeController.cpp b/src/ShadeController.cpp index 86fdd03..87e0b06 100644 --- a/src/ShadeController.cpp +++ b/src/ShadeController.cpp @@ -119,14 +119,16 @@ void SomfyRoom::clear() { strcpy(this->name, ""); } bool SomfyRoom::save() { somfy.commit(); return true; } -void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) { +void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal, const SomfyRemote *skip) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + if(&this->shades[i] == skip) continue; if(this->shades[i].getShadeId() != 255) this->shades[i].processFrame(frame, internal); } } -void SomfyShadeController::markTxPending(uint32_t remoteAddress, uint32_t ticket) { +void SomfyShadeController::markTxPending(uint32_t remoteAddress, uint32_t ticket, const SomfyRemote *skip) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { SomfyShade *shade = &this->shades[i]; + if(shade == skip) continue; if(shade->getShadeId() != 255 && shade->findRemote(remoteAddress)) shade->markTxPending(ticket); } // Group commands move their member shades through processInternalCommand. diff --git a/src/ShadeGpio.cpp b/src/ShadeGpio.cpp index 2b07f10..0024326 100644 --- a/src/ShadeGpio.cpp +++ b/src/ShadeGpio.cpp @@ -9,17 +9,17 @@ void SomfyShade::setGPIOs() { uint8_t p_on = (this->gpioFlags & (uint8_t)gpio_flags_t::LowLevelTrigger) == 0x00 ? HIGH : LOW; uint8_t p_off = (this->gpioFlags & (uint8_t)gpio_flags_t::LowLevelTrigger) == 0x00 ? LOW : HIGH; - int8_t dir = this->direction; + int8_t dir = this->state().direction; if(dir == 0 && this->tiltType == tilt_types::integrated) - dir = this->tiltDirection; + dir = this->state().tiltDirection; else if(this->tiltType == tilt_types::tiltonly) - dir = this->tiltDirection; + dir = this->state().tiltDirection; if(this->shadeType == shade_types::drycontact) { - digitalWrite(this->gpioDown, this->currentPos == 100 ? p_on : p_off); - this->gpioDir = this->currentPos == 100 ? 1 : -1; + digitalWrite(this->gpioDown, this->state().currentPos == 100 ? p_on : p_off); + this->gpioDir = this->state().currentPos == 100 ? 1 : -1; } else if(this->shadeType == shade_types::drycontact2) { - if(this->currentPos == 100) { + if(this->state().currentPos == 100) { digitalWrite(this->gpioDown, p_off); digitalWrite(this->gpioUp, p_on); } @@ -27,7 +27,7 @@ void SomfyShade::setGPIOs() { digitalWrite(this->gpioUp, p_off); digitalWrite(this->gpioDown, p_on); } - this->gpioDir = this->currentPos == 100 ? 1 : -1; + this->gpioDir = this->state().currentPos == 100 ? 1 : -1; } else { switch(dir) { diff --git a/src/ShadeTypes.h b/src/ShadeTypes.h index 7e2921d..0fca5cc 100644 --- a/src/ShadeTypes.h +++ b/src/ShadeTypes.h @@ -35,6 +35,19 @@ enum class shade_types : byte { cgate1 = 0x0F, rgate1 = 0x10 }; +// A garage door or gate worked with one button: each press moves, stops or +// reverses it, so it can't be sent to a position. +inline bool isToggleShade(shade_types type) { + switch(type) { + case shade_types::garage1: + case shade_types::lgate1: + case shade_types::cgate1: + case shade_types::rgate1: + return true; + default: + return false; + } +} // The bits of a shade's or group's flags. enum class somfy_flags_t : byte { SunFlag = 0x01, diff --git a/src/Somfy.h b/src/Somfy.h index b95ab61..417566f 100644 --- a/src/Somfy.h +++ b/src/Somfy.h @@ -9,6 +9,7 @@ #include "RadioScheduler.h" #include "ShadeTypes.h" #include "ShadeSettings.h" +#include "ShadeBehaviour.h" #include "ShadeMotion.h" #include "ShadeSensors.h" #include "ShadeChanges.h" @@ -37,13 +38,6 @@ class SomfyRoom { void unpublish(); }; -// What sending a command did. ticket is the radio job carrying it, 0 when the -// command went out over GPIO instead. -struct send_result_t { - bool sent = false; - uint32_t ticket = 0; - explicit operator bool() const { return this->sent; } -}; class SomfyRemote : public RemoteSettings { // These sizes for the data have been // confirmed. The address is actually 24bits @@ -77,7 +71,9 @@ class SomfyRemote : public RemoteSettings { // command. Nothing about the remote or shade state has changed in that case. virtual bool sendCommand(somfy_commands cmd); virtual bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0); - send_result_t sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority = false); + // hearsItself leaves this remote out of the echo, for a shade whose + // ShadeBehaviour hears its own frame. + send_result_t sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority = false, bool hearsItself = false); bool sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat); bool repeatFrame(uint8_t repeat); virtual uint16_t p_lastRollingCode(uint16_t code); @@ -89,47 +85,16 @@ class SomfyLinkedRemote : public SomfyRemote { public: SomfyLinkedRemote(); }; -class SomfyShade : public SomfyRemote, public ShadeSettings { +// A shade: its identity, settings, remotes and outputs. What it does with +// frames and commands is its ShadeBehaviour; the shade sends for it +// (MotorLink) and passes on what it reports (ShadeEvents). +class SomfyShade : public SomfyRemote, public ShadeSettings, private MotorLink, private ShadeEvents { protected: uint8_t shadeId = 255; - uint64_t moveStart = 0; - uint64_t tiltStart = 0; - ShadeSensors sensors; - float startPos = 0.0f; - float startTiltPos = 0.0f; - bool settingMyPos = false; - bool settingPos = false; - bool settingTiltPos = false; - uint32_t awaitMy = 0; - // The flags bits that are state: sun flag, sunny, windy, demo mode and the - // unused lighted. The setting bits are in ShadeSettings and light. - uint8_t stateFlags = 0; - // Radio jobs whose outcome decides what the motor really did. - ShadeTxState tx; - bool sendStop(int8_t dir, bool tilt); - bool sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos); - // Makes pos/tiltPos the favorite and tells the motor to store it. - bool storeMyPosition(float pos, float tiltPos); - // Repeats for a tilt command, more for a separate tilt motor. - uint8_t tiltRepeats() const; - void runToLimit(int8_t dir, bool tilt, uint32_t since); - // Sets the targets the sun or wind sends the shade to. - void applySensorMove(SensorMove move); - // Sets the targets for a step in dir. False when the step is dropped. - bool applyStep(int8_t dir, uint8_t frameStepSize); public: uint8_t roomId = 0; int8_t sortOrder = 0; bool light = false; - float currentPos = 0.0f; - float currentTiltPos = 0.0f; - int8_t lastMovement = 0; - int8_t direction = 0; // 0 = stopped, 1=down, -1=up. - int8_t tiltDirection = 0; // 0=stopped, 1=clockwise, -1=counter clockwise - float target = 0.0f; - float tiltTarget = 0.0f; - float myPos = -1.0f; - float myTiltPos = -1.0f; SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES]; bool paired = false; int8_t validateJSON(JsonObject &obj); @@ -138,11 +103,12 @@ class SomfyShade : public SomfyRemote, public ShadeSettings { char name[21] = ""; void setShadeId(uint8_t id) { shadeId = id; } uint8_t getShadeId() { return shadeId; } + // Where the shade is and where it is going. + const ShadeBehaviour::State &state() const { return this->behaviour.state(); } uint8_t getFlags() override; // Sets the setting and state bits from a saved or sent flags byte. void setFlags(uint8_t flags); bool save(); - bool isIdle(); bool isInGroup(); void checkMovement(); void processFrame(somfy_frame_t &frame, bool internal = false); @@ -151,17 +117,19 @@ class SomfyShade : public SomfyRemote, public ShadeSettings { void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1); void markTxPending(uint32_t ticket); void processTxEvent(const somfy_tx_event_t &evt); - void setTiltMovement(int8_t dir); - void setMovement(int8_t dir); - void setTarget(float target); - bool isAtTarget(); - bool isToggle(); + bool isToggle() { return isToggleShade(this->shadeType); } bool moveToTarget(float pos, float tilt = -1.0f); // These return false when the radio refused a command they sent. bool moveToTiltTarget(float target); bool sendTiltCommand(somfy_commands cmd); bool sendCommand(somfy_commands cmd); bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0); + // Puts the shade at pos and tilt (-1 leaves one alone) without moving it. + void setPosition(float pos, float tilt); + // Where the config file says the shade was. + void restore(float pos, float tiltPos, float myPos, float myTiltPos) { this->behaviour.restore(pos, tiltPos, myPos, myTiltPos); } + // Forgets the tilt of a shade that has none. + void clearTilt() { this->behaviour.clearTilt(); } bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0); bool unlinkRemote(uint32_t remoteAddress); // Marks the shade as changed; the change fan-out sends shadeState once per @@ -186,27 +154,11 @@ class SomfyShade : public SomfyRemote, public ShadeSettings { // Publishes the MQTT topics of the SHADE_CHANGED_* bits in changed. void publishTopics(uint16_t changed); void commit(); - void commitShadePosition(); - void commitTiltPosition(); - void commitMyPosition(); void clear(); int8_t transformPosition(float fpos); void setGPIOs(); void triggerGPIOs(somfy_frame_t &frame); bool usesPin(uint8_t pin); - // State Setters - int8_t p_direction(int8_t dir); - int8_t p_tiltDirection(int8_t dir); - float p_target(float target); - float p_tiltTarget(float target); - float p_myPos(float pos); - float p_myTiltPos(float pos); - bool p_flag(somfy_flags_t flag, bool val); - bool p_sunFlag(bool val); - bool p_sunny(bool val); - bool p_windy(bool val); - float p_currentPos(float pos); - float p_currentTiltPos(float pos); uint16_t p_lastRollingCode(uint16_t code); bool publish(const char *topic, const char *val, bool retain = false); bool publish(const char *topic, uint8_t val, bool retain = false); @@ -216,6 +168,15 @@ class SomfyShade : public SomfyRemote, public ShadeSettings { bool publish(const char *topic, bool val, bool retain = false); void publishDisco(); void unpublishDisco(); + private: + // MotorLink: the radio or GPIO path every remote sends through. + send_result_t send(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) override; + // ShadeEvents + void onChanged(uint16_t bits) override { this->changed |= bits; } + void onCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress) override { this->emitCommand(cmd, source, sourceAddress); } + void onSaveWanted() override; + void onSensorFlags() override; + ShadeBehaviour behaviour{*this, *this, this->lastFrame, *this, *this, this->shadeId, this->name}; }; class SomfyGroup : public SomfyRemote { protected: @@ -305,8 +266,9 @@ class SomfyShadeController { SomfyGroup * findGroupByRemoteAddress(uint32_t address); // Returns the radio job's ticket, 0 when the radio can't take it. uint32_t sendFrame(somfy_frame_t &frame, uint8_t repeats = 0, uint16_t delayMs = 0, bool priority = false); - void processFrame(somfy_frame_t &frame, bool internal = false); - void markTxPending(uint32_t remoteAddress, uint32_t ticket); + // skip, when set, is left out: a shade that hears its own frame. + void processFrame(somfy_frame_t &frame, bool internal = false, const SomfyRemote *skip = nullptr); + void markTxPending(uint32_t remoteAddress, uint32_t ticket, const SomfyRemote *skip = nullptr); 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 diff --git a/src/SomfyFrame.cpp b/src/SomfyFrame.cpp index 893f2af..e590548 100644 --- a/src/SomfyFrame.cpp +++ b/src/SomfyFrame.cpp @@ -431,3 +431,17 @@ void somfy_frame_t::copy(somfy_frame_t &frame) { this->repeats = frame.repeats; } } +bool startsMovement(somfy_commands cmd) { + switch(cmd) { + case somfy_commands::My: + case somfy_commands::Up: + case somfy_commands::Down: + case somfy_commands::StepUp: + case somfy_commands::StepDown: + case somfy_commands::Toggle: + case somfy_commands::Favorite: + return true; + default: + return false; + } +} diff --git a/src/SomfyFrame.h b/src/SomfyFrame.h index d578f73..2f27e4a 100644 --- a/src/SomfyFrame.h +++ b/src/SomfyFrame.h @@ -39,6 +39,8 @@ somfy_commands translateSomfyCommand(const String& string); somfy_commands flipCommand(somfy_commands cmd); // The command that turns the sun flag on (SunFlag) or off (Flag). somfy_commands sunFlagCommand(bool on); +// Whether cmd can set a shade moving. +bool startsMovement(somfy_commands cmd); struct somfy_rx_t; struct somfy_frame_t { diff --git a/src/SomfyRemote.cpp b/src/SomfyRemote.cpp index 27c5417..484df1a 100644 --- a/src/SomfyRemote.cpp +++ b/src/SomfyRemote.cpp @@ -7,21 +7,6 @@ static const char *TAG = "Somfy"; extern SomfyShadeController somfy; -// Commands that can set a shade moving. -static bool startsMovement(somfy_commands cmd) { - switch(cmd) { - case somfy_commands::My: - case somfy_commands::Up: - case somfy_commands::Down: - case somfy_commands::StepUp: - case somfy_commands::StepDown: - case somfy_commands::Toggle: - case somfy_commands::Favorite: - return true; - default: - return false; - } -} void SomfyRemote::setRemoteAddress(uint32_t address) { this->m_remoteAddress = address; this->m_rollingCodeSynced = false; snprintf(this->m_remotePrefId, sizeof(this->m_remotePrefId), "_%lu", (unsigned long)this->m_remoteAddress); } uint32_t SomfyRemote::getRemoteAddress() { return this->m_remoteAddress; } somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) { return this->flipCommands ? flipCommand(cmd) : cmd; } @@ -52,7 +37,7 @@ bool SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe } bool SomfyRemote::sendCommand(somfy_commands cmd) { return this->sendCommand(cmd, this->repeats); } bool SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { return this->sendCommandAfter(cmd, repeat, stepSize, 0).sent; } -send_result_t SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) { +send_result_t SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority, bool hearsItself) { // Build the frame aside and only take the rolling code once the frame is // queued so a dropped command leaves the remote untouched. somfy_frame_t frame = this->lastFrame; @@ -94,10 +79,12 @@ send_result_t SomfyRemote::sendCommandAfter(somfy_commands cmd, uint8_t repeat, result.sent = true; this->commitRollingCode(frame.rollingCode); this->lastFrame = frame; - somfy.processFrame(this->lastFrame, true); + // A shade that hears its own frame (ShadeBehaviour) is left out here. + const SomfyRemote *skip = hearsItself ? this : nullptr; + somfy.processFrame(this->lastFrame, true, skip); // Idle shades (and group members) this sets moving wait for the frame to go // on air. Stops are sent ahead of everything else so they don't need to. - if(result.ticket != 0 && !priority && startsMovement(frame.cmd)) somfy.markTxPending(frame.remoteAddress, result.ticket); + if(result.ticket != 0 && !priority && startsMovement(frame.cmd)) somfy.markTxPending(frame.remoteAddress, result.ticket, skip); return result; } bool SomfyRemote::isLastCommand(somfy_commands cmd) { diff --git a/src/SomfyShade.cpp b/src/SomfyShade.cpp index 1b42aa2..cf12c75 100644 --- a/src/SomfyShade.cpp +++ b/src/SomfyShade.cpp @@ -1,45 +1,21 @@ #include #include "esp_log.h" #include "Somfy.h" -#include "ShadeMotion.h" static const char *TAG = "Somfy"; extern SomfyShadeController somfy; -#define SETMY_REPEATS 35 -#define TILT_REPEATS 15 - -// How long a shade's simulated movement waits for its command to go on air -// before it starts anyway in case the event was lost. -#define TX_PENDING_TIMEOUT 30000 - void SomfyShade::clear() { this->setShadeId(255); this->setRemoteAddress(0); - this->moveStart = 0; - this->tiltStart = 0; - this->sensors.clear(); - this->startPos = 0.0f; - this->startTiltPos = 0.0f; - this->settingMyPos = false; - this->settingPos = false; - this->settingTiltPos = false; - this->awaitMy = 0; + this->behaviour.clear(); this->flipPosition = false; this->flipCommands = false; this->lastRollingCode = 0; this->shadeType = shade_types::roller; this->tiltType = tilt_types::none; //this->txQueue.clear(); - this->currentPos = 0.0f; - this->currentTiltPos = 0.0f; - this->direction = 0; - this->tiltDirection = 0; - this->target = 0.0f; - this->tiltTarget = 0.0f; - this->myPos = -1.0f; - this->myTiltPos = -1.0f; this->bitLength = somfy.transceiver.config.type; this->proto = somfy.transceiver.config.proto; for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) @@ -74,18 +50,6 @@ bool SomfyShade::linkRemote(uint32_t address, uint16_t rollingCode) { return false; } void SomfyShade::commit() { somfy.commit(); } -void SomfyShade::commitShadePosition() { - Preferences pref; - somfy.isDirty = true; -} -void SomfyShade::commitMyPosition() { - Preferences pref; - somfy.isDirty = true; -} -void SomfyShade::commitTiltPosition() { - Preferences pref; - somfy.isDirty = true; -} bool SomfyShade::unlinkRemote(uint32_t address) { Preferences pref; for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { @@ -97,13 +61,6 @@ bool SomfyShade::unlinkRemote(uint32_t address) { } return false; } -bool SomfyShade::isAtTarget() { - float epsilon = .00001; - if(this->tiltType == tilt_types::tiltonly) return fabs(this->currentTiltPos - this->tiltTarget) < epsilon; - else if(this->tiltType == tilt_types::none) return fabs(this->currentPos - this->target) < epsilon; - return fabs(this->currentPos - this->target) < epsilon && fabs(this->currentTiltPos - this->tiltTarget) < epsilon; -} - bool SomfyShade::isInGroup() { if(this->getShadeId() == 255) return false; for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { @@ -111,408 +68,18 @@ bool SomfyShade::isInGroup() { } return false; } -void SomfyShade::checkMovement() { - const uint64_t curTime = millis(); - const bool sunFlag = this->stateFlags & static_cast(somfy_flags_t::SunFlag); - const bool isSunny = this->stateFlags & static_cast(somfy_flags_t::Sunny); - const bool isWindy = this->stateFlags & static_cast(somfy_flags_t::Windy); - // We need to first evaluate the sensor flags as these could be triggering movement from previous sensor inputs. So - // we must check this before setting the directional items or it will not get processed until the next loop. - int32_t downTime = (int32_t)this->downTime; - int32_t upTime = (int32_t)this->upTime; - int32_t tiltTime = (int32_t)this->tiltTime; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) downTime = upTime = tiltTime = 1; - - - // We are checking movement for essentially 3 types of motors. - // If this is an integrated tilt we need to first tilt in the direction we are moving then move. We know - // what needs to be done by the tilt type. Set a tilt first flag to indicate whether we should be tilting or - // moving. If this is only a tilt action then the regular tilt action should operate fine. - int8_t currDir = this->direction; - int8_t currTiltDir = this->tiltDirection; - this->p_direction(this->currentPos == this->target ? 0 : this->currentPos > this->target ? -1 : 1); - bool tilt_first = this->tiltType == tilt_types::integrated && ((this->direction == -1 && this->currentTiltPos != 0.0f) || (this->direction == 1 && this->currentTiltPos != 100.0f)); - - this->p_tiltDirection(this->currentTiltPos == this->tiltTarget ? 0 : this->currentTiltPos > this->tiltTarget ? -1 : 1); - if(tilt_first) this->p_tiltDirection(this->direction); - else if(this->direction != 0) this->p_tiltDirection(0); - uint8_t currPos = floor(this->currentPos); - uint8_t currTiltPos = floor(this->currentTiltPos); - if(this->direction != 0) this->lastMovement = this->direction; - switch(this->tx.hold.check(this->direction != 0 || this->tiltDirection != 0, (uint32_t)curTime, TX_PENDING_TIMEOUT)) { - case TxHoldCheck::holding: - // The command that set us moving hasn't gone on air yet so the motor - // hasn't started. Hold the simulated position until it does. - this->moveStart = this->tiltStart = curTime; - if(currDir != this->direction || currTiltDir != this->tiltDirection) this->emitState(); - return; - case TxHoldCheck::expired: - ESP_LOGW(TAG, "[Shade %u] Timed out waiting for the radio, moving anyway", this->shadeId); - break; - case TxHoldCheck::none: - break; - } - this->applySensorMove(this->sensors.check(sunFlag, isSunny, isWindy, (uint32_t)curTime)); - - if(!tilt_first && this->direction > 0) { - this->p_currentPos(travelPosition(this->startPos, 1, (uint32_t)(curTime - this->moveStart), downTime)); - if(this->currentPos >= this->target) { - this->p_currentPos(this->target); - //if(this->settingMyPos) Serial.printf("IsAtTarget: %d %f=%f\n", this->isAtTarget(), this->currentPos, this->target); - // If we need to stop the shade do this before we indicate that we are - // not moving otherwise the my function will kick in. - if(this->settingPos) { - // At the end limit the motor stops by itself. If the stop doesn't go - // out the shade is now running on to the limit so leave the tilt be. - if(this->target == 100.0 || this->sendStop(1, false)) { - if(!isAtTarget()) { - ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); - // We now need to move the tilt to the position we requested. - this->moveToTiltTarget(this->tiltTarget); - } - } - } - this->p_direction(0); - this->tiltStart = curTime; - this->startTiltPos = this->currentTiltPos; - if(this->isAtTarget()) this->commitShadePosition(); - } - } - else if(!tilt_first && this->direction < 0) { - // Shades often move slower up so this uses its own travel time. - this->p_currentPos(travelPosition(this->startPos, -1, (uint32_t)(curTime - this->moveStart), upTime)); - if(this->currentPos <= this->target) { - this->p_currentPos(this->target); - //if(this->settingMyPos) Serial.printf("IsAtTarget: %d %f=%f\n", this->isAtTarget(), this->currentPos, this->target); - - // If we need to stop the shade do this before we indicate that we are - // not moving otherwise the my function will kick in. - if(this->settingPos) { - // At the end limit the motor stops by itself. If the stop doesn't go - // out the shade is now running on to the limit so leave the tilt be. - if(this->target == 0.0 || this->sendStop(-1, false)) { - if(!isAtTarget()) { - ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); - // We now need to move the tilt to the position we requested. - this->moveToTiltTarget(this->tiltTarget); - } - } - } - this->p_direction(0); - this->tiltStart = curTime; - this->startTiltPos = this->currentTiltPos; - if(this->isAtTarget()) this->commitShadePosition(); - } - } - if(this->tiltDirection > 0) { - if(tilt_first) this->moveStart = curTime; - this->p_currentTiltPos(travelPosition(this->startTiltPos, 1, (uint32_t)(curTime - this->tiltStart), tiltTime)); - if(tilt_first) { - if(this->currentTiltPos >= 100.0f) { - this->p_currentTiltPos(100.0f); - this->moveStart = curTime; - this->startPos = this->currentPos; - //this->p_tiltDirection(0); - //Serial.println("Setting tiltDirection to 0 (tilt_first)"); - } - } - else if(this->currentTiltPos >= this->tiltTarget) { - this->p_currentTiltPos(this->tiltTarget); - // If we need to stop the shade do this before we indicate that we are - // not moving otherwise the my function will kick in. - if(this->settingTiltPos) { - if(this->tiltType == tilt_types::integrated) { - // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. - //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection); - if(this->tiltTarget != 100.0f || this->currentPos != 100.0f) this->sendStop(1, true); - } - else { - // This is a tilt motor so let it complete if it is going to 100. - if(this->tiltTarget != 100.0f) this->sendStop(1, true); - } - } - this->p_tiltDirection(0); - this->settingTiltPos = false; - if(this->isAtTarget()) this->commitShadePosition(); - } - } - else if(this->tiltDirection < 0) { - if(tilt_first) this->moveStart = curTime; - if(tiltTime == 0) this->p_tiltDirection(0); - this->p_currentTiltPos(travelPosition(this->startTiltPos, -1, (uint32_t)(curTime - this->tiltStart), tiltTime)); - if(tilt_first) { - if(this->currentTiltPos <= 0.0f) { - this->p_currentTiltPos(0.0f); - this->moveStart = curTime; - this->startPos = this->currentPos; - //this->p_tiltDirection(0); - } - } - else if(this->currentTiltPos <= this->tiltTarget) { - this->p_currentTiltPos(this->tiltTarget); - // If we need to stop the shade do this before we indicate that we are - // not moving otherwise the my function will kick in. - if(this->settingTiltPos) { - if(this->tiltType == tilt_types::integrated) { - // If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it. - //Serial.printf("Sending My -- tiltTarget: %.2f, tiltDirection: %d\n", this->tiltTarget, this->tiltDirection); - if(this->tiltTarget != 0.0 || this->currentPos != 0.0) this->sendStop(-1, true); - } - else { - // This is a tilt motor so let it complete if it is going to 0. - if(this->tiltTarget != 0.0) this->sendStop(-1, true); - } - } - this->p_tiltDirection(0); - this->settingTiltPos = false; - ESP_LOGI(TAG, "Stopping at tilt position"); - if(this->isAtTarget()) this->commitShadePosition(); - } - } - if(this->settingMyPos && this->isAtTarget()) { - // Set this position before sending the command. If you don't the processFrame function - // will send the shade back to its original My position. - const float oldMyPos = this->myPos; - const float oldMyTiltPos = this->myTiltPos; - if(this->tiltType != tilt_types::none) { - if(this->myTiltPos == this->currentTiltPos && this->myPos == this->currentPos) this->myPos = this->myTiltPos = -1; - else { - this->p_myPos(this->currentPos); - this->p_myTiltPos(this->currentTiltPos); - } - } - else { - this->p_myTiltPos(-1); - if(this->myPos == this->currentPos) this->p_myPos(-1); - else this->p_myPos(this->currentPos); - } - // Give the motor time to settle at the target before it is told to store it. - this->sendSetMy(200, oldMyPos, oldMyTiltPos); - this->settingMyPos = false; - this->emitState(); - } - else if(currDir != this->direction || currPos != floor(this->currentPos) || currTiltDir != this->tiltDirection || currTiltPos != floor(this->currentTiltPos)) { - // We need to emit on the socket that our state has changed. - this->emitState(); - } -} - - -// 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->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->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->changed |= SHADE_CHANGED_ROLLING_CODE; return old; } -bool SomfyShade::p_flag(somfy_flags_t flag, bool val) { - bool old = !!(this->stateFlags & static_cast(flag)); - if(val) - this->stateFlags |= static_cast(flag); - else - this->stateFlags &= ~(static_cast(flag)); - return old; +uint8_t SomfyShade::getFlags() { return joinShadeFlags(*this, this->light, this->state().stateFlags); } +void SomfyShade::setFlags(uint8_t flags) { + uint8_t stateFlags = 0; + splitShadeFlags(flags, *this, this->light, stateFlags); + this->behaviour.setStateFlags(stateFlags); } -uint8_t SomfyShade::getFlags() { return joinShadeFlags(*this, this->light, this->stateFlags); } -void SomfyShade::setFlags(uint8_t flags) { splitShadeFlags(flags, *this, this->light, this->stateFlags); } -bool SomfyShade::p_sunFlag(bool val) { - bool old = this->p_flag(somfy_flags_t::SunFlag, 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->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->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->changed |= SHADE_CHANGED_DIRECTION; - } - return old; -} -int8_t SomfyShade::p_tiltDirection(int8_t dir) { - int8_t old = this->tiltDirection; - if(old != dir) { - this->tiltDirection = dir; - this->changed |= SHADE_CHANGED_TILT_DIRECTION; - } - return old; -} -float SomfyShade::p_target(float target) { - float old = this->target; - if(old != target) { - this->target = target; - if(this->transformPosition(old) != this->transformPosition(target)) - this->changed |= SHADE_CHANGED_TARGET; - } - return old; -} -float SomfyShade::p_tiltTarget(float target) { - float old = this->tiltTarget; - if(old != target) { - this->tiltTarget = target; - if(this->transformPosition(old) != this->transformPosition(target)) - this->changed |= SHADE_CHANGED_TILT_TARGET; - } - return old; -} -float SomfyShade::p_myPos(float pos) { - float old = this->myPos; - if(old != pos) { - this->myPos = pos; - if(this->transformPosition(old) != this->transformPosition(pos)) - this->changed |= SHADE_CHANGED_MY_POS; - } - return old; -} -float SomfyShade::p_myTiltPos(float pos) { - float old = this->myTiltPos; - if(old != pos) { - this->myTiltPos = pos; - if(this->transformPosition(old) != this->transformPosition(pos)) - this->changed |= SHADE_CHANGED_MY_TILT_POS; - } - return old; -} - int8_t SomfyShade::transformPosition(float fpos) { return displayPosition(fpos, this->flipPosition); } -bool SomfyShade::isIdle() { - return this->isAtTarget() && this->direction == 0 && this->tiltDirection == 0; -} -void SomfyShade::processWaitingFrame() { - if(this->shadeId == 255) { - this->lastFrame.await = 0; - return; - } - if(this->lastFrame.processed) return; - if(this->lastFrame.await > 0 && (millis() > this->lastFrame.await)) { - somfy_commands cmd = this->transformCommand(this->lastFrame.cmd); - switch(cmd) { - case somfy_commands::StepUp: - this->lastFrame.processed = true; - // Simply move the shade up by 1%. - if(this->currentPos > 0) { - this->p_target(floor(this->currentPos) - 1); - this->setMovement(-1); - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - break; - case somfy_commands::StepDown: - this->lastFrame.processed = true; - // Simply move the shade down by 1%. - if(this->currentPos < 100) { - this->p_target(floor(this->currentPos) + 1); - this->setMovement(1); - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - break; - case somfy_commands::Down: - case somfy_commands::Up: - if(this->tiltType == tilt_types::tiltmotor) { // Theoretically this should get here unless it does have a tilt motor. - if(this->lastFrame.repeats >= TILT_REPEATS) { - int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; - this->p_tiltTarget(dir > 0 ? 100.0f : 0.0f); - this->setTiltMovement(dir); - this->lastFrame.processed = true; - ESP_LOGD(TAG, "%s Processing tilt %s after %d repeats", - this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - else { - int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; - this->p_target(dir > 0 ? 100 : 0); - this->setMovement(dir); - this->lastFrame.processed = true; - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - if(this->lastFrame.repeats > TILT_REPEATS + 2) { - this->lastFrame.processed = true; - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - } - else if(this->tiltType == tilt_types::euromode) { - if(this->lastFrame.repeats >= TILT_REPEATS) { - int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; - this->p_target(dir > 0 ? 100.0f : 0.0f); - this->setMovement(dir); - this->lastFrame.processed = true; - ESP_LOGD(TAG, "%s Processing %s after %d repeats", - this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - else { - int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; - this->p_tiltTarget(dir > 0 ? 100 : 0); - this->setTiltMovement(dir); - this->lastFrame.processed = true; - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - if(this->lastFrame.repeats > TILT_REPEATS + 2) { - this->lastFrame.processed = true; - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - } - break; - case somfy_commands::My: - if(this->lastFrame.repeats >= SETMY_REPEATS && this->isIdle()) { - if(floor(this->myPos) == floor(this->currentPos)) { - // We are clearing it. - this->p_myPos(-1); - this->p_myTiltPos(-1); - } - else { - this->p_myPos(this->currentPos); - this->p_myTiltPos(this->currentTiltPos); - } - this->commitMyPosition(); - this->lastFrame.processed = true; - this->emitState(); - } - else if(this->isIdle()) { - if(this->simMy) - this->moveToMyPosition(); // Call out like this (instead of move to target) so that we don't get some of the goofy tilt only problems. - else { - if(this->myPos >= 0.0f && this->myPos <= 100.0f) this->p_target(this->myPos); - if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); - } - this->setMovement(0); - this->lastFrame.processed = true; - this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); - } - else { - this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - } - if(this->lastFrame.repeats > SETMY_REPEATS + 2) this->lastFrame.processed = true; - if(this->lastFrame.processed) { - ESP_LOGD(TAG, "%s Processing MY after %d repeats", this->name, this->lastFrame.repeats); - } - break; - default: - break; - } - } -} SomfyRemote *SomfyShade::findRemote(uint32_t address) { if(this->getRemoteAddress() == address) return this; for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { @@ -528,776 +95,34 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { SomfyRemote *remote = this->findRemote(frame.remoteAddress); if(!remote) return; if(remote != this && frame.cmd != somfy_commands::Sensor) remote->setRollingCode(frame.rollingCode); - const uint64_t curTime = millis(); - this->lastFrame.copy(frame); - int8_t dir = 0; - this->moveStart = this->tiltStart = curTime; - this->startPos = this->currentPos; - this->startTiltPos = this->currentTiltPos; - // If the command is coming from a remote then we are aborting all these positioning operations. - if(!internal) this->settingMyPos = this->settingPos = this->settingTiltPos = false; - somfy_commands cmd = this->transformCommand(frame.cmd); - // At this point we are not processing the combo buttons - // will need to see what the shade does when you press both. - switch(cmd) { - case somfy_commands::Sensor: - this->lastFrame.processed = true; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - { - const uint8_t prevFlags = this->stateFlags; - const bool wasSunny = prevFlags & static_cast(somfy_flags_t::Sunny); - const bool wasWindy = prevFlags & static_cast(somfy_flags_t::Windy); - const uint16_t status = frame.rollingCode << 4; - if (status & static_cast(somfy_flags_t::Sunny)) - this->p_sunny(true); - //this->flags |= static_cast(somfy_flags_t::Sunny); - else - this->p_sunny(false); - //this->flags &= ~(static_cast(somfy_flags_t::Sunny)); - if (status & static_cast(somfy_flags_t::Windy)) - this->p_windy(true); - //this->flags |= static_cast(somfy_flags_t::Windy); - else - this->p_windy(false); - //this->flags &= ~(static_cast(somfy_flags_t::Windy)); - if(frame.rollingCode & static_cast(somfy_flags_t::DemoMode)) - this->stateFlags |= static_cast(somfy_flags_t::DemoMode); - else - this->stateFlags &= ~(static_cast(somfy_flags_t::DemoMode)); - const bool isSunny = this->stateFlags & static_cast(somfy_flags_t::Sunny); - const bool isWindy = this->stateFlags & static_cast(somfy_flags_t::Windy); - this->sensors.onSensorFrame(wasSunny, wasWindy, isSunny, isWindy, (uint32_t)curTime); - this->emitState(); - somfy.updateGroupFlags(); - } - break; - case somfy_commands::Prog: - case somfy_commands::MyUp: - case somfy_commands::MyDown: - case somfy_commands::MyUpDown: - case somfy_commands::UpDown: - this->lastFrame.processed = true; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - break; - - case somfy_commands::Flag: - this->lastFrame.processed = true; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state. - this->p_sunFlag(false); - //this->flags &= ~(static_cast(somfy_flags_t::SunFlag)); - somfy.isDirty = true; - this->emitState(); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - somfy.updateGroupFlags(); - break; - case somfy_commands::SunFlag: - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state. - { - const bool isWindy = this->stateFlags & static_cast(somfy_flags_t::Windy); - const bool isSunny = this->stateFlags & static_cast(somfy_flags_t::Sunny); - //this->flags |= static_cast(somfy_flags_t::SunFlag); - this->p_sunFlag(true); - this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy)); - somfy.isDirty = true; - this->emitState(); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - somfy.updateGroupFlags(); - } - break; - case somfy_commands::Up: - if(this->shadeType == shade_types::drycontact) { - this->lastFrame.processed = true; - return; - } - else if(this->shadeType == shade_types::drycontact2) { - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->currentPos != 0.0f) this->p_target(0); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - return; - } - if(this->tiltType == tilt_types::tiltmotor || this->tiltType == tilt_types::euromode) { - // Wait another half second just in case we are potentially processing a tilt. - if(!internal) this->lastFrame.await = curTime + 500; - else this->lastFrame.processed = true; - } - else { - // If from a remote we will simply be going up. - if(this->tiltType == tilt_types::tiltonly && !internal) this->p_tiltTarget(0.0f); - else if(!internal) { - if(this->tiltType != tilt_types::tiltonly) this->p_target(0.0f); - this->p_tiltTarget(0.0f); - } - this->lastFrame.processed = true; - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - } - break; - case somfy_commands::Down: - if(this->shadeType == shade_types::drycontact) { - this->lastFrame.processed = true; - return; - } - else if(this->shadeType == shade_types::drycontact2) { - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->currentPos != 100.0f) this->p_target(100); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - return; - } - if (!this->sensors.windBlocksDown((uint32_t)curTime)) { - if(this->tiltType == tilt_types::tiltmotor || this->tiltType == tilt_types::euromode) { - // Wait another half seccond just in case we are potentially processing a tilt. - if(!internal) this->lastFrame.await = curTime + 500; - else this->lastFrame.processed = true; - } - else { - this->lastFrame.processed = true; - if(!internal) { - if(this->tiltType != tilt_types::tiltonly) this->p_target(100.0f); - if(this->tiltType != tilt_types::none) this->p_tiltTarget(100.0f); - } - } - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - } - break; - case somfy_commands::My: - if(this->shadeType == shade_types::drycontact2) return; - if(this->isToggle()) { // This is a one button device - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(!this->isIdle()) this->p_target(this->currentPos); - else if(this->currentPos == 100.0f) this->p_target(0.0f); - else if(this->currentPos == 0.0f) this->p_target(100.0f); - else this->p_target(this->lastMovement == -1 ? 100 : 0); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - return; - } - else if(this->shadeType == shade_types::drycontact) { - // In this case we need to toggle the contact but we only should do this if - // this is not a repeat. - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->currentPos == 100.0f) this->p_target(0); - else if(this->currentPos == 0.0f) this->p_target(100); - else this->p_target(this->lastMovement == -1 ? 100 : 0); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - return; - } - if(this->isIdle()) { - if(!internal) { - // This frame is coming from a remote. We are potentially setting - // the my position. - this->lastFrame.await = curTime + 500; - } - else { - if(this->lastFrame.processed) return; - ESP_LOGI(TAG, "Moving to My target"); - this->lastFrame.processed = true; - if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); - if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->p_target(this->myPos); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - } - } - else { - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(!internal) { - if(this->tiltType != tilt_types::tiltonly) this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - } - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - } - break; - case somfy_commands::StepUp: - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - dir = 0; - if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1; - if(!this->applyStep(-1, this->lastFrame.stepSize)) return; - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - break; - case somfy_commands::StepDown: - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return; - dir = 1; - if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1; - if(!this->applyStep(1, this->lastFrame.stepSize)) return; - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - break; - case somfy_commands::Toggle: - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(!this->isIdle()) this->p_target(this->currentPos); - else if(this->currentPos == 100.0f) this->p_target(0); - else if(this->currentPos == 0.0f) this->p_target(100); - else this->p_target(this->lastMovement == -1 ? 100 : 0); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - break; - case somfy_commands::Stop: - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - break; - case somfy_commands::Favorite: - if(this->lastFrame.processed) return; - this->lastFrame.processed = true; - if(this->simMy) { - this->moveToMyPosition(); - } - else { - if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); - if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->p_target(this->myPos); - this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); - } - break; - default: - dir = 0; - break; - } - //if(dir == 0 && this->tiltType == tilt_types::tiltmotor && this->tiltDirection != 0) this->setTiltMovement(0); - this->setMovement(dir); -} -void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) { - // The reason why we are processing all frames here is so - // any linked remotes that may happen to be on the same ESPSomfy RTS - // device can trigger the appropriate actions. - if(this->shadeId == 255) return; - const uint64_t curTime = millis(); - int8_t dir = 0; - this->moveStart = this->tiltStart = curTime; - this->startPos = this->currentPos; - this->startTiltPos = this->currentTiltPos; - // If the command is coming from a remote then we are aborting all these positioning operations. - switch(cmd) { - case somfy_commands::Up: - if(this->tiltType == tilt_types::tiltmotor) { - if(repeat >= TILT_REPEATS) - this->p_tiltTarget(0.0f); - else - this->p_target(0.0f); - } - else if(this->tiltType == tilt_types::tiltonly) { - this->p_target(100.0f); - this->p_currentPos(100.0f); - this->p_tiltTarget(0.0f); - } - else { - this->p_target(0.0f); - this->p_tiltTarget(0.0f); - } - break; - case somfy_commands::Down: - if (!this->sensors.windBlocksDown((uint32_t)curTime)) { - if(this->tiltType == tilt_types::tiltmotor) { - if(repeat >= TILT_REPEATS) - this->p_tiltTarget(100.0f); - else - this->p_target(100.0f); - } - else if(this->tiltType == tilt_types::tiltonly) { - this->p_target(100.0f); - this->p_currentPos(100.0f); - this->p_tiltTarget(100.0f); - } - else { - this->p_target(100.0f); - if(this->tiltType != tilt_types::none) this->p_tiltTarget(100.0f); - } - } - break; - case somfy_commands::My: - if(this->isIdle()) { - ESP_LOGD(TAG, "Shade #%d is idle", this->getShadeId()); - if(this->simMy) { - this->moveToMyPosition(); - } - else { - if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); - if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->p_target(this->myPos); - } - } - else { - if(this->tiltType == tilt_types::tiltonly) { - this->p_target(100.0f); - } - else this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - } - break; - case somfy_commands::StepUp: - // A group step is always one step. - if(!this->applyStep(-1, 1)) return; - break; - case somfy_commands::StepDown: - dir = 1; - if(!this->applyStep(1, 1)) return; - break; - case somfy_commands::Flag: - this->p_sunFlag(false); - if(this->sunSensor) { - somfy.isDirty = true; - this->emitState(); - } - else { - ESP_LOGW(TAG, "Shade does not have sensor %d", this->getFlags()); - } - break; - case somfy_commands::SunFlag: - if(this->sunSensor) { - const bool isWindy = this->stateFlags & static_cast(somfy_flags_t::Windy); - const bool isSunny = this->stateFlags & static_cast(somfy_flags_t::Sunny); - this->p_sunFlag(true); - //this->flags |= static_cast(somfy_flags_t::SunFlag); - this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy)); - somfy.isDirty = true; - this->emitState(); - } - else - ESP_LOGW(TAG, "Shade does not have sensor %d", this->getFlags()); - break; - default: - dir = 0; - break; - } - this->setMovement(dir); -} -void SomfyShade::setTiltMovement(int8_t dir) { - int8_t currDir = this->tiltDirection; - if(dir == 0) { - // The shade tilt is stopped. - this->startTiltPos = this->currentTiltPos; - this->tiltStart = 0; - this->p_tiltDirection(dir); - if(currDir != dir) { - this->commitTiltPosition(); - } - } - else if(this->tiltDirection != dir) { - this->tiltStart = millis(); - this->startTiltPos = this->currentTiltPos; - this->p_tiltDirection(dir); - } - if(this->tiltDirection != currDir) { - this->emitState(); - } -} -void SomfyShade::markTxPending(uint32_t ticket) { - // This runs before the caller sets the new target so direction still says - // whether the shade was moving. A reversal is simulated as soon as it is - // queued even though the motor turns once it is on air. See TxHold::mark - // for which of several queued commands is held. - this->tx.hold.mark(ticket, this->direction != 0 || this->tiltDirection != 0, millis()); -} -void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) { - const ShadeTxActions act = this->tx.onEvent(evt, this->direction != 0 || this->tiltDirection != 0); - if(act.startMovement) { - // The motor starts now so the simulated movement does too. - this->moveStart = this->tiltStart = act.startAt; - } - if(act.cancelMovement) { - // The motor never got the command so it hasn't moved. - ESP_LOGW(TAG, "[Shade %u] Command dropped by the radio, staying at %.2f%%", this->shadeId, this->currentPos); - this->settingPos = this->settingTiltPos = this->settingMyPos = false; - this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - this->emitState(); - } - if(act.commitMy) this->commitMyPosition(); - if(act.restoreMy) { - ESP_LOGW(TAG, "[Shade %u] Favorite not sent in full, keeping the previous one", this->shadeId); - this->p_myPos(act.myOldPos); - this->p_myTiltPos(act.myOldTiltPos); - this->emitState(); - } - if(act.runToLimit) { - ESP_LOGW(TAG, "[Shade %u] Stop dropped by the radio, running on to the limit", this->shadeId); - this->runToLimit(act.limitDir, act.limitTilt, act.limitSince); - this->emitState(); - } -} -bool SomfyShade::sendStop(int8_t dir, bool tilt) { - const uint32_t now = millis(); - // Stops jump the radio queue so the motor stops as close to the target as it can. - if(const send_result_t sent = SomfyRemote::sendCommandAfter(somfy_commands::My, this->repeats, 0, 0, true)) { - this->tx.stopSent(sent.ticket, now, dir, tilt); - return true; - } - ESP_LOGW(TAG, "[Shade %u] Stop not sent, running on to the limit", this->shadeId); - this->runToLimit(dir, tilt, now); - return false; -} -bool SomfyShade::sendSetMy(uint16_t delayMs, float oldPos, float oldTiltPos) { - // The new favorite is already in myPos/myTiltPos. It is committed once the - // whole long press is on air, see processTxEvent. - if(const send_result_t sent = SomfyRemote::sendCommandAfter(somfy_commands::My, SETMY_REPEATS, 0, delayMs)) { - this->tx.setMySent(sent.ticket, oldPos, oldTiltPos); - // Without the radio (GPIO remotes) there is nothing to wait for. - if(sent.ticket == 0) this->commitMyPosition(); - return true; - } - // The motor never learned it so keep the favorite it has. - this->p_myPos(oldPos); - this->p_myTiltPos(oldTiltPos); - return false; -} -void SomfyShade::runToLimit(int8_t dir, bool tilt, uint32_t since) { - // The motor didn't get a stop at the target so it keeps going until its end - // limit. Carry on simulating that from where and when it should have stopped. - if(tilt) { - this->startTiltPos = this->currentTiltPos; - this->tiltStart = since; - this->settingTiltPos = false; - this->p_tiltTarget(limitTarget(dir)); - } - else { - this->startPos = this->currentPos; - this->moveStart = since; - this->settingPos = false; - this->p_target(limitTarget(dir)); - } - // The favorite can't be set where the shade won't stop. - this->settingMyPos = false; -} -void SomfyShade::applySensorMove(SensorMove move) { - const SensorTargets t = sensorTargets(move, this->tiltType, this->myPos, this->myTiltPos); - if(t.setLift) this->p_target(t.lift); - if(t.setTilt) this->p_tiltTarget(t.tilt); -} -bool SomfyShade::applyStep(int8_t dir, uint8_t frameStepSize) { - const StepTargets t = stepTargets(this->tiltType, dir, this->currentPos, this->currentTiltPos, - this->upTime, this->downTime, this->tiltTime, this->stepSize, frameStepSize); - if(t.action == StepAction::ignore) return false; - if(t.setLift) this->p_target(t.lift); - if(t.setTilt) this->p_tiltTarget(t.tilt); - return true; -} -void SomfyShade::setMovement(int8_t dir) { - int8_t currDir = this->direction; - int8_t currTiltDir = this->tiltDirection; - if(dir == 0) { - if(currDir != dir || currTiltDir != dir) this->commitShadePosition(); - } - else { - this->tiltStart = this->moveStart = millis(); - this->startPos = this->currentPos; - this->startTiltPos = this->currentTiltPos; - } - if(this->direction != currDir || currTiltDir != this->tiltDirection) { - this->emitState(); - } -} -bool SomfyShade::setMyPosition(int8_t pos, int8_t tilt) { - if(!this->isIdle()) return true; // Don't do this if it is moving. - bool sent = true; - if(this->tiltType == tilt_types::tiltonly) { - this->p_myPos(-1.0f); - if(tilt != floor(this->currentTiltPos)) { - // The favorite is set once we get there, if the move went out. - if(tilt == floor(this->myTiltPos)) - sent = this->settingMyPos = this->moveToMyPosition(); - else - sent = this->settingMyPos = this->moveToTarget(100, tilt); - } - else if(tilt == floor(this->myTiltPos)) { - // Of so we need to clear the my position. These motors are finicky so send - // a my command to ensure we are actually at the my position then send the clear - // command. There really is no other way to do this. - if(this->currentTiltPos != this->myTiltPos) { - sent = this->settingMyPos = this->moveToMyPosition(); - } - else if((sent = SomfyRemote::sendCommand(somfy_commands::My, this->repeats))) { - this->settingPos = false; - this->settingMyPos = true; - } - } - else return this->storeMyPosition(-1.0f, this->currentTiltPos); - this->commitMyPosition(); - this->emitState(); - } - else if(this->tiltType != tilt_types::none) { - if(tilt < 0) tilt = 0; - if(pos != floor(this->currentPos) || tilt != floor(this->currentTiltPos)) { - // The favorite is set once we get there, if the move went out. - if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos)) - sent = this->settingMyPos = this->moveToMyPosition(); - else - sent = this->settingMyPos = this->moveToTarget(pos, tilt); - } - else if(pos == floor(this->myPos) && tilt == floor(this->myTiltPos)) { - // Of so we need to clear the my position. These motors are finicky so send - // a my command to ensure we are actually at the my position then send the clear - // command. There really is no other way to do this. - if(this->currentPos != this->myPos || this->currentTiltPos != this->myTiltPos) { - sent = this->settingMyPos = this->moveToMyPosition(); - } - else if((sent = SomfyRemote::sendCommand(somfy_commands::My, this->repeats))) { - this->settingPos = false; - this->settingMyPos = true; - } - } - else return this->storeMyPosition(this->currentPos, this->currentTiltPos); - this->commitMyPosition(); - this->emitState(); - } - else { - if(pos != floor(this->currentPos)) { - // The favorite is set once we get there, if the move went out. - if(pos == floor(this->myPos)) - sent = this->settingMyPos = this->moveToMyPosition(); - else - sent = this->settingMyPos = this->moveToTarget(pos); - } - else if(pos == floor(this->myPos)) { - // Of so we need to clear the my position. These motors are finicky so send - // a my command to ensure we are actually at the my position then send the clear - // command. There really is no other way to do this. - if(this->myPos != this->currentPos) { - sent = this->settingMyPos = this->moveToMyPosition(); - } - else if((sent = SomfyRemote::sendCommand(somfy_commands::My, this->repeats))) { - this->settingPos = false; - this->settingMyPos = true; - } - } - else sent = this->storeMyPosition(this->currentPos, -1.0f); - } - return sent; -} -bool SomfyShade::storeMyPosition(float pos, float tiltPos) { - // Set it before sending so processFrame doesn't head for the old one. It is - // only committed once the motor was told about it. - const float oldPos = this->myPos; - const float oldTiltPos = this->myTiltPos; - this->p_myPos(pos); - this->p_myTiltPos(tiltPos); - const bool sent = this->sendSetMy(0, oldPos, oldTiltPos); - this->emitState(); - return sent; -} -bool SomfyShade::moveToMyPosition() { - if(!this->isIdle()) return true; - ESP_LOGI(TAG, "Moving to My Position"); - if(this->tiltType == tilt_types::tiltonly) { - this->p_currentPos(100.0f); - this->p_myPos(-1.0f); - } - if(this->currentPos == this->myPos) { - if(this->tiltType != tilt_types::none) { - if(this->currentTiltPos == this->myTiltPos) return true; // Nothing to see here since we are already here. - } - else - return true; - } - if(this->myPos == -1 && (this->tiltType == tilt_types::none || this->myTiltPos == -1)) return true; - this->settingPos = false; - if(this->simMy) { - ESP_LOGI(TAG, "Moving to simulated favorite"); - if(!this->moveToTarget(this->myPos, this->myTiltPos)) return false; - } - else if(!SomfyRemote::sendCommand(somfy_commands::My, this->repeats)) return false; - if(this->tiltType != tilt_types::tiltonly && this->myPos >= 0.0f && this->myPos <= 100.0f) this->p_target(this->myPos); - if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); - return true; + this->behaviour.onFrame(frame, internal, millis()); } +void SomfyShade::processWaitingFrame() { this->behaviour.onWaitingFrame(millis()); } +void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) { this->behaviour.onGroupCommand(cmd, repeat, millis()); } +void SomfyShade::checkMovement() { this->behaviour.tick(millis()); } +void SomfyShade::markTxPending(uint32_t ticket) { this->behaviour.onTxQueued(ticket, millis()); } +void SomfyShade::processTxEvent(const somfy_tx_event_t &evt) { this->behaviour.onTxEvent(evt); } bool SomfyShade::sendCommand(somfy_commands cmd) { return this->sendCommand(cmd, this->repeats); } bool SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { - ESP_LOGD(TAG, "Send command start"); // This sendCommand function will always be called externally. sendCommand at the remote level // is expected to be called internally when the motor needs commanded. if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; - // If same direction command sent while already moving, stop the state tracking. - // The real motor stops on its own when it receives the same direction again. - if((cmd == somfy_commands::Up && this->direction == -1) || - (cmd == somfy_commands::Down && this->direction == 1)) { - ESP_LOGD(TAG, "Same command as dir"); - if(!SomfyRemote::sendCommand(cmd, repeat)) return false; - this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - this->setMovement(0); - return true; - } - // The simulated state is only moved for commands the radio took. - if(cmd == somfy_commands::Up) { - if(this->tiltType == tilt_types::euromode) { - // In euromode we need to long press for 2 seconds on the - // up command. - if(!SomfyRemote::sendCommand(cmd, TILT_REPEATS)) return false; - this->p_target(0.0f); - } - else { - if(!SomfyRemote::sendCommand(cmd, repeat)) return false; - if(this->tiltType == tilt_types::tiltonly) { - this->p_target(100.0f); - this->p_tiltTarget(0.0f); - this->p_currentPos(100.0f); - } - else this->p_target(0.0f); - if(this->tiltType == tilt_types::integrated) this->p_tiltTarget(0.0f); - } - } - else if(cmd == somfy_commands::Down) { - if(this->tiltType == tilt_types::euromode) { - // In euromode we need to long press for 2 seconds on the - // down command. - if(!SomfyRemote::sendCommand(cmd, TILT_REPEATS)) return false; - this->p_target(100.0f); - } - else { - if(!SomfyRemote::sendCommand(cmd, repeat)) return false; - if(this->tiltType == tilt_types::tiltonly) { - this->p_target(100.0f); - this->p_tiltTarget(100.0f); - this->p_currentPos(100.0f); - } - else this->p_target(100.0f); - if(this->tiltType == tilt_types::integrated) this->p_tiltTarget(100.0f); - } - } - else if(cmd == somfy_commands::My) { - if(this->isToggle() || this->shadeType == shade_types::drycontact) { - if(!SomfyRemote::sendCommand(cmd, repeat)) return false; - } - else if(this->shadeType == shade_types::drycontact2){ - ESP_LOGD(TAG, "Send command start 1"); - return true; - } - else if(this->isIdle()) { - bool sent = this->moveToMyPosition(); - ESP_LOGD(TAG, "Send command end 2"); - return sent; - } - else { - if(!SomfyRemote::sendCommand(cmd, repeat)) return false; - if(this->tiltType != tilt_types::tiltonly) this->p_target(this->currentPos); - this->p_tiltTarget(this->currentTiltPos); - } - } - else if(cmd == somfy_commands::Toggle) { - if(this->bitLength != 80) { if(!SomfyRemote::sendCommand(somfy_commands::My, repeat, stepSize)) return false; } - else if(!SomfyRemote::sendCommand(somfy_commands::Toggle, repeat)) return false; - } - else if(this->isToggle() && cmd == somfy_commands::Prog) { - if(!SomfyRemote::sendCommand(somfy_commands::Toggle, repeat, stepSize)) return false; - } - else { - if(!SomfyRemote::sendCommand(cmd, repeat, stepSize)) return false; - } - ESP_LOGD(TAG, "Send command end"); - return true; + return this->behaviour.sendCommand(cmd, repeat, stepSize, millis()); } -uint8_t SomfyShade::tiltRepeats() const { return this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats; } -bool SomfyShade::sendTiltCommand(somfy_commands cmd) { - float target; - if(cmd == somfy_commands::Up) target = 0.0f; - else if(cmd == somfy_commands::Down) target = 100.0f; - else if(cmd == somfy_commands::My) target = this->currentTiltPos; - else return true; - if(!SomfyRemote::sendCommand(cmd, this->tiltRepeats())) return false; - this->p_tiltTarget(target); - return true; -} -bool SomfyShade::moveToTiltTarget(float target) { - somfy_commands cmd = somfy_commands::My; - if(target < this->currentTiltPos) - cmd = somfy_commands::Up; - else if(target > this->currentTiltPos) - cmd = somfy_commands::Down; - if(target >= 0.0f && target <= 100.0f) { - // Only send a command if the lift is not moving. - if(this->currentPos == this->target || this->tiltType == tilt_types::tiltmotor) { - if(cmd != somfy_commands::My) { - ESP_LOGI(TAG, "Moving Tilt to %.2f%% from %.2f%% using %s", - target, this->currentTiltPos, translateSomfyCommand(cmd).c_str()); - if(!SomfyRemote::sendCommand(cmd, this->tiltRepeats())) return false; - } - // If the blind is currently moving then the command to stop it - // will occur on its own when the tilt target is set. - } - this->p_tiltTarget(target); - } - if(cmd != somfy_commands::My) this->settingTiltPos = true; - return true; -} -bool SomfyShade::moveToTarget(float pos, float tilt) { - ESP_LOGI(TAG, "[Shade %u] moveToTarget(pos=%.2f, tilt=%.2f) settingPos=%d direction=%d currentTarget=%.2f currentPos=%.2f", - this->getShadeId(), pos, tilt, this->settingPos, this->direction, this->target, this->currentPos); - somfy_commands cmd = somfy_commands::My; - if(this->isToggle()) { - // Overload this as we cannot seek a position on a garage door or single button device. - this->p_target(pos); - this->p_currentPos(pos); - this->emitState(); - return true; - } - if(this->tiltType == tilt_types::tiltonly) { - // A tilt only shade is always at 100%. - this->p_target(100.0f); - this->p_myPos(-1.0f); - this->p_currentPos(100.0f); - pos = 100; - if(tilt < this->currentTiltPos) cmd = somfy_commands::Up; - else if(tilt > this->currentTiltPos) cmd = somfy_commands::Down; - } - else { - if(pos < this->currentPos) - cmd = somfy_commands::Up; - else if(pos > this->currentPos) - cmd = somfy_commands::Down; - else if(tilt >= 0 && tilt < this->currentTiltPos) - cmd = somfy_commands::Up; - else if(tilt >= 0 && tilt > this->currentTiltPos) - cmd = somfy_commands::Down; - } - if(cmd != somfy_commands::My) { - if(tilt >= 0) { - ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% tilt %.2f%% from %.2f%% using %s", - pos, this->currentPos, tilt, this->currentTiltPos, translateSomfyCommand(cmd).c_str()); - } else { - ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% using %s", - pos, this->currentPos, translateSomfyCommand(cmd).c_str()); - } - if(!SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::euromode ? TILT_REPEATS : this->repeats)) return false; - this->settingPos = true; - this->p_target(pos); - if(tilt >= 0) { - this->p_tiltTarget(tilt); - this->settingTiltPos = true; - } - } - return true; +bool SomfyShade::sendTiltCommand(somfy_commands cmd) { return this->behaviour.sendTiltCommand(cmd, millis()); } +bool SomfyShade::moveToTarget(float pos, float tilt) { return this->behaviour.moveToTarget(pos, tilt, millis()); } +bool SomfyShade::moveToTiltTarget(float target) { return this->behaviour.moveToTiltTarget(target, millis()); } +bool SomfyShade::setMyPosition(int8_t pos, int8_t tilt) { return this->behaviour.setMyPosition(pos, tilt, millis()); } +bool SomfyShade::moveToMyPosition() { return this->behaviour.moveToMyPosition(millis()); } +void SomfyShade::setPosition(float pos, float tilt) { this->behaviour.setPosition(pos, tilt); } +send_result_t SomfyShade::send(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) { + return SomfyRemote::sendCommandAfter(cmd, repeat, stepSize, delayMs, priority, true); } +void SomfyShade::onSaveWanted() { somfy.isDirty = true; } +void SomfyShade::onSensorFlags() { somfy.updateGroupFlags(); } bool SomfyShade::save() { Preferences pref; this->commit(); this->publish(); return true; } -bool SomfyShade::isToggle() { - switch(this->shadeType) { - case shade_types::garage1: - case shade_types::lgate1: - case shade_types::cgate1: - case shade_types::rgate1: - return true; - default: - break; - } - return false; -} - - - - - - - - - - diff --git a/src/SomfyShadeOps.cpp b/src/SomfyShadeOps.cpp index 89eae0c..062b17f 100644 --- a/src/SomfyShadeOps.cpp +++ b/src/SomfyShadeOps.cpp @@ -12,9 +12,9 @@ bool SomfyShadeOps::findShade(uint8_t shadeId, shade_info_t &info) { info.tiltType = shade->tiltType; info.repeats = shade->repeats; info.bitLength = shade->bitLength; - info.currentPos = shade->currentPos; - info.myPos = shade->myPos; - info.myTiltPos = shade->myTiltPos; + info.currentPos = shade->state().currentPos; + info.myPos = shade->state().myPos; + info.myTiltPos = shade->state().myTiltPos; return true; } bool SomfyShadeOps::findGroup(uint8_t groupId, uint8_t &repeats) { @@ -40,15 +40,7 @@ bool SomfyShadeOps::sendSensorCommand(uint8_t shadeId, int8_t windy, int8_t sunn bool SomfyShadeOps::setMyPosition(uint8_t shadeId, int8_t pos, int8_t tilt) { return somfy.getShadeById(shadeId)->setMyPosition(pos, tilt); } void SomfyShadeOps::setPositions(uint8_t shadeId, float pos, float tilt) { SomfyShade *shade = somfy.getShadeById(shadeId); - // Through the setters, so MQTT hears about it too. - if(pos >= 0.0f) { - shade->p_target(pos); - shade->p_currentPos(pos); - } - if(tilt >= 0.0f) { - shade->p_tiltTarget(tilt); - shade->p_currentTiltPos(tilt); - } + shade->setPosition(pos, tilt); shade->emitState(); } void SomfyShadeOps::setUnpaired(uint8_t shadeId) { diff --git a/src/SomfySnapshot.cpp b/src/SomfySnapshot.cpp index 657e0f6..b5e69a4 100644 --- a/src/SomfySnapshot.cpp +++ b/src/SomfySnapshot.cpp @@ -24,15 +24,15 @@ void snapshotShade(SomfyShade &shade, shade_snapshot_t &out) { out.stepSize = shade.stepSize; out.paired = shade.paired; out.lastRollingCode = shade.lastRollingCode; - out.position = shade.transformPosition(shade.currentPos); - out.target = shade.transformPosition(shade.target); - out.myPos = shade.transformPosition(shade.myPos); - out.direction = shade.direction; + out.position = shade.transformPosition(shade.state().currentPos); + out.target = shade.transformPosition(shade.state().target); + out.myPos = shade.transformPosition(shade.state().myPos); + out.direction = shade.state().direction; out.tiltType = static_cast(shade.tiltType); - out.tiltPosition = shade.transformPosition(shade.currentTiltPos); - out.tiltTarget = shade.transformPosition(shade.tiltTarget); - out.myTiltPos = shade.transformPosition(shade.myTiltPos); - out.tiltDirection = shade.tiltDirection; + out.tiltPosition = shade.transformPosition(shade.state().currentTiltPos); + out.tiltTarget = shade.transformPosition(shade.state().tiltTarget); + out.myTiltPos = shade.transformPosition(shade.state().myTiltPos); + out.tiltDirection = shade.state().tiltDirection; out.shadeType = static_cast(shade.shadeType); out.bitLength = shade.bitLength; out.proto = static_cast(shade.proto); diff --git a/src/outputs/SomfyEvents.cpp b/src/outputs/SomfyEvents.cpp index 9383570..ff87d85 100644 --- a/src/outputs/SomfyEvents.cpp +++ b/src/outputs/SomfyEvents.cpp @@ -17,10 +17,10 @@ void SomfyShade::emitState(uint8_t num, const char *evt) { json->addElem("type", static_cast(this->shadeType)); json->addElem("remoteAddress", (uint32_t)this->getRemoteAddress()); json->addElem("name", this->name); - json->addElem("direction", this->direction); - json->addElem("position", this->transformPosition(this->currentPos)); - json->addElem("target", this->transformPosition(this->target)); - json->addElem("myPos", this->transformPosition(this->myPos)); + json->addElem("direction", this->state().direction); + json->addElem("position", this->transformPosition(this->state().currentPos)); + json->addElem("target", this->transformPosition(this->state().target)); + json->addElem("myPos", this->transformPosition(this->state().myPos)); json->addElem("tiltType", static_cast(this->tiltType)); json->addElem("flipCommands", this->flipCommands); json->addElem("flipPosition", this->flipPosition); @@ -29,10 +29,10 @@ void SomfyShade::emitState(uint8_t num, const char *evt) { json->addElem("light", this->light); json->addElem("sortOrder", this->sortOrder); if(this->tiltType != tilt_types::none) { - json->addElem("tiltDirection", this->tiltDirection); - json->addElem("tiltTarget", this->transformPosition(this->tiltTarget)); - json->addElem("tiltPosition", this->transformPosition(this->currentTiltPos)); - json->addElem("myTiltPos", this->transformPosition(this->myTiltPos)); + json->addElem("tiltDirection", this->state().tiltDirection); + json->addElem("tiltTarget", this->transformPosition(this->state().tiltTarget)); + json->addElem("tiltPosition", this->transformPosition(this->state().currentTiltPos)); + json->addElem("myTiltPos", this->transformPosition(this->state().myTiltPos)); } json->endObject(); sockEmit.endEmit(num); diff --git a/test/test_shadebehaviour/test_shadebehaviour.cpp b/test/test_shadebehaviour/test_shadebehaviour.cpp new file mode 100644 index 0000000..6b31f96 --- /dev/null +++ b/test/test_shadebehaviour/test_shadebehaviour.cpp @@ -0,0 +1,632 @@ +#include +#include +#include "ShadeBehaviour.h" + +// Scenario tests pinning what ShadeBehaviour does today, moved unchanged from +// SomfyShade. Where the copies of a rule disagree the test says "today:" so +// a later reconcile shows up as a test change (SHADE_BEHAVIOUR_PLAN.md). + +#define ASSERT_POS(expected, actual) TEST_ASSERT_FLOAT_WITHIN(0.5f, (expected), (actual)) +#define ASSERT_CMD(expected, actual) TEST_ASSERT_EQUAL_HEX8(static_cast(expected), static_cast(actual)) + +static const uint32_t ADDRESS = 0x123456; +static const uint32_t REMOTE = 0x654321; + +// One command the shade sent through its MotorLink. +struct Sent { + somfy_commands cmd; // As it went on air, after flipCommands. + uint8_t repeat; + uint8_t stepSize; + uint16_t delayMs; + bool priority; + uint32_t ticket; +}; +// Builds the frame the way SomfyRemote::sendCommandAfter does and hands out +// tickets 1, 2, 3, ... gpio sends without a ticket, refuseNext makes the +// radio refuse the next command. +struct FakeLink : MotorLink { + somfy_frame_t &lastFrame; + const RemoteSettings &remote; + std::vector sent; + uint16_t rollingCode = 100; + uint32_t nextTicket = 1; + bool refuseNext = false; + bool gpio = false; + FakeLink(somfy_frame_t &lastFrame, const RemoteSettings &remote) : lastFrame(lastFrame), remote(remote) {} + send_result_t send(somfy_commands cmd, uint8_t repeat, uint8_t stepSize, uint16_t delayMs, bool priority) override { + send_result_t result; + if(this->refuseNext) { + this->refuseNext = false; + return result; + } + somfy_frame_t frame = this->lastFrame; + frame.rollingCode = this->rollingCode + 1; + frame.remoteAddress = ADDRESS; + frame.cmd = this->remote.flipCommands ? flipCommand(cmd) : cmd; + frame.repeats = repeat; + frame.bitLength = this->remote.bitLength; + frame.stepSize = stepSize; + frame.valid = true; + frame.encKey = 0xA0 | static_cast(frame.rollingCode & 0x000F); + frame.processed = false; + if(!this->gpio) { + somfy_tx_job_t job; + encodeTxJob(job, frame, repeat); + result.ticket = this->nextTicket++; + } + result.sent = true; + this->rollingCode = frame.rollingCode; + this->lastFrame = frame; + this->sent.push_back({frame.cmd, repeat, stepSize, delayMs, priority, result.ticket}); + return result; + } +}; +struct Command { + somfy_commands cmd; + std::string source; +}; +struct FakeEvents : ShadeEvents { + uint16_t changed = 0; + std::vector commands; + int saves = 0; + int sensorFlags = 0; + void onChanged(uint16_t bits) override { this->changed |= bits; } + void onCommand(somfy_commands cmd, const char *source, uint32_t) override { this->commands.push_back({cmd, source}); } + void onSaveWanted() override { this->saves++; } + void onSensorFlags() override { this->sensorFlags++; } +}; + +// A shade with its settings, link and events, and a clock. +struct Rig { + ShadeSettings settings; + RemoteSettings remote; + somfy_frame_t lastFrame; + FakeLink link{lastFrame, remote}; + FakeEvents events; + uint8_t id = 1; + char name[21] = "Test"; + ShadeBehaviour b{settings, remote, lastFrame, link, events, id, name}; + uint32_t now = 1000; + uint16_t remoteCode = 500; + Rig() { + this->remote.bitLength = 56; + this->lastFrame.remoteAddress = ADDRESS; + } + const ShadeBehaviour::State &st() const { return this->b.state(); } + // loop() passes for ms, 10 ms apart. + void run(uint32_t ms) { + for(uint32_t t = 0; t < ms; t += 10) { + this->now += 10; + this->b.tick(this->now); + } + } + void event(tx_event_t type, uint32_t ticket) { + somfy_tx_event_t evt; + evt.type = type; + evt.ticket = ticket; + evt.at = this->now; + this->b.onTxEvent(evt); + } + // The radio starts the last job sent. + void onAir() { this->event(tx_event_t::started, this->lastSent().ticket); } + const Sent &lastSent() const { return this->link.sent.back(); } + // A remote press: the first frame, then repeats more of the same frame. + void press(somfy_commands cmd, uint8_t repeats = 0, uint32_t address = ADDRESS) { + somfy_frame_t frame; + frame.valid = true; + frame.cmd = cmd; + frame.remoteAddress = address; + frame.rollingCode = ++this->remoteCode; + this->b.onFrame(frame, false, this->now); + for(uint8_t i = 0; i < repeats; i++) { + this->now += 30; + this->b.onFrame(frame, false, this->now); + } + } + // Receive passes with no frame, as loop() calls onWaitingFrame. + void wait(uint32_t ms) { + for(uint32_t t = 0; t < ms; t += 10) { + this->now += 10; + this->b.onWaitingFrame(this->now); + this->b.tick(this->now); + } + } + void sensorFrame(bool sunny, bool windy) { + somfy_frame_t frame; + frame.valid = true; + frame.cmd = somfy_commands::Sensor; + frame.remoteAddress = ADDRESS; + // The sensor's bits ride in the rolling code, shifted down by four. + frame.rollingCode = (sunny ? 0x02 : 0) | (windy ? 0x01 : 0); + this->b.onFrame(frame, false, this->now); + } +}; + +void setUp() {} +void tearDown() {} + +/********************************************************************* + * Commands from the web, MQTT and Home Assistant + ********************************************************************/ +static void test_down_waits_for_the_radio_then_travels() { + Rig r; + TEST_ASSERT_TRUE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now)); + ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd); + ASSERT_POS(100, r.st().target); + r.run(2000); + // The motor hasn't started until the command is on air. + ASSERT_POS(0, r.st().currentPos); + TEST_ASSERT_EQUAL_INT8(1, r.st().direction); + r.onAir(); + r.run(5000); + ASSERT_POS(50, r.st().currentPos); + r.run(5100); + ASSERT_POS(100, r.st().currentPos); + TEST_ASSERT_EQUAL_INT8(0, r.st().direction); + TEST_ASSERT_TRUE(r.events.saves > 0); + TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_POSITION); + // A run to the end limit sends no stop. + TEST_ASSERT_EQUAL(1, r.link.sent.size()); +} +static void test_refused_command_moves_nothing() { + Rig r; + r.link.refuseNext = true; + TEST_ASSERT_FALSE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now)); + ASSERT_POS(0, r.st().target); + r.run(1000); + TEST_ASSERT_EQUAL_INT8(0, r.st().direction); + TEST_ASSERT_EQUAL(0, r.events.commands.size()); +} +static void test_hold_times_out_without_a_radio_event() { + Rig r; + r.b.sendCommand(somfy_commands::Down, 1, 0, r.now); + r.run(29000); + ASSERT_POS(0, r.st().currentPos); + r.run(2000); + // After 30 s it moves anyway, timed from the end of the hold. + TEST_ASSERT_TRUE(r.st().currentPos > 0); +} +static void test_gpio_command_moves_at_once() { + Rig r; + r.link.gpio = true; + r.b.sendCommand(somfy_commands::Down, 1, 0, r.now); + r.run(5000); + ASSERT_POS(50, r.st().currentPos); +} +static void test_same_direction_again_stops_tracking() { + Rig r; + r.b.sendCommand(somfy_commands::Down, 1, 0, r.now); + r.onAir(); + r.run(3000); + TEST_ASSERT_TRUE(r.b.sendCommand(somfy_commands::Down, 1, 0, r.now)); + // The motor stops by itself when it gets the same direction again. + ASSERT_POS(r.st().currentPos, r.st().target); + r.run(1000); + ASSERT_POS(30, r.st().currentPos); + TEST_ASSERT_EQUAL_INT8(0, r.st().direction); +} +static void test_stop_from_the_web_works_through_the_echo() { + Rig r; + r.b.sendCommand(somfy_commands::Down, 1, 0, r.now); + r.onAir(); + r.run(4000); + // sendCommand only sends Stop; hearing its own frame is what stops the shade. + r.b.sendCommand(somfy_commands::Stop, 1, 0, r.now); + ASSERT_CMD(somfy_commands::Stop, r.lastSent().cmd); + ASSERT_POS(40, r.st().target); + ASSERT_CMD(somfy_commands::Stop, r.events.commands.back().cmd); + TEST_ASSERT_EQUAL_STRING("internal", r.events.commands.back().source.c_str()); +} +static void test_move_to_target_sends_a_stop_there() { + Rig r; + TEST_ASSERT_TRUE(r.b.moveToTarget(40, -1, r.now)); + ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd); + r.onAir(); + r.run(4100); + ASSERT_POS(40, r.st().currentPos); + TEST_ASSERT_EQUAL(2, r.link.sent.size()); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); + TEST_ASSERT_TRUE(r.lastSent().priority); + r.event(tx_event_t::finished, r.lastSent().ticket); + r.run(1000); + ASSERT_POS(40, r.st().currentPos); +} +static void test_dropped_stop_runs_on_to_the_limit() { + Rig r; + r.b.moveToTarget(40, -1, r.now); + r.onAir(); + r.run(4100); + r.event(tx_event_t::dropped, r.lastSent().ticket); + ASSERT_POS(100, r.st().target); + r.run(7000); + ASSERT_POS(100, r.st().currentPos); +} +static void test_refused_stop_runs_on_to_the_limit() { + Rig r; + r.b.moveToTarget(40, -1, r.now); + r.onAir(); + r.run(3900); + r.link.refuseNext = true; + r.run(200); + TEST_ASSERT_EQUAL(1, r.link.sent.size()); + ASSERT_POS(100, r.st().target); +} +static void test_today_a_step_after_move_to_target_sends_a_stop() { + // today: arriving at a lift target never clears settingPos (the tilt + // blocks do clear settingTiltPos), so a later step also sends a stop when + // it arrives. On the "still open" list of LOOP_QUEUE_PLAN.md. + Rig r; + r.b.moveToTarget(40, -1, r.now); + r.onAir(); + r.run(4100); + r.event(tx_event_t::finished, r.lastSent().ticket); + const size_t before = r.link.sent.size(); + r.b.sendCommand(somfy_commands::StepDown, 1, 1, r.now); + r.onAir(); + r.run(2000); + TEST_ASSERT_EQUAL(before + 2, r.link.sent.size()); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); +} +static void test_toggle_on_a_56_bit_shade_sends_my() { + Rig r; + r.b.sendCommand(somfy_commands::Toggle, 1, 0, r.now); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); + r.remote.bitLength = 80; + r.b.sendCommand(somfy_commands::Toggle, 1, 0, r.now); + ASSERT_CMD(somfy_commands::Toggle, r.lastSent().cmd); +} +static void test_flipped_shade_sends_the_other_way() { + Rig r; + r.remote.flipCommands = true; + r.b.sendCommand(somfy_commands::Up, 1, 0, r.now); + // On air it is Down; the shade itself still heads for the top. + ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd); + ASSERT_POS(0, r.st().target); +} + +/********************************************************************* + * Frames from remotes + ********************************************************************/ +static void test_remote_down_and_my_stop() { + Rig r; + r.press(somfy_commands::Down); + ASSERT_POS(100, r.st().target); + ASSERT_CMD(somfy_commands::Down, r.events.commands.back().cmd); + TEST_ASSERT_EQUAL_STRING("remote", r.events.commands.back().source.c_str()); + // A remote's frame is already on air, so nothing is held. + r.run(3000); + ASSERT_POS(30, r.st().currentPos); + r.press(somfy_commands::My); + ASSERT_POS(30, r.st().target); + r.run(100); + TEST_ASSERT_EQUAL_INT8(0, r.st().direction); + TEST_ASSERT_EQUAL(0, r.link.sent.size()); +} +static void test_remote_my_when_idle_goes_to_the_favorite_after_half_a_second() { + Rig r; + r.b.restore(0, 0, 60, -1); + r.press(somfy_commands::My); + ASSERT_POS(0, r.st().target); + r.wait(400); + ASSERT_POS(0, r.st().target); + r.wait(200); + ASSERT_POS(60, r.st().target); +} +static void test_remote_my_held_sets_the_favorite() { + Rig r; + r.b.restore(30, 0, 60, -1); + r.press(somfy_commands::My, 35); + r.wait(600); + ASSERT_POS(30, r.st().myPos); + ASSERT_POS(30, r.st().target); + TEST_ASSERT_TRUE(r.events.saves > 0); +} +static void test_remote_held_up_on_a_tilt_motor_tilts() { + Rig r; + r.settings.shadeType = shade_types::blind; + r.settings.tiltType = tilt_types::tiltmotor; + r.b.restore(50, 50, -1, -1); + r.press(somfy_commands::Up); + r.wait(600); + // A short press moves the lift. + ASSERT_POS(0, r.st().target); + ASSERT_POS(50, r.st().tiltTarget); + + Rig h; + h.settings.shadeType = shade_types::blind; + h.settings.tiltType = tilt_types::tiltmotor; + h.b.restore(50, 50, -1, -1); + h.press(somfy_commands::Up, 15); + h.wait(600); + // A long one moves the tilt. + ASSERT_POS(50, h.st().target); + ASSERT_POS(0, h.st().tiltTarget); +} +static void test_remote_step_uses_the_step_size_last_sent() { + // today: copy() never copies stepSize, so a remote step uses whatever step + // size this shade last sent, or 1 when that was 0. + Rig r; + r.b.restore(50, 0, -1, -1); + r.press(somfy_commands::StepDown); + ASSERT_POS(51, r.st().target); + r.lastFrame.stepSize = 3; + r.lastFrame.processed = true; + r.press(somfy_commands::StepDown); + ASSERT_POS(53, r.st().target); +} +static void test_wind_blocks_a_remote_down() { + Rig r; + r.settings.sunSensor = true; + r.sensorFrame(false, true); + TEST_ASSERT_EQUAL(1, r.events.sensorFlags); + TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_WINDY); + r.press(somfy_commands::Down); + ASSERT_POS(0, r.st().target); +} +static void test_wind_retracts_the_shade() { + Rig r; + r.b.restore(80, 0, -1, -1); + r.sensorFrame(false, true); + r.run(2100); + ASSERT_POS(0, r.st().target); +} +static void test_sun_flag_frame_turns_it_on_and_saves() { + Rig r; + r.press(somfy_commands::SunFlag); + TEST_ASSERT_TRUE(r.st().stateFlags & static_cast(somfy_flags_t::SunFlag)); + TEST_ASSERT_EQUAL(1, r.events.saves); + TEST_ASSERT_EQUAL(1, r.events.sensorFlags); + r.press(somfy_commands::Flag); + TEST_ASSERT_FALSE(r.st().stateFlags & static_cast(somfy_flags_t::SunFlag)); +} + +/********************************************************************* + * Integrated tilt + ********************************************************************/ +static void integrated(Rig &r) { + r.settings.shadeType = shade_types::blind; + r.settings.tiltType = tilt_types::integrated; + r.settings.tiltTime = 2000; +} +static void test_integrated_tilt_turns_the_slats_first() { + Rig r; + integrated(r); + TEST_ASSERT_TRUE(r.b.moveToTarget(40, 100, r.now)); + r.onAir(); + r.run(1500); + // Still turning the slats; the lift waits. + ASSERT_POS(75, r.st().currentTiltPos); + ASSERT_POS(0, r.st().currentPos); + r.run(2500); + ASSERT_POS(100, r.st().currentTiltPos); + ASSERT_POS(20, r.st().currentPos); + r.run(2100); + ASSERT_POS(40, r.st().currentPos); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); + // The radio drops the stop and the shade runs on to 100%. + r.event(tx_event_t::dropped, r.lastSent().ticket); + r.run(6100); + ASSERT_POS(100, r.st().currentPos); +} +static void test_today_integrated_tilt_turns_back_after_a_lift_target() { + // today: moveToTarget without a tilt leaves the tilt target where it was, + // so after the stop the shade turns its slats back with an Up. + Rig r; + integrated(r); + r.b.moveToTarget(40, -1, r.now); + r.onAir(); + r.run(6100); + TEST_ASSERT_EQUAL(3, r.link.sent.size()); + ASSERT_CMD(somfy_commands::My, r.link.sent[1].cmd); + ASSERT_CMD(somfy_commands::Up, r.link.sent[2].cmd); + ASSERT_POS(0, r.st().tiltTarget); + r.onAir(); + r.run(2100); + ASSERT_POS(0, r.st().currentTiltPos); + ASSERT_POS(40, r.st().currentPos); +} + +/********************************************************************* + * Tilt-only shades: the copies disagree + ********************************************************************/ +static Rig *tiltOnly(Rig &r) { + r.settings.shadeType = shade_types::blind; + r.settings.tiltType = tilt_types::tiltonly; + r.b.restore(100, 50, -1, -1); + return &r; +} +static void test_today_up_on_tilt_only_from_the_web() { + Rig r; + tiltOnly(r); + r.b.sendCommand(somfy_commands::Up, 1, 0, r.now); + ASSERT_POS(100, r.st().target); + ASSERT_POS(100, r.st().currentPos); + ASSERT_POS(0, r.st().tiltTarget); +} +static void test_today_up_on_tilt_only_from_a_remote() { + // today: a remote only sets the tilt target, where the web and groups also + // put the lift at 100. + Rig r; + tiltOnly(r); + r.b.restore(60, 50, -1, -1); + r.press(somfy_commands::Up); + ASSERT_POS(60, r.st().target); + ASSERT_POS(0, r.st().tiltTarget); +} +static void test_today_up_on_tilt_only_from_a_group() { + Rig r; + tiltOnly(r); + r.b.restore(60, 50, -1, -1); + r.b.onGroupCommand(somfy_commands::Up, 1, r.now); + ASSERT_POS(100, r.st().target); + ASSERT_POS(100, r.st().currentPos); + ASSERT_POS(0, r.st().tiltTarget); +} +static void test_today_my_while_tilt_only_moves() { + // today: a group's My puts the lift target at 100, a remote's leaves it. + Rig g; + tiltOnly(g); + g.b.restore(60, 50, -1, -1); + g.b.onGroupCommand(somfy_commands::Down, 1, g.now); + g.run(1000); + g.b.onGroupCommand(somfy_commands::My, 1, g.now); + ASSERT_POS(100, g.st().target); + + Rig r; + tiltOnly(r); + r.b.restore(60, 50, -1, -1); + r.press(somfy_commands::Down); + r.run(1000); + r.press(somfy_commands::My); + ASSERT_POS(60, r.st().target); +} + +/********************************************************************* + * Groups + ********************************************************************/ +static void test_group_down_and_step() { + Rig r; + r.b.onGroupCommand(somfy_commands::Down, 1, r.now); + ASSERT_POS(100, r.st().target); + // A group command sends nothing itself. + TEST_ASSERT_EQUAL(0, r.link.sent.size()); + Rig s; + s.b.restore(50, 0, -1, -1); + s.b.onGroupCommand(somfy_commands::StepDown, 1, s.now); + ASSERT_POS(51, s.st().target); +} +static void test_group_sun_flag_needs_a_sensor() { + Rig r; + r.b.onGroupCommand(somfy_commands::SunFlag, 1, r.now); + TEST_ASSERT_FALSE(r.st().stateFlags & static_cast(somfy_flags_t::SunFlag)); + r.settings.sunSensor = true; + r.b.onGroupCommand(somfy_commands::SunFlag, 1, r.now); + TEST_ASSERT_TRUE(r.st().stateFlags & static_cast(somfy_flags_t::SunFlag)); + TEST_ASSERT_EQUAL(1, r.events.saves); + // today: unlike the frame rule, the group rule doesn't ask for the group + // flags to be worked out again; SomfyGroup does that for its own group. + TEST_ASSERT_EQUAL(0, r.events.sensorFlags); +} + +/********************************************************************* + * The favorite + ********************************************************************/ +static void test_set_favorite_here_is_committed_when_all_on_air() { + Rig r; + r.b.restore(30, 0, -1, -1); + TEST_ASSERT_TRUE(r.b.setMyPosition(30, -1, r.now)); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); + TEST_ASSERT_EQUAL_UINT8(35, r.lastSent().repeat); + ASSERT_POS(30, r.st().myPos); + const int saves = r.events.saves; + r.event(tx_event_t::finished, r.lastSent().ticket); + TEST_ASSERT_EQUAL(saves + 1, r.events.saves); +} +static void test_set_favorite_dropped_keeps_the_old_one() { + Rig r; + r.b.restore(30, 0, 70, -1); + r.b.setMyPosition(30, -1, r.now); + r.event(tx_event_t::dropped, r.lastSent().ticket); + ASSERT_POS(70, r.st().myPos); +} +static void test_set_favorite_elsewhere_moves_there_first() { + Rig r; + TEST_ASSERT_TRUE(r.b.setMyPosition(40, -1, r.now)); + ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd); + r.onAir(); + r.run(4100); + // The stop, then the long My 200 ms later that stores it. + ASSERT_POS(40, r.st().myPos); + ASSERT_CMD(somfy_commands::My, r.lastSent().cmd); + TEST_ASSERT_EQUAL_UINT8(35, r.lastSent().repeat); + TEST_ASSERT_EQUAL_UINT16(200, r.lastSent().delayMs); +} +static void test_sim_my_favorite_moves_to_it() { + Rig r; + r.settings.simMy = true; + r.b.restore(0, 0, 60, -1); + TEST_ASSERT_TRUE(r.b.moveToMyPosition(r.now)); + ASSERT_CMD(somfy_commands::Down, r.lastSent().cmd); + ASSERT_POS(60, r.st().target); +} + +/********************************************************************* + * Dry contacts and one-button devices + ********************************************************************/ +static void test_garage_my_toggles_open_and_shut() { + Rig r; + r.settings.shadeType = shade_types::garage1; + r.press(somfy_commands::My); + ASSERT_POS(100, r.st().target); + r.run(10100); + r.press(somfy_commands::My); + ASSERT_POS(0, r.st().target); +} + +/********************************************************************* + * Time, setup and the outside writers + ********************************************************************/ +static void test_travel_across_the_millis_wrap() { + Rig r; + r.now = 0xFFFFFFFF - 2000; + r.press(somfy_commands::Down); + r.run(5000); + ASSERT_POS(50, r.st().currentPos); +} +static void test_restore_set_position_and_clear_tilt() { + Rig r; + r.b.restore(42.6f, 10.2f, 30, 5); + ASSERT_POS(42.6f, r.st().currentPos); + // The targets are the whole positions. + TEST_ASSERT_EQUAL_FLOAT(42.0f, r.st().target); + TEST_ASSERT_EQUAL_FLOAT(10.0f, r.st().tiltTarget); + TEST_ASSERT_EQUAL_UINT16(0, r.events.changed); + r.b.setPosition(70, -1); + ASSERT_POS(70, r.st().currentPos); + ASSERT_POS(70, r.st().target); + ASSERT_POS(10.2f, r.st().currentTiltPos); + TEST_ASSERT_TRUE(r.events.changed & SHADE_CHANGED_POSITION); + r.b.clearTilt(); + TEST_ASSERT_EQUAL_FLOAT(0.0f, r.st().currentTiltPos); + TEST_ASSERT_EQUAL_FLOAT(-1.0f, r.st().myTiltPos); +} + +int main(int argc, char **argv) { + UNITY_BEGIN(); + RUN_TEST(test_down_waits_for_the_radio_then_travels); + RUN_TEST(test_refused_command_moves_nothing); + RUN_TEST(test_hold_times_out_without_a_radio_event); + RUN_TEST(test_gpio_command_moves_at_once); + RUN_TEST(test_same_direction_again_stops_tracking); + RUN_TEST(test_stop_from_the_web_works_through_the_echo); + RUN_TEST(test_move_to_target_sends_a_stop_there); + RUN_TEST(test_dropped_stop_runs_on_to_the_limit); + RUN_TEST(test_refused_stop_runs_on_to_the_limit); + RUN_TEST(test_today_a_step_after_move_to_target_sends_a_stop); + RUN_TEST(test_toggle_on_a_56_bit_shade_sends_my); + RUN_TEST(test_flipped_shade_sends_the_other_way); + RUN_TEST(test_remote_down_and_my_stop); + RUN_TEST(test_remote_my_when_idle_goes_to_the_favorite_after_half_a_second); + RUN_TEST(test_remote_my_held_sets_the_favorite); + RUN_TEST(test_remote_held_up_on_a_tilt_motor_tilts); + RUN_TEST(test_remote_step_uses_the_step_size_last_sent); + RUN_TEST(test_wind_blocks_a_remote_down); + RUN_TEST(test_wind_retracts_the_shade); + RUN_TEST(test_sun_flag_frame_turns_it_on_and_saves); + RUN_TEST(test_integrated_tilt_turns_the_slats_first); + RUN_TEST(test_today_integrated_tilt_turns_back_after_a_lift_target); + RUN_TEST(test_today_up_on_tilt_only_from_the_web); + RUN_TEST(test_today_up_on_tilt_only_from_a_remote); + RUN_TEST(test_today_up_on_tilt_only_from_a_group); + RUN_TEST(test_today_my_while_tilt_only_moves); + RUN_TEST(test_group_down_and_step); + RUN_TEST(test_group_sun_flag_needs_a_sensor); + RUN_TEST(test_set_favorite_here_is_committed_when_all_on_air); + RUN_TEST(test_set_favorite_dropped_keeps_the_old_one); + RUN_TEST(test_set_favorite_elsewhere_moves_there_first); + RUN_TEST(test_sim_my_favorite_moves_to_it); + RUN_TEST(test_garage_my_toggles_open_and_shut); + RUN_TEST(test_travel_across_the_millis_wrap); + RUN_TEST(test_restore_set_position_and_clear_tilt); + return UNITY_END(); +}