#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->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++) this->linkedRemotes[i].setRemoteAddress(0); this->paired = false; this->name[0] = 0x00; this->upTime = 10000; this->downTime = 10000; this->tiltTime = 7000; this->stepSize = 100; this->repeats = 1; this->sortOrder = 255; } bool SomfyShade::linkRemote(uint32_t address, uint16_t rollingCode) { Preferences pref; // Check to see if the remote is already linked. If it is // just return true after setting the rolling code for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { if(this->linkedRemotes[i].getRemoteAddress() == address) { this->linkedRemotes[i].setRollingCode(rollingCode); return true; } } for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { if(this->linkedRemotes[i].getRemoteAddress() == 0) { this->linkedRemotes[i].setRemoteAddress(address); this->linkedRemotes[i].setRollingCode(rollingCode); this->commit(); return true; } } 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++) { if(this->linkedRemotes[i].getRemoteAddress() == address) { this->linkedRemotes[i].setRemoteAddress(0); this->commit(); return true; } } 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++) { if(somfy.groups[i].getGroupId() != 255 && somfy.groups[i].hasShadeId(this->getShadeId())) return true; } return false; } void SomfyShade::checkMovement() { const uint64_t curTime = millis(); const bool sunFlag = this->flags & static_cast(somfy_flags_t::SunFlag); const bool isSunny = this->flags & static_cast(somfy_flags_t::Sunny); const bool isWindy = this->flags & 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->flags & static_cast(flag)); if(val) this->flags |= static_cast(flag); else this->flags &= ~(static_cast(flag)); return old; } 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++) { if(this->linkedRemotes[i].getRemoteAddress() == address) return &this->linkedRemotes[i]; } return nullptr; } void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { // 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; 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->flags; 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->flags |= static_cast(somfy_flags_t::DemoMode); else this->flags &= ~(static_cast(somfy_flags_t::DemoMode)); const bool isSunny = this->flags & static_cast(somfy_flags_t::Sunny); const bool isWindy = this->flags & 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->flags & static_cast(somfy_flags_t::Windy); const bool isSunny = this->flags & 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->hasSunSensor()) { somfy.isDirty = true; this->emitState(); } else { ESP_LOGW(TAG, "Shade does not have sensor %d", this->flags); } break; case somfy_commands::SunFlag: if(this->hasSunSensor()) { const bool isWindy = this->flags & static_cast(somfy_flags_t::Windy); const bool isSunny = this->flags & 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->flags); 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; } 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; } 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::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; }