Added 80bit commands for Step, Stop, and Favorite #280

This commit is contained in:
Robert Strouse 2024-05-07 15:59:50 -07:00
parent c4e1dbe44b
commit e3e843387f
11 changed files with 280 additions and 188 deletions

View file

@ -105,6 +105,7 @@ bool Network::changeAP(const uint8_t *bssid, const int32_t channel) {
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
uint8_t retries = 0;
while(retries < 100) {
esp_task_wdt_reset(); // Make sure we do not reboot here.
wl_status_t stat = WiFi.status();
if(stat == WL_CONNECTED) {
Serial.println("WiFi module connected");
@ -436,6 +437,7 @@ void Network::updateHostname() {
bool Network::connectWiFi() {
if(settings.WIFI.ssid[0] != '\0') {
if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0) {
// If we are connected to the target SSID then just return.
this->disconnected = 0;
return true;
}
@ -536,41 +538,6 @@ bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel)
WiFi.scanDelete();
return chan > 0;
}
/*
int Network::getStrengthBySSID(const char *ssid) {
int32_t strength = -100;
esp_task_wdt_delete(NULL);
int n = WiFi.scanNetworks(false, false, false, 300, 0, ssid);
esp_task_wdt_add(NULL);
for(int i = 0; i < n; i++) {
if(WiFi.SSID(i).compareTo(ssid) == 0) strength = max(WiFi.RSSI(i), strength);
}
if(strength == -100) {
Serial.print("Could not find network [");
Serial.print(ssid);
Serial.print("] Scanned ");
Serial.print(n);
Serial.println(" Networks...");
String network;
for(int i = 0; i < n; i++) {
//WiFi.getNetworkInfo(i, network, encType, RSSI, BSSID, channel, isHidden);
if(network.compareTo(this->ssid) == 0) Serial.print("*");
else Serial.print(" ");
Serial.print(i);
Serial.print(": ");
Serial.print(WiFi.SSID(i).c_str());
Serial.print(" (");
Serial.print(WiFi.RSSI(i));
Serial.print("dBm) CH:");
Serial.print(WiFi.channel(i));
Serial.print(" MAC:");
Serial.print(WiFi.BSSIDstr(i).c_str());
Serial.println();
}
}
return strength;
}
*/
bool Network::openSoftAP() {
Serial.println();
Serial.println("Turning the HotSpot On");

235
Somfy.cpp
View file

@ -58,8 +58,9 @@ somfy_commands translateSomfyCommand(const String& string) {
else if (string.equalsIgnoreCase("Flag")) return somfy_commands::Flag;
else if (string.equalsIgnoreCase("Sensor")) return somfy_commands::Sensor;
else if (string.equalsIgnoreCase("Toggle")) return somfy_commands::Toggle;
else if (string.equalsIgnoreCase("Favorite")) return somfy_commands::Fav;
else if (string.startsWith("fav") || string.startsWith("FAV")) return somfy_commands::Fav;
else if (string.equalsIgnoreCase("Favorite")) return somfy_commands::Favorite;
else if (string.equalsIgnoreCase("Stop")) return somfy_commands::Stop;
else if (string.startsWith("fav") || string.startsWith("FAV")) return somfy_commands::Favorite;
else if (string.startsWith("mud") || string.startsWith("MUD")) return somfy_commands::MyUpDown;
else if (string.startsWith("md") || string.startsWith("MD")) return somfy_commands::MyDown;
else if (string.startsWith("ud") || string.startsWith("UD")) return somfy_commands::UpDown;
@ -107,12 +108,23 @@ String translateSomfyCommand(const somfy_commands cmd) {
return "Sensor";
case somfy_commands::Toggle:
return "Toggle";
case somfy_commands::Fav:
case somfy_commands::Favorite:
return "Favorite";
case somfy_commands::Stop:
return "Stop";
default:
return "Unknown(" + String((uint8_t)cmd) + ")";
}
}
byte somfy_frame_t::calc80Checksum(byte b0, byte b1, byte b2) {
byte cs80 = 0;
cs80 = (((b0 & 0xF0) >> 4) ^ ((b1 & 0xF0) >> 4));
cs80 ^= ((b2 & 0xF0) >> 4);
cs80 ^= (b0 & 0x0F);
cs80 ^= (b1 & 0x0F);
return cs80;
}
void somfy_frame_t::decodeFrame(byte* frame) {
byte decoded[10];
decoded[0] = frame[0];
@ -153,16 +165,22 @@ void somfy_frame_t::decodeFrame(byte* frame) {
// We reuse this memory address so we must reset the processed
// flag. This will ensure we can see frames on the first beat.
this->processed = false;
// Pull in the data for an 80-bit step command.
if(this->cmd == somfy_commands::StepDown) this->cmd = (somfy_commands)((decoded[1] >> 4) | ((decoded[8] & 0x08) << 4));
this->rollingCode = decoded[3] + (decoded[2] << 8);
this->remoteAddress = (decoded[6] + (decoded[5] << 8) + (decoded[4] << 16));
this->valid = this->checksum == checksum && this->remoteAddress > 0 && this->remoteAddress < 16777215;
if (this->cmd != somfy_commands::Sensor && this->valid) this->valid = (this->rollingCode > 0);
// Next lets process some of the RTS extensions for 80-bit frames
if(this->valid && this->proto == radio_proto::RTS && this->bitLength == 80) {
// Do a parity checksum on the 80 bit data.
if((decoded[9] & 0x0F) != this->calc80Checksum(decoded[7], decoded[8], decoded[9])) this->valid = false;
if(this->valid) {
// Translate extensions for stop and favorite.
if(this->cmd == somfy_commands::My) this->cmd = (somfy_commands)((decoded[1] >> 4) | ((decoded[8] & 0x0F) << 4));
// Bit packing to get the step size prohibits translation on the byte.
else if(this->cmd == somfy_commands::StepDown) this->cmd = (somfy_commands)((decoded[1] >> 4) | ((decoded[8] & 0x08) << 4));
}
}
if (this->valid) {
uint8_t csc80 = 0;
uint8_t cs80 = 0;
// Check for valid command.
switch (this->cmd) {
@ -185,25 +203,12 @@ void somfy_frame_t::decodeFrame(byte* frame) {
break;
case somfy_commands::StepUp:
case somfy_commands::StepDown:
// Decode the step size.
this->stepSize = ((decoded[8] & 0x07) << 4) | ((decoded[9] & 0xF0) >> 4);
break;
case somfy_commands::Toggle:
case somfy_commands::Fav:
// These must be 80 bit commands
csc80 = (decoded[9] & 0x0F);
cs80 = (((decoded[7] & 0xF0) >> 4) ^ ((decoded[8] & 0xF0) >> 4));
cs80 ^= ((decoded[9] & 0xF0) >> 4);
cs80 ^= (decoded[7] & 0x0F);
cs80 ^= (decoded[8] & 0x0F);
//cs80 = (((decoded[7] & 0xF0) >> 4) ^ ((decoded[8] & 0xF0) >> 4) ^ ((decoded[9] & 0xF0) >> 4) ^ (decoded[7] & 0x0F) ^ (decoded[8] & 0x0F));
if(csc80 != cs80) this->valid = false;
/*
uint8_t ai = ((decoded[7] & 0xF0) >> 4);
uint8_t aj = ((decoded[8] & 0xF0) >> 4);
uint8_t ak = ((decoded[9] & 0xF0) >> 4);
uint8_t al = (decoded[7] & 0x0F);
uint8_t am = (decoded[8] & 0x0F);
uint8_t an = (decoded[9] & 0x0F);
cs80 = ai ^ aj ^ ak ^ al ^ am;
*/
case somfy_commands::Favorite:
case somfy_commands::Stop:
break;
default:
this->valid = false;
@ -211,23 +216,7 @@ void somfy_frame_t::decodeFrame(byte* frame) {
}
}
if(this->valid && this->encKey == 0) this->valid = false;
if (this->valid) {
/*
Serial.print("KEY:");
Serial.print(this->encKey);
Serial.print(" ADDR:");
Serial.print(this->remoteAddress);
Serial.print(" CMD:");
Serial.print(translateSomfyCommand(this->cmd));
Serial.print(" RCODE:");
Serial.print(this->rollingCode);
Serial.print(" BITS:");
Serial.print(this->bitLength);
Serial.print(" HWSYNC:");
Serial.println(this->hwsync);
*/
}
else {
if (!this->valid) {
Serial.print("INVALID FRAME ");
Serial.print("KEY:");
Serial.print(this->encKey);
@ -268,6 +257,60 @@ void somfy_frame_t::decodeFrame(somfy_rx_t *rx) {
this->rssi = ELECHOUSE_cc1101.getRssi();
this->decodeFrame(rx->payload);
}
void somfy_frame_t::encode80BitFrame(byte *frame, uint8_t repeat) {
switch(this->cmd) {
// Step up and down commands encode the step size into the last 3 bytes.
case somfy_commands::StepUp:
if(repeat == 0) frame[1] = (static_cast<byte>(somfy_commands::StepDown) << 4) | (frame[1] & 0x0F);
if(this->stepSize == 0) this->stepSize = 1;
frame[7] = 132; // For simplicity this appears to be constant.
frame[8] = ((this->stepSize & 0x70) >> 4) | 0x38;
frame[9] = ((this->stepSize & 0x0F) << 4);
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
case somfy_commands::StepDown:
if(repeat == 0) frame[1] = (static_cast<byte>(somfy_commands::StepDown) << 4) | (frame[1] & 0x0F);
if(this->stepSize == 0) this->stepSize = 1;
frame[7] = 132; // For simplicity this appears to be constant.
frame[8] = ((this->stepSize & 0x70) >> 4) | 0x30;
frame[9] = ((this->stepSize & 0x0F) << 4);
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
case somfy_commands::Favorite:
if(repeat == 0) frame[1] = (static_cast<byte>(somfy_commands::My) << 4) | (frame[1] & 0x0F);
frame[7] = repeat > 0 ? 132 : 196;
frame[8] = 44;
frame[9] = 0x90;
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
case somfy_commands::Stop:
if(repeat == 0) frame[1] = (static_cast<byte>(somfy_commands::My) << 4) | (frame[1] & 0x0F);
frame[7] = repeat > 0 ? 132 : 196;
frame[8] = 47;
frame[9] = 0xF0;
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
case somfy_commands::Toggle:
if(repeat == 0) {
frame[0] = 164;
frame[1] |= 0xF0;
}
frame[7] = 196;
frame[8] = 0;
frame[9] = 0x10;
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
case somfy_commands::Prog:
if(repeat > 0) frame[7] = 196 + (repeat * 4);
else frame[7] = 132;
frame[8] = 0;
frame[9] = ((repeat + 1) & 0x0F) << 4;
frame[9] |= this->calc80Checksum(frame[7], frame[8], frame[9]);
break;
default:
break;
}
}
void somfy_frame_t::encodeFrame(byte *frame) {
const byte btn = static_cast<byte>(cmd);
frame[0] = this->encKey; // Encryption key. Doesn't matter much
@ -357,34 +400,7 @@ void somfy_frame_t::encodeFrame(byte *frame) {
}
else {
switch(this->cmd) {
case somfy_commands::StepUp:
frame[7] = 132;
frame[8] = 56;
frame[9] = 22;
break;
case somfy_commands::StepDown:
frame[7] = 132;
frame[8] = 48;
frame[9] = 30;
break;
case somfy_commands::Toggle:
frame[0] = 164;
// This also needs to be a command val of 15.
frame[1] |= 0xF0;
frame[7] = 196;
frame[8] = 0;
frame[9] = 25;
break;
case somfy_commands::Prog:
//frame[0] = 0xA6;
frame[7] = 196;
frame[8] = 0;
frame[9] = 25;
break;
default:
break;
}
if(this->bitLength == 80) this->encode80BitFrame(&frame[0], 0);
}
byte checksum = 0;
@ -584,6 +600,7 @@ bool SomfyShadeController::begin() {
}
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);
@ -593,6 +610,7 @@ void SomfyShadeController::commit() {
}
void SomfyShadeController::writeBackup() {
if(git.lockFS) return;
esp_task_wdt_reset(); // Make sure we don't reset inadvertently.
ShadeConfigFile file;
file.begin("/controller.backup", false);
file.backup(this);
@ -2382,7 +2400,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
else {
Serial.println("Moving to My target");
this->lastFrame.processed = true;
if(this->myTiltPos >= 0.0f && this->myTiltPos >= 100.0f) this->p_tiltTarget(this->myTiltPos);
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);
}
@ -2405,6 +2423,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
// the motor must tilt in the direction first then move
// so we have to calculate the target with this in mind.
if(this->stepSize == 0) return; // Avoid divide by 0.
if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1;
if(this->tiltType == tilt_types::integrated) {
// With integrated tilt this is more involved than ne would think because the step command can be moving not just the tilt
// but the lift. So a determination needs to be made as to whether we are currently moving and it should stop.
@ -2417,22 +2436,22 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
// Set the tilt position. This should stop the lift movement.
this->p_target(this->currentPos);
if(this->tiltTime == 0) return; // Avoid divide by 0.
this->p_tiltTarget(max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize))))));
this->p_tiltTarget(max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
else {
// We only have the lift to move.
if(this->upTime == 0) return; // Avoid divide by 0.
this->p_tiltTarget(this->currentTiltPos);
this->p_target(max(0.0f, this->currentPos - (100.0f/(static_cast<float>(this->upTime/static_cast<float>(this->stepSize))))));
this->p_target(max(0.0f, this->currentPos - (100.0f/(static_cast<float>(this->upTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
}
else if(this->tiltType == tilt_types::tiltonly) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->p_tiltTarget(max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize))))));
this->p_tiltTarget(max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
else if(this->currentPos > 0.0f) {
if(this->downTime == 0 || this->stepSize == 0) return;
this->p_target(max(0.0f, this->currentPos - (100.0f/(static_cast<float>(this->upTime/static_cast<float>(this->stepSize))))));
this->p_target(max(0.0f, this->currentPos - (100.0f/(static_cast<float>(this->upTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
break;
@ -2445,6 +2464,8 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
// the motor must tilt in the direction first then move
// so we have to calculate the target with this in mind.
if(this->stepSize == 0) return; // Avoid divide by 0.
if(this->lastFrame.stepSize == 0) this->lastFrame.stepSize = 1;
if(this->tiltType == tilt_types::integrated) {
// With integrated tilt this is more involved than ne would think because the step command can be moving not just the tilt
// but the lift. So a determination needs to be made as to whether we are currently moving and it should stop.
@ -2457,22 +2478,22 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
// Set the tilt position. This should stop the lift movement.
this->p_target(this->currentPos);
if(this->tiltTime == 0) return; // Avoid divide by 0.
this->p_tiltTarget(min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize))))));
this->p_tiltTarget(min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
else {
// We only have the lift to move.
this->p_tiltTarget(this->currentTiltPos);
if(this->downTime == 0) return; // Avoid divide by 0.
this->p_target(min(100.0f, this->currentPos + (100.0f/(static_cast<float>(this->downTime/static_cast<float>(this->stepSize))))));
this->p_target(min(100.0f, this->currentPos + (100.0f/(static_cast<float>(this->downTime/static_cast<float>(this->stepSize* this->lastFrame.stepSize))))));
}
}
else if(this->tiltType == tilt_types::tiltonly) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->p_target(min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize))))));
this->p_target(min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
else if(this->currentPos < 100.0f) {
if(this->downTime == 0 || this->stepSize == 0) return;
this->p_target(min(100.0f, this->currentPos + (100.0f/(static_cast<float>(this->downTime/static_cast<float>(this->stepSize))))));
this->p_target(min(100.0f, this->currentPos + (100.0f/(static_cast<float>(this->downTime/static_cast<float>(this->stepSize * this->lastFrame.stepSize))))));
}
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
break;
@ -2485,6 +2506,23 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
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);
break;
case somfy_commands::Favorite:
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;
@ -2820,7 +2858,7 @@ void SomfyShade::moveToMyPosition() {
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
void SomfyShade::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
// 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;
@ -2875,14 +2913,14 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
}
}
else if(cmd == somfy_commands::Toggle) {
if(this->bitLength != 80) SomfyRemote::sendCommand(somfy_commands::My, repeat);
if(this->bitLength != 80) SomfyRemote::sendCommand(somfy_commands::My, repeat, stepSize);
else SomfyRemote::sendCommand(somfy_commands::Toggle, repeat);
}
else if(this->shadeType == shade_types::garage1 && cmd == somfy_commands::Prog) {
SomfyRemote::sendCommand(somfy_commands::Toggle, repeat);
SomfyRemote::sendCommand(somfy_commands::Toggle, repeat, stepSize);
}
else {
SomfyRemote::sendCommand(cmd, repeat);
SomfyRemote::sendCommand(cmd, repeat, stepSize);
}
}
void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
@ -3848,12 +3886,13 @@ void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe
somfy.processFrame(this->lastFrame, true);
}
void SomfyRemote::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) {
void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
this->lastFrame.rollingCode = this->getNextRollingCode();
this->lastFrame.remoteAddress = this->getRemoteAddress();
this->lastFrame.cmd = this->transformCommand(cmd);
this->lastFrame.repeats = repeat;
this->lastFrame.bitLength = this->bitLength;
this->lastFrame.stepSize = stepSize;
// Match the encKey to the rolling code. These keys range from 160 to 175.
this->lastFrame.encKey = 0xA0 | static_cast<uint8_t>(this->lastFrame.rollingCode & 0x000F);
this->lastFrame.proto = this->proto;
@ -3924,24 +3963,10 @@ void SomfyShadeController::sendFrame(somfy_frame_t &frame, uint8_t repeat) {
byte frm[10];
frame.encodeFrame(frm);
this->transceiver.sendFrame(frm, frame.bitLength == 56 ? 2 : 12, frame.bitLength);
// Transform the repeat bytes
switch(frame.cmd) {
case somfy_commands::StepUp:
case somfy_commands::StepDown:
break;
case somfy_commands::Prog:
frm[7] = 132;
frm[9] = 63;
break;
case somfy_commands::Toggle:
frm[7] = 136;
frm[9] = 34;
break;
default:
frm[9] = 46;
break;
}
for(uint8_t i = 0; i < repeat; i++) {
// For each 80-bit frame we need to adjust the byte encoding for the
// silence.
if(frame.bitLength == 80) frame.encode80BitFrame(&frm[0], i + 1);
this->transceiver.sendFrame(frm, frame.bitLength == 56 ? 7 : 6, frame.bitLength);
esp_task_wdt_reset();
}
@ -4330,7 +4355,7 @@ void RECEIVE_ATTR Transceiver::handleReceive() {
else if (duration > tempo_synchro_sw_min && duration < tempo_synchro_sw_max && somfy_rx.cpt_synchro_hw >= 4) {
// If we have a full hardware sync then we should look for the software sync. If we have a software sync
// bit and enough hardware sync bits then we should start receiving data. It turns out that a 56 bit packet
// with give 4 or 14 bits of hardware sync. An 80 bit packet give 12 or 24 bits of hw sync. Early on
// with give 4 or 14 bits of hardware sync. An 80 bit packet gives 12, 13 or 24 bits of hw sync. Early on
// I had some shorter and longer hw syncs but I can no longer repeat this.
memset(somfy_rx.payload, 0x00, sizeof(somfy_rx.payload));
somfy_rx.previous_bit = 0x00;
@ -4339,8 +4364,10 @@ void RECEIVE_ATTR Transceiver::handleReceive() {
// Keep an eye on this as it is possible that we might get fewer or more synchro bits.
if (somfy_rx.cpt_synchro_hw <= 7) somfy_rx.bit_length = 56;
else if (somfy_rx.cpt_synchro_hw == 14) somfy_rx.bit_length = 56;
else if (somfy_rx.cpt_synchro_hw == 13) somfy_rx.bit_length = 80; // The RS485 device sends this sync.
else if (somfy_rx.cpt_synchro_hw == 12) somfy_rx.bit_length = 80;
else if (somfy_rx.cpt_synchro_hw > 17) somfy_rx.bit_length = 80;
else somfy_rx.bit_length = 56;
somfy_rx.status = receiving_data;
}
else {
@ -4535,6 +4562,8 @@ void Transceiver::emitFrame(somfy_frame_t *frame, somfy_rx_t *rx) {
json->addElem("proto", static_cast<uint8_t>(frame->proto));
json->addElem("valid", frame->valid);
json->addElem("sync", frame->hwsync);
if(frame->cmd == somfy_commands::StepUp || frame->cmd == somfy_commands::StepDown)
json->addElem("stepSize", frame->stepSize);
json->beginArray("pulses");
if(rx) {
for(uint16_t i = 0; i < rx->pulseCount; i++) {

View file

@ -47,7 +47,8 @@ enum class somfy_commands : byte {
RTWProto = 0xF, // RTW Protocol
// Command extensions for 80 bit frames
StepUp = 0x8B,
Fav = 0x90,
Favorite = 0xC1,
Stop = 0xF1
};
enum class group_types : byte {
channel = 0x00
@ -185,6 +186,8 @@ struct somfy_frame_t {
uint16_t pulseCount = 0;
uint8_t stepSize = 0;
void print();
void encode80BitFrame(byte *frame, uint8_t repeat);
byte calc80Checksum(byte b0, byte b1, byte b2);
void encodeFrame(byte *frame);
void decodeFrame(byte* frame);
void decodeFrame(somfy_rx_t *rx);
@ -242,7 +245,7 @@ class SomfyRemote {
void setLight(bool bHasLight);
void setSimMy(bool bSimMy);
virtual void sendCommand(somfy_commands cmd);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
void sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat);
void repeatFrame(uint8_t repeat);
virtual uint16_t p_lastRollingCode(uint16_t code);
@ -320,7 +323,7 @@ class SomfyShade : public SomfyRemote {
void moveToTiltTarget(float target);
void sendTiltCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat);
void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0);
bool unlinkRemote(uint32_t remoteAddress);
void emitState(const char *evt = "shadeState");

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
Web.cpp
View file

@ -343,6 +343,7 @@ void Web::handleShadeCommand(WebServer& server) {
HTTPMethod method = server.method();
uint8_t shadeId = 255;
uint8_t target = 255;
uint8_t stepSize = 0;
int8_t repeat = -1;
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
@ -351,10 +352,11 @@ void Web::handleShadeCommand(WebServer& server) {
if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command"));
else if (server.hasArg("target")) target = atoi(server.arg("target").c_str());
if (server.hasArg("repeat")) repeat = atoi(server.arg("repeat").c_str());
if(server.hasArg("stepSize")) stepSize = atoi(server.arg("stepSize").c_str());
}
else if (server.hasArg("plain")) {
Serial.println("Sending Shade Command");
DynamicJsonDocument doc(256);
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
this->handleDeserializationError(server, err);
@ -372,6 +374,7 @@ void Web::handleShadeCommand(WebServer& server) {
target = obj["target"].as<uint8_t>();
}
if (obj.containsKey("repeat")) repeat = obj["repeat"].as<uint8_t>();
if(obj.containsKey("stepSize")) stepSize = obj["stepSize"].as<uint8_t>();
}
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}"));
@ -382,15 +385,13 @@ void Web::handleShadeCommand(WebServer& server) {
// Send the command to the shade.
if (target <= 100)
shade->moveToTarget(shade->transformPosition(target));
else if (repeat > 0)
shade->sendCommand(command, repeat);
else
shade->sendCommand(command);
shade->sendCommand(command, repeat > 0 ? repeat : shade->repeats, stepSize);
JsonResponse resp;
resp.beginResponse(&server, g_content, sizeof(g_content));
resp.beginArray();
resp.beginObject();
shade->toJSONRef(resp);
resp.endArray();
resp.endObject();
resp.endResponse();
}
else {
@ -414,7 +415,7 @@ void Web::handleRepeatCommand(WebServer& server) {
if(server.hasArg("command")) command = translateSomfyCommand(server.arg("command"));
if(server.hasArg("repeat")) repeat = atoi(server.arg("repeat").c_str());
if(shadeId == 255 && groupId == 255 && server.hasArg("plain")) {
DynamicJsonDocument doc(256);
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
this->handleDeserializationError(server, err);

View file

@ -1494,4 +1494,49 @@ i.icss-bars {
i.icss-bars:after {
top: 0.36em;
left: 0;
}
}
i.icss-hand {
width: .6em;
height: .5em;
border-radius: .35em .3em .5em .5em;
margin: .5em .1em 0 .21em;
}
i.icss-hand:before {
width: .1em;
height: .55em;
background: currentColor;
left: .5em;
bottom: .3em;
border-radius: 80% / 20%;
box-shadow: -.13em -.1em 0, -.265em -.15em 0, -.4em -.11em 0;
}
i.icss-hand:after {
width: .12em;
height: .43em;
background: currentColor;
bottom: .25em;
left: -.06em;
border-radius: .04em;
transform: rotate(-16deg);
border-radius: .04em 70% .04em .04em / .04em 70% .04em .04em;
}
i.icss-bookmark {
width: 1em;
height: 1em;
background-color: transparent;
margin: 0;
}
i.icss-bookmark:before {
width: .5em;
height: .8em;
transform: translate(-50%, -50%);
border: .25em solid currentColor;
border-color: currentColor currentColor transparent currentColor;
border-radius: .03em;
top: 50%;
left: 50%;
}

View file

@ -8,9 +8,9 @@
<meta name="apple-mobile-web-app-title" content="ESPSomfy RTS App">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="stylesheet" href="main.css?v=2.4.3b" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.4.3b" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.4.3b" type="text/css" />
<link rel="stylesheet" href="main.css?v=2.4.3c" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.4.3c" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.4.3c" type="text/css" />
<link rel="icon" type="image/png" href="favicon.png" />
<!-- iPad retina icon -->
@ -114,7 +114,7 @@
rel="apple-touch-startup-image">
<script type="text/javascript" src="index.js?v=2.4.2r"></script>
<script type="text/javascript" src="index.js?v=2.4.3c"></script>
</head>
<body>
<div id="divContainer" class="container main" data-auth="false">
@ -775,11 +775,11 @@
</div>
</div>
</div>
<div id="divVirtualRemote" style="display:none;" class="subtab-content">
<div id="divVirtualRemote" style="display:none;" class="subtab-content" data-bitlength="56">
<div class="field-group" style="margin-top:-18px;display:inline-block;width:100%;">
<select id="selVRMotor" style="width:100%;">
<select id="selVRMotor" style="width:100%;" onchange="document.getElementById('divVirtualRemote').setAttribute('data-bitlength', this.options[this.selectedIndex].getAttribute('data-bitlength'));">
</select>
<label for="selVRMotor">Select a Motor</label>
<label for="selVRMotor">Select a Motor or Group</label>
</div>
<div class="vr-button vr-updownmy">
<span>Remote Buttons</span>
@ -789,12 +789,38 @@
<div class="button-outline" data-cmd="down" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>
</div>
</div>
<div class="vr-button vr-stop vr-80bit">
<span>Stop</span>
<div>
<div class="button-outline toggle-button" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="stop" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-hand" style=""></i></div>
</div>
</div>
<div class="vr-button vr-updownmy">
<span>Toggle Button</span>
<div>
<div class="button-outline toggle-button" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="toggle" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-somfy-toggle" style="margin-top:-4px;"></i></div>
</div>
</div>
<div class="vr-button vr-updown vr-80bit">
<span>Step</span>
<div style="margin-right:7px;width:470px;font-size:.8em;">
<input id="vrslidStepSize" name="stepSize" type="range" min="1" max="127" step="1" style="width:100%;" data-bind="stepSize" value="1" oninput="document.getElementById('vrspanStepSize').innerText = this.value;" />
<div>
<label style="color:#00bcd4;">Step Size:</label><span id="vrspanStepSize">1</span>
</div>
</div>
<div>
<div class="button-outline" style="width:59px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="StepUp" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span></span><i class="icss-somfy-up" style="margin-top:4px;"></i></div>
<div class="button-outline" style="width:59px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="StepDown" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span></span><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>
</div>
</div>
<div class="vr-button vr-favorite vr-80bit">
<span>Favorite</span>
<div>
<div class="button-outline toggle-button" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="favorite" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-bookmark" style=""></i></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Up + Down</span>
<div>
@ -819,18 +845,6 @@
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="MyDown" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span>my</span><span> + </span><i class="icss-somfy-down"></i></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Step Up</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="StepUp" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span>step </span><i class="icss-somfy-up"></i></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Step Down</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="StepDown" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span>step </span><i class="icss-somfy-down"></i></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Prog</span>
<div>
@ -838,15 +852,10 @@
</div>
</div>
<div class="vr-button vr-updown">
<span>Sun Flag On</span>
<span>Sun Flag</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="SunFlag" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><div class="button-sunflag" data-on="true" style="margin:0px;"><i class="icss-sun-c" style="background:white;"></i><i class="icss-sun-o" style="left:0px;color:white;"></i></div><span>on</span></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Sun Flag Off</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="Flag" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><div class="button-sunflag" data-on="false" style="margin:0px;"><i class="icss-sun-c" style="background:transparent;"></i><i class="icss-sun-o" style="left:0px;color:white;"></i></div><span>off</span></div>
<div class="button-outline" style="width: 59px; text-align: center; border-radius: 33%; font-size: 2em; padding: 10px; height: 52px;" data-cmd="SunFlag" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><div class="button-sunflag" data-on="true" style="margin:0px;margin-left:4px;"><i class="icss-sun-c" style="background:white;"></i><i class="icss-sun-o" style="left:0px;color:white;"></i></div></div>
<div class="button-outline" style="width: 59px; text-align: center; border-radius: 33%; font-size: 2em; padding: 10px; height: 52px;" data-cmd="Flag" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><div class="button-sunflag" data-on="false" style="margin:0px;margin-left:4px;"><i class="icss-sun-c" style="background:transparent;"></i><i class="icss-sun-o" style="left:0px;color:white;"></i></div></div>
</div>
</div>
<div class="vr-button">

View file

@ -2301,8 +2301,11 @@ class Somfy {
opt.setAttribute('data-type', 'shade');
opt.setAttribute('data-shadetype', shade.shadeType);
opt.setAttribute('data-shadeid', shade.shadeId);
opt.setAttribute('data-bitlength', shade.bitLength);
optGroup.appendChild(opt);
}
let sopt = vrList.options[vrList.selectedIndex];
document.getElementById('divVirtualRemote').setAttribute('data-bitlength', sopt ? sopt.getAttribute('data-bitlength') : 'none');
document.getElementById('divShadeList').innerHTML = divCfg;
let shadeControls = document.getElementById('divShadeControls');
shadeControls.innerHTML = divCtl;
@ -2611,9 +2614,13 @@ class Somfy {
opt.setAttribute('data-address', group.remoteAddress);
opt.setAttribute('data-type', 'group');
opt.setAttribute('data-groupid', group.groupId);
opt.setAttribute('data-bitlength', group.bitLength);
optGroup.appendChild(opt);
}
}
let sopt = vrList.options[vrList.selectedIndex];
document.getElementById('divVirtualRemote').setAttribute('data-bitlength', sopt ? sopt.getAttribute('data-bitlength') : 'none');
document.getElementById('divGroupList').innerHTML = divCfg;
let groupControls = document.getElementById('divGroupControls');
groupControls.innerHTML = divCtl;
@ -2891,7 +2898,7 @@ class Somfy {
proto = '-V';
break;
}
let html = `<span>${frame.encKey}</span><span>${frame.address}</span><span>${frame.command}</span><span>${frame.rcode}</span><span>${frame.rssi}dBm</span><span>${frame.bits}${proto}</span><span>${fnFmtTime(frame.time)}</span><div class="frame-pulses">`;
let html = `<span>${frame.encKey}</span><span>${frame.address}</span><span>${frame.command}<sup>${frame.stepSize ? frame.stepSize : ''}</sup></span><span>${frame.rcode}</span><span>${frame.rssi}dBm</span><span>${frame.bits}${proto}</span><span>${fnFmtTime(frame.time)}</span><div class="frame-pulses">`;
for (let i = 0; i < frame.pulses.length; i++) {
if (i !== 0) html += ',';
html += `${frame.pulses[i]}`;
@ -3687,22 +3694,47 @@ class Somfy {
return div;
}
sendCommand(shadeId, command, repeat, cb) {
console.log(`Sending Shade command ${shadeId}-${command}`);
let obj = { shadeId: shadeId };
if (isNaN(parseInt(command, 10))) obj.command = command;
else obj.target = parseInt(command, 10);
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
let obj = {};
if (typeof shadeId.shadeId !== 'undefined') {
obj = shadeId;
cb = command;
shadeId = obj.shadeId;
repeat = obj.repeat;
command = obj.command;
}
else {
obj = { shadeId: shadeId };
if (isNaN(parseInt(command, 10))) obj.command = command;
else obj.target = parseInt(command, 10);
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
}
putJSON('/shadeCommand', obj, (err, shade) => {
if (typeof cb === 'function') cb(err, shade);
});
}
sendCommandRepeat(shadeId, command, repeat, cb) {
//console.log(`Sending Shade command ${shadeId}-${command}`);
let obj = { shadeId: shadeId, command: command };
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
let obj = {};
if (typeof shadeId.shadeId !== 'undefined') {
obj = shadeId;
cb = command;
shadeId = obj.shadeId;
repeat = obj.repeat;
command = obj.command;
}
else {
obj = { shadeId: shadeId, command: command };
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
}
putJSON('/repeatCommand', obj, (err, shade) => {
if (typeof cb === 'function') cb(err, shade);
});
/*
putJSON(`/repeatCommand?shadeId=${shadeId}&command=${command}`, null, (err, shade) => {
if(typeof cb === 'function') cb(err, shade);
});
*/
}
sendGroupRepeat(groupId, command, repeat, cb) {
let obj = { groupId: groupId, command: command };
@ -3721,7 +3753,6 @@ class Somfy {
cmd: el.getAttribute('data-cmd')
};
ui.fromElement(el.parentElement.parentElement, o);
console.log(o);
switch (o.type) {
case 'shade':
o.shadeId = parseInt(opt.getAttribute('data-shadeId'), 10);
@ -3744,17 +3775,17 @@ class Somfy {
else if (o.type === 'group')
somfy.sendGroupRepeat(o.groupId, o.cmd, null, fnRepeatCommand);
else
somfy.sendCommandRepeat(o.shadeId, o.cmd, null, fnRepeatCommand);
somfy.sendCommandRepeat(o, fnRepeatCommand);
}
}
o.command = o.cmd;
if (o.cmd === 'Sensor') {
somfy.sendSetSensor(o);
}
else if (o.type === 'group')
somfy.sendGroupCommand(o.groupId, o.cmd, null, (err, group) => { fnRepeatCommand(err, group); });
else
somfy.sendCommand(o.shadeId, o.cmd, null, (err, shade) => { fnRepeatCommand(err, shade); });
somfy.sendCommand(o, (err, shade) => { fnRepeatCommand(err, shade); });
}
sendSetSensor(obj, cb) {
putJSON('/setSensor', obj, (err, device) => {

View file

@ -702,6 +702,12 @@ div.wait-overlay > .lds-roller {
cursor: pointer;
}
#divVirtualRemote[data-bitlength="56"] div.vr-button.vr-80bit {
display:none;
}
#divVirtualRemote[data-bitlength="none"] div.vr-button {
display:none;
}
.shade-positioner {
position: absolute;
width: 100%;
@ -804,6 +810,7 @@ div.frame-header > span {
}
div.frame-row > span:nth-child(3),
div.frame-header > span:nth-child(3) {
white-space:nowrap;
width: 80px;
text-align:center;
}