v2.1.6 Update

* Added Virtual Remote
* Added Dry Contact
* Fixed processed flag
* Added backup and restore options
This commit is contained in:
Robert Strouse 2023-09-16 11:19:49 -07:00
parent 7b255e3acb
commit 4ad2cf7567
17 changed files with 1206 additions and 378 deletions

View file

@ -3,13 +3,17 @@
#include <Preferences.h>
#include "ConfigFile.h"
#include "Utils.h"
#include "ConfigSettings.h"
extern Preferences pref;
#define SHADE_HDR_VER 13
#define SHADE_HDR_SIZE 28
#define SHADE_HDR_VER 14
#define SHADE_HDR_SIZE 56
#define SHADE_REC_SIZE 256
#define GROUP_REC_SIZE 184
#define TRANS_REC_SIZE 74
extern ConfigSettings settings;
bool ConfigFile::begin(const char* filename, bool readOnly) {
this->file = LittleFS.open(filename, readOnly ? "r" : "w");
@ -43,7 +47,12 @@ bool ConfigFile::writeHeader(const config_header_t &hdr) {
this->writeUInt16(hdr.shadeRecordSize);
this->writeUInt8(hdr.shadeRecords);
this->writeUInt16(hdr.groupRecordSize);
this->writeUInt8(hdr.groupRecords, CFG_REC_END);
this->writeUInt8(hdr.groupRecords);
this->writeUInt16(hdr.settingsRecordSize);
this->writeUInt16(hdr.netRecordSize);
this->writeUInt16(hdr.transRecordSize);
this->writeString(settings.serverId, sizeof(hdr.serverId), CFG_REC_END);
return true;
}
bool ConfigFile::readHeader() {
@ -60,6 +69,13 @@ bool ConfigFile::readHeader() {
else this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize);
this->header.groupRecords = this->readUInt8(this->header.groupRecords);
}
if(this->header.version > 13) {
this->header.settingsRecordSize = this->readUInt16(this->header.settingsRecordSize);
this->header.netRecordSize = this->readUInt16(this->header.netRecordSize);
this->header.transRecordSize = this->readUInt16(this->header.transRecordSize);
this->readString(this->header.serverId, sizeof(this->header.serverId));
}
Serial.printf("version:%u len:%u shadeSize:%u shadeRecs:%u groupSize:%u groupRecs: %u pos:%d\n", this->header.version, this->header.length, this->header.shadeRecordSize, this->header.shadeRecords, this->header.groupRecordSize, this->header.groupRecords, this->file.position());
return true;
}
@ -97,6 +113,42 @@ bool ConfigFile::readString(char *buff, size_t len) {
_rtrim(buff);
return true;
}
bool ConfigFile::readVarString(char *buff, size_t len) {
if(!this->file) return false;
memset(buff, 0x00, len);
uint8_t quotes = 0;
uint16_t i = 0;
uint16_t j = 0;
while(j < len) {
uint8_t val;
j++;
if(this->file.read(&val, 1) == 1) {
switch(val) {
case CFG_VALUE_SEP:
if(quotes >= 2) {
_rtrim(buff);
return true;
}
break;
case CFG_REC_END:
return true;
case CFG_TOK_QUOTE:
quotes++;
continue;
}
buff[i++] = val;
if(i == len) {
_rtrim(buff);
return true;
}
}
else
return false;
}
_rtrim(buff);
return true;
}
bool ConfigFile::writeString(const char *val, size_t len, const char tok) {
if(!this->isOpen()) return false;
int slen = strlen(val);
@ -112,11 +164,25 @@ bool ConfigFile::writeString(const char *val, size_t len, const char tok) {
return this->writeChar(tok);
return true;
}
bool ConfigFile::writeVarString(const char *val, const char tok) {
if(!this->isOpen()) return false;
int slen = strlen(val);
this->writeChar(CFG_TOK_QUOTE);
if(slen > 0) if(this->file.write((uint8_t *)val, slen) != slen) return false;
this->writeChar(CFG_TOK_QUOTE);
if(tok != CFG_TOK_NONE) return this->writeChar(tok);
return true;
}
bool ConfigFile::writeChar(const char val) {
if(!this->isOpen()) return false;
if(this->file.write(static_cast<uint8_t>(val)) == 1) return true;
return false;
}
bool ConfigFile::writeInt8(const int8_t val, const char tok) {
char buff[5];
snprintf(buff, sizeof(buff), "%4d", val);
return this->writeString(buff, sizeof(buff), tok);
}
bool ConfigFile::writeUInt8(const uint8_t val, const char tok) {
char buff[4];
snprintf(buff, sizeof(buff), "%3u", val);
@ -145,6 +211,12 @@ char ConfigFile::readChar(const char defVal) {
if(this->file.read(&ch, 1) == 1) return (char)ch;
return defVal;
}
int8_t ConfigFile::readInt8(const int8_t defVal) {
char buff[5];
if(this->readString(buff, sizeof(buff)))
return static_cast<int8_t>(atoi(buff));
return defVal;
}
uint8_t ConfigFile::readUInt8(const uint8_t defVal) {
char buff[4];
if(this->readString(buff, sizeof(buff)))
@ -209,20 +281,51 @@ bool ShadeConfigFile::save(SomfyShadeController *s) {
this->header.version = SHADE_HDR_VER;
this->header.shadeRecordSize = SHADE_REC_SIZE;
this->header.length = SHADE_HDR_SIZE;
this->header.shadeRecords = SOMFY_MAX_SHADES;
this->header.shadeRecords = s->shadeCount();
this->header.groupRecordSize = GROUP_REC_SIZE;
this->header.groupRecords = SOMFY_MAX_GROUPS;
this->header.groupRecords = s->groupCount();
this->header.settingsRecordSize = 0;
this->header.netRecordSize = 0;
this->header.transRecordSize = 0;
this->writeHeader();
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &s->shades[i];
if(shade->getShadeId() != 255)
this->writeShadeRecord(shade);
}
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
SomfyGroup *group = &s->groups[i];
if(group->getGroupId() != 255)
this->writeGroupRecord(group);
}
return true;
}
bool ShadeConfigFile::backup(SomfyShadeController *s) {
this->header.version = SHADE_HDR_VER;
this->header.shadeRecordSize = SHADE_REC_SIZE;
this->header.length = SHADE_HDR_SIZE;
this->header.shadeRecords = s->shadeCount();
this->header.groupRecordSize = GROUP_REC_SIZE;
this->header.groupRecords = s->groupCount();
this->header.settingsRecordSize = settings.calcSettingsRecSize();
this->header.netRecordSize = settings.calcNetRecSize();
this->header.transRecordSize = TRANS_REC_SIZE;
this->writeHeader();
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &s->shades[i];
if(shade->getShadeId() != 255)
this->writeShadeRecord(shade);
}
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
SomfyGroup *group = &s->groups[i];
if(group->getGroupId() != 255)
this->writeGroupRecord(group);
}
this->writeSettingsRecord();
this->writeNetRecord();
this->writeTransRecord(s->transceiver.config);
return true;
}
bool ShadeConfigFile::validate() {
this->readHeader();
if(this->header.version < 1) {
@ -235,23 +338,27 @@ bool ShadeConfigFile::validate() {
Serial.println(this->header.shadeRecordSize);
return false;
}
/*
if(this->header.shadeRecords != SOMFY_MAX_SHADES) {
Serial.print("Invalid Shade Record Count:");
Serial.println(this->header.shadeRecords);
return false;
}
*/
if(this->header.version > 10) {
if(this->header.groupRecordSize < 100) {
Serial.print("Invalid Group Record Size:");
Serial.println(this->header.groupRecordSize);
return false;
}
/*
if(this->header.groupRecords != SOMFY_MAX_GROUPS) {
Serial.print("Invalid Group Record Count:");
Serial.println(this->header.groupRecords);
return false;
}
*/
}
if(this->file.position() != this->header.length) {
Serial.printf("File not positioned at %u end of header: %d\n", this->header.length, this->file.position());
@ -261,6 +368,11 @@ bool ShadeConfigFile::validate() {
// We should know the file size based upon the record information in the header
uint32_t fsize = this->header.length + (this->header.shadeRecordSize * this->header.shadeRecords);
if(this->header.version > 10) fsize += (this->header.groupRecordSize * this->header.groupRecords);
if(this->header.version > 13) {
fsize += (this->header.settingsRecordSize);
fsize += (this->header.netRecordSize);
fsize += (this->header.transRecordSize);
}
if(this->file.size() != fsize) {
Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size());
}
@ -306,25 +418,189 @@ bool ShadeConfigFile::load(SomfyShadeController *s, const char *filename) {
}
return false;
}
bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
bool ShadeConfigFile::restore(SomfyShadeController *s, const char *filename, restore_options_t &opts) {
ShadeConfigFile file;
if(file.begin(filename, true)) {
bool success = file.restoreFile(s, filename, opts);
file.end();
return success;
}
return false;
}
bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, restore_options_t &opts) {
bool opened = false;
if(!this->isOpen()) {
Serial.println("Opening shade config file");
Serial.println("Opening shade restore file");
this->begin(filename, true);
opened = true;
}
else {
//this->file.seek(0, SeekSet);
}
if(!this->validate()) {
Serial.println("Shade config file invalid!");
Serial.println("Shade restore file invalid!");
if(opened) this->end();
return false;
}
if(opts.shades) {
Serial.println("Restoring Shades...");
// We should be valid so start reading.
pref.begin("ShadeCodes");
for(uint8_t i = 0; i < this->header.shadeRecords; i++) {
SomfyShade *shade = &s->shades[i];
this->readShadeRecord(&s->shades[i]);
if(i > 0) Serial.print(",");
Serial.print(s->shades[i].getShadeId());
}
Serial.println("");
if(this->header.shadeRecords < SOMFY_MAX_SHADES) {
uint8_t ndx = this->header.shadeRecords;
// Clear out any positions that are not in the shade file.
while(ndx < SOMFY_MAX_SHADES) {
((SomfyShade *)&s->shades[ndx++])->clear();
}
}
Serial.println("Restoring Groups...");
for(uint8_t i = 0; i < this->header.groupRecords; i++) {
if(i > 0) Serial.print(",");
Serial.print(s->groups[i].getGroupId());
this->readGroupRecord(&s->groups[i]);
}
Serial.println("");
if(this->header.groupRecords < SOMFY_MAX_GROUPS) {
uint8_t ndx = this->header.groupRecords;
// Clear out any positions that are not in the shade file.
while(ndx < SOMFY_MAX_GROUPS) {
((SomfyGroup *)&s->groups[ndx++])->clear();
}
}
}
else {
Serial.println("Shade data ignored");
// FF past the shades and groups.
this->file.seek(this->file.position()
+ (this->header.shadeRecords * this->header.shadeRecordSize)
+ (this->header.groupRecords * this->header.groupRecordSize), SeekSet); // Start at the beginning of the file after the header.
}
if(opts.settings) {
// First read out the data.
this->readSettingsRecord();
}
else {
this->file.seek(this->file.position() + this->header.settingsRecordSize, SeekSet);
}
if(opts.network) {
this->readNetRecord();
}
else {
this->file.seek(this->file.position() + this->header.netRecordSize, SeekSet);
}
if(opts.shades) s->commit();
if(opts.transceiver)
{
this->readTransRecord(s->transceiver.config);
s->transceiver.save();
}
if(opts.settings || opts.network) settings.save();
if(opts.settings) settings.NTP.save();
if(opts.network) {
settings.IP.save();
settings.WIFI.save();
settings.Ethernet.save();
}
return true;
}
bool ShadeConfigFile::readNetRecord() {
if(this->header.netRecordSize > 0) {
Serial.println("Reading network settings from file...");
settings.connType = static_cast<conn_types>(this->readUInt8(static_cast<uint8_t>(conn_types::unset)));
settings.IP.dhcp = this->readBool(true);
char ip[24];
this->readVarString(ip, sizeof(ip));
settings.IP.ip.fromString(ip);
this->readVarString(ip, sizeof(ip));
settings.IP.gateway.fromString(ip);
this->readVarString(ip, sizeof(ip));
settings.IP.subnet.fromString(ip);
this->readVarString(ip, sizeof(ip));
settings.IP.dns1.fromString(ip);
this->readVarString(ip, sizeof(ip));
settings.IP.dns2.fromString(ip);
// Now lets check to see if we are the same board. If we are then we will restore
// the ethernet phy settings.
if(strncmp(settings.serverId, this->header.serverId, sizeof(settings.serverId)) == 0) {
settings.Ethernet.boardType = this->readUInt8(1);
settings.Ethernet.phyType = static_cast<eth_phy_type_t>(this->readUInt8(0));
settings.Ethernet.CLKMode = static_cast<eth_clock_mode_t>(this->readUInt8(0));
settings.Ethernet.phyAddress = this->readInt8(1);
settings.Ethernet.PWRPin = this->readInt8(1);
settings.Ethernet.MDCPin = this->readInt8(16);
settings.Ethernet.MDIOPin = this->readInt8(23);
}
else {
// We are not going to get the network adapter settings.
Serial.println("Skipping Ethernet adapter settings (Chip ids do not match)...");
this->seekChar(CFG_REC_END);
}
}
return true;
}
bool ShadeConfigFile::readTransRecord(transceiver_config_t &cfg) {
if(this->header.transRecordSize > 0) {
Serial.println("Reading Transceiver settings from file...");
cfg.enabled = this->readBool(false);
cfg.proto = static_cast<radio_proto>(this->readUInt8(0));
cfg.type = this->readUInt8(56);
cfg.SCKPin = this->readUInt8(cfg.SCKPin);
cfg.CSNPin = this->readUInt8(cfg.CSNPin);
cfg.MOSIPin = this->readUInt8(cfg.MOSIPin);
cfg.MISOPin = this->readUInt8(cfg.MISOPin);
cfg.TXPin = this->readUInt8(cfg.TXPin);
cfg.RXPin = this->readUInt8(cfg.RXPin);
cfg.frequency = this->readFloat(cfg.frequency);
cfg.rxBandwidth = this->readFloat(cfg.rxBandwidth);
cfg.deviation = this->readFloat(cfg.deviation);
cfg.txPower = this->readInt8(cfg.txPower);
}
return true;
}
bool ShadeConfigFile::readSettingsRecord() {
if(this->header.settingsRecordSize > 0) {
Serial.println("Reading settings from file...");
char ver[24];
this->readVarString(ver, sizeof(ver));
this->readVarString(settings.hostname, sizeof(settings.hostname));
this->readVarString(settings.NTP.ntpServer, sizeof(settings.NTP.ntpServer));
this->readVarString(settings.NTP.posixZone, sizeof(settings.NTP.posixZone));
settings.ssdpBroadcast = this->readBool(false);
}
return true;
}
bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) {
pref.begin("ShadeCodes");
group->setGroupId(this->readUInt8(255));
group->groupType = static_cast<group_types>(this->readUInt8(0));
group->setRemoteAddress(this->readUInt32(0));
this->readString(group->name, sizeof(group->name));
group->proto = static_cast<radio_proto>(this->readUInt8(0));
group->bitLength = this->readUInt8(56);
if(group->getRemoteAddress() != 0) {
uint16_t rc = pref.getUShort(group->getRemotePrefId(), 0);
group->lastRollingCode = max(rc, group->lastRollingCode);
if(rc < group->lastRollingCode) pref.putUShort(group->getRemotePrefId(), group->lastRollingCode);
}
uint8_t lsd = 0;
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
uint8_t shadeId = this->readUInt8(0);
// Do this to eliminate gaps.
if(shadeId > 0) group->linkedShades[lsd++] = shadeId;
}
if(this->header.version >= 12) group->repeats = this->readUInt8(1);
if(this->header.version >= 13) group->sortOrder = this->readUInt8(group->getGroupId() - 1);
else group->sortOrder = group->getGroupId() - 1;
if(group->getGroupId() == 255) group->clear();
else group->compressLinkedShadeIds();
pref.end();
return true;
}
bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) {
pref.begin("ShadeCodes");
shade->setShadeId(this->readUInt8(255));
shade->paired = this->readBool(false);
shade->shadeType = static_cast<shade_types>(this->readUInt8(0));
@ -370,7 +646,6 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
shade->currentTiltPos = 0;
shade->tiltType = tilt_types::none;
}
if(this->header.version < 3) {
shade->currentPos = shade->currentPos * 100;
shade->currentTiltPos = shade->currentTiltPos * 100;
@ -383,35 +658,45 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
if(this->header.version >= 13) shade->sortOrder = this->readUInt8(shade->getShadeId() - 1);
else shade->sortOrder = shade->getShadeId() - 1;
if(shade->getShadeId() == 255) shade->clear();
}
for(uint8_t i = 0; i < this->header.groupRecords; i++) {
SomfyGroup *group = &s->groups[i];
group->setGroupId(this->readUInt8(255));
group->groupType = static_cast<group_types>(this->readUInt8(0));
group->setRemoteAddress(this->readUInt32(0));
this->readString(group->name, sizeof(group->name));
group->proto = static_cast<radio_proto>(this->readUInt8(0));
group->bitLength = this->readUInt8(56);
if(group->getRemoteAddress() != 0) {
uint16_t rc = pref.getUShort(group->getRemotePrefId(), 0);
group->lastRollingCode = max(rc, group->lastRollingCode);
if(rc < group->lastRollingCode) pref.putUShort(group->getRemotePrefId(), group->lastRollingCode);
}
uint8_t lsd = 0;
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
uint8_t shadeId = this->readUInt8(0);
// Do this to eliminate gaps.
if(shadeId > 0) group->linkedShades[lsd++] = shadeId;
}
if(this->header.version >= 12) group->repeats = this->readUInt8(1);
if(this->header.version >= 13) group->sortOrder = this->readUInt8(group->getGroupId() - 1);
else group->sortOrder = group->getGroupId() - 1;
if(group->getGroupId() == 255) group->clear();
else group->compressLinkedShadeIds();
else if(shade->tiltType == tilt_types::tiltonly) {
shade->myPos = shade->currentPos = shade->target = 100.0f;
}
pref.end();
return true;
}
bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
bool opened = false;
if(!this->isOpen()) {
Serial.println("Opening shade config file");
this->begin(filename, true);
opened = true;
}
if(!this->validate()) {
Serial.println("Shade config file invalid!");
if(opened) this->end();
return false;
}
// We should be valid so start reading.
for(uint8_t i = 0; i < this->header.shadeRecords; i++) {
this->readShadeRecord(&s->shades[i]);
}
if(this->header.shadeRecords < SOMFY_MAX_SHADES) {
uint8_t ndx = this->header.shadeRecords;
// Clear out any positions that are not in the shade file.
while(ndx < SOMFY_MAX_SHADES) {
((SomfyShade *)&s->shades[ndx++])->clear();
}
}
for(uint8_t i = 0; i < this->header.groupRecords; i++) {
this->readGroupRecord(&s->groups[i]);
}
if(this->header.groupRecords < SOMFY_MAX_GROUPS) {
uint8_t ndx = this->header.groupRecords;
// Clear out any positions that are not in the shade file.
while(ndx < SOMFY_MAX_GROUPS) {
((SomfyGroup *)&s->groups[ndx++])->clear();
}
}
if(opened) {
Serial.println("Closing shade config file");
this->end();
@ -456,7 +741,7 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
}
this->writeUInt16(shade->lastRollingCode);
if(shade->getShadeId() != 255) {
this->writeUInt8(shade->flags & 0x0F);
this->writeUInt8(shade->flags & 0xFF);
this->writeFloat(shade->myPos, 5);
this->writeFloat(shade->myTiltPos, 5);
this->writeFloat(shade->currentPos, 5);
@ -476,6 +761,47 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
this->writeUInt8(shade->sortOrder, CFG_REC_END);
return true;
}
bool ShadeConfigFile::writeSettingsRecord() {
this->writeVarString(settings.fwVersion);
this->writeVarString(settings.hostname);
this->writeVarString(settings.NTP.ntpServer);
this->writeVarString(settings.NTP.posixZone);
this->writeBool(settings.ssdpBroadcast, CFG_REC_END);
return true;
}
bool ShadeConfigFile::writeNetRecord() {
this->writeUInt8(static_cast<uint8_t>(settings.connType));
this->writeBool(settings.IP.dhcp);
this->writeVarString(settings.IP.ip.toString().c_str());
this->writeVarString(settings.IP.gateway.toString().c_str());
this->writeVarString(settings.IP.subnet.toString().c_str());
this->writeVarString(settings.IP.dns1.toString().c_str());
this->writeVarString(settings.IP.dns2.toString().c_str());
this->writeUInt8(settings.Ethernet.boardType);
this->writeUInt8(static_cast<uint8_t>(settings.Ethernet.phyType));
this->writeUInt8(static_cast<uint8_t>(settings.Ethernet.CLKMode));
this->writeInt8(settings.Ethernet.phyAddress);
this->writeInt8(settings.Ethernet.PWRPin);
this->writeInt8(settings.Ethernet.MDCPin);
this->writeInt8(settings.Ethernet.MDIOPin, CFG_REC_END);
return true;
}
bool ShadeConfigFile::writeTransRecord(transceiver_config_t &cfg) {
this->writeBool(cfg.enabled);
this->writeUInt8(static_cast<uint8_t>(cfg.proto));
this->writeUInt8(cfg.type);
this->writeUInt8(cfg.SCKPin);
this->writeUInt8(cfg.CSNPin);
this->writeUInt8(cfg.MOSIPin);
this->writeUInt8(cfg.MISOPin);
this->writeUInt8(cfg.TXPin);
this->writeUInt8(cfg.RXPin);
this->writeFloat(cfg.frequency, 3);
this->writeFloat(cfg.rxBandwidth, 2);
this->writeFloat(cfg.deviation, 2);
this->writeInt8(cfg.txPower, CFG_REC_END);
return true;
}
bool ShadeConfigFile::exists() { return LittleFS.exists("/shades.cfg"); }
bool ShadeConfigFile::getAppVersion(appver_t &ver) {
char app[15];

View file

@ -1,12 +1,16 @@
#include <ArduinoJson.h>
#include <LittleFS.h>
#include "Somfy.h"
#include "ConfigSettings.h"
#ifndef configfile_h
#define configfile_h
#define CFG_VALUE_SEP ','
#define CFG_REC_END '\n'
#define CFG_TOK_NONE 0x00
#define CFG_TOK_QUOTE '"'
struct config_header_t {
uint8_t version = 1;
@ -14,6 +18,10 @@ struct config_header_t {
uint8_t shadeRecords = 0;
uint16_t groupRecordSize = 0;
uint8_t groupRecords = 0;
uint16_t settingsRecordSize = 0;
uint16_t netRecordSize = 0;
uint16_t transRecordSize = 0;
char serverId[10] = ""; // This must match the server id size in the ConfigSettings.
int8_t length = 0;
};
class ConfigFile {
@ -25,7 +33,6 @@ class ConfigFile {
bool _opened = false;
public:
config_header_t header;
bool save();
void end();
bool isOpen();
bool seekRecordByIndex(uint16_t ndx);
@ -36,14 +43,18 @@ class ConfigFile {
bool writeSeparator();
bool writeRecordEnd();
bool writeChar(const char val);
bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP);
bool writeBool(const bool val, const char tok = CFG_VALUE_SEP);
bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP);
bool readString(char *buff, size_t len);
bool readVarString(char *buff, size_t len);
bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP);
bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP);
char readChar(const char defVal = '\0');
int8_t readInt8(const int8_t defVal = 0);
uint8_t readUInt8(const uint8_t defVal = 0);
uint16_t readUInt16(const uint16_t defVal = 0);
uint32_t readUInt32(const uint32_t defVal = 0);
@ -54,14 +65,25 @@ class ShadeConfigFile : public ConfigFile {
protected:
bool writeShadeRecord(SomfyShade *shade);
bool writeGroupRecord(SomfyGroup *group);
bool writeSettingsRecord();
bool writeNetRecord();
bool writeTransRecord(transceiver_config_t &cfg);
bool readShadeRecord(SomfyShade *shade);
bool readGroupRecord(SomfyGroup *group);
bool readSettingsRecord();
bool readNetRecord();
bool readTransRecord(transceiver_config_t &cfg);
public:
static bool getAppVersion(appver_t &ver);
static bool exists();
static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
static bool restore(SomfyShadeController *somfy, const char *filename, restore_options_t &opts);
bool begin(const char *filename, bool readOnly = false);
bool begin(bool readOnly = false);
bool save(SomfyShadeController *sofmy);
bool backup(SomfyShadeController *somfy);
bool loadFile(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
bool restoreFile(SomfyShadeController *somfy, const char *filename, restore_options_t &opts);
void end();
//bool seekRecordById(uint8_t id);
bool validate();

View file

@ -7,6 +7,15 @@
Preferences pref;
void restore_options_t::fromJSON(JsonObject &obj) {
if(obj.containsKey("shades")) this->shades = obj["shades"];
if(obj.containsKey("settings")) this->settings = obj["settings"];
if(obj.containsKey("network")) this->network = obj["network"];
if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"];
}
bool BaseSettings::load() { return true; }
bool BaseSettings::loadFile(const char *filename) {
size_t filesize = 10;
@ -126,6 +135,29 @@ void ConfigSettings::print() {
}
void ConfigSettings::emitSockets() {}
void ConfigSettings::emitSockets(uint8_t num) {}
uint16_t ConfigSettings::calcSettingsRecSize() {
return strlen(this->fwVersion) + 3
+ strlen(this->hostname) + 3
+ strlen(this->NTP.ntpServer) + 3
+ strlen(this->NTP.posixZone) + 3
+ 6; // ssdpbroadcast
}
uint16_t ConfigSettings::calcNetRecSize() {
return 4 // connType
+ 6 // dhcp
+ this->IP.ip.toString().length() + 3
+ this->IP.gateway.toString().length() + 3
+ this->IP.subnet.toString().length() + 3
+ this->IP.dns1.toString().length() + 3
+ this->IP.dns2.toString().length() + 3
+ 4 // ETH.boardType
+ 4 // ETH.phyType
+ 4 // ETH.clkMode
+ 5 // ETH.phyAddress
+ 5 // ETH.PWRPin
+ 5 // ETH.MDCPin
+ 5; // ETH.MDIOPin
}
bool MQTTSettings::begin() {
this->load();
return true;
@ -309,7 +341,6 @@ bool SecuritySettings::fromJSON(JsonObject &obj) {
return true;
}
bool SecuritySettings::toJSON(JsonObject &obj) {
IPAddress ipEmpty(0,0,0,0);
obj["type"] = static_cast<uint8_t>(this->type);
obj["username"] = this->username;
obj["password"] = this->password;

View file

@ -3,12 +3,19 @@
#ifndef configsettings_h
#define configsettings_h
#define FW_VERSION "v2.1.5"
#define FW_VERSION "v2.1.6"
enum DeviceStatus {
DS_OK = 0,
DS_ERROR = 1,
DS_FWUPDATE = 2
};
struct restore_options_t {
bool settings = false;
bool shades = false;
bool network = false;
bool transceiver = false;
void fromJSON(JsonObject &obj);
};
class BaseSettings {
public:
@ -50,6 +57,7 @@ class WifiSettings: BaseSettings {
bool save();
bool load();
void print();
};
class EthernetSettings: BaseSettings {
public:
@ -153,6 +161,8 @@ class ConfigSettings: BaseSettings {
void emitSockets();
void emitSockets(uint8_t num);
bool toJSON(DynamicJsonDocument &doc);
uint16_t calcSettingsRecSize();
uint16_t calcNetRecSize();
};
#endif

View file

@ -197,6 +197,7 @@ bool MQTTClass::disconnect() {
this->unsubscribe("shades/+/mypos/set");
this->unsubscribe("shades/+/myTiltPos/set");
this->unsubscribe("shades/+/sunFlag/set");
this->unsubscribe("groups/+/direction/set");
mqttClient.disconnect();
}
return true;

228
Somfy.cpp
View file

@ -139,15 +139,17 @@ void somfy_frame_t::decodeFrame(byte* frame) {
this->cmd = (somfy_commands)(this->encKey - 133);
}
}
else this->proto = radio_proto::RTS;
// 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;
Serial.println("Processed set to 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->rollingCode > 0);
if (this->cmd != somfy_commands::Sensor && this->valid) this->valid = (this->rollingCode > 0);
if (this->valid) {
// Check for valid command.
switch (this->cmd) {
@ -542,6 +544,12 @@ void SomfyShadeController::commit() {
this->isDirty = false;
this->lastCommit = millis();
}
void SomfyShadeController::writeBackup() {
ShadeConfigFile file;
file.begin("/controller.backup", false);
file.backup(this);
file.end();
}
SomfyShade * SomfyShadeController::getShadeById(uint8_t shadeId) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
if(this->shades[i].getShadeId() == shadeId) return &this->shades[i];
@ -793,7 +801,10 @@ void SomfyShade::checkMovement() {
const bool isWindy = this->flags & static_cast<uint8_t>(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) downTime = upTime = tiltTime = 1;
// We are checking movement for essentially 3 types of motors.
@ -855,7 +866,7 @@ void SomfyShade::checkMovement() {
}
if(!tilt_first && this->direction > 0) {
if(this->downTime == 0) {
if(downTime == 0) {
this->direction = 0;
this->currentPos = 100.0;
}
@ -865,15 +876,15 @@ void SomfyShade::checkMovement() {
// The starting posion is a float value from 0-1 that indicates how much the shade is open. So
// if we take the starting position * the total down time then this will tell us how many ms it
// has moved in the down position.
int32_t msFrom0 = (int32_t)floor((this->startPos/100) * this->downTime);
int32_t msFrom0 = (int32_t)floor((this->startPos/100) * downTime);
// So if the start position is .1 it is 10% closed so we have a 1000ms (1sec) of time to account for
// before we add any more time.
msFrom0 += (curTime - this->moveStart);
// Now we should have the total number of ms that the shade moved from the top. But just so we
// don't have any rounding errors make sure that it is not greater than the max down time.
msFrom0 = min((int32_t)this->downTime, msFrom0);
if(msFrom0 >= this->downTime) {
msFrom0 = min(downTime, msFrom0);
if(msFrom0 >= downTime) {
this->currentPos = 100.0f;
this->direction = 0;
}
@ -882,7 +893,7 @@ void SomfyShade::checkMovement() {
// a ratio of how much time has travelled over the total time to go 100%.
// We should now have the number of ms it will take to reach the shade fully close.
this->currentPos = (min(max((float)0.0, (float)msFrom0 / (float)this->downTime), (float)1.0)) * 100;
this->currentPos = (min(max((float)0.0, (float)msFrom0 / (float)downTime), (float)1.0)) * 100;
// If the current position is >= 1 then we are at the bottom of the shade.
if(this->currentPos >= 100) {
this->direction = 0;
@ -911,7 +922,7 @@ void SomfyShade::checkMovement() {
}
}
else if(!tilt_first && this->direction < 0) {
if(this->upTime == 0) {
if(upTime == 0) {
this->direction = 0;
this->currentPos = 0;
}
@ -920,15 +931,15 @@ void SomfyShade::checkMovement() {
// often move slower in the up position so since we are using a relative position the up time
// can be calculated.
// 10000ms from 100 to 0;
int32_t msFrom100 = (int32_t)this->upTime - (int32_t)floor((this->startPos/100) * this->upTime);
int32_t msFrom100 = upTime - (int32_t)floor((this->startPos/100) * upTime);
msFrom100 += (curTime - this->moveStart);
msFrom100 = min((int32_t)this->upTime, msFrom100);
if(msFrom100 >= this->upTime) {
msFrom100 = min(upTime, msFrom100);
if(msFrom100 >= upTime) {
this->currentPos = 0.0;
this->direction = 0;
}
// We should now have the number of ms it will take to reach the shade fully open.
this->currentPos = ((float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)this->upTime), (float)1.0)) * 100;
this->currentPos = ((float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)upTime), (float)1.0)) * 100;
// If we are at the top of the shade then set the movement to 0.
if(this->currentPos <= 0.0) {
this->direction = 0;
@ -957,15 +968,15 @@ void SomfyShade::checkMovement() {
}
if(this->tiltDirection > 0) {
if(tilt_first) this->moveStart = curTime;
int32_t msFrom0 = (int32_t)floor((this->startTiltPos/100) * this->tiltTime);
int32_t msFrom0 = (int32_t)floor((this->startTiltPos/100) * tiltTime);
msFrom0 += (curTime - this->tiltStart);
msFrom0 = min((int32_t)this->tiltTime, msFrom0);
if(msFrom0 >= this->tiltTime) {
msFrom0 = min(tiltTime, msFrom0);
if(msFrom0 >= tiltTime) {
this->currentTiltPos = 100.0f;
this->tiltDirection = 0;
}
else {
this->currentTiltPos = (min(max((float)0.0, (float)msFrom0 / (float)this->tiltTime), (float)1.0)) * 100;
this->currentTiltPos = (min(max((float)0.0, (float)msFrom0 / (float)tiltTime), (float)1.0)) * 100;
if(this->currentTiltPos >= 100) {
this->tiltDirection = 0;
this->currentTiltPos = 100.0f;
@ -1000,19 +1011,19 @@ void SomfyShade::checkMovement() {
}
else if(this->tiltDirection < 0) {
if(tilt_first) this->moveStart = curTime;
if(this->tiltTime == 0) {
if(tiltTime == 0) {
this->tiltDirection = 0;
this->currentTiltPos = 0;
}
else {
int32_t msFrom100 = (int32_t)this->tiltTime - (int32_t)floor((this->startTiltPos/100) * this->tiltTime);
int32_t msFrom100 = tiltTime - (int32_t)floor((this->startTiltPos/100) * tiltTime);
msFrom100 += (curTime - this->tiltStart);
msFrom100 = min((int32_t)this->tiltTime, msFrom100);
if(msFrom100 >= this->tiltTime) {
msFrom100 = min(tiltTime, msFrom100);
if(msFrom100 >= tiltTime) {
this->currentTiltPos = 0.0f;
this->tiltDirection = 0;
}
this->currentTiltPos = ((float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)this->tiltTime), (float)1.0)) * 100;
this->currentTiltPos = ((float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)tiltTime), (float)1.0)) * 100;
// If we are at the top of the shade then set the movement to 0.
if(this->currentTiltPos <= 0.0f) {
this->tiltDirection = 0;
@ -1133,15 +1144,9 @@ void SomfyShade::load() {
}
pref.end();
}
void SomfyShade::publish() {
void SomfyShade::publishState() {
if(mqtt.connected()) {
char topic[32];
snprintf(topic, sizeof(topic), "shades/%u/shadeId", this->shadeId);
mqtt.publish(topic, this->shadeId);
snprintf(topic, sizeof(topic), "shades/%u/name", this->shadeId);
mqtt.publish(topic, this->name);
snprintf(topic, sizeof(topic), "shades/%u/remoteAddress", this->shadeId);
mqtt.publish(topic, this->getRemoteAddress());
snprintf(topic, sizeof(topic), "shades/%u/position", this->shadeId);
mqtt.publish(topic, this->transformPosition(this->currentPos));
snprintf(topic, sizeof(topic), "shades/%u/direction", this->shadeId);
@ -1154,16 +1159,6 @@ void SomfyShade::publish() {
mqtt.publish(topic, this->transformPosition(this->myPos));
snprintf(topic, sizeof(topic), "shades/%u/myTiltPos", this->shadeId);
mqtt.publish(topic, this->transformPosition(this->myTiltPos));
snprintf(topic, sizeof(topic), "shades/%u/shadeType", this->shadeId);
mqtt.publish(topic, static_cast<uint8_t>(this->shadeType));
snprintf(topic, sizeof(topic), "shades/%u/tiltType", this->shadeId);
mqtt.publish(topic, static_cast<uint8_t>(this->tiltType));
snprintf(topic, sizeof(topic), "shades/%u/flags", this->shadeId);
mqtt.publish(topic, this->flags);
snprintf(topic, sizeof(topic), "shades/%u/flipCommands", this->shadeId);
mqtt.publish(topic, this->flipCommands);
snprintf(topic, sizeof(topic), "shades/%u/flipPosition", this->shadeId);
mqtt.publish(topic, this->flipPosition);
if(this->tiltType != tilt_types::none) {
snprintf(topic, sizeof(topic), "shades/%u/tiltDirection", this->shadeId);
mqtt.publish(topic, this->tiltDirection);
@ -1177,11 +1172,53 @@ void SomfyShade::publish() {
const uint8_t isWindy = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Windy));
snprintf(topic, sizeof(topic), "shades/%u/sunSensor", this->shadeId);
mqtt.publish(topic, this->hasSunSensor());
if(this->hasSunSensor()) {
snprintf(topic, sizeof(topic), "shades/%u/sunFlag", this->shadeId);
mqtt.publish(topic, sunFlag);
snprintf(topic, sizeof(topic), "shades/%u/sunny", this->shadeId);
mqtt.publish(topic, isSunny);
snprintf(topic, sizeof(topic), "shades/%u/windy", this->shadeId);
}
mqtt.publish(topic, isWindy);
}
}
void SomfyShade::publish() {
if(mqtt.connected()) {
char topic[32];
snprintf(topic, sizeof(topic), "shades/%u/shadeId", this->shadeId);
mqtt.publish(topic, this->shadeId);
snprintf(topic, sizeof(topic), "shades/%u/name", this->shadeId);
mqtt.publish(topic, this->name);
snprintf(topic, sizeof(topic), "shades/%u/remoteAddress", this->shadeId);
mqtt.publish(topic, this->getRemoteAddress());
snprintf(topic, sizeof(topic), "shades/%u/shadeType", this->shadeId);
mqtt.publish(topic, static_cast<uint8_t>(this->shadeType));
snprintf(topic, sizeof(topic), "shades/%u/tiltType", this->shadeId);
mqtt.publish(topic, static_cast<uint8_t>(this->tiltType));
snprintf(topic, sizeof(topic), "shades/%u/flags", this->shadeId);
mqtt.publish(topic, this->flags);
snprintf(topic, sizeof(topic), "shades/%u/flipCommands", this->shadeId);
mqtt.publish(topic, this->flipCommands);
snprintf(topic, sizeof(topic), "shades/%u/flipPosition", this->shadeId);
mqtt.publish(topic, this->flipPosition);
this->publishState();
}
}
void SomfyGroup::publishState() {
if(mqtt.connected()) {
char topic[32];
snprintf(topic, sizeof(topic), "groups/%u/direction", this->groupId);
mqtt.publish(topic, this->direction);
snprintf(topic, sizeof(topic), "groups/%u/lastRollingCode", this->groupId);
mqtt.publish(topic, this->lastRollingCode);
const uint8_t sunFlag = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
const uint8_t isSunny = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny));
const uint8_t isWindy = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Windy));
snprintf(topic, sizeof(topic), "groups/%u/sunFlag", this->groupId);
mqtt.publish(topic, sunFlag);
snprintf(topic, sizeof(topic), "groups/%u/sunny", this->groupId);
mqtt.publish(topic, isSunny);
snprintf(topic, sizeof(topic), "groups/%u/windy", this->groupId);
mqtt.publish(topic, isWindy);
}
}
@ -1194,25 +1231,13 @@ void SomfyGroup::publish() {
mqtt.publish(topic, this->name);
snprintf(topic, sizeof(topic), "groups/%u/remoteAddress", this->groupId);
mqtt.publish(topic, this->getRemoteAddress());
snprintf(topic, sizeof(topic), "groups/%u/direction", this->groupId);
mqtt.publish(topic, this->direction);
snprintf(topic, sizeof(topic), "groups/%u/lastRollingCode", this->groupId);
mqtt.publish(topic, this->lastRollingCode);
snprintf(topic, sizeof(topic), "groups/%u/groupType", this->groupId);
mqtt.publish(topic, static_cast<uint8_t>(this->groupType));
snprintf(topic, sizeof(topic), "groups/%u/flags", this->groupId);
mqtt.publish(topic, this->flags);
const uint8_t sunFlag = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::SunFlag));
const uint8_t isSunny = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny));
const uint8_t isWindy = !!(this->flags & static_cast<uint8_t>(somfy_flags_t::Windy));
snprintf(topic, sizeof(topic), "groups/%u/sunSensor", this->groupId);
mqtt.publish(topic, this->hasSunSensor());
snprintf(topic, sizeof(topic), "groups/%u/sunFlag", this->groupId);
mqtt.publish(topic, sunFlag);
snprintf(topic, sizeof(topic), "groups/%u/sunny", this->groupId);
mqtt.publish(topic, isSunny);
snprintf(topic, sizeof(topic), "groups/%u/windy", this->groupId);
mqtt.publish(topic, isWindy);
this->publishState();
}
}
@ -1234,14 +1259,6 @@ void SomfyShade::emitState(uint8_t num, const char *evt) {
else sockEmit.sendToClient(num, evt, buf);
if(mqtt.connected()) {
char topic[32];
//snprintf(topic, sizeof(topic), "shades/%u/shadeType", this->shadeId);
//mqtt.publish(topic, static_cast<uint8_t>(this->shadeType));
//snprintf(topic, sizeof(topic), "shades/%u/remoteAddress", this->shadeId);
//mqtt.publish(topic, this->getRemoteAddress());
//snprintf(topic, sizeof(topic), "shades/%u/tiltType", this->shadeId);
//mqtt.publish(topic, static_cast<uint8_t>(this->tiltType));
//snprintf(topic, sizeof(topic), "shades/%u/lastRollingCode", this->shadeId);
//mqtt.publish(topic, this->lastRollingCode);
snprintf(topic, sizeof(topic), "shades/%u/position", this->shadeId);
mqtt.publish(topic, this->transformPosition(this->currentPos));
snprintf(topic, sizeof(topic), "shades/%u/direction", this->shadeId);
@ -1450,12 +1467,13 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
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) return;
{
const uint8_t prevFlags = this->flags;
const bool wasSunny = prevFlags & static_cast<uint8_t>(somfy_flags_t::Sunny);
@ -1529,10 +1547,14 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
case somfy_commands::MyDown:
case somfy_commands::MyUpDown:
case somfy_commands::UpDown:
this->lastFrame.processed = true;
if(this->shadeType == shade_types::drycontact) return;
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
break;
case somfy_commands::Flag:
this->lastFrame.processed = true;
if(this->shadeType == shade_types::drycontact) return;
this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::SunFlag));
somfy.isDirty = true;
this->emitState();
@ -1540,17 +1562,26 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
somfy.updateGroupFlags();
break;
case somfy_commands::SunFlag:
if(this->shadeType == shade_types::drycontact) return;
{
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag);
if (!isWindy)
{
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
if (isSunny && this->sunDone)
if (isSunny && this->sunDone) {
if(this->tiltType == tilt_types::tiltonly)
this->tiltTarget = this->myTiltPos >= 0 ? this->myTiltPos : 100.0f;
else
this->target = this->myPos >= 0 ? this->myPos : 100.0f;
else if (!isSunny && this->noSunDone)
}
else if (!isSunny && this->noSunDone) {
if(this->tiltType == tilt_types::tiltonly)
this->tiltTarget = 0.0f;
else
this->target = 0.0f;
}
}
somfy.isDirty = true;
this->emitState();
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
@ -1558,6 +1589,10 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
}
break;
case somfy_commands::Up:
if(this->shadeType == shade_types::drycontact) {
this->lastFrame.processed = true;
return;
}
if(this->tiltType == tilt_types::tiltmotor) {
// Wait another half second just in case we are potentially processing a tilt.
if(!internal) this->lastFrame.await = curTime + 500;
@ -1565,12 +1600,20 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
}
else {
// If from a remote we will simply be going up.
if(!internal) this->target = this->tiltTarget = 0.0f;
if(this->tiltType == tilt_types::tiltonly && !internal) this->tiltTarget = 0.0f;
else if(!internal) {
if(this->tiltType != tilt_types::tiltonly) this->target = 0.0f;
this->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;
}
if (!this->windLast || (curTime - this->windLast) >= SOMFY_NO_WIND_REMOTE_TIMEOUT) {
if(this->tiltType == tilt_types::tiltmotor) {
// Wait another half seccond just in case we are potentially processing a tilt.
@ -1580,7 +1623,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
else {
this->lastFrame.processed = true;
if(!internal) {
this->target = 100.0f;
if(this->tiltType != tilt_types::tiltonly) this->target = 100.0f;
if(this->tiltType != tilt_types::none) this->tiltTarget = 100.0f;
}
}
@ -1588,6 +1631,15 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
}
break;
case somfy_commands::My:
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;
this->target = this->currentPos = this->currentPos > 0 ? 0 : 100;
this->emitState();
return;
}
if(this->isIdle()) {
if(!internal) {
// This frame is coming from a remote. We are potentially setting
@ -1597,20 +1649,21 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
else {
this->lastFrame.processed = true;
if(this->myTiltPos >= 0.0f && this->myTiltPos >= 100.0f) this->tiltTarget = this->myTiltPos;
if(this->myPos >= 0.0f && this->myPos <= 100.0f) this->target = this->myPos;
if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->target = this->myPos;
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
}
}
else {
this->lastFrame.processed = true;
if(!internal) {
this->target = this->currentPos;
if(this->tiltType != tilt_types::tiltonly) this->target = this->currentPos;
this->tiltTarget = this->currentTiltPos;
}
}
break;
case somfy_commands::StepUp:
this->lastFrame.processed = true;
if(this->shadeType == shade_types::drycontact) return;
if(this->lastFrame.repeats != 0) return;
dir = 0;
// With the step commands and integrated shades
@ -1620,6 +1673,10 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->tiltTarget = max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize)))));
}
else if(this->tiltType == tilt_types::tiltonly) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->tiltTarget = max(0.0f, this->currentTiltPos - (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize)))));
}
else if(this->currentPos > 0.0f) {
if(this->downTime == 0 || this->stepSize == 0) return;
this->target = max(0.0f, this->currentPos - (100.0f/(static_cast<float>(this->upTime/static_cast<float>(this->stepSize)))));
@ -1627,7 +1684,6 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
break;
case somfy_commands::StepDown:
this->lastFrame.processed = true;
if(this->lastFrame.repeats != 0) return;
dir = 1;
// With the step commands and integrated shades
// the motor must tilt in the direction first then move
@ -1636,6 +1692,10 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->tiltTarget = min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize)))));
}
else if(this->tiltType == tilt_types::tiltonly) {
if(this->tiltTime == 0 || this->stepSize == 0) return;
this->target = min(100.0f, this->currentTiltPos + (100.0f/(static_cast<float>(this->tiltTime/static_cast<float>(this->stepSize)))));
}
else if(this->currentPos < 100.0f) {
if(this->downTime == 0 || this->stepSize == 0) return;
this->target = min(100.0f, this->currentPos + (100.0f/(static_cast<float>(this->downTime/static_cast<float>(this->stepSize)))));
@ -1643,9 +1703,9 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
break;
case somfy_commands::Toggle:
if(!this->isIdle()) {
this->target = this->currentPos;
}
if(this->lastFrame.processed) return;
this->lastFrame.processed = true;
if(!this->isIdle()) this->target = this->currentPos;
else if(this->currentPos == 100.0f) this->target = 0;
else if(this->currentPos == 0.0f) this->target = 100;
else this->target = this->lastMovement == -1 ? 100 : 0;
@ -1677,7 +1737,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
this->target = 0.0f;
}
else if(this->tiltType == tilt_types::tiltonly) {
this->currentPos = this->target = 100.0f; // We are always 100% with this.
this->myPos = this->currentPos = this->target = 100.0f; // We are always 100% with this.
this->tiltTarget = 0.0f;
}
else
@ -1692,7 +1752,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
this->target = 100.0f;
}
else if(this->tiltType == tilt_types::tiltonly) {
this->currentPos = this->target = 100.0f;
this->myPos = this->currentPos = this->target = 100.0f;
this->tiltTarget = 100.0f;
}
else {
@ -1708,7 +1768,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->target = this->myPos;
}
else {
if(this->tiltType == tilt_types::tiltonly) this->currentPos = this->target = 100.0f;
if(this->tiltType == tilt_types::tiltonly) this->myPos = this->currentPos = this->target = 100.0f;
else this->target = this->currentPos;
this->tiltTarget = this->currentTiltPos;
}
@ -1948,13 +2008,19 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
if(cmd == somfy_commands::Up) {
SomfyRemote::sendCommand(cmd, repeat);
if(this->tiltType != tilt_types::tiltonly) this->target = 0.0f;
else this->tiltTarget = 0.0f;
else {
this->myPos = this->currentPos = this->target = 100.0f;
this->tiltTarget = 0.0f;
}
if(this->tiltType == tilt_types::integrated) this->tiltTarget = 0.0f;
}
else if(cmd == somfy_commands::Down) {
SomfyRemote::sendCommand(cmd, repeat);
if(this->tiltType != tilt_types::tiltonly) this->target = 100.0f;
else this->tiltTarget = 100.0f;
else {
this->myPos = this->currentPos = this->target = 100.0f;
this->tiltTarget = 100.0f;
}
if(this->tiltType == tilt_types::integrated) this->tiltTarget = 100.0f;
}
else if(cmd == somfy_commands::My) {
@ -2052,7 +2118,7 @@ void SomfyShade::moveToTarget(float pos, float tilt) {
return;
}
if(this->tiltType == tilt_types::tiltonly) {
this->currentPos = this->target = 100.0f;
this->myPos = this->currentPos = this->target = 100.0f;
pos = 100;
if(tilt < this->currentTiltPos) cmd = somfy_commands::Up;
else if(tilt > this->currentTiltPos) cmd = somfy_commands::Down;
@ -2567,6 +2633,9 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) {
Serial.print(this->lastFrame.rollingCode);
Serial.print(" REPEAT:");
Serial.println(repeat);
// We have to set the processed to clear this if we are sending
// another command.
this->lastFrame.processed = false;
somfy.sendFrame(this->lastFrame, repeat);
somfy.processFrame(this->lastFrame, true);
}
@ -2661,6 +2730,7 @@ uint16_t SomfyRemote::getNextRollingCode() {
Serial.printf("Getting Next Rolling code %d\n", this->lastRollingCode);
return code;
}
uint16_t SomfyRemote::setRollingCode(uint16_t code) {
if(this->lastRollingCode != code) {
pref.begin("ShadeCodes");

View file

@ -58,7 +58,8 @@ enum class shade_types : byte {
garage1 = 0x05,
garage3 = 0x06,
rdrapery = 0x07,
cdrapery = 0x08
cdrapery = 0x08,
drycontact = 0x09,
};
enum class tilt_types : byte {
none = 0x00,
@ -281,6 +282,7 @@ class SomfyShade : public SomfyRemote {
void moveToMyPosition();
void processWaitingFrame();
void publish();
void publishState();
void commit();
void commitShadePosition();
void commitTiltPosition();
@ -308,6 +310,7 @@ class SomfyGroup : public SomfyRemote {
bool hasShadeId(uint8_t shadeId);
void compressLinkedShadeIds();
void publish();
void publishState();
void updateFlags();
void emitState(const char *evt = "groupState");
void emitState(uint8_t num, const char *evt = "groupState");
@ -455,6 +458,7 @@ class SomfyShadeController {
void publish();
void processWaitingFrame();
void commit();
void writeBackup();
bool loadShadesFile(const char *filename);
bool loadLegacy();
};

Binary file not shown.

Binary file not shown.

305
Web.cpp
View file

@ -4,6 +4,7 @@
#include <Update.h>
#include "mbedtls/md.h"
#include "ConfigSettings.h"
#include "ConfigFile.h"
#include "Web.h"
#include "Utils.h"
#include "SSDP.h"
@ -345,7 +346,7 @@ void Web::handleRepeatCommand(WebServer& server) {
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
if(server.hasArg("shadeId")) shadeId = atoi(server.arg("shadeId").c_str());
else if(server.hasArg("groupId")) shadeId = atoi(server.arg("groupId").c_str());
else if(server.hasArg("groupId")) groupId = atoi(server.arg("groupId").c_str());
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")) {
@ -547,6 +548,132 @@ void Web::handleTiltCommand(WebServer &server) {
else
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
}
void Web::handleShade(WebServer &server) {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_GET) {
if (server.hasArg("shadeId")) {
int shadeId = atoi(server.arg("shadeId").c_str());
SomfyShade* shade = somfy.getShadeById(shadeId);
if (shade) {
DynamicJsonDocument doc(2048);
JsonObject obj = doc.to<JsonObject>();
shade->toJSON(obj);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
}
else {
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}"));
}
}
else if (method == HTTP_PUT || method == HTTP_POST) {
// We are updating an existing shade.
if (server.hasArg("plain")) {
Serial.println("Updating a shade");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
switch (err.code()) {
case DeserializationError::InvalidInput:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
break;
case DeserializationError::NoMemory:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
break;
default:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
break;
}
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("shadeId")) {
SomfyShade* shade = somfy.getShadeById(obj["shadeId"]);
if (shade) {
shade->fromJSON(obj);
shade->save();
DynamicJsonDocument sdoc(2048);
JsonObject sobj = sdoc.to<JsonObject>();
shade->toJSON(sobj);
serializeJson(sdoc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
}
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}"));
}
else
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
}
void Web::handleGroup(WebServer &server) {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_GET) {
if (server.hasArg("groupId")) {
int groupId = atoi(server.arg("groupId").c_str());
SomfyGroup* group = somfy.getGroupById(groupId);
if (group) {
DynamicJsonDocument doc(2048);
JsonObject obj = doc.to<JsonObject>();
group->toJSON(obj);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
}
else {
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}"));
}
}
else if (method == HTTP_PUT || method == HTTP_POST) {
// We are updating an existing group.
if (server.hasArg("plain")) {
Serial.println("Updating a group");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
switch (err.code()) {
case DeserializationError::InvalidInput:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
break;
case DeserializationError::NoMemory:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
break;
default:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
break;
}
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("groupId")) {
SomfyGroup* group = somfy.getGroupById(obj["groupId"]);
if (group) {
group->fromJSON(obj);
group->save();
DynamicJsonDocument sdoc(2048);
JsonObject sobj = sdoc.to<JsonObject>();
group->toJSON(sobj);
serializeJson(sdoc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}"));
}
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}"));
}
else
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
}
void Web::handleDiscovery(WebServer &server) {
HTTPMethod method = apiServer.method();
if (method == HTTP_POST || method == HTTP_GET) {
@ -614,6 +741,8 @@ void Web::begin() {
apiServer.on("/groupCommand", []() { webServer.handleGroupCommand(apiServer); });
apiServer.on("/tiltCommand", []() { webServer.handleTiltCommand(apiServer); });
apiServer.on("/repeatCommand", []() { webServer.handleRepeatCommand(apiServer); });
apiServer.on("/shade", HTTP_GET, [] () { webServer.handleShade(apiServer); });
apiServer.on("/group", HTTP_GET, [] () { webServer.handleGroup(apiServer); });
// Web Interface
server.on("/tiltCommand", []() { webServer.handleTiltCommand(server); });
@ -647,9 +776,10 @@ void Web::begin() {
snprintf(filename, sizeof(filename), "attachment; filename=\"ESPSomfyRTS %s.backup\"", iso);
Serial.println(filename);
server.sendHeader(F("Content-Disposition"), filename);
server.sendHeader(F("Access-Control-Expose-Headers"), F("Content-Disposition"));
Serial.println("Saving current shade information");
somfy.commit();
File file = LittleFS.open("/shades.cfg", "r");
somfy.writeBackup();
File file = LittleFS.open("/controller.backup", "r");
if (!file) {
Serial.println("Error opening shades.cfg");
server.send(500, _encoding_text, "shades.cfg");
@ -660,10 +790,46 @@ void Web::begin() {
server.on("/restore", HTTP_POST, []() {
webServer.sendCORSHeaders(server);
server.sendHeader("Connection", "close");
if(webServer.uploadSuccess) {
server.send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}");
restore_options_t opts;
if(server.hasArg("data")) {
Serial.println(server.arg("data"));
StaticJsonDocument<256> doc;
DeserializationError err = deserializeJson(doc, server.arg("data"));
if (err) {
switch (err.code()) {
case DeserializationError::InvalidInput:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
break;
case DeserializationError::NoMemory:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
break;
default:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
break;
}
Serial.println("An error occurred when deserializing the restore data");
return;
}
else {
JsonObject obj = doc.as<JsonObject>();
opts.fromJSON(obj);
}
}
else {
Serial.println("No restore options sent. Using defaults...");
opts.shades = true;
}
ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
Serial.println("Rebooting ESP for restored settings...");
rebootDelay.reboot = true;
rebootDelay.rebootTime = millis() + 1000;
}
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
webServer.uploadSuccess = false;
Serial.printf("Restore: %s\n", upload.filename.c_str());
// Begin by opening a new temporary file.
File fup = LittleFS.open("/shades.tmp", "w");
@ -675,10 +841,7 @@ void Web::begin() {
fup.close();
}
else if (upload.status == UPLOAD_FILE_END) {
// TODO: Do some validation of the file.
Serial.println("Validating restore");
// Go through the uploaded file to determine if it is valid.
if(somfy.loadShadesFile("/shades.tmp")) somfy.commit();
webServer.uploadSuccess = true;
}
});
server.on("/index.js", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/index.js", "text/javascript"); });
@ -691,6 +854,8 @@ void Web::begin() {
server.on("/controller", []() { webServer.handleController(server); });
server.on("/shades", []() { webServer.handleGetShades(server); });
server.on("/groups", []() { webServer.handleGetGroups(server); });
server.on("/shade", []() { webServer.handleShade(server); });
server.on("/group", []() { webServer.handleGroup(server); });
server.on("/getNextShade", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
@ -829,128 +994,6 @@ void Web::begin() {
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error saving Somfy Group.\"}"));
}
});
server.on("/shade", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_GET) {
if (server.hasArg("shadeId")) {
int shadeId = atoi(server.arg("shadeId").c_str());
SomfyShade* shade = somfy.getShadeById(shadeId);
if (shade) {
DynamicJsonDocument doc(2048);
JsonObject obj = doc.to<JsonObject>();
shade->toJSON(obj);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
}
else {
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}"));
}
}
else if (method == HTTP_PUT || method == HTTP_POST) {
// We are updating an existing shade.
if (server.hasArg("plain")) {
Serial.println("Updating a shade");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
switch (err.code()) {
case DeserializationError::InvalidInput:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
break;
case DeserializationError::NoMemory:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
break;
default:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
break;
}
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("shadeId")) {
SomfyShade* shade = somfy.getShadeById(obj["shadeId"]);
if (shade) {
shade->fromJSON(obj);
shade->save();
DynamicJsonDocument sdoc(2048);
JsonObject sobj = sdoc.to<JsonObject>();
shade->toJSON(sobj);
serializeJson(sdoc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
}
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}"));
}
});
server.on("/group", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
if (method == HTTP_GET) {
if (server.hasArg("groupId")) {
int groupId = atoi(server.arg("groupId").c_str());
SomfyGroup* group = somfy.getGroupById(groupId);
if (group) {
DynamicJsonDocument doc(2048);
JsonObject obj = doc.to<JsonObject>();
group->toJSON(obj);
serializeJson(doc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
}
else {
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}"));
}
}
else if (method == HTTP_PUT || method == HTTP_POST) {
// We are updating an existing group.
if (server.hasArg("plain")) {
Serial.println("Updating a group");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
switch (err.code()) {
case DeserializationError::InvalidInput:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
break;
case DeserializationError::NoMemory:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
break;
default:
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
break;
}
}
else {
JsonObject obj = doc.as<JsonObject>();
if (obj.containsKey("groupId")) {
SomfyGroup* group = somfy.getGroupById(obj["groupId"]);
if (group) {
group->fromJSON(obj);
group->save();
DynamicJsonDocument sdoc(2048);
JsonObject sobj = sdoc.to<JsonObject>();
group->toJSON(sobj);
serializeJson(sdoc, g_content);
server.send(200, _encoding_json, g_content);
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}"));
}
}
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}"));
}
});
server.on("/groupOptions", []() {
webServer.sendCORSHeaders(server);
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
@ -1586,6 +1629,7 @@ void Web::begin() {
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
webServer.uploadSuccess = false;
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
@ -1604,6 +1648,7 @@ void Web::begin() {
else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
webServer.uploadSuccess = true;
}
else {
Update.printError(Serial);
@ -1644,6 +1689,7 @@ void Web::begin() {
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
webServer.uploadSuccess = false;
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { //start with max available size and tell it we are updating the file system.
Update.printError(Serial);
@ -1661,6 +1707,7 @@ void Web::begin() {
}
else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
webServer.uploadSuccess = true;
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
somfy.commit();
}

3
Web.h
View file

@ -3,6 +3,7 @@
#define webserver_h
class Web {
public:
bool uploadSuccess = false;
void sendCORSHeaders(WebServer &server);
void sendCacheHeaders(uint32_t seconds=604800);
void startup();
@ -19,6 +20,8 @@ class Web {
void handleTiltCommand(WebServer &server);
void handleDiscovery(WebServer &server);
void handleNotFound(WebServer &server);
void handleShade(WebServer &server);
void handleGroup(WebServer &server);
void begin();
void loop();
void end();

View file

@ -1 +1 @@
2.1.5
2.1.6

View file

@ -874,6 +874,35 @@ i.icss-lightbulb-o {
left: .1em;
top: -.5em;
}
i.icss-lightbulb {
width: .35em;
height: .1em;
border-radius: .1em;
margin: .7em .325em .2em;
box-shadow: 0 .13em, 0 .19em 0 -.03em, 0 .22em 0 -0.035em;
}
i.icss-lightbulb:before {
width: .65em;
height: .65em;
border-width: 0.068em;
border-style: solid;
border-radius: 100% 100% 100% .2em;
background-color: rgba(255,255,0, var(--shade-position, 0%));
left: 50%;
transform: translateX(-50%) rotate(-45deg);
bottom: .11em;
}
i.icss-lightbulb:after {
width: .25em;
height: .2em;
border-radius: 100%;
box-shadow: inset -0.05em 0.05em;
left: .1em;
top: -.5em;
}
i.icss-upload {
width: 1em;
height: .6em;

View file

@ -3,11 +3,11 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<link rel="stylesheet" href="main.css?v=2.1.5" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.1.5" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.1.5" type="text/css" />
<link rel="stylesheet" href="main.css?v=2.1.6p" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.1.6p" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.1.6p" type="text/css" />
<link rel="icon" type="image/png" href="favicon.png" />
<script type="text/javascript" src="index.js?v=2.1.5"></script>
<script type="text/javascript" src="index.js?v=2.1.6p"></script>
</head>
<body>
<div id="divContainer" class="container main" data-auth="false">
@ -274,7 +274,7 @@
</div>
</div>
<div id="divSomfySettings" style="display:none;">
<div class="subtab-container"><span class="selected" data-grpid="divSomfyMotors">Shades</span><span data-grpid="divSomfyGroups">Groups</span></div>
<div class="subtab-container"><span class="selected" data-grpid="divSomfyMotors">Shades</span><span data-grpid="divSomfyGroups">Groups</span><span data-grpid="divVirtualRemote">Virtual Remote</span></div>
<div id="divSomfyMotors" class="subtab-content" style="padding-top:10px;">
<div id="divShadeListContainer">
<div style="font-size:.8em;">Drag each item to set the order in which they appear in the list.</div>
@ -285,7 +285,7 @@
</button>
</div>
</div>
<div id="somfyShade" style="width:100%;display:none;">
<div id="somfyShade" style="width:100%;display:none;" data-bitlength="56">
<div style="display:inline-block;float:right;position:relative;"><span id="spanShadeId">*</span>/<span id="spanMaxShades">5</span></div>
<div class="field-group" style="padding:0px;">
<div class="field-group" style="margin-top:-18px;display:inline-block;width:77px;">
@ -318,6 +318,7 @@
<option value="3">Awning</option>
<option value="5">Garage (1-button)</option>
<option value="6">Garage (3-button)</option>
<option value="9">Dry Contact</option>
</select>
<label for="selShadeType">Type</label>
</div>
@ -536,7 +537,81 @@
</div>
</div>
<div id="divVirtualRemote" style="display:none;" class="subtab-content">
<div class="field-group" style="margin-top:-18px;display:inline-block;width:100%;">
<select id="selVRMotor" style="width:100%;">
</select>
<label for="selVRMotor">Select a Motor</label>
</div>
<div class="vr-button vr-updownmy">
<span>Remote Buttons</span>
<div>
<div class="button-outline" data-cmd="up" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-somfy-up"></i></div>
<div class="button-outline" data-cmd="my" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);" style="font-size: 2em; padding: 10px;"><span>my</span></div>
<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-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">
<span>Up + Down</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="UpDown" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><i class="icss-somfy-up"></i><span> + </span><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>
</div>
</div>
<div class="vr-button vr-updownmy">
<span>Up + My + Down</span>
<div>
<div class="button-outline" data-cmd="MyUpDown" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span style="padding: 10px;">all</span></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>My + Up</span>
<div>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="MyUp" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span>my</span><span> + </span><i class="icss-somfy-up"></i></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>My + Down</span>
<div>
<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>
<div class="button-outline" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="Prog" onmousedown="somfy.sendVRCommand(this);" ontouchstart="somfy.sendVRCommand(this);"><span>prog</span></div>
</div>
</div>
<div class="vr-button vr-updown">
<span>Sun Flag On</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>
</div>
</div>
</div>
<div id="divRadioSettings" style="display:none;">
<div class="subtab-container"><span class="selected" data-grpid="divTransceiverSettings">Transceiver</span><span data-grpid="divFrameLog">Logs</span></div>

View file

@ -712,7 +712,7 @@ class UIBinder {
tval = tval.fmt(fld.getAttribute('data-fmtmask'), fld.getAttribute('data-fmtempty') || '');
break;
case 'duration':
tval = dataBinder.formatDuration(tval, $this.attr('data-fmtmask'));
tval = ui.formatDuration(tval, $this.attr('data-fmtmask'));
break;
}
this.setValue(fld, tval);
@ -1197,7 +1197,7 @@ var security = new Security();
class General {
initialized = false;
appVersion = 'v2.1.5';
appVersion = 'v2.1.6';
reloadApp = false;
init() {
if (this.initialized) return;
@ -1938,6 +1938,24 @@ class Somfy {
let divCtl = '';
shades.sort((a, b) => { return a.sortOrder - b.sortOrder });
console.log(shades);
let vrList = document.getElementById('selVRMotor');
// First get the optiongroup for the shades.
let optGroup = document.getElementById('optgrpVRShades');
if (typeof shades === 'undefined' || shades.length === 0) {
if (typeof optGroup !== 'undefined') optGroup.remove();
}
else {
if (typeof optGroup === 'undefined' || !optGroup) {
optGroup = document.createElement('optgroup');
optGroup.setAttribute('id', 'optgrpVRShades');
optGroup.setAttribute('label', 'Shades');
vrList.appendChild(optGroup);
}
else {
optGroup.innerHTML = '';
}
}
for (let i = 0; i < shades.length; i++) {
let shade = shades[i];
divCfg += `<div class="somfyShade shade-draggable" draggable="true" data-shadeid="${shade.shadeId}" data-remoteaddress="${shade.remoteAddress}" data-tilt="${shade.tiltType}" data-shadetype="${shade.shadeType}">`;
@ -1978,6 +1996,9 @@ class Somfy {
case 8:
divCtl += ' icss-cdrapery';
break;
case 9:
divCtl += ' icss-lightbulb';
break;
default:
divCtl += ' icss-window-shade';
break;
@ -1990,7 +2011,7 @@ class Somfy {
divCtl += `<span class="shadectl-name">${shade.name}</span>`;
if (shade.tiltType === 3)
divCtl += `<span class="shadectl-mypos"><label>My Tilt: </label><span id="spanMyTiltPos">${shade.myTiltPos > 0 ? shade.myTiltPos + '%' : '---'}</span>`
else if(shade.shadeType !== 5) {
else if(shade.shadeType !== 5 && shade.shadeType !== 9) {
divCtl += `<span class="shadectl-mypos"><label>My: </label><span id="spanMyPos">${shade.myPos > 0 ? shade.myPos + '%' : '---'}</span>`;
if (shade.myTiltPos > 0 && shade.tiltType !== 3) divCtl += `<label> Tilt: </label><span id="spanMyTiltPos">${shade.myTiltPos > 0 ? shade.myTiltPos + '%' : '---'}</span>`;
}
@ -2004,6 +2025,13 @@ class Somfy {
divCtl += `<div class="button-outline cmd-button toggle-button" style="width:127px;text-align:center;border-radius:33%;font-size:2em;padding:10px;" data-cmd="toggle" data-shadeid="${shade.shadeId}"><i class="icss-somfy-toggle" style="margin-top:-4px;"></i></div>`;
divCtl += '</div></div>';
divCtl += '</div>';
let opt = document.createElement('option');
opt.innerHTML = shade.name;
opt.setAttribute('data-address', shade.remoteAddress);
opt.setAttribute('data-type', 'shade');
opt.setAttribute('data-shadetype', shade.shadeType);
opt.setAttribute('data-shadeid', shade.shadeId);
optGroup.appendChild(opt);
}
document.getElementById('divShadeList').innerHTML = divCfg;
let shadeControls = document.getElementById('divShadeControls');
@ -2172,8 +2200,28 @@ class Somfy {
setGroupsList(groups) {
let divCfg = '';
let divCtl = '';
let vrList = document.getElementById('selVRMotor');
// First get the optiongroup for the shades.
let optGroup = document.getElementById('optgrpVRGroups');
if (typeof groups === 'undefined' || groups.length === 0) {
if (typeof optGroup !== 'undefined') optGroup.remove();
}
else {
if (typeof optGroup === 'undefined' || !optGroup) {
optGroup = document.createElement('optgroup');
optGroup.setAttribute('id', 'optgrpVRGroups');
optGroup.setAttribute('label', 'Groups');
vrList.appendChild(optGroup);
}
else {
optGroup.innerHTML = '';
}
}
if (typeof groups !== 'undefined') {
groups.sort((a, b) => { return a.sortOrder - b.sortOrder });
for (let i = 0; i < groups.length; i++) {
let group = groups[i];
divCfg += `<div class="somfyGroup group-draggable" draggable="true" data-groupid="${group.groupId}" data-remoteaddress="${group.remoteAddress}">`;
@ -2203,6 +2251,12 @@ class Somfy {
divCtl += `<div class="button-outline cmd-button my-button" data-cmd="my" data-groupid="${group.groupId}" style="font-size:2em;padding:10px;"><span>my</span></div>`;
divCtl += `<div class="button-outline cmd-button" data-cmd="down" data-groupid="${group.groupId}"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>`;
divCtl += '</div></div>';
let opt = document.createElement('option');
opt.innerHTML = group.name;
opt.setAttribute('data-address', group.remoteAddress);
opt.setAttribute('data-type', 'group');
opt.setAttribute('data-groupid', group.groupId);
optGroup.appendChild(opt);
}
}
document.getElementById('divGroupList').innerHTML = divCfg;
@ -2530,6 +2584,7 @@ class Somfy {
let tilt = parseInt(document.getElementById('selTiltType').value, 10);
let sun = true;
let light = false;
let lift = true;
let ico = document.getElementById('icoShade');
let type = parseInt(sel.value, 10);
document.getElementById('somfyShade').setAttribute('data-shadetype', type);
@ -2545,6 +2600,7 @@ class Somfy {
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (!ico.classList.contains('icss-window-blind')) ico.classList.add('icss-window-blind');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
break;
case 2:
document.getElementById('divTiltSettings').style.display = 'none';
@ -2556,6 +2612,7 @@ class Somfy {
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
tilt = false;
break;
case 3:
@ -2568,6 +2625,7 @@ class Somfy {
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (!ico.classList.contains('icss-awning')) ico.classList.add('icss-awning');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
tilt = false;
break;
case 4:
@ -2580,6 +2638,7 @@ class Somfy {
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (!ico.classList.contains('icss-shutter')) ico.classList.add('icss-shutter');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
tilt = false;
break;
case 6:
@ -2593,6 +2652,7 @@ class Somfy {
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (!ico.classList.contains('icss-garage')) ico.classList.add('icss-garage');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
light = true;
sun = false;
tilt = false;
@ -2607,6 +2667,7 @@ class Somfy {
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
tilt = false;
break;
case 8:
@ -2619,8 +2680,26 @@ class Somfy {
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (ico.classList.contains('icss-lightbulb')) ico.classList.remove('icss-lightbulb');
tilt = false;
break;
case 9:
document.getElementById('divTiltSettings').style.display = 'none';
if (ico.classList.contains('icss-window-shade')) ico.classList.remove('icss-window-shade');
if (ico.classList.contains('icss-ldrapery')) ico.classList.remove('icss-ldrapery');
if (ico.classList.contains('icss-rdrapery')) ico.classList.remove('icss-rdrapery');
if (ico.classList.contains('icss-cdrapery')) ico.classList.remove('icss-cdrapery');
if (ico.classList.contains('icss-window-blind')) ico.classList.remove('icss-window-blind');
if (ico.classList.contains('icss-shutter')) ico.classList.remove('icss-shutter');
if (ico.classList.contains('icss-garage')) ico.classList.remove('icss-garage');
if (ico.classList.contains('icss-awning')) ico.classList.remove('icss-awning');
if (!ico.classList.contains('icss-lightbulb')) ico.classList.add('icss-lightbulb');
lift = false;
tilt = false;
light = false;
sun = false;
break;
default:
if (ico.classList.contains('icss-window-blind')) ico.classList.remove('icss-window-blind');
@ -2636,10 +2715,13 @@ class Somfy {
break;
}
document.getElementById('fldTiltTime').parentElement.style.display = tilt ? 'inline-block' : 'none';
document.getElementById('divLiftSettings').style.display = tilt === 3 ? 'none' : '';
if (lift && tilt == 3) lift = false;
document.getElementById('divLiftSettings').style.display = lift ? '' : 'none';
document.querySelector('#divSomfyButtons i.icss-window-tilt').style.display = tilt ? '' : 'none';
document.getElementById('divSunSensor').style.display = sun ? '' : 'none';
document.getElementById('divLightSwitch').style.display = light ? '' : 'none';
if (!light) document.getElementById('cbHasLight').checked = false;
if (!sun) document.getElementById('cbHasSunsensor').checked = false;
}
onShadeBitLengthChanged(el) {
document.getElementById('somfyShade').setAttribute('data-bitlength', el.value);
@ -2664,12 +2746,14 @@ class Somfy {
}
else {
console.log(shade);
let elShade = document.getElementById('somfyShade')
shade.name = '';
shade.downTime = shade.upTime = 10000;
shade.tiltTime = 7000;
shade.flipCommands = shade.flipPosition = false;
ui.toElement(document.getElementById('somfyShade'), shade);
ui.toElement(elShade, shade);
this.showEditShade(true);
elShade.setAttribute('data-bitlength', shade.bitLength);
}
});
}
@ -3183,7 +3267,6 @@ class Somfy {
}, true);
return div;
}
unpairShade(shadeId) {
let div = document.createElement('div');
let html = `<div id="divPairing" class="instructions" data-type="link-remote" data-shadeid="${shadeId}">`;
@ -3224,23 +3307,68 @@ class Somfy {
if(typeof cb === 'function') cb(err, shade);
});
}
sendGroupCommand(groupId, command, repeat) {
sendGroupRepeat(groupId, command, repeat, cb) {
let obj = { groupId: groupId, command: command };
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
putJSON(`/repeatCommand?groupId=${groupId}&command=${command}`, null, (err, group) => {
if (typeof cb === 'function') cb(err, group);
});
}
sendVRCommand(el) {
let pnl = document.getElementById('divVirtualRemote');
let dd = pnl.querySelector('#selVRMotor');
let opt = dd.selectedOptions[0];
let o = {
type: opt.getAttribute('data-type'),
address: opt.getAttribute('data-address'),
cmd: el.getAttribute('data-cmd')
};
switch (o.type) {
case 'shade':
o.shadeId = parseInt(opt.getAttribute('data-shadeId'), 10);
o.shadeType = parseInt(opt.getAttribute('data-shadeType'), 10);
break;
case 'group':
o.groupId = parseInt(opt.getAttribute('data-groupId'), 10);
break;
}
console.log(o);
let fnRepeatCommand = (err, shade) => {
if (this.btnTimer) {
clearTimeout(this.btnTimer);
this.btnTimer = null;
}
if (err) return;
if (mouseDown) {
if (o.type === 'group')
somfy.sendGroupRepeat(o.groupId, o.cmd, null, fnRepeatCommand);
else
somfy.sendCommandRepeat(o.shadeId, o.cmd, null, fnRepeatCommand);
}
}
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); });
}
sendGroupCommand(groupId, command, repeat, cb) {
console.log(`Sending Group command ${groupId}-${command}`);
let obj = { groupId: groupId };
if (isNaN(parseInt(command, 10))) obj.command = command;
if (typeof repeat === 'number') obj.repeat = parseInt(repeat);
putJSON('/groupCommand', obj, (err, group) => {
if (typeof cb === 'function') cb(err, group);
});
}
sendTiltCommand(shadeId, command) {
sendTiltCommand(shadeId, command, cb) {
console.log(`Sending Tilt command ${shadeId}-${command}`);
if (isNaN(parseInt(command, 10)))
putJSON('/tiltCommand', { shadeId: shadeId, command: command }, (err, shade) => {
if (typeof cb === 'function') cb(err, shade);
});
else
putJSON('/tiltCommand', { shadeId: shadeId, target: parseInt(command, 10) }, (err, shade) => {
if (typeof cb === 'function') cb(err, shade);
});
}
linkRemote(shadeId) {
@ -3602,18 +3730,80 @@ class Firmware {
let agt = navigator.userAgent.toLowerCase();
return /Android|iPhone|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent);
}
backup() {
async backup() {
let overlay = ui.waitMessage(document.getElementById('divContainer'));
return await new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onreadystatechange = (evt) => {
if (xhr.readyState === 4 && xhr.status === 200) {
let obj = window.URL.createObjectURL(xhr.response);
var link = document.createElement('a');
link.href = baseUrl.length > 0 ? `${baseUrl}/backup` : '/backup';
link.setAttribute('download', 'backup');
document.body.appendChild(link);
let header = xhr.getResponseHeader('content-disposition');
let fname = 'backup';
if (typeof header !== 'undefined') {
let start = header.indexOf('filename="');
if (start >= 0) {
let length = header.length;
fname = header.substring(start + 10, length - 1);
}
}
console.log(fname);
link.setAttribute('download', fname);
link.setAttribute('href', obj);
link.click();
link.remove();
setTimeout(() => { window.URL.revokeObjectURL(obj); console.log('Revoked object'); }, 0);
}
};
xhr.onload = (evt) => {
if (typeof overlay !== 'undefined') overlay.remove();
let status = xhr.status;
if (status !== 200) {
let err = xhr.response || {};
err.htmlError = status;
err.service = `GET /backup`;
if (typeof err.desc === 'undefined') err.desc = xhr.statusText || httpStatusText[xhr.status || 500];
console.log('Done');
reject(err);
}
else {
resolve();
}
};
xhr.onerror = (evt) => {
if (typeof overlay !== 'undefined') overlay.remove();
let err = {
htmlError: xhr.status || 500,
service: `GET /backup`
};
if (typeof err.desc === 'undefined') err.desc = xhr.statusText || httpStatusText[xhr.status || 500];
console.log(err);
reject(err);
};
xhr.onabort = (evt) => {
if (typeof overlay !== 'undefined') overlay.remove();
console.log('Aborted');
if (typeof overlay !== 'undefined') overlay.remove();
reject({ htmlError: status, service: 'GET /backup' });
};
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}/backup` : '/backup', true);
xhr.send();
});
}
restore() {
let div = this.createFileUploader('/restore');
let inst = div.querySelector('div[id=divInstText]');
inst.innerHTML = '<div style="font-size:14px;margin-bottom:20px;">Select a backup file that you would like to restore then press the Upload File button.</div>';
let html = '<div style="font-size:14px;">Select a backup file that you would like to restore and the options you would like to restore then press the Upload File button.</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 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="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="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>';
inst.innerHTML = html;
document.getElementById('divContainer').appendChild(div);
}
createFileUploader(service) {
@ -3632,7 +3822,7 @@ class Firmware {
html += `<div class="progress-bar" id="progFileUpload" style="--progress:0%;margin-top:10px;display:none;"></div>`;
html += `<div class="button-container">`;
html += `<button id="btnBackupCfg" type="button" style="display:none;width:auto;padding-left:20px;padding-right:20px;margin-right:4px;" onclick="firmware.backup();">Backup</button>`;
html += `<button id="btnUploadFile" type="button" style="width:auto;padding-left:20px;padding-right:20px;margin-right:4px;display:inline-block;" onclick="firmware.uploadFile('${service}', document.getElementById('divUploadFile'));">Upload File</button>`;
html += `<button id="btnUploadFile" type="button" style="width:auto;padding-left:20px;padding-right:20px;margin-right:4px;display:inline-block;" onclick="firmware.uploadFile('${service}', document.getElementById('divUploadFile'), ui.fromElement(document.getElementById('divUploadFile')));">Upload File</button>`;
html += `<button id="btnClose" type="button" style="width:auto;padding-left:20px;padding-right:20px;display:inline-block;" onclick="document.getElementById('divUploadFile').remove();">Cancel</button></div>`;
html += `</form><div>`;
div.innerHTML = html;
@ -3641,8 +3831,16 @@ class Firmware {
updateFirmware() {
let div = this.createFileUploader('/updateFirmware');
let inst = div.querySelector('div[id=divInstText]');
inst.innerHTML = '<div style="font-size:14px;margin-bottom:20px;">Select a binary file [SomfyController.ino.esp32.bin] containing the device firmware then press the Upload File button.</div>';
let html = '<div style="font-size:14px;margin-bottom:20px;">Select a binary file [SomfyController.ino.esp32.bin] containing the device firmware then press the Upload File button.</div>';
if (this.isMobile()) {
html += `<div style="width:100%;color:red;text-align:center;font-weight:bold;"><span style="margin-top:7px;width:100%;background:yellow;padding:3px;display:inline-block;border-radius:5px;background:white;">WARNING<span></div>`;
html += '<hr/><div style="font-size:14px;margin-bottom:10px;">This browser does not support automatic backups. It is highly recommended that you back up your configuration using the backup button before proceeding.</div>';
}
else
html += '<hr/><div style="font-size:14px;margin-bottom:10px;">A backup file for your configuration will be downloaded to your browser. If the firmware update process fails please restore this file using the restore button after going through the onboarding process.</div>'
inst.innerHTML = html;
document.getElementById('divContainer').appendChild(div);
if (this.isMobile()) document.getElementById('btnBackupCfg').style.display = 'inline-block';
}
updateApplication() {
let div = this.createFileUploader('/updateApplication');
@ -3658,10 +3856,12 @@ class Firmware {
document.getElementById('divContainer').appendChild(div);
if(this.isMobile()) document.getElementById('btnBackupCfg').style.display = 'inline-block';
}
async uploadFile(service, el) {
async uploadFile(service, el, data) {
let field = el.querySelector('input[type="file"]');
let filename = field.value;
console.log(filename);
let formData = new FormData();
formData.append('file', field.files[0]);
switch (service) {
case '/updateApplication':
if (typeof filename !== 'string' || filename.length === 0) {
@ -3673,28 +3873,15 @@ class Firmware {
return;
}
if (!this.isMobile()) {
// The first thing we need to do is backup the configuration. So lets do this
// in a promise.
await new Promise((resolve, reject) => {
firmware.backup();
console.log('Starting backup');
try {
// Next we need to download the current configuration data.
getText('/shades.cfg', (err, cfg) => {
if (err)
reject(err);
else {
resolve();
console.log(cfg);
await firmware.backup();
console.log('Backup Complete');
}
});
} catch (err) {
catch (err) {
ui.serviceError(el, err);
reject(err);
return;
}
}).catch((err) => {
ui.serviceError(el, err);
});
}
break;
case '/updateFirmware':
@ -3706,6 +3893,17 @@ class Firmware {
ui.errorMessage(el, 'This file is not a valid firmware binary file.');
return;
}
if (!this.isMobile()) {
console.log('Starting backup');
try {
await firmware.backup();
console.log('Backup Complete');
}
catch(err) {
ui.serviceError(el, err);
return;
}
}
break;
case '/restore':
if (typeof filename !== 'string' || filename.length === 0) {
@ -3716,9 +3914,16 @@ class Firmware {
ui.errorMessage(el, 'This file is not a valid backup file');
return;
}
if (!data.shades && !data.settings && !data.network && !data.transceiver) {
ui.errorMessage(el, 'No restore options have been selected');
return;
}
console.log(data);
formData.append('data', JSON.stringify(data));
console.log(formData.get('data'));
//return;
break;
}
let formData = new FormData();
let btnUpload = el.querySelector('button[id="btnUploadFile"]');
let btnCancel = el.querySelector('button[id="btnClose"]');
let btnBackup = el.querySelector('button[id="btnBackupCfg"]');
@ -3729,7 +3934,6 @@ class Firmware {
let prog = el.querySelector('div[id="progFileUpload"]');
prog.style.display = '';
btnSelectFile.style.visibility = 'hidden';
formData.append('file', field.files[0]);
let xhr = new XMLHttpRequest();
//xhr.open('POST', service, true);

View file

@ -608,6 +608,7 @@ div.wait-overlay > .lds-roller {
display: block;
overflow: hidden;
}
.vr-button,
.somfyGroupCtl,
.somfyShadeCtl {
height:60px;
@ -627,6 +628,11 @@ div.wait-overlay > .lds-roller {
font-size: 48px;
position:relative;
}
.vr-button {
align-items:center;
margin-top:-4px;
}
.vr-button > span,
.somfyGroupCtl .group-name,
.somfyShadeCtl .shade-name {
display:inline-block;
@ -652,6 +658,7 @@ div.wait-overlay > .lds-roller {
white-space: nowrap;
font-size: 12px;
}
.vr-button > div,
.somfyGroupCtl .groupctl-buttons,
.somfyShadeCtl .shadectl-buttons {
margin-top:3px;

View file

@ -123,24 +123,23 @@
}
.shadectl-buttons[data-shadetype="6"] > .cmd-button[data-cmd="sunflag"],
.shadectl-buttons[data-shadetype="5"] > .cmd-button[data-cmd="sunflag"],
.shadectl-buttons[data-shadetype="9"] > .cmd-button[data-cmd="sunflag"],
.shadectl-buttons[data-shadetype="9"] > .button-outline[data-cmd="my"],
.shadectl-buttons[data-shadetype="9"] > .button-outline[data-cmd="up"],
.shadectl-buttons[data-shadetype="9"] > .button-outline[data-cmd="down"],
.shadectl-buttons[data-shadetype="5"] > .button-outline[data-cmd="my"],
.shadectl-buttons[data-shadetype="5"] > .button-outline[data-cmd="up"],
.shadectl-buttons[data-shadetype="5"] > .button-outline[data-cmd="down"] {
display: none;
}
.shadectl-buttons:not([data-shadetype="5"]) > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="0"] > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="1"] > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="2"] > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="3"] > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="4"] > .button-outline[data-cmd="toggle"],
.shadectl-buttons[data-shadetype="6"] > .button-outline[data-cmd="toggle"] {
.shadectl-buttons:not([data-shadetype="5"]):not([data-shadetype="9"]) > .button-outline[data-cmd="toggle"] {
display: none;
}
#somfyShade[data-bitlength="56"] #divStepSettings,
#somfyShade[data-shadetype="5"] #divStepSettings,
#somfyShade[data-shadetype="6"] #divStepSettings {
display:none;
#somfyShade[data-shadetype="6"] #divStepSettings,
#somfyShade[data-shadetype="9"] #divStepSettings {
display: none;
}
.group-draggable,
.shade-draggable {