mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
From the review of #100: - setMyPosition on a moving shade replies busy ("The shade is moving"), and setPositions with only a tilt, for a shade without one, replies bad request ("The shade has no tilt"). Both said ok before. - PROTOCOL.md lists where the ops differ from the routes, and how sendRemoteCommand is answered. - The LoopQueue hold comment covers protocol 2 replies and an unpair held through a frequency scan. - One shadeRequestIsForGroup() for the group cases; ProtoOps shares one reader for "one or both" fields and one for ids, and the radio refused text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
290 lines
12 KiB
C++
290 lines
12 KiB
C++
#ifndef SOMFY_H
|
|
#define SOMFY_H
|
|
#include <Preferences.h>
|
|
#include "ConfigSettings.h"
|
|
#include "WResp.h"
|
|
|
|
#include "SomfyFrame.h"
|
|
#include "TxBuffer.h"
|
|
#include "RadioScheduler.h"
|
|
#include "ShadeTypes.h"
|
|
#include "ShadeSettings.h"
|
|
#include "ShadeBehaviour.h"
|
|
#include "ShadeMotion.h"
|
|
#include "ShadeSensors.h"
|
|
#include "ShadeChanges.h"
|
|
#include "Transceiver.h"
|
|
|
|
enum class gpio_flags_t : byte {
|
|
LowLevelTrigger = 0x01
|
|
};
|
|
struct somfy_relay_t {
|
|
uint32_t remoteAddress = 0;
|
|
uint8_t sync = 0;
|
|
byte frame[10] = {0};
|
|
};
|
|
|
|
class SomfyRoom {
|
|
public:
|
|
uint8_t roomId = 0;
|
|
char name[21] = "";
|
|
int8_t sortOrder = 0;
|
|
void clear();
|
|
bool save();
|
|
bool fromJSON(JsonObject &obj);
|
|
void emitState(const char *evt = "roomState");
|
|
void emitState(uint8_t num, const char *evt = "roomState");
|
|
void publish();
|
|
void unpublish();
|
|
};
|
|
|
|
class SomfyRemote : public RemoteSettings {
|
|
// These sizes for the data have been
|
|
// confirmed. The address is actually 24bits
|
|
// and the rolling code is 16 bits.
|
|
protected:
|
|
char m_remotePrefId[11] = "";
|
|
uint32_t m_remoteAddress = 0;
|
|
// lastRollingCode is known to match NVS for this address, so the next code
|
|
// can be worked out without opening NVS.
|
|
bool m_rollingCodeSynced = false;
|
|
public:
|
|
radio_proto proto = radio_proto::RTS;
|
|
uint8_t gpioFlags = 0;
|
|
int8_t gpioDir = 0;
|
|
uint8_t gpioUp = 0;
|
|
uint8_t gpioDown = 0;
|
|
uint8_t gpioMy = 0;
|
|
uint32_t gpioRelease = 0;
|
|
somfy_frame_t lastFrame;
|
|
uint16_t lastRollingCode = 0;
|
|
virtual bool isLastCommand(somfy_commands cmd);
|
|
char *getRemotePrefId() {return m_remotePrefId;}
|
|
virtual void setRemoteAddress(uint32_t address);
|
|
virtual uint32_t getRemoteAddress();
|
|
uint16_t peekNextRollingCode();
|
|
void commitRollingCode(uint16_t code);
|
|
virtual uint16_t setRollingCode(uint16_t code);
|
|
// 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);
|
|
virtual bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
|
|
// 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);
|
|
somfy_commands transformCommand(somfy_commands cmd);
|
|
virtual void triggerGPIOs(somfy_frame_t &frame);
|
|
|
|
};
|
|
class SomfyLinkedRemote : public SomfyRemote {
|
|
public:
|
|
SomfyLinkedRemote();
|
|
};
|
|
// 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;
|
|
public:
|
|
uint8_t roomId = 0;
|
|
int8_t sortOrder = 0;
|
|
bool light = false;
|
|
SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
|
|
bool paired = false;
|
|
int8_t validateJSON(JsonObject &obj);
|
|
int8_t fromJSON(JsonObject &obj);
|
|
|
|
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(); }
|
|
bool isIdle() const { return this->behaviour.isIdle(); }
|
|
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 isInGroup();
|
|
void checkMovement();
|
|
void processFrame(somfy_frame_t &frame, bool internal = false);
|
|
// The shade itself or the linked remote with this address, else nullptr.
|
|
SomfyRemote *findRemote(uint32_t address);
|
|
void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1);
|
|
void markTxPending(uint32_t ticket);
|
|
void processTxEvent(const somfy_tx_event_t &evt);
|
|
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
|
|
// loop() pass.
|
|
void emitState();
|
|
// Sends evt about the shade to every client now.
|
|
void emitState(const char *evt);
|
|
void emitState(uint8_t num, const char *evt = "shadeState");
|
|
// What changed since the fan-out last sent the shade (SHADE_CHANGED_* bits).
|
|
uint16_t changed = 0;
|
|
// A setting changed: protocol 2 clients get the whole shade.
|
|
void configChanged() { this->changed |= SHADE_CHANGED_CONFIG; }
|
|
void emitCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
|
|
void emitCommand(uint8_t num, somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
|
|
// False when the radio refused a command it sent.
|
|
bool setMyPosition(int8_t pos, int8_t tilt = -1);
|
|
bool moveToMyPosition();
|
|
void processWaitingFrame();
|
|
void publish();
|
|
void unpublish();
|
|
static void unpublish(uint8_t id);
|
|
static void unpublish(uint8_t id, const char *topic);
|
|
void publishState();
|
|
// Publishes the MQTT topics of the SHADE_CHANGED_* bits in changed.
|
|
void publishTopics(uint16_t changed);
|
|
void commit();
|
|
void clear();
|
|
int8_t transformPosition(float fpos);
|
|
void setGPIOs();
|
|
void triggerGPIOs(somfy_frame_t &frame);
|
|
bool usesPin(uint8_t pin);
|
|
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);
|
|
bool publish(const char *topic, int8_t val, bool retain = false);
|
|
bool publish(const char *topic, uint32_t val, bool retain = false);
|
|
bool publish(const char *topic, uint16_t val, bool retain = false);
|
|
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:
|
|
uint8_t groupId = 255;
|
|
public:
|
|
uint8_t roomId = 0;
|
|
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; }
|
|
uint8_t getGroupId() { return groupId; }
|
|
bool save();
|
|
void clear();
|
|
bool fromJSON(JsonObject &obj);
|
|
bool linkShade(uint8_t shadeId);
|
|
bool unlinkShade(uint8_t shadeId);
|
|
bool hasShadeId(uint8_t shadeId);
|
|
void compressLinkedShadeIds();
|
|
void publish();
|
|
void unpublish();
|
|
static void unpublish(uint8_t id);
|
|
static void unpublish(uint8_t id, const char *topic);
|
|
void publishState();
|
|
void updateFlags();
|
|
// Marks the group as changed; the change fan-out sends groupState once per
|
|
// loop() pass.
|
|
void emitState();
|
|
// Sends evt about the group to every client now.
|
|
void emitState(const char *evt);
|
|
void emitState(uint8_t num, const char *evt = "groupState");
|
|
// What changed since the fan-out last sent the group (GROUP_CHANGED_* bits).
|
|
uint8_t changed = 0;
|
|
// A setting or its shades changed: protocol 2 clients get the whole group.
|
|
void configChanged() { this->changed |= GROUP_CHANGED_CONFIG; }
|
|
bool sendCommand(somfy_commands cmd);
|
|
bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
|
|
int8_t p_direction(int8_t dir);
|
|
bool publish(const char *topic, uint8_t val, bool retain = false);
|
|
bool publish(const char *topic, int8_t val, bool retain = false);
|
|
bool publish(const char *topic, uint32_t val, bool retain = false);
|
|
bool publish(const char *topic, uint16_t val, bool retain = false);
|
|
bool publish(const char *topic, bool val, bool retain = false);
|
|
};
|
|
class SomfyShadeController {
|
|
protected:
|
|
uint8_t m_shadeIds[SOMFY_MAX_SHADES];
|
|
uint32_t lastCommit = 0;
|
|
public:
|
|
bool isDirty = false;
|
|
uint32_t startingAddress;
|
|
uint8_t getNextRoomId();
|
|
uint8_t getNextShadeId();
|
|
uint8_t getNextGroupId();
|
|
int8_t getMaxRoomOrder();
|
|
int8_t getMaxShadeOrder();
|
|
int8_t getMaxGroupOrder();
|
|
SomfyShadeController();
|
|
Transceiver transceiver;
|
|
SomfyRoom *addRoom();
|
|
SomfyShade *addShade();
|
|
SomfyGroup *addGroup();
|
|
bool deleteRoom(uint8_t roomId);
|
|
bool deleteShade(uint8_t shadeId);
|
|
bool deleteGroup(uint8_t groupId);
|
|
bool begin();
|
|
void loop();
|
|
void end();
|
|
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
|
|
SomfyRoom rooms[SOMFY_MAX_ROOMS];
|
|
SomfyShade shades[SOMFY_MAX_SHADES];
|
|
SomfyGroup groups[SOMFY_MAX_GROUPS];
|
|
bool linkRepeater(uint32_t address);
|
|
bool unlinkRepeater(uint32_t address);
|
|
uint8_t repeaterCount();
|
|
uint8_t roomCount();
|
|
uint8_t shadeCount();
|
|
uint8_t groupCount();
|
|
void updateGroupFlags();
|
|
SomfyShade * getShadeById(uint8_t shadeId);
|
|
SomfyRoom * getRoomById(uint8_t roomId);
|
|
SomfyGroup * getGroupById(uint8_t groupId);
|
|
SomfyShade * findShadeByRemoteAddress(uint32_t address);
|
|
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);
|
|
// 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
|
|
// last pass, its changed MQTT topics and one state event. loop() only.
|
|
void sendChanges();
|
|
void publish();
|
|
void processWaitingFrame();
|
|
void commit();
|
|
void writeBackup();
|
|
String backupData;
|
|
bool loadShadesFile(const char *filename);
|
|
};
|
|
|
|
#endif
|