Add repeater function #261

This commit is contained in:
Robert Strouse 2024-02-06 17:50:33 -08:00
parent b458435ddf
commit 4e0d89e7db
15 changed files with 430 additions and 81 deletions

View file

@ -7,12 +7,13 @@
extern Preferences pref; extern Preferences pref;
#define SHADE_HDR_VER 20 #define SHADE_HDR_VER 21
#define SHADE_HDR_SIZE 66 #define SHADE_HDR_SIZE 76
#define SHADE_REC_SIZE 276 #define SHADE_REC_SIZE 276
#define GROUP_REC_SIZE 194 #define GROUP_REC_SIZE 194
#define TRANS_REC_SIZE 74 #define TRANS_REC_SIZE 74
#define ROOM_REC_SIZE 29 #define ROOM_REC_SIZE 29
#define REPEATER_REC_SIZE 77
extern ConfigSettings settings; extern ConfigSettings settings;
@ -51,7 +52,8 @@ bool ConfigFile::writeHeader(const config_header_t &hdr) {
this->writeUInt8(hdr.shadeRecords); this->writeUInt8(hdr.shadeRecords);
this->writeUInt16(hdr.groupRecordSize); this->writeUInt16(hdr.groupRecordSize);
this->writeUInt8(hdr.groupRecords); this->writeUInt8(hdr.groupRecords);
this->writeUInt16(hdr.repeaterRecordSize);
this->writeUInt8(hdr.repeaterRecords);
this->writeUInt16(hdr.settingsRecordSize); this->writeUInt16(hdr.settingsRecordSize);
this->writeUInt16(hdr.netRecordSize); this->writeUInt16(hdr.netRecordSize);
this->writeUInt16(hdr.transRecordSize); this->writeUInt16(hdr.transRecordSize);
@ -76,6 +78,10 @@ bool ConfigFile::readHeader() {
else this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize); else this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize);
this->header.groupRecords = this->readUInt8(this->header.groupRecords); this->header.groupRecords = this->readUInt8(this->header.groupRecords);
} }
if(this->header.version >= 21) {
this->header.repeaterRecordSize = this->readUInt16(this->header.repeaterRecordSize);
this->header.repeaterRecords = this->readUInt8(this->header.repeaterRecords);
}
if(this->header.version > 13) { if(this->header.version > 13) {
this->header.settingsRecordSize = this->readUInt16(this->header.settingsRecordSize); this->header.settingsRecordSize = this->readUInt16(this->header.settingsRecordSize);
this->header.netRecordSize = this->readUInt16(this->header.netRecordSize); this->header.netRecordSize = this->readUInt16(this->header.netRecordSize);
@ -292,6 +298,8 @@ bool ShadeConfigFile::save(SomfyShadeController *s) {
this->header.shadeRecords = s->shadeCount(); this->header.shadeRecords = s->shadeCount();
this->header.groupRecordSize = GROUP_REC_SIZE; this->header.groupRecordSize = GROUP_REC_SIZE;
this->header.groupRecords = s->groupCount(); this->header.groupRecords = s->groupCount();
this->header.repeaterRecords = 1;
this->header.repeaterRecordSize = REPEATER_REC_SIZE;
this->header.settingsRecordSize = 0; this->header.settingsRecordSize = 0;
this->header.netRecordSize = 0; this->header.netRecordSize = 0;
this->header.transRecordSize = 0; this->header.transRecordSize = 0;
@ -311,6 +319,7 @@ bool ShadeConfigFile::save(SomfyShadeController *s) {
if(group->getGroupId() != 255) if(group->getGroupId() != 255)
this->writeGroupRecord(group); this->writeGroupRecord(group);
} }
this->writeRepeaterRecord(s);
return true; return true;
} }
bool ShadeConfigFile::backup(SomfyShadeController *s) { bool ShadeConfigFile::backup(SomfyShadeController *s) {
@ -322,6 +331,8 @@ bool ShadeConfigFile::backup(SomfyShadeController *s) {
this->header.shadeRecords = s->shadeCount(); this->header.shadeRecords = s->shadeCount();
this->header.groupRecordSize = GROUP_REC_SIZE; this->header.groupRecordSize = GROUP_REC_SIZE;
this->header.groupRecords = s->groupCount(); this->header.groupRecords = s->groupCount();
this->header.repeaterRecords = 1;
this->header.repeaterRecordSize = REPEATER_REC_SIZE;
this->header.settingsRecordSize = settings.calcSettingsRecSize(); this->header.settingsRecordSize = settings.calcSettingsRecSize();
this->header.netRecordSize = settings.calcNetRecSize(); this->header.netRecordSize = settings.calcNetRecSize();
this->header.transRecordSize = TRANS_REC_SIZE; this->header.transRecordSize = TRANS_REC_SIZE;
@ -341,6 +352,7 @@ bool ShadeConfigFile::backup(SomfyShadeController *s) {
if(group->getGroupId() != 255) if(group->getGroupId() != 255)
this->writeGroupRecord(group); this->writeGroupRecord(group);
} }
this->writeRepeaterRecord(s);
this->writeSettingsRecord(); this->writeSettingsRecord();
this->writeNetRecord(); this->writeNetRecord();
this->writeTransRecord(s->transceiver.config); this->writeTransRecord(s->transceiver.config);
@ -394,6 +406,9 @@ bool ShadeConfigFile::validate() {
fsize += (this->header.netRecordSize); fsize += (this->header.netRecordSize);
fsize += (this->header.transRecordSize); fsize += (this->header.transRecordSize);
} }
if(this->header.version >= 21) {
fsize += (this->header.repeaterRecordSize * this->header.repeaterRecords);
}
if(this->file.size() != fsize) { if(this->file.size() != fsize) {
Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size()); Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size());
} }
@ -442,6 +457,17 @@ bool ShadeConfigFile::validate() {
} }
} }
} }
if(this->header.version >= 21) {
recs = 0;
while(recs < this->header.repeaterRecords) {
uint32_t pos = this->file.position();
if(!this->seekChar(CFG_REC_END)) {
Serial.printf("Failed to find the repeater record end %d\n", recs);
}
recs++;
}
}
this->file.seek(startPos, SeekSet); this->file.seek(startPos, SeekSet);
return true; return true;
} }
@ -519,6 +545,15 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename,
+ (this->header.shadeRecords * this->header.shadeRecordSize) + (this->header.shadeRecords * this->header.shadeRecordSize)
+ (this->header.groupRecords * this->header.groupRecordSize), SeekSet); // Start at the beginning of the file after the header. + (this->header.groupRecords * this->header.groupRecordSize), SeekSet); // Start at the beginning of the file after the header.
} }
if(opts.repeaters) {
Serial.println("Restoring Repeaters...");
if(this->header.repeaterRecords > 0) {
memset(s->repeaters, 0x00, sizeof(uint32_t) * SOMFY_MAX_REPEATERS);
for(uint8_t i = 0; i < this->header.repeaterRecords; i++) {
this->readRepeaterRecord(s);
}
}
}
if(opts.settings) { if(opts.settings) {
// First read out the data. // First read out the data.
this->readSettingsRecord(); this->readSettingsRecord();
@ -654,6 +689,18 @@ bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) {
} }
return true; return true;
} }
bool ShadeConfigFile::readRepeaterRecord(SomfyShadeController *s) {
uint32_t startPos = this->file.position();
for(uint8_t i; i < SOMFY_MAX_REPEATERS; i++) {
s->linkRepeater(this->readUInt32(0));
}
if(this->file.position() != startPos + this->header.repeaterRecordSize) {
Serial.println("Reading to end of repeater record");
this->seekChar(CFG_REC_END);
}
return true;
}
bool ShadeConfigFile::readRoomRecord(SomfyRoom *room) { bool ShadeConfigFile::readRoomRecord(SomfyRoom *room) {
uint32_t startPos = this->file.position(); uint32_t startPos = this->file.position();
room->roomId = this->readUInt8(0); room->roomId = this->readUInt8(0);
@ -795,6 +842,11 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
((SomfyGroup *)&s->groups[ndx++])->clear(); ((SomfyGroup *)&s->groups[ndx++])->clear();
} }
} }
if(this->header.repeaterRecords > 0) {
memset(s->repeaters, 0x00, sizeof(uint32_t) * SOMFY_MAX_REPEATERS);
for(uint8_t i = 0; i < this->header.repeaterRecords; i++)
this->readRepeaterRecord(s);
}
if(opened) { if(opened) {
Serial.println("Closing shade config file"); Serial.println("Closing shade config file");
this->end(); this->end();
@ -817,13 +869,18 @@ bool ShadeConfigFile::writeGroupRecord(SomfyGroup *group) {
this->writeUInt8(group->roomId, CFG_REC_END); this->writeUInt8(group->roomId, CFG_REC_END);
return true; return true;
} }
bool ShadeConfigFile::writeRepeaterRecord(SomfyShadeController *s) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
this->writeUInt32(s->repeaters[i], i == SOMFY_MAX_REPEATERS - 1 ? CFG_REC_END : CFG_VALUE_SEP);
}
return true;
}
bool ShadeConfigFile::writeRoomRecord(SomfyRoom *room) { bool ShadeConfigFile::writeRoomRecord(SomfyRoom *room) {
this->writeUInt8(room->roomId); this->writeUInt8(room->roomId);
this->writeString(room->name, sizeof(room->name)); this->writeString(room->name, sizeof(room->name));
this->writeUInt8(room->sortOrder, CFG_REC_END); this->writeUInt8(room->sortOrder, CFG_REC_END);
return true; return true;
} }
bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) { bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) {
shade->myTiltPos = -1; shade->myTiltPos = -1;

View file

@ -14,6 +14,8 @@
struct config_header_t { struct config_header_t {
uint8_t version = 1; uint8_t version = 1;
uint8_t repeaterRecords = 0;
uint8_t repeaterRecordSize = 0;
uint8_t roomRecords = 0; uint8_t roomRecords = 0;
uint16_t roomRecordSize = 0; uint16_t roomRecordSize = 0;
uint16_t shadeRecordSize = 0; uint16_t shadeRecordSize = 0;
@ -65,12 +67,14 @@ class ConfigFile {
}; };
class ShadeConfigFile : public ConfigFile { class ShadeConfigFile : public ConfigFile {
protected: protected:
bool writeRepeaterRecord(SomfyShadeController *s);
bool writeRoomRecord(SomfyRoom *room); bool writeRoomRecord(SomfyRoom *room);
bool writeShadeRecord(SomfyShade *shade); bool writeShadeRecord(SomfyShade *shade);
bool writeGroupRecord(SomfyGroup *group); bool writeGroupRecord(SomfyGroup *group);
bool writeSettingsRecord(); bool writeSettingsRecord();
bool writeNetRecord(); bool writeNetRecord();
bool writeTransRecord(transceiver_config_t &cfg); bool writeTransRecord(transceiver_config_t &cfg);
bool readRepeaterRecord(SomfyShadeController *s);
bool readRoomRecord(SomfyRoom *room); bool readRoomRecord(SomfyRoom *room);
bool readShadeRecord(SomfyShade *shade); bool readShadeRecord(SomfyShade *shade);
bool readGroupRecord(SomfyGroup *group); bool readGroupRecord(SomfyGroup *group);

View file

@ -14,6 +14,7 @@ void restore_options_t::fromJSON(JsonObject &obj) {
if(obj.containsKey("settings")) this->settings = obj["settings"]; if(obj.containsKey("settings")) this->settings = obj["settings"];
if(obj.containsKey("network")) this->network = obj["network"]; if(obj.containsKey("network")) this->network = obj["network"];
if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"]; if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"];
if(obj.containsKey("repeaters")) this->repeaters = obj["repeaters"];
} }
int8_t appver_t::compare(appver_t &ver) { int8_t appver_t::compare(appver_t &ver) {
if(this->major == ver.major && this->minor == ver.minor && this->build == ver.build) return 0; if(this->major == ver.major && this->minor == ver.minor && this->build == ver.build) return 0;

View file

@ -3,7 +3,7 @@
#ifndef configsettings_h #ifndef configsettings_h
#define configsettings_h #define configsettings_h
#define FW_VERSION "v2.3.3" #define FW_VERSION "v2.4.0"
enum DeviceStatus { enum DeviceStatus {
DS_OK = 0, DS_OK = 0,
DS_ERROR = 1, DS_ERROR = 1,
@ -14,6 +14,7 @@ struct restore_options_t {
bool shades = false; bool shades = false;
bool network = false; bool network = false;
bool transceiver = false; bool transceiver = false;
bool repeaters = false;
void fromJSON(JsonObject &obj); void fromJSON(JsonObject &obj);
}; };
struct appver_t { struct appver_t {

169
Somfy.cpp
View file

@ -29,6 +29,7 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to
#define SETMY_REPEATS 35 #define SETMY_REPEATS 35
#define TILT_REPEATS 15 #define TILT_REPEATS 15
#define TX_QUEUE_DELAY 100
int sort_asc(const void *cmp1, const void *cmp2) { int sort_asc(const void *cmp1, const void *cmp2) {
int a = *((uint8_t *)cmp1); int a = *((uint8_t *)cmp1);
@ -613,7 +614,7 @@ void SomfyShade::clear() {
this->lastRollingCode = 0; this->lastRollingCode = 0;
this->shadeType = shade_types::roller; this->shadeType = shade_types::roller;
this->tiltType = tilt_types::none; this->tiltType = tilt_types::none;
this->txQueue.clear(); //this->txQueue.clear();
this->currentPos = 0.0f; this->currentPos = 0.0f;
this->currentTiltPos = 0.0f; this->currentTiltPos = 0.0f;
this->direction = 0; this->direction = 0;
@ -804,6 +805,17 @@ void SomfyGroup::compressLinkedShadeIds() {
} }
} }
} }
void SomfyShadeController::compressRepeaters() {
for(uint8_t i = 0, j = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(this->repeaters[i] != 0) {
if(i != j) {
this->repeaters[j] = this->repeaters[i];
this->repeaters[i] = 0;
}
j++;
}
}
}
bool SomfyGroup::hasShadeId(uint8_t shadeId) { bool SomfyGroup::hasShadeId(uint8_t shadeId) {
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) { for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
if(this->linkedShades[i] == 0) break; if(this->linkedShades[i] == 0) break;
@ -3164,8 +3176,9 @@ bool SomfyRemote::toJSON(JsonObject &obj) {
void SomfyRemote::setRemoteAddress(uint32_t address) { this->m_remoteAddress = address; snprintf(this->m_remotePrefId, sizeof(this->m_remotePrefId), "_%lu", (unsigned long)this->m_remoteAddress); } void SomfyRemote::setRemoteAddress(uint32_t address) { this->m_remoteAddress = address; snprintf(this->m_remotePrefId, sizeof(this->m_remotePrefId), "_%lu", (unsigned long)this->m_remoteAddress); }
uint32_t SomfyRemote::getRemoteAddress() { return this->m_remoteAddress; } uint32_t SomfyRemote::getRemoteAddress() { return this->m_remoteAddress; }
void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) { void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
if(this->shades[i].getShadeId() != 255) this->shades[i].processFrame(frame, internal); if(this->shades[i].getShadeId() != 255) this->shades[i].processFrame(frame, internal);
}
} }
void SomfyShadeController::processWaitingFrame() { void SomfyShadeController::processWaitingFrame() {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
@ -3300,7 +3313,13 @@ int8_t SomfyShadeController::getMaxRoomOrder() {
} }
return order; return order;
} }
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 SomfyShadeController::roomCount() {
uint8_t count = 0; uint8_t count = 0;
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
@ -3411,6 +3430,30 @@ SomfyShade *SomfyShadeController::addShade() {
} }
return shade; return shade;
} }
bool SomfyShadeController::unlinkRepeater(uint32_t address) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(this->repeaters[i] == address) this->repeaters[i] = 0;
}
this->compressRepeaters();
this->isDirty = true;
return true;
}
bool SomfyShadeController::linkRepeater(uint32_t address) {
bool bSet = false;
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(!bSet && this->repeaters[i] == address) bSet = true;
else if(bSet && this->repeaters[i] == address) this->repeaters[i] = 0;
}
if(!bSet) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(this->repeaters[i] == 0) {
this->repeaters[i] = address;
return true;
}
}
}
return true;
}
SomfyRoom *SomfyShadeController::addRoom(JsonObject &obj) { SomfyRoom *SomfyShadeController::addRoom(JsonObject &obj) {
SomfyRoom *room = this->addRoom(); SomfyRoom *room = this->addRoom();
if(room) { if(room) {
@ -3721,6 +3764,13 @@ bool SomfyShadeController::toJSON(JsonObject &obj) {
this->toJSONGroups(arrGroups); this->toJSONGroups(arrGroups);
return true; return true;
} }
bool SomfyShadeController::toJSONRepeaters(JsonArray &arr) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(somfy.repeaters[i] != 0) arr.add(somfy.repeaters[i]);
}
return true;
}
bool SomfyShadeController::toJSONRooms(JsonArray &arr) { bool SomfyShadeController::toJSONRooms(JsonArray &arr) {
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
SomfyRoom &room = this->rooms[i]; SomfyRoom &room = this->rooms[i];
@ -3791,6 +3841,7 @@ static const uint32_t tempo_if_gap = 30415; // Gap between frames
static int16_t bitMin = SYMBOL * TOLERANCE_MIN; static int16_t bitMin = SYMBOL * TOLERANCE_MIN;
static somfy_rx_t somfy_rx; static somfy_rx_t somfy_rx;
static somfy_rx_queue_t rx_queue; static somfy_rx_queue_t rx_queue;
static somfy_tx_queue_t tx_queue;
bool somfy_tx_queue_t::pop(somfy_tx_t *tx) { bool somfy_tx_queue_t::pop(somfy_tx_t *tx) {
// Read the oldest index. // Read the oldest index.
for(int8_t i = MAX_TX_BUFFER - 1; i >= 0; i--) { for(int8_t i = MAX_TX_BUFFER - 1; i >= 0; i--) {
@ -3798,39 +3849,44 @@ bool somfy_tx_queue_t::pop(somfy_tx_t *tx) {
uint8_t ndx = this->index[i]; uint8_t ndx = this->index[i];
memcpy(tx, &this->items[ndx], sizeof(somfy_tx_t)); memcpy(tx, &this->items[ndx], sizeof(somfy_tx_t));
this->items[ndx].clear(); this->items[ndx].clear();
this->length--; if(this->length > 0) this->length--;
this->index[i] = 255; this->index[i] = 255;
return true; return true;
} }
} }
return false; return false;
} }
bool somfy_tx_queue_t::push(uint32_t await, somfy_commands cmd, uint8_t repeats) { void somfy_tx_queue_t::push(somfy_rx_t *rx) { this->push(rx->cpt_synchro_hw, rx->payload, rx->bit_length); }
void somfy_tx_queue_t::push(uint8_t hwsync, uint8_t *payload, uint8_t bit_length) {
if(this->length >= MAX_TX_BUFFER) { if(this->length >= MAX_TX_BUFFER) {
// We have overflowed the buffer simply empty the last item
// in this instance we will simply throw it away.
uint8_t ndx = this->index[MAX_TX_BUFFER - 1]; uint8_t ndx = this->index[MAX_TX_BUFFER - 1];
if(ndx < MAX_TX_BUFFER) this->items[ndx].clear();
this->index[MAX_TX_BUFFER - 1] = 255; this->index[MAX_TX_BUFFER - 1] = 255;
this->length = MAX_TX_BUFFER - 1; this->length--;
if (ndx < MAX_TX_BUFFER)
this->items[ndx].clear();
} }
// Place the command in the first empty slot. Empty slots are those uint8_t first = 0;
// with a millis of 0. We will shift the indexes right so that this // Place this record in the first empty slot. There will
// is indexed int slot 0. // be one since we cleared a space above should there
// be an overflow.
for(uint8_t i = 0; i < MAX_TX_BUFFER; i++) { for(uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
if(this->items[i].await == 0) { if(this->items[i].bit_length == 0) {
this->items[i].await = await; first = i;
this->items[i].cmd = cmd; this->items[i].bit_length = bit_length;
this->items[i].repeats = repeats; this->items[i].hwsync = hwsync;
// Move the index so that it is the at position 0. The oldest item will fall off. memcpy(&this->items[i].payload, payload, sizeof(this->items[i].payload));
for(uint8_t j = MAX_TX_BUFFER - 1; j > 0; j--) { break;
this->index[j] = this->index[j - 1];
}
this->length++;
this->index[0] = i;
return true;
} }
} }
return false; // Move the index so that it is the at position 0. The oldest item will fall off.
for(uint8_t i = MAX_TX_BUFFER - 1; i > 0; i--) {
this->index[i] = this->index[i - 1];
}
this->length++;
// When popping from the queue we always pull from the end
this->index[0] = first;
this->delay_time = millis() + TX_QUEUE_DELAY; // We do not want to process this frame until a full frame beat has passed.
} }
void somfy_rx_queue_t::init() { void somfy_rx_queue_t::init() {
Serial.println("Initializing RX Queue"); Serial.println("Initializing RX Queue");
@ -3847,13 +3903,14 @@ bool somfy_rx_queue_t::pop(somfy_rx_t *rx) {
uint8_t ndx = this->index[i]; uint8_t ndx = this->index[i];
memcpy(rx, &this->items[this->index[i]], sizeof(somfy_rx_t)); memcpy(rx, &this->items[this->index[i]], sizeof(somfy_rx_t));
this->items[ndx].clear(); this->items[ndx].clear();
this->length--; if(this->length > 0) this->length--;
this->index[i] = 255; this->index[i] = 255;
return true; return true;
} }
} }
return false; return false;
} }
void Transceiver::sendFrame(byte *frame, uint8_t sync, uint8_t bitLength) { void Transceiver::sendFrame(byte *frame, uint8_t sync, uint8_t bitLength) {
if(!this->config.enabled) return; if(!this->config.enabled) return;
uint32_t pin = 1 << this->config.TXPin; uint32_t pin = 1 << this->config.TXPin;
@ -3926,7 +3983,7 @@ void Transceiver::sendFrame(byte *frame, uint8_t sync, uint8_t bitLength) {
delayMicroseconds(13717); delayMicroseconds(13717);
delayMicroseconds(13717); delayMicroseconds(13717);
} }
Serial.println("Frame Sent...");
/* /*
if(bitLength == 80) if(bitLength == 80)
delayMicroseconds(15055); delayMicroseconds(15055);
@ -4026,7 +4083,7 @@ void RECEIVE_ATTR Transceiver::handleReceive() {
if (somfy_rx.status == receiving_data && somfy_rx.cpt_bits >= somfy_rx.bit_length) { if (somfy_rx.status == receiving_data && somfy_rx.cpt_bits >= somfy_rx.bit_length) {
// Since we are operating within the interrupt all data really needs to be static // Since we are operating within the interrupt all data really needs to be static
// for the handoff to the frame decoder. For this reason we are buffering up to // for the handoff to the frame decoder. For this reason we are buffering up to
// 3 total frames. Althought it may not matter considering the lenght of a packet // 3 total frames. Althought it may not matter considering the length of a packet
// will likely not push over the loop timing. For now lets assume that there // will likely not push over the loop timing. For now lets assume that there
// may be some pressure on the loop for features. // may be some pressure on the loop for features.
if(rx_queue.length >= MAX_RX_BUFFER) { if(rx_queue.length >= MAX_RX_BUFFER) {
@ -4127,14 +4184,13 @@ void Transceiver::emitFrequencyScan(uint8_t num) {
if(num >= 255) sockEmit.sendToClients("frequencyScan", buf); if(num >= 255) sockEmit.sendToClients("frequencyScan", buf);
else sockEmit.sendToClient(num, "frequencyScan", buf); else sockEmit.sendToClient(num, "frequencyScan", buf);
} }
bool Transceiver::receive() { bool Transceiver::receive(somfy_rx_t *rx) {
// Check to see if there is anything in the buffer // Check to see if there is anything in the buffer
if(rx_queue.length > 0) { if(rx_queue.length > 0) {
//Serial.printf("Processing receive %d\n", rx_queue.length); //Serial.printf("Processing receive %d\n", rx_queue.length);
somfy_rx_t rx; rx_queue.pop(rx);
rx_queue.pop(&rx); this->frame.decodeFrame(rx);
this->frame.decodeFrame(&rx); this->emitFrame(&this->frame, rx);
this->emitFrame(&this->frame, &rx);
return this->frame.valid; return this->frame.valid;
} }
return false; return false;
@ -4555,21 +4611,54 @@ bool Transceiver::begin() {
return true; return true;
} }
void Transceiver::loop() { void Transceiver::loop() {
somfy_rx_t rx;
if(rxmode == 3) { if(rxmode == 3) {
if(rx_queue.length > 0) { if(this->receive(&rx))
//Serial.printf("Processing receive %d\n", rx_queue.length); this->processFrequencyScan(true);
somfy_rx_t rx;
rx_queue.pop(&rx);
this->frame.decodeFrame(&rx);
this->processFrequencyScan(this->frame.valid);
}
else else
this->processFrequencyScan(false); this->processFrequencyScan(false);
} }
else if (this->receive()) else if (this->receive(&rx)) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(somfy.repeaters[i] == frame.remoteAddress) {
tx_queue.push(&rx);
Serial.println("Queued repeater frame...");
break;
}
}
somfy.processFrame(this->frame, false); somfy.processFrame(this->frame, false);
else }
else {
somfy.processWaitingFrame(); somfy.processWaitingFrame();
// Check to see if there is anything in the buffer
if(tx_queue.length > 0 && millis() > tx_queue.delay_time && somfy_rx.cpt_synchro_hw == 0) {
this->beginTransmit();
somfy_tx_t tx;
tx_queue.pop(&tx);
Serial.printf("Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length);
for(uint8_t j = 0; j < 10; j++) {
Serial.print(tx.payload[j]);
if(j < 9) Serial.print(", ");
}
Serial.println("]");
this->sendFrame(tx.payload, tx.hwsync, tx.bit_length);
tx_queue.delay_time = millis() + TX_QUEUE_DELAY;
/*
while(tx_queue.length > 0 && tx_queue.pop(&tx)) {
Serial.printf("Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length);
for(uint8_t j = 0; j < 10; j++) {
Serial.print(tx.payload[j]);
if(j < 9) Serial.print(", ");
}
Serial.println("]");
this->sendFrame(tx.payload, tx.hwsync, tx.bit_length);
}
*/
this->endTransmit();
}
}
} }
somfy_frame_t& Transceiver::lastFrame() { return this->frame; } somfy_frame_t& Transceiver::lastFrame() { return this->frame; }
void Transceiver::beginTransmit() { void Transceiver::beginTransmit() {

43
Somfy.h
View file

@ -7,6 +7,7 @@
#define SOMFY_MAX_LINKED_REMOTES 7 #define SOMFY_MAX_LINKED_REMOTES 7
#define SOMFY_MAX_GROUPED_SHADES 32 #define SOMFY_MAX_GROUPED_SHADES 32
#define SOMFY_MAX_ROOMS 16 #define SOMFY_MAX_ROOMS 16
#define SOMFY_MAX_REPEATERS 7
#define SECS_TO_MILLIS(x) ((x) * 1000) #define SECS_TO_MILLIS(x) ((x) * 1000)
#define MINS_TO_MILLIS(x) SECS_TO_MILLIS((x) * 60) #define MINS_TO_MILLIS(x) SECS_TO_MILLIS((x) * 60)
@ -77,7 +78,7 @@ somfy_commands translateSomfyCommand(const String& string);
#define MAX_TIMINGS 300 #define MAX_TIMINGS 300
#define MAX_RX_BUFFER 3 #define MAX_RX_BUFFER 3
#define MAX_TX_BUFFER 3 #define MAX_TX_BUFFER 5
typedef enum { typedef enum {
waiting_synchro = 0, waiting_synchro = 0,
@ -115,34 +116,35 @@ struct somfy_rx_queue_t {
uint8_t length = 0; uint8_t length = 0;
uint8_t index[MAX_RX_BUFFER]; uint8_t index[MAX_RX_BUFFER];
somfy_rx_t items[MAX_RX_BUFFER]; somfy_rx_t items[MAX_RX_BUFFER];
//void push(somfy_rx_t *rx); void push(somfy_rx_t *rx);
bool pop(somfy_rx_t *rx); bool pop(somfy_rx_t *rx);
}; };
struct somfy_tx_t { struct somfy_tx_t {
void clear() { void clear() {
this->await = 0; this->hwsync = 0;
this->cmd = somfy_commands::Unknown0; this->bit_length = 0;
this->repeats = 0; memset(this->payload, 0x00, sizeof(this->payload));
} }
uint32_t await = 0; uint8_t hwsync = 0;
somfy_commands cmd; uint8_t bit_length = 0;
uint8_t repeats; uint8_t payload[10] = {};
}; };
struct somfy_tx_queue_t { struct somfy_tx_queue_t {
somfy_tx_queue_t() { somfy_tx_queue_t() { this->clear(); }
this->clear();
}
void clear() { void clear() {
for (uint8_t i = 0; i < MAX_TX_BUFFER; i++) { for (uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
this->index[i] = 255; this->index[i] = 255;
this->items[i].clear(); this->items[i].clear();
} }
this->length = 0;
} }
unsigned long delay_time = 0;
uint8_t length = 0; uint8_t length = 0;
uint8_t index[MAX_TX_BUFFER]; uint8_t index[MAX_TX_BUFFER] = {255};
somfy_tx_t items[MAX_TX_BUFFER]; somfy_tx_t items[MAX_TX_BUFFER];
bool pop(somfy_tx_t *tx); bool pop(somfy_tx_t *tx);
bool push(uint32_t await, somfy_commands cmd, uint8_t repeats); void push(somfy_rx_t *rx); // Used for repeats
void push(uint8_t hwsync, byte *payload, uint8_t bit_length);
}; };
enum class somfy_flags_t : byte { enum class somfy_flags_t : byte {
@ -157,6 +159,11 @@ enum class somfy_flags_t : byte {
enum class gpio_flags_t : byte { enum class gpio_flags_t : byte {
LowLevelTrigger = 0x01 LowLevelTrigger = 0x01
}; };
struct somfy_relay_t {
uint32_t remoteAddress = 0;
uint8_t sync = 0;
byte frame[10] = {0};
};
struct somfy_frame_t { struct somfy_frame_t {
bool valid = false; bool valid = false;
bool processed = false; bool processed = false;
@ -270,7 +277,7 @@ class SomfyShade : public SomfyRemote {
#ifdef USE_NVS #ifdef USE_NVS
void load(); void load();
#endif #endif
somfy_tx_queue_t txQueue; //somfy_tx_queue_t txQueue;
float currentPos = 0.0f; float currentPos = 0.0f;
float currentTiltPos = 0.0f; float currentTiltPos = 0.0f;
//uint16_t movement = 0; //uint16_t movement = 0;
@ -479,7 +486,7 @@ class Transceiver {
bool begin(); bool begin();
void loop(); void loop();
bool end(); bool end();
bool receive(); bool receive(somfy_rx_t *rx);
void clearReceived(); void clearReceived();
void enableReceive(); void enableReceive();
void disableReceive(); void disableReceive();
@ -523,14 +530,20 @@ class SomfyShadeController {
bool begin(); bool begin();
void loop(); void loop();
void end(); void end();
void compressRepeaters();
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
SomfyRoom rooms[SOMFY_MAX_ROOMS]; SomfyRoom rooms[SOMFY_MAX_ROOMS];
SomfyShade shades[SOMFY_MAX_SHADES]; SomfyShade shades[SOMFY_MAX_SHADES];
SomfyGroup groups[SOMFY_MAX_GROUPS]; SomfyGroup groups[SOMFY_MAX_GROUPS];
bool linkRepeater(uint32_t address);
bool unlinkRepeater(uint32_t address);
bool toJSON(DynamicJsonDocument &doc); bool toJSON(DynamicJsonDocument &doc);
bool toJSON(JsonObject &obj); bool toJSON(JsonObject &obj);
bool toJSONRooms(JsonArray &arr); bool toJSONRooms(JsonArray &arr);
bool toJSONShades(JsonArray &arr); bool toJSONShades(JsonArray &arr);
bool toJSONGroups(JsonArray &arr); bool toJSONGroups(JsonArray &arr);
bool toJSONRepeaters(JsonArray &arr);
uint8_t repeaterCount();
uint8_t roomCount(); uint8_t roomCount();
uint8_t shadeCount(); uint8_t shadeCount();
uint8_t groupCount(); uint8_t groupCount();

Binary file not shown.

Binary file not shown.

Binary file not shown.

92
Web.cpp
View file

@ -312,6 +312,14 @@ void Web::handleController(WebServer &server) {
this->chunkShadesResponse(server); this->chunkShadesResponse(server);
server.sendContent(",\"groups\":"); server.sendContent(",\"groups\":");
this->chunkGroupsResponse(server); this->chunkGroupsResponse(server);
server.sendContent(",\"repeaters\":");
{
DynamicJsonDocument doc(512);
JsonArray r = doc.to<JsonArray>();
somfy.toJSONRepeaters(r);
serializeJson(doc, g_content);
server.sendContent(g_content);
}
server.sendContent("}"); server.sendContent("}");
server.sendContent("", 0); server.sendContent("", 0);
} }
@ -331,6 +339,19 @@ void Web::handleLoginContext(WebServer &server) {
serializeJson(doc, g_content); serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content); server.send(200, _encoding_json, g_content);
} }
void Web::handleGetRepeaters(WebServer &server) {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_POST || method == HTTP_GET) {
DynamicJsonDocument doc(512);
JsonArray r = doc.to<JsonArray>();
somfy.toJSONRepeaters(r);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(404, _encoding_text, _response_404);
}
void Web::handleGetRooms(WebServer &server) { void Web::handleGetRooms(WebServer &server) {
webServer.sendCORSHeaders(server); webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
@ -1654,6 +1675,77 @@ void Web::begin() {
} }
} }
}); });
server.on("/linkRepeater", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_PUT || method == HTTP_POST) {
// We are adding a linked repeater.
uint32_t address = 0;
if (server.hasArg("plain")) {
Serial.println("Linking a repeater");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
webServer.handleDeserializationError(server, err);
return;
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("address")) address = obj["address"];
else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"];
}
}
else if(server.hasArg("address"))
address = atoi(server.arg("address").c_str());
if(address == 0)
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
else {
somfy.linkRepeater(address);
DynamicJsonDocument doc(512);
JsonArray r = doc.to<JsonArray>();
somfy.toJSONRepeaters(r);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
}
});
server.on("/unlinkRepeater", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_PUT || method == HTTP_POST) {
// We are adding a linked repeater.
uint32_t address = 0;
if (server.hasArg("plain")) {
Serial.println("Unlinking a repeater");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
webServer.handleDeserializationError(server, err);
return;
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("address")) address = obj["address"];
else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"];
}
}
else if(server.hasArg("address"))
address = atoi(server.arg("address").c_str());
if(address == 0)
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
else {
somfy.unlinkRepeater(address);
DynamicJsonDocument doc(512);
JsonArray r = doc.to<JsonArray>();
somfy.toJSONRepeaters(r);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
}
});
server.on("/unlinkRemote", []() { server.on("/unlinkRemote", []() {
webServer.sendCORSHeaders(server); webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }

1
Web.h
View file

@ -12,6 +12,7 @@ class Web {
void handleStreamFile(WebServer &server, const char *filename, const char *encoding); void handleStreamFile(WebServer &server, const char *filename, const char *encoding);
void handleController(WebServer &server); void handleController(WebServer &server);
void handleLoginContext(WebServer &server); void handleLoginContext(WebServer &server);
void handleGetRepeaters(WebServer &server);
void handleGetRooms(WebServer &server); void handleGetRooms(WebServer &server);
void handleGetShades(WebServer &server); void handleGetShades(WebServer &server);
void handleGetGroups(WebServer &server); void handleGetGroups(WebServer &server);

View file

@ -1 +1 @@
2.3.3 2.4.0

View file

@ -3,11 +3,11 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="main.css?v=2.3.3" type="text/css" /> <link rel="stylesheet" href="main.css?v=2.4.0" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.3.3" type="text/css" /> <link rel="stylesheet" href="widgets.css?v=2.4.0" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.3.3" type="text/css" /> <link rel="stylesheet" href="icons.css?v=2.4.0" type="text/css" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<script type="text/javascript" src="index.js?v=2.3.3"></script> <script type="text/javascript" src="index.js?v=2.4.0"></script>
</head> </head>
<body> <body>
<div id="divContainer" class="container main" data-auth="false"> <div id="divContainer" class="container main" data-auth="false">
@ -295,7 +295,7 @@
</div> </div>
</div> </div>
<div id="divSomfySettings" style="display:none;"> <div id="divSomfySettings" style="display:none;">
<div class="subtab-container"><span class="selected" data-grpid="divSomfyRooms">Rooms</span><span class="" data-grpid="divSomfyMotors">Shades</span><span data-grpid="divSomfyGroups">Groups</span><span data-grpid="divVirtualRemote">Virtual Remote</span></div> <div class="subtab-container"><span class="selected" data-grpid="divSomfyRooms">Rooms</span><span class="" data-grpid="divSomfyMotors">Shades</span><span data-grpid="divSomfyGroups">Groups</span><span data-grpid="divRepeater">Repeater</span><span data-grpid="divVirtualRemote">Virtual Remote</span></div>
<div id="divSomfyRooms" class="subtab-content" style="padding-top:10px;"> <div id="divSomfyRooms" class="subtab-content" style="padding-top:10px;">
<div id="divRoomListContainer"> <div id="divRoomListContainer">
<div style="font-size:.8em;">Drag each item to set the order in which they appear in the list.</div> <div style="font-size:.8em;">Drag each item to set the order in which they appear in the list.</div>
@ -631,6 +631,17 @@
</div> </div>
</div> </div>
<div id="divRepeater" style="display:none;padding-top:10px;" class="subtab-content">
<div id="divRepeaterListContainer">
<div style="font-size:.8em;">All repeated remote addresses are listed below.</div>
<div id="divRepeatList" class="edit-repeaterlist"></div>
<div class="button-container">
<button id="btnLinkRepeater" type="button" onclick="somfy.linkRepeatRemote();">
Scan Remote
</button>
</div>
</div>
</div>
<div id="divVirtualRemote" style="display:none;" class="subtab-content"> <div id="divVirtualRemote" style="display:none;" class="subtab-content">
<div class="field-group" style="margin-top:-18px;display:inline-block;width:100%;"> <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%;">
@ -926,24 +937,24 @@
mouseDown = false; mouseDown = false;
}); });
(async () => { await init(); })(); (async () => { await init(); })();
/* /*
security.init(); security.init();
//(async () => { await general.init(); })(); //(async () => { await general.init(); })();
general.init(); general.init();
wifi.init(); wifi.init();
somfy.init(); somfy.init();
mqtt.init(); mqtt.init();
firmware.init(); firmware.init();
*/ */
/* /*
let o = { let o = {
general: { hostname: 'hostname', ntpServer: 'pool.ntp.org', posixZone: '<-10>10', ssdpBroadcast: true } general: { hostname: 'hostname', ntpServer: 'pool.ntp.org', posixZone: '<-10>10', ssdpBroadcast: true }
} }
*/ */
//ui.toElement(document.getElementById('divContainer'), o); //ui.toElement(document.getElementById('divContainer'), o);
//console.log(ui.fromElement(document.getElementById('divContainer'))); //console.log(ui.fromElement(document.getElementById('divContainer')));
//(async () => { await initSockets(); })(); //(async () => { await initSockets(); })();
</script> </script>
</body> </body>
</html> </html>

View file

@ -1264,7 +1264,7 @@ var security = new Security();
class General { class General {
initialized = false; initialized = false;
appVersion = 'v2.3.3'; appVersion = 'v2.4.0';
reloadApp = false; reloadApp = false;
init() { init() {
if (this.initialized) return; if (this.initialized) return;
@ -1957,6 +1957,7 @@ class Somfy {
this.setRoomsList(somfy.rooms); this.setRoomsList(somfy.rooms);
this.setShadesList(somfy.shades); this.setShadesList(somfy.shades);
this.setGroupsList(somfy.groups); this.setGroupsList(somfy.groups);
this.setRepeaterList(somfy.repeaters);
if (typeof somfy.version !== 'undefined') firmware.procFwStatus(somfy.version); if (typeof somfy.version !== 'undefined') firmware.procFwStatus(somfy.version);
} }
}); });
@ -2203,6 +2204,18 @@ class Somfy {
}); });
}); });
} }
setRepeaterList(addresses) {
let divCfg = '';
if (typeof addresses !== 'undefined') {
for (let i = 0; i < addresses.length; i++) {
divCfg += `<div class="somfyRepeater" data-address="${addresses[i]}"><div class="repeater-name">${addresses[i]}</div>`;
divCfg += `<div class="button-outline" onclick="somfy.unlinkRepeater(${addresses[i]});"><i class="icss-trash"></i></div>`;
divCfg += '</div>';
}
}
document.getElementById('divRepeatList').innerHTML = divCfg;
}
setShadesList(shades) { setShadesList(shades) {
let divCfg = ''; let divCfg = '';
let divCtl = ''; let divCtl = '';
@ -2832,6 +2845,16 @@ class Somfy {
this.setLinkedRemotesList(shade); this.setLinkedRemotesList(shade);
}); });
} }
else {
lnk = document.getElementById('divLinkRepeater');
if (lnk) {
putJSONSync(`/linkRepeater`, {address:frame.address}, (err, repeaters) => {
lnk.remove();
if (err) ui.serviceError(err);
else this.setRepeaterList(repeaters);
});
}
}
let frames = document.getElementById('divFrames'); let frames = document.getElementById('divFrames');
let row = document.createElement('div'); let row = document.createElement('div');
row.classList.add('frame-row'); row.classList.add('frame-row');
@ -3092,6 +3115,8 @@ class Somfy {
showEditRoom(bShow) { showEditRoom(bShow) {
let el = document.getElementById('divLinking'); let el = document.getElementById('divLinking');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divLinkRepeater');
if (el) el.remove();
el = document.getElementById('divPairing'); el = document.getElementById('divPairing');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divRollingCode'); el = document.getElementById('divRollingCode');
@ -3108,6 +3133,8 @@ class Somfy {
showEditShade(bShow) { showEditShade(bShow) {
let el = document.getElementById('divLinking'); let el = document.getElementById('divLinking');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divLinkRepeater');
if (el) el.remove();
el = document.getElementById('divPairing'); el = document.getElementById('divPairing');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divRollingCode'); el = document.getElementById('divRollingCode');
@ -3124,6 +3151,8 @@ class Somfy {
showEditGroup(bShow) { showEditGroup(bShow) {
let el = document.getElementById('divLinking'); let el = document.getElementById('divLinking');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divLinkRepeater');
if (el) el.remove();
el = document.getElementById('divPairing'); el = document.getElementById('divPairing');
if (el) el.remove(); if (el) el.remove();
el = document.getElementById('divRollingCode'); el = document.getElementById('divRollingCode');
@ -3332,6 +3361,15 @@ class Somfy {
} }
}); });
} }
updateRepeatList() {
getJSONSync('/repeaters', (err, repeaters) => {
if (err) {
console.log(err);
ui.serviceError(err);
}
else this.setRepeaterList(repeaters);
});
}
deleteRoom(roomId) { deleteRoom(roomId) {
let valid = true; let valid = true;
if (isNaN(roomId) || roomId >= 255 || roomId <= 0) { if (isNaN(roomId) || roomId >= 255 || roomId <= 0) {
@ -3740,6 +3778,20 @@ class Somfy {
document.getElementById('somfyShade').appendChild(div); document.getElementById('somfyShade').appendChild(div);
return div; return div;
} }
linkRepeatRemote() {
let div = document.createElement('div');
let html = `<div id="divLinkRepeater" class="instructions" data-type="link-repeatremote" style="border-radius:27px;">`;
html += '<div>Press any button on the remote to repeat its signals.</div>';
html += '<div class="sub-message">When assigned, ESPSomfy RTS will act as a repeater and repeat any frames for the identified remotes. Only repeat remotes when ESPSomfy RTS reliably hears the remote but the motor does not. Repeating unnecessary radio signals will degrade radio performance.</div>'
html += '<div class="sub-message">Once a signal is detected from the remote this window will close and the remote signals will be repeated.</div>'
html += '<hr></hr>';
html += `<div><div class="button-container"><button id="btnStopLinking" type="button" style="padding-left:20px;padding-right:20px;" onclick="document.getElementById('divLinkRepeater').remove();">Cancel</button></div>`;
html += '</div>';
div.innerHTML = html;
document.getElementById('divConfigPnl').appendChild(div);
return div;
}
linkGroupShade(groupId) { linkGroupShade(groupId) {
let div = document.createElement('div'); let div = document.createElement('div');
let html = `<div id="divLinkGroup" class="inst-overlay wizard" data-type="link-shade" data-groupid="${groupId}" data-stepid="1">`; let html = `<div id="divLinkGroup" class="inst-overlay wizard" data-type="link-shade" data-groupid="${groupId}" data-stepid="1">`;
@ -3956,6 +4008,17 @@ class Somfy {
}); });
return div; return div;
} }
unlinkRepeater(address) {
let prompt = ui.promptMessage('Are you sure you want to stop repeating frames from this address?', () => {
putJSONSync('/unlinkRepeater', { address: address }, (err, repeaters) => {
if (err) ui.serviceError(err);
else this.setRepeaterList(repeaters);
prompt.remove();
});
});
}
unlinkRemote(shadeId, remoteAddress) { unlinkRemote(shadeId, remoteAddress) {
let prompt = ui.promptMessage('Are you sure you want to unlink this remote from the shade?', () => { let prompt = ui.promptMessage('Are you sure you want to unlink this remote from the shade?', () => {
let obj = { let obj = {
@ -4192,6 +4255,7 @@ class Firmware {
html += `<div style="font-size:14px;">Restoring network settings from a different board than the original will ignore Ethernet chip settings. Security, MQTT and WiFi will also not be restored since backup files do not contain passwords.</div><hr/>`; html += `<div style="font-size:14px;">Restoring network settings from a different board than the original will ignore Ethernet chip settings. Security, MQTT and WiFi will also not be restored since backup files do not contain passwords.</div><hr/>`;
html += '<div style="font-size:14px;margin-bottom:27px;text-align:left;margin-left:70px;">'; html += '<div style="font-size:14px;margin-bottom:27px;text-align:left;margin-left:70px;">';
html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreShades" type="checkbox" data-bind="shades" style="display:inline-block;" checked="true" /><label for="cbRestoreShades" style="display:inline-block;cursor:pointer;color:white;">Restore Shades and Groups</label></div>`; html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreShades" type="checkbox" data-bind="shades" style="display:inline-block;" checked="true" /><label for="cbRestoreShades" style="display:inline-block;cursor:pointer;color:white;">Restore Shades and Groups</label></div>`;
html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreRepeaters" type="checkbox" data-bind="repeaters" style="display:inline-block;" /><label for="cbRestoreRepeaters" style="display:inline-block;cursor:pointer;color:white;">Restore Repeaters</label></div>`;
html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreSystem" type="checkbox" data-bind="settings" style="display:inline-block;" /><label for="cbRestoreSystem" style="display:inline-block;cursor:pointer;color:white;">Restore System Settings</label></div>`; html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreSystem" type="checkbox" data-bind="settings" style="display:inline-block;" /><label for="cbRestoreSystem" style="display:inline-block;cursor:pointer;color:white;">Restore System Settings</label></div>`;
html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreNetwork" type="checkbox" data-bind="network" style="display:inline-block;" /><label for="cbRestoreNetwork" style="display:inline-block;cursor:pointer;color:white;">Restore Network Settings</label></div>` html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreNetwork" type="checkbox" data-bind="network" style="display:inline-block;" /><label for="cbRestoreNetwork" style="display:inline-block;cursor:pointer;color:white;">Restore Network Settings</label></div>`
html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreTransceiver" type="checkbox" data-bind="transceiver" style="display:inline-block;" /><label for="cbRestoreTransceiver" style="display:inline-block;cursor:pointer;color:white;">Restore Radio Settings</label></div>`; html += `<div class="field-group" style="vertical-align:middle;width:auto;"><input id="cbRestoreTransceiver" type="checkbox" data-bind="transceiver" style="display:inline-block;" /><label for="cbRestoreTransceiver" style="display:inline-block;cursor:pointer;color:white;">Restore Radio Settings</label></div>`;
@ -4559,11 +4623,15 @@ class Firmware {
ui.errorMessage('You must select a valid backup file to proceed.'); ui.errorMessage('You must select a valid backup file to proceed.');
return; return;
} }
else if (!filename.endsWith('.backup') || filename.indexOf('ESPSomfyRTS') === -1) { else if (field.files[0].size > 20480) {
ui.errorMessage(el, `This file is ${field.files[0].size.fmt("#,##0")} bytes in length. This file is too large to be a valid backup file.`);
return;
}
else if (!filename.endsWith('.backup')) {
ui.errorMessage(el, 'This file is not a valid backup file'); ui.errorMessage(el, 'This file is not a valid backup file');
return; return;
} }
if (!data.shades && !data.settings && !data.network && !data.transceiver) { if (!data.shades && !data.settings && !data.network && !data.transceiver && !data.repeaters) {
ui.errorMessage(el, 'No restore options have been selected'); ui.errorMessage(el, 'No restore options have been selected');
return; return;
} }

View file

@ -46,6 +46,10 @@
left: 0px; left: 0px;
color: white; color: white;
} }
#divLinkRepeat {
border-radius:24px;
}
.edit-repeaterlist,
.edit-roomlist, .edit-roomlist,
.edit-grouplist, .edit-grouplist,
.edit-motorlist { .edit-motorlist {
@ -61,6 +65,7 @@
max-height:calc(100% - 77px); max-height:calc(100% - 77px);
overflow:auto; overflow:auto;
} }
.instructions .sub-message,
.prompt-message .sub-message { .prompt-message .sub-message {
font-size: 17px; font-size: 17px;
padding-left: 10px; padding-left: 10px;
@ -82,6 +87,7 @@
.wizard[data-stepid="6"] .wizard-step:not([data-stepid="6"]) { display: none; } .wizard[data-stepid="6"] .wizard-step:not([data-stepid="6"]) { display: none; }
.wizard[data-stepid="7"] .wizard-step:not([data-stepid="7"]) { display: none; } .wizard[data-stepid="7"] .wizard-step:not([data-stepid="7"]) { display: none; }
.somfyRepeater,
.somfyRoom, .somfyRoom,
.somfyGroup, .somfyGroup,
.somfyShade { .somfyShade {
@ -93,6 +99,8 @@
} }
.linked-shade > div, .linked-shade > div,
.linked-shade > span, .linked-shade > span,
.somfyRepeater > div,
.somfyRepeater > span,
.somfyRoom > div, .somfyRoom > div,
.somfyRoom > span, .somfyRoom > span,
.somfyGroup > div, .somfyGroup > div,
@ -103,12 +111,16 @@
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
} }
.repeater-name,
.linked-shade { .linked-shade {
width: 100%; width: 100%;
padding-bottom: 4px; padding-bottom: 4px;
display: inline-table; display: inline-table;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
}
.repeater-name {
text-align:left;
} }
.linked-shade > .linkedshade-name { width: 100%; } .linked-shade > .linkedshade-name { width: 100%; }
.pin-digit { .pin-digit {