refactor: hold shade settings in ShadeSettings and RemoteSettings

The shade behaviour module (SHADE_BEHAVIOUR_PLAN.md, PR B) reads its
settings through read-only structs, so they get one home each:
RemoteSettings (flipCommands, bitLength, repeats) under SomfyRemote,
shared with groups, and ShadeSettings (types, travel times, step size,
flipPosition, simMy, sunSensor) under SomfyShade. Both are base structs,
so shade->upTime and group->repeats keep working.

A shade's flags byte is split by owner: simMy and sunSensor are settings,
light is a field, and the sun flag, sunny, windy, demo mode and lighted
bits are stateFlags. getFlags()/setFlags() put the byte together and take
it apart for the config file, JSON, MQTT, events and group flags, so
nothing saved or sent changes. Groups keep their own flags byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-16 20:31:39 +02:00
parent 8a13e0c3a9
commit fa7aaf0d49
13 changed files with 207 additions and 67 deletions

View file

@ -827,7 +827,7 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) {
if(this->header.version < 5 && j == 4) break; // Prior to version 5 we only supported 5 linked remotes.
}
shade->lastRollingCode = this->readUInt16(0);
if(this->header.version > 7) shade->flags = this->readUInt8(0);
if(this->header.version > 7) shade->setFlags(this->readUInt8(0));
if(shade->getRemoteAddress() != 0) {
// If the last rolling code stored on the nvs is less than the rc we currently have
// then we need to set it.
@ -995,7 +995,7 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
}
this->writeUInt16(shade->lastRollingCode);
if(shade->getShadeId() != 255) {
this->writeUInt8(shade->flags & 0xFF);
this->writeUInt8(shade->getFlags());
this->writeFloat(shade->myPos, 5);
this->writeFloat(shade->myTiltPos, 5);
this->writeFloat(shade->currentPos, 5);

50
src/ShadeSettings.h Normal file
View file

@ -0,0 +1,50 @@
#ifndef SHADE_SETTINGS_H
#define SHADE_SETTINGS_H
#include <Arduino.h>
#include "ShadeTypes.h"
// The settings a shade's behaviour reads and never changes. The user sets
// them through JSON, the config file and ModelEditor. Kept free of the shade
// model so they can be used in the unit tests on the PC (pio test -e native).
// What shades and groups share: how their frames go on air.
struct RemoteSettings {
bool flipCommands = false;
uint8_t bitLength = 0;
uint8_t repeats = 1;
};
// What only a shade has.
struct ShadeSettings {
shade_types shadeType = shade_types::roller;
tilt_types tiltType = tilt_types::none;
uint32_t upTime = 10000;
uint32_t downTime = 10000;
uint32_t tiltTime = 7000;
uint16_t stepSize = 100;
bool flipPosition = false;
// Reaches the favorite by moving to it rather than sending My.
bool simMy = false;
bool sunSensor = false;
};
// The bits of a shade's flags byte that are settings. The rest are state the
// shade changes (sun flag, sunny, windy, demo mode) or unused (lighted).
static const uint8_t SHADE_SETTING_FLAGS = static_cast<uint8_t>(somfy_flags_t::SunSensor) |
static_cast<uint8_t>(somfy_flags_t::Light) | static_cast<uint8_t>(somfy_flags_t::SimMy);
// The flags byte as it is saved and sent, put together from where its bits live.
inline uint8_t joinShadeFlags(const ShadeSettings &settings, bool light, uint8_t stateFlags) {
uint8_t flags = stateFlags & ~SHADE_SETTING_FLAGS;
if(settings.sunSensor) flags |= static_cast<uint8_t>(somfy_flags_t::SunSensor);
if(light) flags |= static_cast<uint8_t>(somfy_flags_t::Light);
if(settings.simMy) flags |= static_cast<uint8_t>(somfy_flags_t::SimMy);
return flags;
}
// Splits a saved or sent flags byte back to where its bits live.
inline void splitShadeFlags(uint8_t flags, ShadeSettings &settings, bool &light, uint8_t &stateFlags) {
settings.sunSensor = flags & static_cast<uint8_t>(somfy_flags_t::SunSensor);
light = flags & static_cast<uint8_t>(somfy_flags_t::Light);
settings.simMy = flags & static_cast<uint8_t>(somfy_flags_t::SimMy);
stateFlags = flags & ~SHADE_SETTING_FLAGS;
}
#endif

View file

@ -8,6 +8,7 @@
#include "TxBuffer.h"
#include "RadioScheduler.h"
#include "ShadeTypes.h"
#include "ShadeSettings.h"
#include "ShadeMotion.h"
#include "ShadeSensors.h"
#include "ShadeChanges.h"
@ -43,7 +44,7 @@ struct send_result_t {
uint32_t ticket = 0;
explicit operator bool() const { return this->sent; }
};
class SomfyRemote {
class SomfyRemote : public RemoteSettings {
// These sizes for the data have been
// confirmed. The address is actually 24bits
// and the rolling code is 16 bits.
@ -62,11 +63,7 @@ class SomfyRemote {
uint8_t gpioMy = 0;
uint32_t gpioRelease = 0;
somfy_frame_t lastFrame;
bool flipCommands = false;
uint16_t lastRollingCode = 0;
uint8_t flags = 0;
uint8_t bitLength = 0;
uint8_t repeats = 1;
virtual bool isLastCommand(somfy_commands cmd);
char *getRemotePrefId() {return m_remotePrefId;}
virtual void setRemoteAddress(uint32_t address);
@ -74,12 +71,8 @@ class SomfyRemote {
uint16_t peekNextRollingCode();
void commitRollingCode(uint16_t code);
virtual uint16_t setRollingCode(uint16_t code);
bool hasSunSensor();
bool hasLight();
bool simMy();
void setSunSensor(bool bHasSensor);
void setLight(bool bHasLight);
void setSimMy(bool bSimMy);
// The flags byte (somfy_flags_t) as it is saved and sent.
virtual uint8_t getFlags() { return 0; }
// The send functions return false only when the radio could not take the
// command. Nothing about the remote or shade state has changed in that case.
virtual bool sendCommand(somfy_commands cmd);
@ -96,7 +89,7 @@ class SomfyLinkedRemote : public SomfyRemote {
public:
SomfyLinkedRemote();
};
class SomfyShade : public SomfyRemote {
class SomfyShade : public SomfyRemote, public ShadeSettings {
protected:
uint8_t shadeId = 255;
uint64_t moveStart = 0;
@ -108,6 +101,9 @@ class SomfyShade : public SomfyRemote {
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);
@ -124,9 +120,7 @@ class SomfyShade : public SomfyRemote {
public:
uint8_t roomId = 0;
int8_t sortOrder = 0;
bool flipPosition = false;
shade_types shadeType = shade_types::roller;
tilt_types tiltType = tilt_types::none;
bool light = false;
float currentPos = 0.0f;
float currentTiltPos = 0.0f;
int8_t lastMovement = 0;
@ -144,10 +138,9 @@ class SomfyShade : public SomfyRemote {
char name[21] = "";
void setShadeId(uint8_t id) { shadeId = id; }
uint8_t getShadeId() { return shadeId; }
uint32_t upTime = 10000;
uint32_t downTime = 10000;
uint32_t tiltTime = 7000;
uint16_t stepSize = 100;
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();
@ -232,6 +225,10 @@ class SomfyGroup : public SomfyRemote {
int8_t sortOrder = 0;
group_types groupType = group_types::channel;
int8_t direction = 0; // 0 = stopped, 1=down, -1=up.
// Its linked shades' flags together, see updateFlags.
uint8_t flags = 0;
uint8_t getFlags() override { return this->flags; }
bool hasSunSensor() { return (this->flags & static_cast<uint8_t>(somfy_flags_t::SunSensor)) > 0; }
char name[21] = "";
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES];
void setGroupId(uint8_t id) { groupId = id; }

View file

@ -33,7 +33,7 @@ void SomfyGroup::updateFlags() {
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
if(this->linkedShades[i] != 0) {
SomfyShade *shade = somfy.getShadeById(this->linkedShades[i]);
if(shade) this->flags |= shade->flags;
if(shade) this->flags |= shade->getFlags();
}
else break;
}

View file

@ -80,9 +80,9 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) {
if(!obj["hasTilt"].isNull()) this->tiltType = static_cast<bool>(obj["hasTilt"]) ? tilt_types::none : tilt_types::tiltmotor;
if(!obj["bitLength"].isNull()) this->bitLength = obj["bitLength"];
if(!obj["proto"].isNull()) this->proto = static_cast<radio_proto>(obj["proto"].as<uint8_t>());
if(!obj["sunSensor"].isNull()) this->setSunSensor(obj["sunSensor"]);
if(!obj["simMy"].isNull()) this->setSimMy(obj["simMy"]);
if(!obj["light"].isNull()) this->setLight(obj["light"]);
if(!obj["sunSensor"].isNull()) this->sunSensor = obj["sunSensor"];
if(!obj["simMy"].isNull()) this->simMy = obj["simMy"];
if(!obj["light"].isNull()) this->light = obj["light"];
if(!obj["gpioFlags"].isNull()) this->gpioFlags = obj["gpioFlags"];
if(!obj["gpioLLTrigger"].isNull()) {
if(obj["gpioLLTrigger"].as<bool>())
@ -150,7 +150,7 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) {
this->linkedRemotes[j].setRemoteAddress(linkedAddresses[j]);
}
}
if(!obj["flags"].isNull()) this->flags = obj["flags"];
if(!obj["flags"].isNull()) this->setFlags(obj["flags"].as<uint8_t>());
if(this->proto == radio_proto::GP_Remote || this->proto == radio_proto::GP_Relay) {
if(!obj["gpioUp"].isNull()) this->gpioUp = obj["gpioUp"];
if(!obj["gpioDown"].isNull()) this->gpioDown = obj["gpioDown"];

View file

@ -22,17 +22,11 @@ static bool startsMovement(somfy_commands cmd) {
return false;
}
}
bool SomfyRemote::simMy() { return (this->flags & static_cast<uint8_t>(somfy_flags_t::SimMy)) > 0; }
void SomfyRemote::setSimMy(bool bSimMy) { bSimMy ? this->flags |= static_cast<uint8_t>(somfy_flags_t::SimMy) : this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::SimMy)); }
bool SomfyRemote::hasSunSensor() { return (this->flags & static_cast<uint8_t>(somfy_flags_t::SunSensor)) > 0;}
bool SomfyRemote::hasLight() { return (this->flags & static_cast<uint8_t>(somfy_flags_t::Light)) > 0; }
void SomfyRemote::setSunSensor(bool bHasSensor ) { bHasSensor ? this->flags |= static_cast<uint8_t>(somfy_flags_t::SunSensor) : this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::SunSensor)); }
void SomfyRemote::setLight(bool bHasLight ) { bHasLight ? this->flags |= static_cast<uint8_t>(somfy_flags_t::Light) : this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::Light)); }
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; }
bool SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat) {
uint8_t flags = (this->flags >> 4) & 0x0F;
uint8_t flags = (this->getFlags() >> 4) & 0x0F;
if(isWindy > 0) flags |= 0x01;
if(isSunny > 0) flags |= 0x02;
if(isWindy == 0) flags &= ~0x01;

View file

@ -113,9 +113,9 @@ bool SomfyShade::isInGroup() {
}
void SomfyShade::checkMovement() {
const uint64_t curTime = millis();
const bool sunFlag = this->flags & static_cast<uint8_t>(somfy_flags_t::SunFlag);
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool sunFlag = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::SunFlag);
const bool isSunny = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->stateFlags & static_cast<uint8_t>(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;
@ -318,13 +318,15 @@ uint16_t SomfyShade::p_lastRollingCode(uint16_t code) {
return old;
}
bool SomfyShade::p_flag(somfy_flags_t flag, bool val) {
bool old = !!(this->flags & static_cast<uint8_t>(flag));
bool old = !!(this->stateFlags & static_cast<uint8_t>(flag));
if(val)
this->flags |= static_cast<uint8_t>(flag);
this->stateFlags |= static_cast<uint8_t>(flag);
else
this->flags &= ~(static_cast<uint8_t>(flag));
this->stateFlags &= ~(static_cast<uint8_t>(flag));
return old;
}
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;
@ -487,7 +489,7 @@ void SomfyShade::processWaitingFrame() {
this->emitState();
}
else if(this->isIdle()) {
if(this->simMy())
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);
@ -542,7 +544,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
this->lastFrame.processed = true;
if(this->shadeType == shade_types::drycontact || this->shadeType == shade_types::drycontact2) return;
{
const uint8_t prevFlags = this->flags;
const uint8_t prevFlags = this->stateFlags;
const bool wasSunny = prevFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool wasWindy = prevFlags & static_cast<uint8_t>(somfy_flags_t::Windy);
const uint16_t status = frame.rollingCode << 4;
@ -559,11 +561,11 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
this->p_windy(false);
//this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::Windy));
if(frame.rollingCode & static_cast<uint8_t>(somfy_flags_t::DemoMode))
this->flags |= static_cast<uint8_t>(somfy_flags_t::DemoMode);
this->stateFlags |= static_cast<uint8_t>(somfy_flags_t::DemoMode);
else
this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::DemoMode));
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
this->stateFlags &= ~(static_cast<uint8_t>(somfy_flags_t::DemoMode));
const bool isSunny = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Windy);
this->sensors.onSensorFrame(wasSunny, wasWindy, isSunny, isWindy, (uint32_t)curTime);
this->emitState();
somfy.updateGroupFlags();
@ -594,8 +596,8 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
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->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
//this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag);
this->p_sunFlag(true);
this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy));
@ -746,7 +748,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
case somfy_commands::Favorite:
if(this->lastFrame.processed) return;
this->lastFrame.processed = true;
if(this->simMy()) {
if(this->simMy) {
this->moveToMyPosition();
}
else {
@ -813,7 +815,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
case somfy_commands::My:
if(this->isIdle()) {
ESP_LOGD(TAG, "Shade #%d is idle", this->getShadeId());
if(this->simMy()) {
if(this->simMy) {
this->moveToMyPosition();
}
else {
@ -839,18 +841,18 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
break;
case somfy_commands::Flag:
this->p_sunFlag(false);
if(this->hasSunSensor()) {
if(this->sunSensor) {
somfy.isDirty = true;
this->emitState();
}
else {
ESP_LOGW(TAG, "Shade does not have sensor %d", this->flags);
ESP_LOGW(TAG, "Shade does not have sensor %d", this->getFlags());
}
break;
case somfy_commands::SunFlag:
if(this->hasSunSensor()) {
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
if(this->sunSensor) {
const bool isWindy = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->stateFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
this->p_sunFlag(true);
//this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag);
this->applySensorMove(this->sensors.onSunFlag(isSunny, isWindy));
@ -858,7 +860,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
this->emitState();
}
else
ESP_LOGW(TAG, "Shade does not have sensor %d", this->flags);
ESP_LOGW(TAG, "Shade does not have sensor %d", this->getFlags());
break;
default:
dir = 0;
@ -1095,7 +1097,7 @@ bool SomfyShade::moveToMyPosition() {
}
if(this->myPos == -1 && (this->tiltType == tilt_types::none || this->myTiltPos == -1)) return true;
this->settingPos = false;
if(this->simMy()) {
if(this->simMy) {
ESP_LOGI(TAG, "Moving to simulated favorite");
if(!this->moveToTarget(this->myPos, this->myTiltPos)) return false;
}

View file

@ -36,13 +36,13 @@ void snapshotShade(SomfyShade &shade, shade_snapshot_t &out) {
out.shadeType = static_cast<uint8_t>(shade.shadeType);
out.bitLength = shade.bitLength;
out.proto = static_cast<uint8_t>(shade.proto);
out.flags = shade.flags;
out.flags = shade.getFlags();
out.flipCommands = shade.flipCommands;
out.flipPosition = shade.flipPosition;
out.inGroup = shade.isInGroup();
out.sunSensor = shade.hasSunSensor();
out.light = shade.hasLight();
out.simMy = shade.simMy();
out.sunSensor = shade.sunSensor;
out.light = shade.light;
out.simMy = shade.simMy;
out.repeats = shade.repeats;
out.sortOrder = shade.sortOrder;
out.gpioUp = shade.gpioUp;
@ -74,7 +74,7 @@ void snapshotGroup(SomfyGroup &group, group_snapshot_t &out) {
out.flags = 0;
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES && group.linkedShades[i] != 0; i++) {
SomfyShade *shade = somfy.getShadeById(group.linkedShades[i]);
if(shade) out.flags |= shade->flags;
if(shade) out.flags |= shade->getFlags();
}
out.linkedShadeCount = 0;
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {

View file

@ -193,7 +193,7 @@ void SomfyShade::publish() {
this->publish("remoteAddress", this->getRemoteAddress(), true);
this->publish("shadeType", static_cast<uint8_t>(this->shadeType), true);
this->publish("tiltType", static_cast<uint8_t>(this->tiltType), true);
this->publish("flags", this->flags, true);
this->publish("flags", this->getFlags(), true);
this->publish("flipCommands", this->flipCommands, true);
this->publish("flipPosition", this->flipPosition, true);
this->publishState();

View file

@ -24,9 +24,9 @@ void SomfyShade::emitState(uint8_t num, const char *evt) {
json->addElem("tiltType", static_cast<uint8_t>(this->tiltType));
json->addElem("flipCommands", this->flipCommands);
json->addElem("flipPosition", this->flipPosition);
json->addElem("flags", this->flags);
json->addElem("sunSensor", this->hasSunSensor());
json->addElem("light", this->hasLight());
json->addElem("flags", this->getFlags());
json->addElem("sunSensor", this->sunSensor);
json->addElem("light", this->light);
json->addElem("sortOrder", this->sortOrder);
if(this->tiltType != tilt_types::none) {
json->addElem("tiltDirection", this->tiltDirection);
@ -82,7 +82,7 @@ void SomfyGroup::emitState(uint8_t num, const char *evt) {
SomfyShade *shade = somfy.getShadeById(this->linkedShades[i]);
if(shade) {
json->addElem(this->linkedShades[i]);
flags |= shade->flags;
flags |= shade->getFlags();
}
}
}