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 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-16 21:21:38 +02:00
parent fa7aaf0d49
commit 4f8eac03ac
18 changed files with 2182 additions and 1343 deletions

View file

@ -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<float>(this->readUInt8(255));
myPos = static_cast<float>(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.

1215
src/ShadeBehaviour.cpp Normal file

File diff suppressed because it is too large Load diff

167
src/ShadeBehaviour.h Normal file
View file

@ -0,0 +1,167 @@
#ifndef SHADE_BEHAVIOUR_H
#define SHADE_BEHAVIOUR_H
#include <Arduino.h>
#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

View file

@ -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.

View file

@ -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) {

View file

@ -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,

View file

@ -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

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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) {

File diff suppressed because it is too large Load diff

View file

@ -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) {

View file

@ -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<uint8_t>(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<uint8_t>(shade.shadeType);
out.bitLength = shade.bitLength;
out.proto = static_cast<uint8_t>(shade.proto);

View file

@ -17,10 +17,10 @@ void SomfyShade::emitState(uint8_t num, const char *evt) {
json->addElem("type", static_cast<uint8_t>(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<uint8_t>(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);