mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
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>
341 lines
12 KiB
C++
341 lines
12 KiB
C++
#include <Preferences.h>
|
|
#include <esp_task_wdt.h>
|
|
#include "esp_log.h"
|
|
#include "ConfigSettings.h"
|
|
#include "Somfy.h"
|
|
#include "ConfigFile.h"
|
|
#include "GitOTA.h"
|
|
#include "ModelEditor.h"
|
|
|
|
static const char *TAG = "Somfy";
|
|
|
|
extern SomfyShadeController somfy;
|
|
extern ConfigSettings settings;
|
|
extern GitUpdater git;
|
|
|
|
void SomfyShadeController::end() { this->transceiver.disableReceive(); }
|
|
SomfyShadeController::SomfyShadeController() {
|
|
memset(this->m_shadeIds, 255, sizeof(this->m_shadeIds));
|
|
uint64_t mac = ESP.getEfuseMac();
|
|
this->startingAddress = mac & 0x0FFFFF;
|
|
}
|
|
SomfyShade *SomfyShadeController::findShadeByRemoteAddress(uint32_t address) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
SomfyShade &shade = this->shades[i];
|
|
if(shade.getRemoteAddress() == address) return &shade;
|
|
else {
|
|
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) {
|
|
if(shade.linkedRemotes[j].getRemoteAddress() == address) return &shade;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
SomfyGroup *SomfyShadeController::findGroupByRemoteAddress(uint32_t address) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
SomfyGroup &group = this->groups[i];
|
|
if(group.getRemoteAddress() == address) return &group;
|
|
}
|
|
return nullptr;
|
|
}
|
|
void SomfyShadeController::updateGroupFlags() {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
SomfyGroup *group = &this->groups[i];
|
|
if(group && group->getGroupId() != 255) {
|
|
uint8_t flags = group->flags;
|
|
group->updateFlags();
|
|
if(flags != group->flags)
|
|
group->emitState();
|
|
}
|
|
}
|
|
}
|
|
bool SomfyShadeController::begin() {
|
|
Preferences pref;
|
|
// Load up all the configuration data.
|
|
//ShadeConfigFile::getAppVersion(this->appVersion);
|
|
ESP_LOGI(TAG, "App Version:%u.%u.%u", settings.appVersion.major, settings.appVersion.minor, settings.appVersion.build);
|
|
if(ShadeConfigFile::exists()) {
|
|
ESP_LOGI(TAG, "shades.cfg exists so we are using that");
|
|
ShadeConfigFile::load(this);
|
|
}
|
|
else {
|
|
ESP_LOGI(TAG, "Starting clean");
|
|
}
|
|
this->transceiver.begin();
|
|
|
|
// Set the radio type for shades that have yet to be specified.
|
|
bool saveFlag = false;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
SomfyShade *shade = &this->shades[i];
|
|
if(shade->getShadeId() != 255 && shade->bitLength == 0) {
|
|
//Serial.printf("Setting bit length to %d\n", this->transceiver.config.type);
|
|
shade->bitLength = this->transceiver.config.type;
|
|
saveFlag = true;
|
|
}
|
|
}
|
|
if(saveFlag) somfy.commit();
|
|
return true;
|
|
}
|
|
void SomfyShadeController::commit() {
|
|
if(git.lockFS) return;
|
|
esp_task_wdt_reset(); // Make sure we don't reset inadvertently.
|
|
ShadeConfigFile file;
|
|
file.begin();
|
|
file.save(this);
|
|
file.end();
|
|
this->isDirty = false;
|
|
this->lastCommit = millis();
|
|
}
|
|
void SomfyShadeController::writeBackup() {
|
|
if(git.lockFS) return;
|
|
esp_task_wdt_reset(); // Make sure we don't reset inadvertently.
|
|
this->backupData = "";
|
|
this->backupData.reserve(16384);
|
|
ShadeConfigFile file;
|
|
file.beginRAM(&this->backupData);
|
|
file.backup(this);
|
|
file.end();
|
|
}
|
|
SomfyRoom * SomfyShadeController::getRoomById(uint8_t roomId) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
|
if(this->rooms[i].roomId == roomId) return &this->rooms[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
SomfyShade * SomfyShadeController::getShadeById(uint8_t shadeId) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
if(this->shades[i].getShadeId() == shadeId) return &this->shades[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
SomfyGroup * SomfyShadeController::getGroupById(uint8_t groupId) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
if(this->groups[i].getGroupId() == groupId) return &this->groups[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
void SomfyRoom::clear() {
|
|
this->roomId = 0;
|
|
strcpy(this->name, "");
|
|
}
|
|
bool SomfyRoom::save() { somfy.commit(); return true; }
|
|
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, 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.
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
SomfyGroup *group = &this->groups[i];
|
|
if(group->getGroupId() == 255 || group->getRemoteAddress() != remoteAddress) continue;
|
|
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
|
if(group->linkedShades[j] == 0) continue;
|
|
SomfyShade *shade = this->getShadeById(group->linkedShades[j]);
|
|
if(shade) shade->markTxPending(ticket);
|
|
}
|
|
}
|
|
}
|
|
void SomfyShadeController::processTxEvent(const somfy_tx_event_t &evt) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
|
if(this->shades[i].getShadeId() != 255) this->shades[i].processTxEvent(evt);
|
|
}
|
|
void SomfyShadeController::processWaitingFrame() {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
|
if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame();
|
|
}
|
|
// A deleted shade, group or room leaves a gap, so a new one takes the lowest
|
|
// free id. Ids stop two short of the maximum, as they always have.
|
|
uint8_t SomfyShadeController::getNextShadeId() {
|
|
uint8_t ids[SOMFY_MAX_SHADES];
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) ids[i] = this->shades[i].getShadeId();
|
|
const uint8_t id = firstFreeId(ids, SOMFY_MAX_SHADES, SOMFY_MAX_SHADES - 2);
|
|
return id == 0 ? 255 : id;
|
|
}
|
|
int8_t SomfyShadeController::getMaxShadeOrder() {
|
|
int8_t orders[SOMFY_MAX_SHADES];
|
|
size_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
|
if(this->shades[i].getShadeId() != 255) orders[count++] = this->shades[i].sortOrder;
|
|
return nextSortOrder(orders, count) - 1;
|
|
}
|
|
int8_t SomfyShadeController::getMaxGroupOrder() {
|
|
int8_t orders[SOMFY_MAX_GROUPS];
|
|
size_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++)
|
|
if(this->groups[i].getGroupId() != 255) orders[count++] = this->groups[i].sortOrder;
|
|
return nextSortOrder(orders, count) - 1;
|
|
}
|
|
uint8_t SomfyShadeController::getNextGroupId() {
|
|
uint8_t ids[SOMFY_MAX_GROUPS];
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) ids[i] = this->groups[i].getGroupId();
|
|
const uint8_t id = firstFreeId(ids, SOMFY_MAX_GROUPS, SOMFY_MAX_GROUPS - 2);
|
|
return id == 0 ? 255 : id;
|
|
}
|
|
uint8_t SomfyShadeController::getNextRoomId() {
|
|
uint8_t ids[SOMFY_MAX_ROOMS];
|
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) ids[i] = this->rooms[i].roomId;
|
|
return firstFreeId(ids, SOMFY_MAX_ROOMS, SOMFY_MAX_ROOMS - 2);
|
|
}
|
|
int8_t SomfyShadeController::getMaxRoomOrder() {
|
|
int8_t orders[SOMFY_MAX_ROOMS];
|
|
size_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++)
|
|
if(this->rooms[i].roomId != 0) orders[count++] = this->rooms[i].sortOrder;
|
|
return nextSortOrder(orders, count) - 1;
|
|
}
|
|
uint8_t SomfyShadeController::repeaterCount() {
|
|
uint8_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
|
if(this->repeaters[i] != 0) count++;
|
|
}
|
|
return count;
|
|
}
|
|
uint8_t SomfyShadeController::roomCount() {
|
|
uint8_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
|
if(this->rooms[i].roomId != 0) count++;
|
|
}
|
|
return count;
|
|
}
|
|
uint8_t SomfyShadeController::shadeCount() {
|
|
uint8_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
if(this->shades[i].getShadeId() != 255) count++;
|
|
}
|
|
return count;
|
|
}
|
|
uint8_t SomfyShadeController::groupCount() {
|
|
uint8_t count = 0;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
if(this->groups[i].getGroupId() != 255) count++;
|
|
}
|
|
return count;
|
|
}
|
|
SomfyShade *SomfyShadeController::addShade() {
|
|
Preferences pref;
|
|
uint8_t shadeId = this->getNextShadeId();
|
|
// So the next shade id will be the first one we run into with an id of 255 so
|
|
// if it gets deleted in the middle then it will get the first slot that is empty.
|
|
// There is no apparent way around this. In the future we might actually add an indexer
|
|
// to it for sorting later. The time has come so the sort order is set below.
|
|
if(shadeId == 255) return nullptr;
|
|
SomfyShade *shade = &this->shades[shadeId - 1];
|
|
if(shade) {
|
|
shade->setShadeId(shadeId);
|
|
shade->sortOrder = this->getMaxShadeOrder() + 1;
|
|
ESP_LOGD(TAG, "Sort order set to %d", shade->sortOrder);
|
|
this->isDirty = true;
|
|
}
|
|
return shade;
|
|
}
|
|
bool SomfyShadeController::unlinkRepeater(uint32_t address) {
|
|
removeFromList(this->repeaters, SOMFY_MAX_REPEATERS, address);
|
|
this->isDirty = true;
|
|
return true;
|
|
}
|
|
// False when every repeater slot is taken.
|
|
bool SomfyShadeController::linkRepeater(uint32_t address) {
|
|
if(!addToList(this->repeaters, SOMFY_MAX_REPEATERS, address)) return false;
|
|
this->isDirty = true;
|
|
return true;
|
|
}
|
|
SomfyRoom *SomfyShadeController::addRoom() {
|
|
uint8_t roomId = this->getNextRoomId();
|
|
// So the next room id will be the first one we run into with an id of 0 so
|
|
if(roomId == 0) return nullptr;
|
|
SomfyRoom *room = &this->rooms[roomId - 1];
|
|
if(room) {
|
|
room->roomId = roomId;
|
|
room->sortOrder = this->getMaxRoomOrder() + 1;
|
|
this->isDirty = true;
|
|
}
|
|
return room;
|
|
}
|
|
SomfyGroup *SomfyShadeController::addGroup() {
|
|
uint8_t groupId = this->getNextGroupId();
|
|
// So the next shade id will be the first one we run into with an id of 255 so
|
|
// if it gets deleted in the middle then it will get the first slot that is empty.
|
|
// There is no apparent way around this. In the future we might actually add an indexer
|
|
// to it for sorting later.
|
|
if(groupId == 255) return nullptr;
|
|
SomfyGroup *group = &this->groups[groupId - 1];
|
|
if(group) {
|
|
group->setGroupId(groupId);
|
|
group->sortOrder = this->getMaxGroupOrder() + 1;
|
|
this->isDirty = true;
|
|
}
|
|
return group;
|
|
}
|
|
uint32_t SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat, uint16_t delayMs, bool priority) {
|
|
somfy_tx_job_t job;
|
|
encodeTxJob(job, frame, repeat);
|
|
job.firstRepeat = 1;
|
|
return this->transceiver.submitTx(job, priority, delayMs);
|
|
}
|
|
bool SomfyShadeController::deleteShade(uint8_t shadeId) {
|
|
Preferences pref;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
if(this->shades[i].getShadeId() == shadeId) {
|
|
shades[i].emitState("shadeRemoved");
|
|
shades[i].unpublish();
|
|
this->shades[i].clear();
|
|
}
|
|
}
|
|
this->commit();
|
|
return true;
|
|
}
|
|
bool SomfyShadeController::deleteRoom(uint8_t roomId) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
|
if(this->rooms[i].roomId == roomId) {
|
|
rooms[i].unpublish();
|
|
for(uint8_t j = 0; j < SOMFY_MAX_SHADES; j++) {
|
|
if(shades[j].roomId == roomId) {
|
|
shades[j].roomId = 0;
|
|
shades[j].emitState();
|
|
}
|
|
}
|
|
for(uint8_t j = 0; j < SOMFY_MAX_GROUPS; j++) {
|
|
if(groups[j].roomId == roomId) {
|
|
groups[j].roomId = 0;
|
|
groups[j].emitState();
|
|
}
|
|
}
|
|
rooms[i].emitState("roomRemoved");
|
|
this->rooms[i].clear();
|
|
}
|
|
}
|
|
this->commit();
|
|
return true;
|
|
}
|
|
bool SomfyShadeController::deleteGroup(uint8_t groupId) {
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
if(this->groups[i].getGroupId() == groupId) {
|
|
groups[i].emitState("groupRemoved");
|
|
groups[i].unpublish();
|
|
this->groups[i].clear();
|
|
}
|
|
}
|
|
this->commit();
|
|
return true;
|
|
}
|
|
bool SomfyShadeController::loadShadesFile(const char *filename) { return ShadeConfigFile::load(this, filename); }
|
|
void SomfyShadeController::loop() {
|
|
this->transceiver.loop();
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
if(this->shades[i].getShadeId() != 255) {
|
|
this->shades[i].checkMovement();
|
|
this->shades[i].setGPIOs();
|
|
}
|
|
}
|
|
// Only commit the file once per second.
|
|
if(this->isDirty && millis() - this->lastCommit > 1000) {
|
|
this->commit();
|
|
}
|
|
}
|