mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2025-12-13 11:02:12 +01:00
parent
a0c24ceb07
commit
d228a21c83
19 changed files with 4095 additions and 1781 deletions
122
ConfigFile.cpp
122
ConfigFile.cpp
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
extern Preferences pref;
|
||||
|
||||
#define SHADE_HDR_VER 10
|
||||
#define SHADE_HDR_SIZE 16
|
||||
#define SHADE_HDR_VER 11
|
||||
#define SHADE_HDR_SIZE 24
|
||||
#define SHADE_REC_SIZE 248
|
||||
#define GROUP_REC_SIZE 176
|
||||
|
||||
bool ConfigFile::begin(const char* filename, bool readOnly) {
|
||||
this->file = LittleFS.open(filename, readOnly ? "r" : "w");
|
||||
|
|
@ -39,8 +40,10 @@ bool ConfigFile::writeHeader(const config_header_t &hdr) {
|
|||
if(!this->isOpen()) return false;
|
||||
this->writeUInt8(hdr.version);
|
||||
this->writeUInt8(hdr.length);
|
||||
this->writeUInt8(hdr.recordSize);
|
||||
this->writeUInt8(hdr.records, CFG_REC_END);
|
||||
this->writeUInt8(hdr.shadeRecordSize);
|
||||
this->writeUInt8(hdr.shadeRecords);
|
||||
this->writeUInt8(hdr.groupRecordSize);
|
||||
this->writeUInt8(hdr.groupRecords, CFG_REC_END);
|
||||
return true;
|
||||
}
|
||||
bool ConfigFile::readHeader() {
|
||||
|
|
@ -49,11 +52,16 @@ bool ConfigFile::readHeader() {
|
|||
Serial.printf("Reading header at %u\n", this->file.position());
|
||||
this->header.version = this->readUInt8(this->header.version);
|
||||
this->header.length = this->readUInt8(0);
|
||||
this->header.recordSize = this->readUInt8(this->header.recordSize);
|
||||
this->header.records = this->readUInt8(this->header.records);
|
||||
Serial.printf("version:%u len:%u size:%u recs:%u pos:%d\n", this->header.version, this->header.length, this->header.recordSize, this->header.records, this->file.position());
|
||||
this->header.shadeRecordSize = this->readUInt8(this->header.shadeRecordSize);
|
||||
this->header.shadeRecords = this->readUInt8(this->header.shadeRecords);
|
||||
if(this->header.version > 10) {
|
||||
this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize);
|
||||
this->header.groupRecords = this->readUInt8(this->header.groupRecords);
|
||||
}
|
||||
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;
|
||||
}
|
||||
/*
|
||||
bool ConfigFile::seekRecordByIndex(uint16_t ndx) {
|
||||
if(!this->file) {
|
||||
return false;
|
||||
|
|
@ -61,6 +69,7 @@ bool ConfigFile::seekRecordByIndex(uint16_t ndx) {
|
|||
if(((this->header.recordSize * ndx) + this->header.length) > this->file.size()) return false;
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
bool ConfigFile::readString(char *buff, size_t len) {
|
||||
if(!this->file) return false;
|
||||
memset(buff, 0x00, len);
|
||||
|
|
@ -129,7 +138,6 @@ bool ConfigFile::writeFloat(const float val, const uint8_t prec, const char tok)
|
|||
bool ConfigFile::writeBool(const bool val, const char tok) {
|
||||
return this->writeString(val ? "true" : "false", 6, tok);
|
||||
}
|
||||
|
||||
char ConfigFile::readChar(const char defVal) {
|
||||
uint8_t ch;
|
||||
if(this->file.read(&ch, 1) == 1) return (char)ch;
|
||||
|
|
@ -173,7 +181,7 @@ bool ConfigFile::readBool(const bool defVal) {
|
|||
}
|
||||
return defVal;
|
||||
}
|
||||
|
||||
/*
|
||||
bool ShadeConfigFile::seekRecordById(uint8_t id) {
|
||||
if(this->isOpen()) return false;
|
||||
this->file.seek(this->header.length, SeekSet); // Start at the beginning of the file after the header.
|
||||
|
|
@ -191,19 +199,26 @@ bool ShadeConfigFile::seekRecordById(uint8_t id) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
bool ShadeConfigFile::begin(bool readOnly) { return this->begin("/shades.cfg", readOnly); }
|
||||
bool ShadeConfigFile::begin(const char *filename, bool readOnly) { return ConfigFile::begin(filename, readOnly); }
|
||||
void ShadeConfigFile::end() { ConfigFile::end(); }
|
||||
bool ShadeConfigFile::save(SomfyShadeController *s) {
|
||||
this->header.version = SHADE_HDR_VER;
|
||||
this->header.recordSize = SHADE_REC_SIZE;
|
||||
this->header.shadeRecordSize = SHADE_REC_SIZE;
|
||||
this->header.length = SHADE_HDR_SIZE;
|
||||
this->header.records = SOMFY_MAX_SHADES;
|
||||
this->header.shadeRecords = SOMFY_MAX_SHADES;
|
||||
this->header.groupRecordSize = GROUP_REC_SIZE;
|
||||
this->header.groupRecords = SOMFY_MAX_GROUPS;
|
||||
this->writeHeader();
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &s->shades[i];
|
||||
this->writeShadeRecord(shade);
|
||||
}
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
SomfyGroup *group = &s->groups[i];
|
||||
this->writeGroupRecord(group);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ShadeConfigFile::validate() {
|
||||
|
|
@ -213,39 +228,70 @@ bool ShadeConfigFile::validate() {
|
|||
Serial.println(this->header.version);
|
||||
return false;
|
||||
}
|
||||
if(this->header.recordSize < 100) {
|
||||
Serial.print("Invalid Record Size:");
|
||||
Serial.println(this->header.recordSize);
|
||||
if(this->header.shadeRecordSize < 100) {
|
||||
Serial.print("Invalid Shade Record Size:");
|
||||
Serial.println(this->header.shadeRecordSize);
|
||||
return false;
|
||||
}
|
||||
if(this->header.records != 32) {
|
||||
Serial.print("Invalid Record Count:");
|
||||
Serial.println(this->header.records);
|
||||
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());
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should know the file size based upon the record information in the header
|
||||
if(this->file.size() != this->header.length + (this->header.recordSize * this->header.records)) {
|
||||
Serial.printf("File size is not correct should be %d and got %d\n", this->header.length + (this->header.recordSize * this->header.records), this->file.size());
|
||||
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->file.size() != fsize) {
|
||||
Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size());
|
||||
}
|
||||
// Next check to see if the records match the header length.
|
||||
uint8_t recs = 0;
|
||||
uint32_t startPos = this->file.position();
|
||||
while(recs < this->header.records) {
|
||||
while(recs < this->header.shadeRecords) {
|
||||
uint32_t pos = this->file.position();
|
||||
if(!this->seekChar(CFG_REC_END)) {
|
||||
Serial.printf("Failed to find the record end %d\n", recs);
|
||||
Serial.printf("Failed to find the shade record end %d\n", recs);
|
||||
return false;
|
||||
}
|
||||
if(this->file.position() - pos != this->header.recordSize) {
|
||||
Serial.printf("Record length is %d and should be %d\n", this->file.position() - pos, this->header.recordSize);
|
||||
if(this->file.position() - pos != this->header.shadeRecordSize) {
|
||||
Serial.printf("Shade record length is %d and should be %d\n", this->file.position() - pos, this->header.shadeRecordSize);
|
||||
return false;
|
||||
}
|
||||
recs++;
|
||||
}
|
||||
if(this->header.version > 10) {
|
||||
recs = 0;
|
||||
while(recs < this->header.groupRecords) {
|
||||
uint32_t pos = this->file.position();
|
||||
if(!this->seekChar(CFG_REC_END)) {
|
||||
Serial.printf("Failed to find the group record end %d\n", recs);
|
||||
return false;
|
||||
}
|
||||
recs++;
|
||||
if(this->file.position() - pos != this->header.groupRecordSize) {
|
||||
Serial.printf("Group record length is %d and should be %d\n", this->file.position() - pos, this->header.groupRecordSize);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->file.seek(startPos, SeekSet);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -275,7 +321,7 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
|
|||
}
|
||||
// We should be valid so start reading.
|
||||
pref.begin("ShadeCodes");
|
||||
for(uint8_t i = 0; i < this->header.records; i++) {
|
||||
for(uint8_t i = 0; i < this->header.shadeRecords; i++) {
|
||||
SomfyShade *shade = &s->shades[i];
|
||||
shade->setShadeId(this->readUInt8(255));
|
||||
shade->paired = this->readBool(false);
|
||||
|
|
@ -337,6 +383,21 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
|
|||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
pref.end();
|
||||
if(opened) {
|
||||
Serial.println("Closing shade config file");
|
||||
|
|
@ -344,6 +405,19 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
bool ShadeConfigFile::writeGroupRecord(SomfyGroup *group) {
|
||||
this->writeUInt8(group->getGroupId());
|
||||
this->writeUInt8(static_cast<uint8_t>(group->groupType));
|
||||
this->writeUInt32(group->getRemoteAddress());
|
||||
this->writeString(group->name, sizeof(group->name));
|
||||
this->writeUInt8(static_cast<uint8_t>(group->proto));
|
||||
this->writeUInt8(group->bitLength);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
||||
if(j == SOMFY_MAX_GROUPED_SHADES - 1) this->writeUInt8(group->linkedShades[j], CFG_REC_END);
|
||||
else this->writeUInt8(group->linkedShades[j]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
|
||||
if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) {
|
||||
shade->myTiltPos = -1;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@
|
|||
|
||||
struct config_header_t {
|
||||
uint8_t version = 1;
|
||||
uint16_t recordSize = 0;
|
||||
uint16_t records = 0;
|
||||
uint8_t shadeRecordSize = 0;
|
||||
uint8_t shadeRecords = 0;
|
||||
uint8_t groupRecordSize = 0;
|
||||
uint8_t groupRecords = 0;
|
||||
uint8_t length = 0;
|
||||
};
|
||||
class ConfigFile {
|
||||
|
|
@ -51,6 +53,7 @@ class ConfigFile {
|
|||
class ShadeConfigFile : public ConfigFile {
|
||||
protected:
|
||||
bool writeShadeRecord(SomfyShade *shade);
|
||||
bool writeGroupRecord(SomfyGroup *group);
|
||||
public:
|
||||
static bool getAppVersion(appver_t &ver);
|
||||
static bool exists();
|
||||
|
|
@ -60,7 +63,7 @@ class ShadeConfigFile : public ConfigFile {
|
|||
bool save(SomfyShadeController *sofmy);
|
||||
bool loadFile(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
|
||||
void end();
|
||||
bool seekRecordById(uint8_t id);
|
||||
//bool seekRecordById(uint8_t id);
|
||||
bool validate();
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ bool ConfigSettings::begin() {
|
|||
(uint16_t)((chipId >> 8) & 0xff),
|
||||
(uint16_t)chipId & 0xff);
|
||||
this->load();
|
||||
this->Security.begin();
|
||||
this->IP.begin();
|
||||
this->WIFI.begin();
|
||||
this->Ethernet.begin();
|
||||
this->NTP.begin();
|
||||
|
|
@ -106,6 +108,7 @@ bool ConfigSettings::toJSON(JsonObject &obj) {
|
|||
obj["connType"] = static_cast<uint8_t>(this->connType);
|
||||
return true;
|
||||
}
|
||||
bool ConfigSettings::requiresAuth() { return this->Security.type != security_types::None; }
|
||||
bool ConfigSettings::fromJSON(JsonObject &obj) {
|
||||
if(obj.containsKey("ssdpBroadcast")) this->ssdpBroadcast = obj["ssdpBroadcast"];
|
||||
if(obj.containsKey("hostname")) this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname));
|
||||
|
|
@ -113,6 +116,7 @@ bool ConfigSettings::fromJSON(JsonObject &obj) {
|
|||
return true;
|
||||
}
|
||||
void ConfigSettings::print() {
|
||||
this->Security.print();
|
||||
Serial.printf("Connection Type: %u\n", (unsigned int) this->connType);
|
||||
this->NTP.print();
|
||||
if(this->connType == conn_types::wifi || this->connType == conn_types::unset) this->WIFI.print();
|
||||
|
|
@ -226,8 +230,125 @@ bool NTPSettings::apply() {
|
|||
setenv("TZ", this->posixZone, 1);
|
||||
return true;
|
||||
}
|
||||
WifiSettings::WifiSettings() {}
|
||||
IPSettings::IPSettings() {}
|
||||
bool IPSettings::begin() {
|
||||
this->load();
|
||||
return true;
|
||||
}
|
||||
bool IPSettings::fromJSON(JsonObject &obj) {
|
||||
if(obj.containsKey("dhcp")) this->dhcp = obj["dhcp"];
|
||||
this->parseIPAddress(obj, "ip", &this->ip);
|
||||
this->parseIPAddress(obj, "gateway", &this->gateway);
|
||||
this->parseIPAddress(obj, "subnet", &this->subnet);
|
||||
this->parseIPAddress(obj, "dns1", &this->dns1);
|
||||
this->parseIPAddress(obj, "dns2", &this->dns2);
|
||||
return true;
|
||||
}
|
||||
bool IPSettings::toJSON(JsonObject &obj) {
|
||||
IPAddress ipEmpty(0,0,0,0);
|
||||
obj["dhcp"] = this->dhcp;
|
||||
obj["ip"] = this->ip == ipEmpty ? "" : this->ip.toString();
|
||||
obj["gateway"] = this->gateway == ipEmpty ? "" : this->gateway.toString();
|
||||
obj["subnet"] = this->subnet == ipEmpty ? "" : this->subnet.toString();
|
||||
obj["dns1"] = this->dns1 == ipEmpty ? "" : this->dns1.toString();
|
||||
obj["dns2"] = this->dns2 == ipEmpty ? "" : this->dns2.toString();
|
||||
return true;
|
||||
}
|
||||
bool IPSettings::save() {
|
||||
pref.begin("IP");
|
||||
pref.clear();
|
||||
pref.putBool("dhcp", this->dhcp);
|
||||
pref.putString("ip", this->ip.toString());
|
||||
pref.putString("gateway", this->gateway.toString());
|
||||
pref.putString("subnet", this->subnet.toString());
|
||||
pref.putString("dns1", this->dns1.toString());
|
||||
pref.putString("dns2", this->dns2.toString());
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
bool IPSettings::load() {
|
||||
pref.begin("IP");
|
||||
this->dhcp = pref.getBool("dhcp", true);
|
||||
char buff[16];
|
||||
if(pref.isKey("ip")) {
|
||||
pref.getString("ip", buff, sizeof(buff));
|
||||
this->ip.fromString(buff);
|
||||
}
|
||||
if(pref.isKey("gateway")) {
|
||||
pref.getString("gateway", buff, sizeof(buff));
|
||||
this->gateway.fromString(buff);
|
||||
}
|
||||
if(pref.isKey("subnet")) {
|
||||
pref.getString("subnet", buff, sizeof(buff));
|
||||
this->subnet.fromString(buff);
|
||||
}
|
||||
if(pref.isKey("dns1")) {
|
||||
pref.getString("dns1", buff, sizeof(buff));
|
||||
this->dns1.fromString(buff);
|
||||
}
|
||||
if(pref.isKey("dns2")) {
|
||||
pref.getString("dns2", buff, sizeof(buff));
|
||||
this->dns2.fromString(buff);
|
||||
}
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
bool SecuritySettings::begin() {
|
||||
this->load();
|
||||
return true;
|
||||
}
|
||||
bool SecuritySettings::fromJSON(JsonObject &obj) {
|
||||
if(obj.containsKey("type")) this->type = static_cast<security_types>(obj["type"].as<uint8_t>());
|
||||
this->parseValueString(obj, "username", this->username, sizeof(this->username));
|
||||
this->parseValueString(obj, "password", this->password, sizeof(this->password));
|
||||
this->parseValueString(obj, "pin", this->pin, sizeof(this->pin));
|
||||
if(obj.containsKey("permissions")) this->permissions = obj["permissions"];
|
||||
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;
|
||||
obj["pin"] = this->pin;
|
||||
obj["permissions"] = this->permissions;
|
||||
return true;
|
||||
}
|
||||
bool SecuritySettings::save() {
|
||||
pref.begin("SEC");
|
||||
pref.clear();
|
||||
pref.putChar("type", static_cast<uint8_t>(this->type));
|
||||
pref.putString("username", this->username);
|
||||
pref.putString("password", this->password);
|
||||
pref.putString("pin", this->pin);
|
||||
pref.putChar("permissions", this->permissions);
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
bool SecuritySettings::load() {
|
||||
pref.begin("SEC");
|
||||
this->type = static_cast<security_types>(pref.getChar("type", 0));
|
||||
if(pref.isKey("username")) pref.getString("username", this->username, sizeof(this->username));
|
||||
if(pref.isKey("password")) pref.getString("password", this->password, sizeof(this->password));
|
||||
if(pref.isKey("pin")) pref.getString("pin", this->pin, sizeof(this->pin));
|
||||
if(pref.isKey("permissions")) this->permissions = pref.getChar("permissions", this->permissions);
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
void SecuritySettings::print() {
|
||||
Serial.print("SECURITY Type:");
|
||||
Serial.print(static_cast<uint8_t>(this->type));
|
||||
Serial.print(" Username:[");
|
||||
Serial.print(this->username);
|
||||
Serial.print("] Password:[");
|
||||
Serial.print(this->password);
|
||||
Serial.print("] Pin:[");
|
||||
Serial.print(this->pin);
|
||||
Serial.print("] Permissions:");
|
||||
Serial.println(this->permissions);
|
||||
}
|
||||
|
||||
WifiSettings::WifiSettings() {}
|
||||
bool WifiSettings::begin() {
|
||||
this->load();
|
||||
return true;
|
||||
|
|
@ -318,7 +439,6 @@ bool EthernetSettings::begin() {
|
|||
return true;
|
||||
}
|
||||
bool EthernetSettings::fromJSON(JsonObject &obj) {
|
||||
if(obj.containsKey("dhcp")) this->dhcp = obj["dhcp"];
|
||||
if(obj.containsKey("boardType")) this->boardType = obj["boardType"];
|
||||
if(obj.containsKey("phyAddress")) this->phyAddress = obj["phyAddress"];
|
||||
if(obj.containsKey("CLKMode")) this->CLKMode = static_cast<eth_clock_mode_t>(obj["CLKMode"]);
|
||||
|
|
@ -326,33 +446,21 @@ bool EthernetSettings::fromJSON(JsonObject &obj) {
|
|||
if(obj.containsKey("PWRPin")) this->PWRPin = obj["PWRPin"];
|
||||
if(obj.containsKey("MDCPin")) this->MDCPin = obj["MDCPin"];
|
||||
if(obj.containsKey("MDIOPin")) this->MDIOPin = obj["MDIOPin"];
|
||||
this->parseIPAddress(obj, "ip", &this->ip);
|
||||
this->parseIPAddress(obj, "gateway", &this->gateway);
|
||||
this->parseIPAddress(obj, "subnet", &this->subnet);
|
||||
this->parseIPAddress(obj, "dns1", &this->dns1);
|
||||
this->parseIPAddress(obj, "dns2", &this->dns2);
|
||||
return true;
|
||||
}
|
||||
bool EthernetSettings::toJSON(JsonObject &obj) {
|
||||
obj["boardType"] = this->boardType;
|
||||
obj["phyAddress"] = this->phyAddress;
|
||||
obj["dhcp"] = this->dhcp;
|
||||
obj["CLKMode"] = static_cast<uint8_t>(this->CLKMode);
|
||||
obj["phyType"] = static_cast<uint8_t>(this->phyType);
|
||||
obj["PWRPin"] = this->PWRPin;
|
||||
obj["MDCPin"] = this->MDCPin;
|
||||
obj["MDIOPin"] = this->MDIOPin;
|
||||
obj["ip"] = this->ip.toString();
|
||||
obj["gateway"] = this->gateway.toString();
|
||||
obj["subnet"] = this->subnet.toString();
|
||||
obj["dns1"] = this->dns1.toString();
|
||||
obj["dns2"] = this->dns2.toString();
|
||||
return true;
|
||||
}
|
||||
bool EthernetSettings::save() {
|
||||
pref.begin("ETH");
|
||||
pref.clear();
|
||||
pref.putBool("dhcp", this->dhcp);
|
||||
pref.putChar("boardType", this->boardType);
|
||||
pref.putChar("phyAddress", this->phyAddress);
|
||||
pref.putChar("phyType", static_cast<uint8_t>(this->phyType));
|
||||
|
|
@ -360,17 +468,11 @@ bool EthernetSettings::save() {
|
|||
pref.putChar("PWRPin", this->PWRPin);
|
||||
pref.putChar("MDCPin", this->MDCPin);
|
||||
pref.putChar("MDIOPin", this->MDIOPin);
|
||||
pref.putString("ip", this->ip.toString());
|
||||
pref.putString("gateway", this->gateway.toString());
|
||||
pref.putString("subnet", this->subnet.toString());
|
||||
pref.putString("dns1", this->dns1.toString());
|
||||
pref.putString("dns2", this->dns2.toString());
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
bool EthernetSettings::load() {
|
||||
pref.begin("ETH");
|
||||
this->dhcp = pref.getBool("dhcp", true);
|
||||
this->boardType = pref.getChar("boardType", this->boardType);
|
||||
this->phyType = static_cast<eth_phy_type_t>(pref.getChar("phyType", ETH_PHY_LAN8720));
|
||||
this->CLKMode = static_cast<eth_clock_mode_t>(pref.getChar("CLKMode", ETH_CLOCK_GPIO0_IN));
|
||||
|
|
@ -378,30 +480,10 @@ bool EthernetSettings::load() {
|
|||
this->PWRPin = pref.getChar("PWRPin", this->PWRPin);
|
||||
this->MDCPin = pref.getChar("MDCPin", this->MDCPin);
|
||||
this->MDIOPin = pref.getChar("MDIOPin", this->MDIOPin);
|
||||
|
||||
char buff[16];
|
||||
pref.getString("ip", buff, sizeof(buff));
|
||||
this->ip.fromString(buff);
|
||||
pref.getString("gateway", buff, sizeof(buff));
|
||||
this->gateway.fromString(buff);
|
||||
pref.getString("subnet", buff, sizeof(buff));
|
||||
this->subnet.fromString(buff);
|
||||
pref.getString("dns1", buff, sizeof(buff));
|
||||
this->dns1.fromString(buff);
|
||||
pref.getString("dns2", buff, sizeof(buff));
|
||||
this->dns2.fromString(buff);
|
||||
pref.end();
|
||||
return true;
|
||||
}
|
||||
void EthernetSettings::print() {
|
||||
Serial.println("Ethernet Settings");
|
||||
Serial.printf("Board:%d PHYType:%d CLK:%d ADDR:%d PWR:%d MDC:%d MDIO:%d\n", this->boardType, this->phyType, this->CLKMode, this->phyAddress, this->PWRPin, this->MDCPin, this->MDIOPin);
|
||||
Serial.print(" GATEWAY: ");
|
||||
Serial.println(this->gateway);
|
||||
Serial.print(" SUBNET: ");
|
||||
Serial.println(this->subnet);
|
||||
Serial.print(" DNS1: ");
|
||||
Serial.println(this->dns1);
|
||||
Serial.print(" DNS2: ");
|
||||
Serial.println(this->dns2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#ifndef configsettings_h
|
||||
#define configsettings_h
|
||||
|
||||
#define FW_VERSION "v1.7.3"
|
||||
#define FW_VERSION "v2.0.0"
|
||||
enum DeviceStatus {
|
||||
DS_OK = 0,
|
||||
DS_ERROR = 1,
|
||||
|
|
@ -55,12 +55,6 @@ class EthernetSettings: BaseSettings {
|
|||
public:
|
||||
EthernetSettings();
|
||||
uint8_t boardType = 0; // These board types are enumerated in the ui and used to set the chip settings.
|
||||
bool dhcp = true;
|
||||
IPAddress ip;
|
||||
IPAddress subnet = IPAddress(255,255,255,0);
|
||||
IPAddress gateway;
|
||||
IPAddress dns1;
|
||||
IPAddress dns2;
|
||||
eth_phy_type_t phyType = ETH_PHY_LAN8720;
|
||||
eth_clock_mode_t CLKMode = ETH_CLOCK_GPIO0_IN;
|
||||
int8_t phyAddress = ETH_PHY_ADDR;
|
||||
|
|
@ -76,6 +70,44 @@ class EthernetSettings: BaseSettings {
|
|||
void print();
|
||||
|
||||
};
|
||||
class IPSettings: BaseSettings {
|
||||
public:
|
||||
IPSettings();
|
||||
bool dhcp = true;
|
||||
IPAddress ip;
|
||||
IPAddress subnet = IPAddress(255,255,255,0);
|
||||
IPAddress gateway = IPAddress(0,0,0,0);
|
||||
IPAddress dns1 = IPAddress(0,0,0,0);
|
||||
IPAddress dns2 = IPAddress(0,0,0,0);
|
||||
bool begin();
|
||||
bool fromJSON(JsonObject &obj);
|
||||
bool toJSON(JsonObject &obj);
|
||||
bool load();
|
||||
bool save();
|
||||
void print();
|
||||
};
|
||||
enum class security_types : byte {
|
||||
None = 0x00,
|
||||
PinEntry = 0x01,
|
||||
Password = 0x02
|
||||
};
|
||||
enum class security_permissions : byte {
|
||||
ConfigOnly = 0x01
|
||||
};
|
||||
class SecuritySettings: BaseSettings {
|
||||
public:
|
||||
security_types type = security_types::None;
|
||||
char username[33] = "";
|
||||
char password[33] = "";
|
||||
char pin[5] = "";
|
||||
uint8_t permissions = 0;
|
||||
bool begin();
|
||||
bool save();
|
||||
bool load();
|
||||
void print();
|
||||
bool toJSON(JsonObject &obj);
|
||||
bool fromJSON(JsonObject &obj);
|
||||
};
|
||||
class MQTTSettings: BaseSettings {
|
||||
public:
|
||||
bool enabled = false;
|
||||
|
|
@ -97,7 +129,6 @@ enum class conn_types : byte {
|
|||
ethernet = 0x02,
|
||||
ethernetpref = 0x03
|
||||
};
|
||||
|
||||
class ConfigSettings: BaseSettings {
|
||||
public:
|
||||
char serverId[10] = "";
|
||||
|
|
@ -106,10 +137,13 @@ class ConfigSettings: BaseSettings {
|
|||
const char* fwVersion = FW_VERSION;
|
||||
bool ssdpBroadcast = true;
|
||||
uint8_t status;
|
||||
IPSettings IP;
|
||||
WifiSettings WIFI;
|
||||
EthernetSettings Ethernet;
|
||||
NTPSettings NTP;
|
||||
MQTTSettings MQTT;
|
||||
SecuritySettings Security;
|
||||
bool requiresAuth();
|
||||
bool fromJSON(JsonObject &obj);
|
||||
bool toJSON(JsonObject &obj);
|
||||
bool begin();
|
||||
|
|
|
|||
161
MQTT.cpp
161
MQTT.cpp
|
|
@ -32,80 +32,112 @@ bool MQTTClass::loop() {
|
|||
return true;
|
||||
}
|
||||
void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) {
|
||||
//Serial.print("MQTT Topic:");
|
||||
//Serial.print(topic);
|
||||
//Serial.print(" payload:");
|
||||
//for(uint32_t i=0; i<length; i++)
|
||||
// Serial.print((char)payload[i]);
|
||||
//Serial.println();
|
||||
Serial.print("MQTT Topic:");
|
||||
Serial.print(topic);
|
||||
Serial.print(" payload:");
|
||||
for(uint32_t i=0; i<length; i++)
|
||||
Serial.print((char)payload[i]);
|
||||
Serial.println();
|
||||
|
||||
// We need to start at the last slash in the data
|
||||
int16_t ndx = strlen(topic) - 1;
|
||||
// ------------------+
|
||||
// shades/1/target/set
|
||||
while(ndx >= 0 && topic[ndx] != '/') ndx--; // Back off the set command
|
||||
uint16_t end_command = --ndx;
|
||||
// --------------+----
|
||||
// shades/1/target/set
|
||||
while(ndx >= 0 && topic[ndx] != '/') ndx--; // Get the start of the leaf.
|
||||
// --------+----------
|
||||
// shades/1/target/set
|
||||
uint16_t start_command = ndx + 1;
|
||||
uint16_t id_end = --ndx;
|
||||
while(ndx >= 0 && topic[ndx] != '/') ndx--;
|
||||
// ------+------------
|
||||
// shades/1/target/set
|
||||
uint16_t id_start = ndx + 1;
|
||||
char shadeId[4];
|
||||
uint8_t len = strlen(topic);
|
||||
|
||||
uint8_t slashes = 0;
|
||||
uint16_t ndx = strlen(topic) - 1;
|
||||
while(ndx > 0) {
|
||||
if(topic[ndx] == '/') slashes++;
|
||||
if(slashes == 4) break;
|
||||
ndx--;
|
||||
}
|
||||
char entityId[4];
|
||||
char command[32];
|
||||
memset(command, 0x00, sizeof(command));
|
||||
memset(shadeId, 0x00, sizeof(shadeId));
|
||||
for(uint16_t i = 0;id_start <= id_end; i++)
|
||||
shadeId[i] = topic[id_start++];
|
||||
for(uint16_t i = 0;start_command <= end_command; i++)
|
||||
command[i] = topic[start_command++];
|
||||
|
||||
char entityType[7];
|
||||
char value[10];
|
||||
memset(command, 0x00, sizeof(command));
|
||||
memset(entityId, 0x00, sizeof(entityId));
|
||||
memset(entityType, 0x00, sizeof(entityType));
|
||||
memset(value, 0x00, sizeof(value));
|
||||
for(uint8_t i = 0; i < length; i++)
|
||||
value[i] = payload[i];
|
||||
uint8_t i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(entityType))
|
||||
entityType[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(entityId))
|
||||
entityId[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(command))
|
||||
command[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
for(uint8_t j = 0; j < length && j < sizeof(value); j++)
|
||||
value[j] = payload[j];
|
||||
|
||||
Serial.print("MQTT Command:[");
|
||||
Serial.print("MQTT type:[");
|
||||
Serial.print(entityType);
|
||||
Serial.print("] command:[");
|
||||
Serial.print(command);
|
||||
Serial.print("] shadeId:");
|
||||
Serial.print(shadeId);
|
||||
Serial.print("] entityId:");
|
||||
Serial.print(entityId);
|
||||
Serial.print(" value:");
|
||||
Serial.println(value);
|
||||
SomfyShade* shade = somfy.getShadeById(atoi(shadeId));
|
||||
if (shade) {
|
||||
int val = atoi(value);
|
||||
if(strncmp(command, "target", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->moveToTarget(atoi(value));
|
||||
if(strncmp(entityType, "shades", sizeof(entityType)) == 0) {
|
||||
SomfyShade* shade = somfy.getShadeById(atoi(entityId));
|
||||
if (shade) {
|
||||
int val = atoi(value);
|
||||
if(strncmp(command, "target", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->moveToTarget(atoi(value));
|
||||
}
|
||||
if(strncmp(command, "tiltTarget", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->moveToTiltTarget(atoi(value));
|
||||
}
|
||||
else if(strncmp(command, "direction", sizeof(command)) == 0) {
|
||||
if(val < 0)
|
||||
shade->sendCommand(somfy_commands::Up);
|
||||
else if(val > 0)
|
||||
shade->sendCommand(somfy_commands::Down);
|
||||
else
|
||||
shade->sendCommand(somfy_commands::My);
|
||||
}
|
||||
else if(strncmp(command, "mypos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(val);
|
||||
}
|
||||
else if(strncmp(command, "myTiltPos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(shade->myPos, val);
|
||||
}
|
||||
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
|
||||
if(val >= 0) shade->sendCommand(somfy_commands::SunFlag);
|
||||
else shade->sendCommand(somfy_commands::Flag);
|
||||
}
|
||||
}
|
||||
if(strncmp(command, "tiltTarget", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->moveToTiltTarget(atoi(value));
|
||||
}
|
||||
else if(strncmp(command, "direction", sizeof(command)) == 0) {
|
||||
if(val < 0)
|
||||
shade->sendCommand(somfy_commands::Up);
|
||||
else if(val > 0)
|
||||
shade->sendCommand(somfy_commands::Down);
|
||||
else
|
||||
shade->sendCommand(somfy_commands::My);
|
||||
}
|
||||
else if(strncmp(command, "mypos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(val);
|
||||
}
|
||||
else if(strncmp(command, "myTiltPos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(shade->myPos, val);
|
||||
}
|
||||
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
|
||||
if(val >= 0) shade->sendCommand(somfy_commands::SunFlag);
|
||||
else shade->sendCommand(somfy_commands::Flag);
|
||||
}
|
||||
else if(strncmp(entityType, "groups", sizeof(entityType)) == 0) {
|
||||
SomfyGroup* group = somfy.getGroupById(atoi(entityId));
|
||||
if (group) {
|
||||
int val = atoi(value);
|
||||
if(strncmp(command, "direction", sizeof(command)) == 0) {
|
||||
if(val < 0)
|
||||
group->sendCommand(somfy_commands::Up);
|
||||
else if(val > 0)
|
||||
group->sendCommand(somfy_commands::Down);
|
||||
else
|
||||
group->sendCommand(somfy_commands::My);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,6 +164,7 @@ bool MQTTClass::connect() {
|
|||
this->subscribe("shades/+/mypos/set");
|
||||
this->subscribe("shades/+/myTiltPos/set");
|
||||
this->subscribe("shades/+/sunFlag/set");
|
||||
this->subscribe("groups/+/direction/set");
|
||||
mqttClient.setCallback(MQTTClass::receive);
|
||||
this->lastConnect = millis();
|
||||
return true;
|
||||
|
|
|
|||
26
Network.cpp
26
Network.cpp
|
|
@ -113,6 +113,13 @@ void Network::setConnected(conn_types connType) {
|
|||
Serial.print(" (");
|
||||
Serial.print(this->strength);
|
||||
Serial.println("dbm)");
|
||||
if(settings.IP.dhcp) {
|
||||
settings.IP.ip = WiFi.localIP();
|
||||
settings.IP.subnet = WiFi.subnetMask();
|
||||
settings.IP.gateway = WiFi.gatewayIP();
|
||||
settings.IP.dns1 = WiFi.dnsIP(0);
|
||||
settings.IP.dns2 = WiFi.dnsIP(1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Serial.print("Successfully Connected to Ethernet!!! ");
|
||||
|
|
@ -123,6 +130,13 @@ void Network::setConnected(conn_types connType) {
|
|||
Serial.print(" ");
|
||||
Serial.print(ETH.linkSpeed());
|
||||
Serial.println("Mbps");
|
||||
if(settings.IP.dhcp) {
|
||||
settings.IP.ip = ETH.localIP();
|
||||
settings.IP.subnet = ETH.subnetMask();
|
||||
settings.IP.gateway = ETH.gatewayIP();
|
||||
settings.IP.dns1 = ETH.dnsIP(0);
|
||||
settings.IP.dns2 = ETH.dnsIP(1);
|
||||
}
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "{\"connected\":true,\"speed\":%d,\"fullduplex\":%s}", ETH.linkSpeed(), ETH.fullDuplex() ? "true" : "false");
|
||||
sockEmit.sendToClients("ethernet", buf);
|
||||
|
|
@ -202,7 +216,11 @@ bool Network::connectWired() {
|
|||
if(!this->ethStarted) {
|
||||
this->ethStarted = true;
|
||||
WiFi.mode(WIFI_OFF);
|
||||
|
||||
if(!settings.IP.dhcp)
|
||||
if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2))
|
||||
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
else
|
||||
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
WiFi.onEvent(this->networkEvent);
|
||||
if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) {
|
||||
Serial.println("Ethernet Begin failed");
|
||||
|
|
@ -253,7 +271,11 @@ bool Network::connectWiFi() {
|
|||
this->connectStart = millis();
|
||||
Serial.print("Set hostname to:");
|
||||
Serial.println(WiFi.getHostname());
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
if(!settings.IP.dhcp)
|
||||
if(!WiFi.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2))
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
else
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
WiFi.setSleep(false);
|
||||
WiFi.mode(WIFI_STA);
|
||||
delay(100);
|
||||
|
|
|
|||
334
Somfy.cpp
334
Somfy.cpp
|
|
@ -440,6 +440,14 @@ SomfyShade *SomfyShadeController::findShadeByRemoteAddress(uint32_t address) {
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
SomfyGroup *SomfyShadeController::findGroupByRemoteAddress(uint32_t address) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
SomfyGroup &group = this->groups[i];
|
||||
if(group.getRemoteAddress() == address) return &group;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SomfyShadeController::loadLegacy() {
|
||||
Serial.println("Loading Legacy shades using NVS");
|
||||
pref.begin("Shades", true);
|
||||
|
|
@ -554,6 +562,12 @@ SomfyShade * SomfyShadeController::getShadeById(uint8_t shadeId) {
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
SomfyGroup * SomfyShadeController::getGroupById(uint8_t groupId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(this->groups[i].getGroupId() == groupId) return &this->groups[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void SomfyShade::clear() {
|
||||
this->setShadeId(255);
|
||||
this->setRemoteAddress(0);
|
||||
|
|
@ -599,6 +613,10 @@ void SomfyShade::clear() {
|
|||
this->tiltTime = 7000;
|
||||
this->stepSize = 100;
|
||||
}
|
||||
void SomfyGroup::clear() {
|
||||
this->setGroupId(255);
|
||||
this->setRemoteAddress(0);
|
||||
}
|
||||
bool SomfyShade::linkRemote(uint32_t address, uint16_t rollingCode) {
|
||||
// Check to see if the remote is already linked. If it is
|
||||
// just return true after setting the rolling code
|
||||
|
|
@ -632,6 +650,22 @@ bool SomfyShade::linkRemote(uint32_t address, uint16_t rollingCode) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
bool SomfyGroup::linkShade(uint8_t shadeId) {
|
||||
// Check to see if the shade is already linked. If it is just return true
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == shadeId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == 0) {
|
||||
this->linkedShades[i] = shadeId;
|
||||
somfy.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void SomfyShade::commit() { somfy.commit(); }
|
||||
void SomfyShade::commitShadePosition() {
|
||||
somfy.isDirty = true;
|
||||
|
|
@ -694,7 +728,30 @@ bool SomfyShade::unlinkRemote(uint32_t address) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
bool SomfyGroup::unlinkShade(uint8_t shadeId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == shadeId) {
|
||||
this->linkedShades[i] = 0;
|
||||
somfy.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool SomfyGroup::hasShadeId(uint8_t shadeId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == shadeId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool SomfyShade::isAtTarget() { return this->currentPos == this->target && this->currentTiltPos == this->tiltTarget; }
|
||||
bool SomfyShade::isInGroup() {
|
||||
if(this->getShadeId() == 255) return false;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(somfy.groups[i].getGroupId() != 255 && somfy.groups[i].hasShadeId(this->getShadeId())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void SomfyShade::checkMovement() {
|
||||
const uint64_t curTime = millis();
|
||||
const bool sunFlag = this->flags & static_cast<uint8_t>(somfy_flags_t::SunFlag);
|
||||
|
|
@ -1096,7 +1153,6 @@ void SomfyShade::publish() {
|
|||
}
|
||||
}
|
||||
void SomfyShade::emitState(const char *evt) { this->emitState(255, evt); }
|
||||
int8_t SomfyShade::transformPosition(float fpos) { return static_cast<int8_t>(this->flipPosition && fpos >= 0.00f ? floor(100.0f - fpos) : floor(fpos)); }
|
||||
void SomfyShade::emitState(uint8_t num, const char *evt) {
|
||||
char buf[420];
|
||||
if(this->tiltType != tilt_types::none)
|
||||
|
|
@ -1114,12 +1170,16 @@ 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/position", this->shadeId);
|
||||
mqtt.publish(topic, this->transformPosition(this->currentPos));
|
||||
snprintf(topic, sizeof(topic), "shades/%u/direction", this->shadeId);
|
||||
mqtt.publish(topic, this->direction);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/target", this->shadeId);
|
||||
mqtt.publish(topic, this->transformPosition(this->target));
|
||||
snprintf(topic, sizeof(topic), "shades/%u/remoteAddress", this->shadeId);
|
||||
mqtt.publish(topic, this->getRemoteAddress());
|
||||
snprintf(topic, sizeof(topic), "shades/%u/lastRollingCode", this->shadeId);
|
||||
mqtt.publish(topic, this->lastRollingCode);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/mypos", this->shadeId);
|
||||
|
|
@ -1148,6 +1208,40 @@ void SomfyShade::emitState(uint8_t num, const char *evt) {
|
|||
}
|
||||
}
|
||||
}
|
||||
void SomfyGroup::emitState(const char *evt) { this->emitState(255, evt); }
|
||||
void SomfyGroup::emitState(uint8_t num, const char *evt) {
|
||||
ClientSocketEvent e(evt);
|
||||
char buf[30];
|
||||
snprintf(buf, sizeof(buf), "{\"groupId\":%d,", this->groupId);
|
||||
e.appendMessage(buf);
|
||||
snprintf(buf, sizeof(buf), "\"remoteAddress\":%d,", this->getRemoteAddress());
|
||||
e.appendMessage(buf);
|
||||
snprintf(buf, sizeof(buf), "\"name\":\"%s\",", this->name);
|
||||
e.appendMessage(buf);
|
||||
snprintf(buf, sizeof(buf), "\"shades\":[");
|
||||
e.appendMessage(buf);
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] != 255) {
|
||||
snprintf(buf, sizeof(buf), "%s%d", i != 0 ? "," : "", this->linkedShades[i]);
|
||||
e.appendMessage(buf);
|
||||
}
|
||||
}
|
||||
e.appendMessage("]}");
|
||||
if(num >= 255) sockEmit.sendToClients(&e);
|
||||
else sockEmit.sendToClient(num, &e);
|
||||
if(mqtt.connected()) {
|
||||
char topic[32];
|
||||
snprintf(topic, sizeof(topic), "groups/%u/type", this->groupId);
|
||||
mqtt.publish(topic, static_cast<uint8_t>(this->groupType));
|
||||
snprintf(topic, sizeof(topic), "groups/%u/remoteAddress", this->groupId);
|
||||
mqtt.publish(topic, this->getRemoteAddress());
|
||||
snprintf(topic, sizeof(topic), "groups/%u/lastRollingCode", this->groupId);
|
||||
mqtt.publish(topic, this->lastRollingCode);
|
||||
snprintf(topic, sizeof(topic), "groups/%u/direction", this->groupId);
|
||||
mqtt.publish(topic, this->direction);
|
||||
}
|
||||
}
|
||||
int8_t SomfyShade::transformPosition(float fpos) { return static_cast<int8_t>(this->flipPosition && fpos >= 0.00f ? floor(100.0f - fpos) : floor(fpos)); }
|
||||
bool SomfyShade::isIdle() { return this->direction == 0 && this->tiltDirection == 0; }
|
||||
void SomfyShade::processWaitingFrame() {
|
||||
if(this->shadeId == 255) {
|
||||
|
|
@ -1355,19 +1449,15 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
|
|||
case somfy_commands::SunFlag:
|
||||
{
|
||||
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)
|
||||
this->target = this->myPos >= 0 ? this->myPos : 100.0f;
|
||||
else if (!isSunny && this->noSunDone)
|
||||
this->target = 0.0f;
|
||||
}
|
||||
|
||||
somfy.isDirty = true;
|
||||
this->emitState();
|
||||
}
|
||||
|
|
@ -1464,6 +1554,85 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
|
|||
//if(dir == 0 && this->tiltType == tilt_types::tiltmotor && this->tiltDirection != 0) this->setTiltMovement(0);
|
||||
this->setMovement(dir);
|
||||
}
|
||||
void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
|
||||
// The reason why we are processing all frames here is so
|
||||
// any linked remotes that may happen to be on the same ESPSomfy RTS
|
||||
// device can trigger the appropriate actions.
|
||||
if(this->shadeId == 255) return;
|
||||
const uint64_t curTime = millis();
|
||||
int8_t dir = 0;
|
||||
this->moveStart = this->tiltStart = curTime;
|
||||
this->startPos = this->currentPos;
|
||||
this->startTiltPos = this->currentTiltPos;
|
||||
// If the command is coming from a remote then we are aborting all these positioning operations.
|
||||
switch(cmd) {
|
||||
case somfy_commands::Up:
|
||||
if(this->tiltType == tilt_types::tiltmotor) {
|
||||
if(repeat >= TILT_REPEATS)
|
||||
this->tiltTarget = 0.0f;
|
||||
else
|
||||
this->target = 0.0f;
|
||||
}
|
||||
else
|
||||
this->target = this->tiltTarget = 0.0f;
|
||||
break;
|
||||
case somfy_commands::Down:
|
||||
if (!this->windLast || (curTime - this->windLast) >= SOMFY_NO_WIND_REMOTE_TIMEOUT) {
|
||||
if(this->tiltType == tilt_types::tiltmotor) {
|
||||
if(repeat >= TILT_REPEATS)
|
||||
this->tiltTarget = 100.0f;
|
||||
else
|
||||
this->target = 100.0f;
|
||||
}
|
||||
else {
|
||||
this->target = 100.0f;
|
||||
if(this->tiltType != tilt_types::none) this->tiltTarget = 100.0f;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case somfy_commands::My:
|
||||
if(this->isIdle()) {
|
||||
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;
|
||||
}
|
||||
else {
|
||||
this->target = this->currentPos;
|
||||
this->tiltTarget = this->currentTiltPos;
|
||||
}
|
||||
break;
|
||||
case somfy_commands::StepUp:
|
||||
// With the step commands and integrated shades
|
||||
// the motor must tilt in the direction first then move
|
||||
// so we have to calculate the target with this in mind.
|
||||
if(this->tiltType == tilt_types::integrated && this->currentTiltPos > 0.0f) {
|
||||
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)))));
|
||||
}
|
||||
break;
|
||||
case somfy_commands::StepDown:
|
||||
dir = 1;
|
||||
// With the step commands and integrated shades
|
||||
// the motor must tilt in the direction first then move
|
||||
// so we have to calculate the target with this in mind.
|
||||
if(this->tiltType == tilt_types::integrated && this->currentTiltPos < 100.0f) {
|
||||
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->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)))));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
dir = 0;
|
||||
break;
|
||||
}
|
||||
this->setMovement(dir);
|
||||
}
|
||||
void SomfyShade::setTiltMovement(int8_t dir) {
|
||||
int8_t currDir = this->tiltDirection;
|
||||
if(dir == 0) {
|
||||
|
|
@ -1617,6 +1786,32 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
|
|||
SomfyRemote::sendCommand(cmd, repeat);
|
||||
}
|
||||
}
|
||||
void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat) {
|
||||
// This sendCommand function will always be called externally. sendCommand at the remote level
|
||||
// is expected to be called internally when the motor needs commanded.
|
||||
if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type;
|
||||
SomfyRemote::sendCommand(cmd, repeat);
|
||||
switch(cmd) {
|
||||
case somfy_commands::My:
|
||||
this->direction = 0;
|
||||
break;
|
||||
case somfy_commands::Up:
|
||||
this->direction = -1;
|
||||
break;
|
||||
case somfy_commands::Down:
|
||||
this->direction = 1;
|
||||
break;
|
||||
}
|
||||
this->emitState();
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] != 0) {
|
||||
SomfyShade * shade = somfy.getShadeById(this->linkedShades[i]);
|
||||
if(shade) shade->processInternalCommand(cmd, repeat);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SomfyShade::sendTiltCommand(somfy_commands cmd) {
|
||||
if(cmd == somfy_commands::Up) {
|
||||
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : 1);
|
||||
|
|
@ -1718,6 +1913,7 @@ bool SomfyShade::save() {
|
|||
this->commit();
|
||||
return true;
|
||||
}
|
||||
bool SomfyGroup::save() { somfy.commit(); return true; }
|
||||
bool SomfyShade::fromJSON(JsonObject &obj) {
|
||||
if(obj.containsKey("name")) strlcpy(this->name, obj["name"], sizeof(this->name));
|
||||
if(obj.containsKey("upTime")) this->upTime = obj["upTime"];
|
||||
|
|
@ -1775,6 +1971,18 @@ bool SomfyShade::fromJSON(JsonObject &obj) {
|
|||
if(obj.containsKey("flags")) this->flags = obj["flags"];
|
||||
return true;
|
||||
}
|
||||
bool SomfyShade::toJSONRef(JsonObject &obj) {
|
||||
obj["shadeId"] = this->getShadeId();
|
||||
obj["name"] = this->name;
|
||||
obj["remoteAddress"] = this->m_remoteAddress;
|
||||
obj["paired"] = this->paired;
|
||||
obj["shadeType"] = static_cast<uint8_t>(this->shadeType);
|
||||
obj["bitLength"] = this->bitLength;
|
||||
obj["proto"] = static_cast<uint8_t>(this->proto);
|
||||
obj["flags"] = this->flags;
|
||||
SomfyRemote::toJSON(obj);
|
||||
return true;
|
||||
}
|
||||
bool SomfyShade::toJSON(JsonObject &obj) {
|
||||
//Serial.print("Serializing Shade:");
|
||||
//Serial.print(this->getShadeId());
|
||||
|
|
@ -1806,6 +2014,7 @@ bool SomfyShade::toJSON(JsonObject &obj) {
|
|||
obj["flags"] = this->flags;
|
||||
obj["flipCommands"] = this->flipCommands;
|
||||
obj["flipPosition"] = this->flipPosition;
|
||||
obj["inGroup"] = this->isInGroup();
|
||||
SomfyRemote::toJSON(obj);
|
||||
JsonArray arr = obj.createNestedArray("linkedRemotes");
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) {
|
||||
|
|
@ -1848,7 +2057,7 @@ bool SomfyGroup::toJSON(JsonObject &obj) {
|
|||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
JsonObject lsd = arr.createNestedObject();
|
||||
shade->toJSON(lsd);
|
||||
shade->toJSONRef(lsd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1908,6 +2117,26 @@ uint8_t SomfyShadeController::getNextShadeId() {
|
|||
}
|
||||
return 255;
|
||||
}
|
||||
uint8_t SomfyShadeController::getNextGroupId() {
|
||||
// There is no shortcut for this since the deletion of
|
||||
// a group in the middle makes all of this very difficult.
|
||||
for(uint8_t i = 1; i < SOMFY_MAX_GROUPS - 1; i++) {
|
||||
bool id_exists = false;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPS; j++) {
|
||||
SomfyGroup *group = &this->groups[j];
|
||||
if(group->getGroupId() == i) {
|
||||
id_exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!id_exists) {
|
||||
Serial.print("Got next Group Id:");
|
||||
Serial.print(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 255;
|
||||
}
|
||||
uint8_t SomfyShadeController::shadeCount() {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
|
|
@ -1915,19 +2144,27 @@ uint8_t SomfyShadeController::shadeCount() {
|
|||
}
|
||||
return count;
|
||||
}
|
||||
uint32_t SomfyShadeController::getNextRemoteAddress(uint8_t shadeId) {
|
||||
uint32_t address = this->startingAddress + shadeId;
|
||||
uint8_t SomfyShadeController::groupCount() {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(this->groups[i].getGroupId() != 255) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
uint32_t SomfyShadeController::getNextRemoteAddress(uint8_t id) {
|
||||
uint32_t address = this->startingAddress + id;
|
||||
uint8_t i = 0;
|
||||
// The assumption here is that the max number of groups will
|
||||
// always be less than or equal to the max number of shades.
|
||||
while(i < SOMFY_MAX_SHADES) {
|
||||
if(this->shades[i].getShadeId() != 255) {
|
||||
if(this->shades[i].getRemoteAddress() == address) {
|
||||
address++;
|
||||
i = 0; // Start over we cannot share addresses.
|
||||
}
|
||||
else i++;
|
||||
if((i < SOMFY_MAX_SHADES && this->shades[i].getShadeId() != 255 && this->shades[i].getRemoteAddress() == address) ||
|
||||
(i < SOMFY_MAX_GROUPS && this->groups[i].getGroupId() != 255 && this->groups[i].getRemoteAddress() == address)) {
|
||||
address++;
|
||||
i = 0; // Start over we cannot share addresses.
|
||||
}
|
||||
else i++;
|
||||
}
|
||||
i = 0;
|
||||
return address;
|
||||
}
|
||||
SomfyShade *SomfyShadeController::addShade(JsonObject &obj) {
|
||||
|
|
@ -1999,6 +2236,29 @@ SomfyShade *SomfyShadeController::addShade() {
|
|||
}
|
||||
return shade;
|
||||
}
|
||||
SomfyGroup *SomfyShadeController::addGroup(JsonObject &obj) {
|
||||
SomfyGroup *group = this->addGroup();
|
||||
if(group) {
|
||||
group->fromJSON(obj);
|
||||
group->save();
|
||||
group->emitState("groupAdded");
|
||||
}
|
||||
return group;
|
||||
}
|
||||
SomfyGroup *SomfyShadeController::addGroup() {
|
||||
uint8_t groupId = this->getNextGroupId();
|
||||
// So the next shade id will be the first one we run into with an id of 255 so
|
||||
// if it gets deleted in the middle then it will get the first slot that is empty.
|
||||
// There is no apparent way around this. In the future we might actually add an indexer
|
||||
// to it for sorting later.
|
||||
if(groupId == 255) return nullptr;
|
||||
SomfyGroup *group = &this->groups[groupId - 1];
|
||||
if(group) {
|
||||
group->setGroupId(groupId);
|
||||
this->isDirty = true;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) {
|
||||
if(this->flipCommands) {
|
||||
switch(cmd) {
|
||||
|
|
@ -2092,6 +2352,17 @@ bool SomfyShadeController::deleteShade(uint8_t shadeId) {
|
|||
this->commit();
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::deleteGroup(uint8_t groupId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(this->groups[i].getGroupId() == groupId) {
|
||||
shades[i].emitState("groupRemoved");
|
||||
this->groups[i].clear();
|
||||
}
|
||||
}
|
||||
this->commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SomfyShadeController::loadShadesFile(const char *filename) { return ShadeConfigFile::load(this, filename); }
|
||||
uint16_t SomfyRemote::getNextRollingCode() {
|
||||
pref.begin("ShadeCodes");
|
||||
|
|
@ -2113,18 +2384,28 @@ uint16_t SomfyRemote::setRollingCode(uint16_t code) {
|
|||
}
|
||||
bool SomfyShadeController::toJSON(DynamicJsonDocument &doc) {
|
||||
doc["maxShades"] = SOMFY_MAX_SHADES;
|
||||
doc["maxGroups"] = SOMFY_MAX_GROUPS;
|
||||
doc["maxGroupedShades"] = SOMFY_MAX_GROUPED_SHADES;
|
||||
doc["maxLinkedRemotes"] = SOMFY_MAX_LINKED_REMOTES;
|
||||
doc["startingAddress"] = this->startingAddress;
|
||||
JsonObject objRadio = doc.createNestedObject("transceiver");
|
||||
this->transceiver.toJSON(objRadio);
|
||||
JsonArray arr = doc.createNestedArray("shades");
|
||||
JsonArray arrShades = doc.createNestedArray("shades");
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &this->shades[i];
|
||||
if(shade->getShadeId() != 255) {
|
||||
JsonObject oshade = arr.createNestedObject();
|
||||
JsonObject oshade = arrShades.createNestedObject();
|
||||
shade->toJSON(oshade);
|
||||
}
|
||||
}
|
||||
JsonArray arrGroups = doc.createNestedArray("groups");
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
SomfyGroup *group = &this->groups[i];
|
||||
if(group->getGroupId() != 255) {
|
||||
JsonObject ogroup = arrGroups.createNestedObject();
|
||||
group->toJSON(ogroup);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::toJSON(JsonObject &obj) {
|
||||
|
|
@ -2133,11 +2414,13 @@ bool SomfyShadeController::toJSON(JsonObject &obj) {
|
|||
obj["startingAddress"] = this->startingAddress;
|
||||
JsonObject oradio = obj.createNestedObject("transceiver");
|
||||
this->transceiver.toJSON(oradio);
|
||||
JsonArray arr = obj.createNestedArray("shades");
|
||||
this->toJSON(arr);
|
||||
JsonArray arrShades = obj.createNestedArray("shades");
|
||||
this->toJSONShades(arrShades);
|
||||
JsonArray arrGroups = obj.createNestedArray("groups");
|
||||
this->toJSONGroups(arrGroups);
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::toJSON(JsonArray &arr) {
|
||||
bool SomfyShadeController::toJSONShades(JsonArray &arr) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade &shade = this->shades[i];
|
||||
if(shade.getShadeId() != 255) {
|
||||
|
|
@ -2147,6 +2430,16 @@ bool SomfyShadeController::toJSON(JsonArray &arr) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::toJSONGroups(JsonArray &arr) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
SomfyGroup &group = this->groups[i];
|
||||
if(group.getGroupId() != 255) {
|
||||
JsonObject ogroup = arr.createNestedObject();
|
||||
group.toJSON(ogroup);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void SomfyShadeController::loop() {
|
||||
this->transceiver.loop();
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
|
|
@ -2183,7 +2476,6 @@ static const uint32_t tempo_if_gap = 30415; // Gap between frames
|
|||
static int16_t bitMin = SYMBOL * TOLERANCE_MIN;
|
||||
static somfy_rx_t somfy_rx;
|
||||
static somfy_rx_queue_t rx_queue;
|
||||
|
||||
bool somfy_tx_queue_t::pop(somfy_tx_t *tx) {
|
||||
// Read the oldest index.
|
||||
for(int8_t i = MAX_TX_BUFFER - 1; i >= 0; i--) {
|
||||
|
|
@ -2232,7 +2524,6 @@ void somfy_rx_queue_t::init() {
|
|||
memset(&this->index[0], 0xFF, MAX_RX_BUFFER);
|
||||
this->length = 0;
|
||||
}
|
||||
|
||||
bool somfy_rx_queue_t::pop(somfy_rx_t *rx) {
|
||||
// Read off the data from the oldest index.
|
||||
//Serial.println("Popping RX Queue");
|
||||
|
|
@ -2328,7 +2619,6 @@ void Transceiver::sendFrame(byte *frame, uint8_t sync, uint8_t bitLength) {
|
|||
delayMicroseconds(30415);
|
||||
*/
|
||||
}
|
||||
|
||||
void RECEIVE_ATTR Transceiver::handleReceive() {
|
||||
static unsigned long last_time = 0;
|
||||
const long time = micros();
|
||||
|
|
|
|||
31
Somfy.h
31
Somfy.h
|
|
@ -46,6 +46,9 @@ enum class somfy_commands : byte {
|
|||
// Command extensions for 80 bit frames
|
||||
StepUp = 0x8B
|
||||
};
|
||||
enum class group_types : byte {
|
||||
channel = 0x00
|
||||
};
|
||||
enum class shade_types : byte {
|
||||
roller = 0x00,
|
||||
blind = 0x01,
|
||||
|
|
@ -227,6 +230,7 @@ class SomfyShade : public SomfyRemote {
|
|||
float myTiltPos = -1.0f;
|
||||
SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
|
||||
bool paired = false;
|
||||
bool toJSONRef(JsonObject &obj);
|
||||
bool fromJSON(JsonObject &obj);
|
||||
bool toJSON(JsonObject &obj) override;
|
||||
char name[21] = "";
|
||||
|
|
@ -238,8 +242,10 @@ class SomfyShade : public SomfyRemote {
|
|||
uint16_t stepSize = 100;
|
||||
bool save();
|
||||
bool isIdle();
|
||||
bool isInGroup();
|
||||
void checkMovement();
|
||||
void processFrame(somfy_frame_t &frame, bool internal = false);
|
||||
void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1);
|
||||
void setTiltMovement(int8_t dir);
|
||||
void setMovement(int8_t dir);
|
||||
void setTarget(float target);
|
||||
|
|
@ -267,12 +273,22 @@ class SomfyGroup : public SomfyRemote {
|
|||
protected:
|
||||
uint8_t groupId = 255;
|
||||
public:
|
||||
group_types groupType = group_types::channel;
|
||||
int8_t direction = 0; // 0 = stopped, 1=down, -1=up.
|
||||
char name[21] = "";
|
||||
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES];
|
||||
void setGroupId(uint8_t id) { groupId = id; }
|
||||
uint8_t getGroupId() { return groupId; }
|
||||
bool save();
|
||||
void clear();
|
||||
bool fromJSON(JsonObject &obj);
|
||||
bool toJSON(JsonObject &obj);
|
||||
bool linkShade(uint8_t shadeId);
|
||||
bool unlinkShade(uint8_t shadeId);
|
||||
bool hasShadeId(uint8_t shadeId);
|
||||
void emitState(const char *evt = "groupState");
|
||||
void emitState(uint8_t num, const char *evt = "groupState");
|
||||
void sendCommand(somfy_commands cmd, uint8_t repeat = 1);
|
||||
};
|
||||
struct transceiver_config_t {
|
||||
bool printBuffer = false;
|
||||
|
|
@ -280,8 +296,8 @@ struct transceiver_config_t {
|
|||
uint8_t type = 56; // 56 or 80 bit protocol.
|
||||
radio_proto proto = radio_proto::RTS;
|
||||
uint8_t SCKPin = 18;
|
||||
uint8_t TXPin = 12;
|
||||
uint8_t RXPin = 13;
|
||||
uint8_t TXPin = 13;
|
||||
uint8_t RXPin = 12;
|
||||
uint8_t MOSIPin = 23;
|
||||
uint8_t MISOPin = 19;
|
||||
uint8_t CSNPin = 5;
|
||||
|
|
@ -381,22 +397,31 @@ class SomfyShadeController {
|
|||
bool isDirty = false;
|
||||
uint32_t startingAddress;
|
||||
uint8_t getNextShadeId();
|
||||
uint8_t getNextGroupId();
|
||||
uint32_t getNextRemoteAddress(uint8_t shadeId);
|
||||
SomfyShadeController();
|
||||
Transceiver transceiver;
|
||||
SomfyShade *addShade();
|
||||
SomfyShade *addShade(JsonObject &obj);
|
||||
SomfyGroup *addGroup();
|
||||
SomfyGroup *addGroup(JsonObject &obj);
|
||||
bool deleteShade(uint8_t shadeId);
|
||||
bool deleteGroup(uint8_t groupId);
|
||||
bool begin();
|
||||
void loop();
|
||||
void end();
|
||||
SomfyShade shades[SOMFY_MAX_SHADES];
|
||||
SomfyGroup groups[SOMFY_MAX_GROUPS];
|
||||
bool toJSON(DynamicJsonDocument &doc);
|
||||
bool toJSON(JsonArray &arr);
|
||||
bool toJSON(JsonObject &obj);
|
||||
bool toJSONShades(JsonArray &arr);
|
||||
bool toJSONGroups(JsonArray &arr);
|
||||
uint8_t shadeCount();
|
||||
uint8_t groupCount();
|
||||
SomfyShade * getShadeById(uint8_t shadeId);
|
||||
SomfyGroup * getGroupById(uint8_t groupId);
|
||||
SomfyShade * findShadeByRemoteAddress(uint32_t address);
|
||||
SomfyGroup * findGroupByRemoteAddress(uint32_t address);
|
||||
void sendFrame(somfy_frame_t &frame, uint8_t repeats = 0);
|
||||
void processFrame(somfy_frame_t &frame, bool internal = false);
|
||||
void emitState(uint8_t num = 255);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
14
Web.h
14
Web.h
|
|
@ -1,3 +1,4 @@
|
|||
#include <WebServer.h>
|
||||
#ifndef webserver_h
|
||||
#define webserver_h
|
||||
class Web {
|
||||
|
|
@ -5,8 +6,21 @@ class Web {
|
|||
void sendCORSHeaders();
|
||||
void sendCacheHeaders(uint32_t seconds=604800);
|
||||
void startup();
|
||||
void handleLogin(WebServer &server);
|
||||
void handleLogout(WebServer &server);
|
||||
void handleStreamFile(WebServer &server, const char *filename, const char *encoding);
|
||||
void handleController(WebServer &server);
|
||||
void handleLoginContext(WebServer &server);
|
||||
void handleGetShades(WebServer &server);
|
||||
void handleGetGroups(WebServer &server);
|
||||
void begin();
|
||||
void loop();
|
||||
void end();
|
||||
// Web Handlers
|
||||
bool createAPIToken(const IPAddress ipAddress, char *token);
|
||||
bool createAPIToken(const char *payload, char *token);
|
||||
bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token);
|
||||
bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token);
|
||||
bool isAuthenticated(WebServer &server, bool cfg = false);
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.7.2
|
||||
2.0.0
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
<title>Configure CC1101</title>
|
||||
<link rel="stylesheet" href="main.css" type="text/css" />
|
||||
<link rel="stylesheet" href="icons.css" type="text/css" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
input[data-type="int8_t"],
|
||||
input[data-type="uint8_t"] {
|
||||
width: 47px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input[data-type="uint16_t"] {
|
||||
width: 77px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input[data-type="float"] {
|
||||
width: 77px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
let runRepair = false;
|
||||
function runRemoteRepair() {
|
||||
let obj = { address: parseInt(document.getElementById('fldRepairAddress').value, 10), command: 'Down', rcode: parseInt(document.getElementById('fldRepairRCode').value, 10), repeats: 1 };
|
||||
if (runRepair) {
|
||||
// Call the service.
|
||||
console.log(obj);
|
||||
putJSON('/sendRemoteCommand', obj, (err, ret) => {
|
||||
console.log(ret);
|
||||
document.getElementById('fldRepairRCode').value = obj.rcode + 1;
|
||||
setTimeout(() => { runRemoteRepair() }, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
function stopRemoteRepair() {
|
||||
runRepair = false;
|
||||
}
|
||||
|
||||
let baseUrl = ''; //'http://192.168.1.204'; This is the current server.
|
||||
|
||||
Number.prototype.round = function (dec) { return Number(Math.round(this + 'e' + dec) + 'e-' + dec); };
|
||||
Number.prototype.fmt = function (format, empty) {
|
||||
if (isNaN(this)) return empty || '';
|
||||
if (typeof format === 'undefined') return this.toString();
|
||||
let isNegative = this < 0;
|
||||
let tok = ['#', '0'];
|
||||
let pfx = '', sfx = '', fmt = format.replace(/[^#\.0\,]/g, '');
|
||||
let dec = fmt.lastIndexOf('.') > 0 ? fmt.length - (fmt.lastIndexOf('.') + 1) : 0,
|
||||
fw = '', fd = '', vw = '', vd = '', rw = '', rd = '';
|
||||
let val = String(Math.abs(this).round(dec));
|
||||
let ret = '', commaChar = ',', decChar = '.';
|
||||
for (var i = 0; i < format.length; i++) {
|
||||
let c = format.charAt(i);
|
||||
if (c === '#' || c === '0' || c === '.' || c === ',')
|
||||
break;
|
||||
pfx += c;
|
||||
}
|
||||
for (let i = format.length - 1; i >= 0; i--) {
|
||||
let c = format.charAt(i);
|
||||
if (c === '#' || c === '0' || c === '.' || c === ',')
|
||||
break;
|
||||
sfx = c + sfx;
|
||||
}
|
||||
if (dec > 0) {
|
||||
let dp = val.lastIndexOf('.');
|
||||
if (dp === -1) {
|
||||
val += '.'; dp = 0;
|
||||
}
|
||||
else
|
||||
dp = val.length - (dp + 1);
|
||||
while (dp < dec) {
|
||||
val += '0';
|
||||
dp++;
|
||||
}
|
||||
fw = fmt.substring(0, fmt.lastIndexOf('.'));
|
||||
fd = fmt.substring(fmt.lastIndexOf('.') + 1);
|
||||
vw = val.substring(0, val.lastIndexOf('.'));
|
||||
vd = val.substring(val.lastIndexOf('.') + 1);
|
||||
let ds = val.substring(val.lastIndexOf('.'), val.length);
|
||||
for (let i = 0; i < fd.length; i++) {
|
||||
if (fd.charAt(i) === '#' && vd.charAt(i) !== '0') {
|
||||
rd += vd.charAt(i);
|
||||
continue;
|
||||
} else if (fd.charAt(i) === '#' && vd.charAt(i) === '0') {
|
||||
var np = vd.substring(i);
|
||||
if (np.match('[1-9]')) {
|
||||
rd += vd.charAt(i);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
else if (fd.charAt(i) === '0' || fd.charAt(i) === '#')
|
||||
rd += vd.charAt(i);
|
||||
}
|
||||
if (rd.length > 0) rd = decChar + rd;
|
||||
}
|
||||
else {
|
||||
fw = fmt;
|
||||
vw = val;
|
||||
}
|
||||
var cg = fw.lastIndexOf(',') >= 0 ? fw.length - fw.lastIndexOf(',') - 1 : 0;
|
||||
var nw = Math.abs(Math.floor(this.round(dec)));
|
||||
if (!(nw === 0 && fw.substr(fw.length - 1) === '#') || fw.substr(fw.length - 1) === '0') {
|
||||
var gc = 0;
|
||||
for (let i = vw.length - 1; i >= 0; i--) {
|
||||
rw = vw.charAt(i) + rw;
|
||||
gc++;
|
||||
if (gc === cg && i !== 0) {
|
||||
rw = commaChar + rw;
|
||||
gc = 0;
|
||||
}
|
||||
}
|
||||
if (fw.length > rw.length) {
|
||||
var pstart = fw.indexOf('0');
|
||||
if (pstart > 0) {
|
||||
var plen = fw.length - pstart;
|
||||
var pos = fw.length - rw.length - 1;
|
||||
while (rw.length < plen) {
|
||||
let pc = fw.charAt(pos);
|
||||
if (pc === ',') pc = commaChar;
|
||||
rw = pc + rw;
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNegative) rw = '-' + rw;
|
||||
if (rd.length === 0 && rw.length === 0) return '';
|
||||
//console.log(pfx + rw + rd + sfx);
|
||||
return pfx + rw + rd + sfx;
|
||||
};
|
||||
let fields = [
|
||||
{ t: 'bool', i: 'printBuffer', d: 'Print the bytes received to the serial console', def: false },
|
||||
{ t: 'bool', i: 'internalCCMode', d: 'Use internal transmission mode FIFO buffers.', def:true },
|
||||
{ t: 'uint8_t', i: 'modulationMode', d: 'Modulation mode', o: [{ v: 0, n: '2-FSK' }, { v: 1, n: 'GFSK' }, { v: 2, n: 'ASK/OOK' }, { v: 3, n: '4-FSK' }, { v: 4, n: 'MSK' }] },
|
||||
{ t: 'float', i: 'frequency', d: 'Basic frequency', def:433.24 },
|
||||
{ t: 'float', i: 'deviation', d: 'Set the Frequency deviation in kHz. Value from 1.58 to 380.85. Default is 47.60 kHz.', def:47.6 },
|
||||
{ t: 'uint8_t', i: 'channel', d: 'The channel number from 0 to 255', def:0 },
|
||||
{ t: 'float', i: 'channelSpacing', d: 'Channel spacing in multiplied by the channel number and added to the base frequency in kHz. 25.39 to 405.45. Default 199.95', def:199.95 },
|
||||
{ t: 'float', i: 'rxBandwidth', d: 'Receive bandwidth in kHz. Value from 58.03 to 812.50. Default is 812.50kHz', def:812.5 },
|
||||
{ t: 'float', i: 'dataRate', d: 'The data rate in kBaud. 0.02 to 1621.83 Default is 99.97.', def:99.97 },
|
||||
{ t: 'int8_t', i: 'txPower', d: 'Transmission power {-30, -20, -15, -10, -6, 0, 5, 7, 10, 11, 12}. Default is 12.', def:12 },
|
||||
{
|
||||
t: 'uint8_t', i: 'syncMode', d: 'Sync Mode', def:0, o: [
|
||||
{ v: 0, n: 'No preamble/sync' },
|
||||
{ v: 1, n: '16 sync word bits detected' },
|
||||
{ v: 2, n: '16/16 sync words bits detected.' },
|
||||
{ v: 3, n: '30/32 sync word bits detected,' },
|
||||
{ v: 4, n: 'No preamble/sync carrier above threshold' },
|
||||
{ v: 5, n: '15/16 + carrier above threshold.' },
|
||||
{ v: 6, n: '16/16 + carrier-sense above threshold' },
|
||||
{ v: 7, n: '0/32 + carrier-sense above threshold' }]
|
||||
},
|
||||
{ t: 'uint16_t', i: 'syncWordHigh', d: 'The High sync word used for the sync mode.', def:211 },
|
||||
{ t: 'uint16_t', i: 'syncWordLow', d: 'The Low sync word used for the sync mode.', def:145 },
|
||||
{
|
||||
t: 'uint8_t', i: 'addrCheckMode', d: 'Address filter check mode', def:0, o: [
|
||||
{ v: 0, n: 'No address filtration' },
|
||||
{ v: 1, n: 'Check address without broadcast.' },
|
||||
{ v: 2, n: 'Address check with 0 as broadcast.' },
|
||||
{ v: 3, n: 'Address check with 0 or 255 as broadcast.' }]
|
||||
},
|
||||
{ t: 'uint8_t', i: 'checkAddr', d: 'Packet filter address depending on addrCheck settings.', def:0 },
|
||||
{ t: 'bool', i: 'dataWhitening', d: 'Indicates whether data whitening should be applied.', def:false },
|
||||
{
|
||||
t: 'uint8_t', i: 'pktFormat', d: 'Packet formatting', def:0, o: [
|
||||
{ v: 0, n: 'Use FIFO buffers for RX and TX' },
|
||||
{ v: 1, n: 'Synchronous serial mode. RX on GDO0 and TX on either GDOx pins.' },
|
||||
{ v: 2, n: 'Random TX mode. Send data using PN9 generator.' },
|
||||
{ v: 3, n: 'Asynchronous serial mode. RX on GDO0 and TX on either GDOx pins.' }]
|
||||
},
|
||||
{
|
||||
t: 'uint8_t', i: 'pktLengthMode', d: 'Type of packets', def:1, o: [
|
||||
{ v: 0, n: 'Fixed packet length' },
|
||||
{ v: 1, n: 'Variable packet length' },
|
||||
{ v: 2, n: 'Infinite packet length' },
|
||||
{ v: 3, n: 'Reserved' }]
|
||||
},
|
||||
{ t: 'uint8_t', i: 'pktLength', d: 'Packet length', def:0 },
|
||||
{ t: 'bool', i: 'useCRC', d: 'Indicates whether CRC is to be used.', def:true },
|
||||
{ t: 'bool', i: 'autoFlushCRC', d: 'Automatically flush RX FIFO when CRC fails. If more than one packet is in the buffer it too will be flushed.', def:false },
|
||||
{ t: 'bool', i: 'disableDCFilter', d: 'Digital blocking filter for demodulator. Only for data rates <= 250k.', def:false },
|
||||
{ t: 'bool', i: 'enableManchester', d: 'Enable/disable Manchester encoding.', def:false },
|
||||
{ t: 'bool', i: 'enableFEC', d: 'Enable/disable forward error correction.', def:false },
|
||||
{
|
||||
t: 'uint8_t', i: 'minPreambleBytes', d: 'The minimum number of preamble bytes to be transmitted.', def:0, o: [
|
||||
{ v: 0, n: '2bytes' },
|
||||
{ v: 1, n: '3bytes' },
|
||||
{ v: 2, n: '4bytes' },
|
||||
{ v: 3, n: '6bytes' },
|
||||
{ v: 4, n: '8bytes' },
|
||||
{ v: 5, n: '12bytes' },
|
||||
{ v: 6, n: '16bytes' },
|
||||
{ v: 7, n: '24bytes' }]
|
||||
},
|
||||
{ t: 'uint8_t', i: 'pqtThreshold', d: 'Preamble quality estimator threshold.', def:0 },
|
||||
{ t: 'bool', i: 'appendStatus', d: 'Appends the RSSI and LQI values to the TX packed as well as the CRC.', def: false }
|
||||
];
|
||||
function resetDefaults() {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let f = fields[i];
|
||||
if (typeof f.def === 'undefined') continue;
|
||||
let el = document.getElementById(`id_${f.i}`);
|
||||
switch (f.t) {
|
||||
case 'bool':
|
||||
el.checked = f.def;
|
||||
break;
|
||||
default:
|
||||
el.value = f.def;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function loadConfig() {
|
||||
let overlay = waitMessage(document.getElementById('divFields'));
|
||||
getJSON('/getRadio', (err, radio) => {
|
||||
overlay.remove();
|
||||
if (err) {
|
||||
console.log(err);
|
||||
serviceError(document.getElementById('divFields'), err);
|
||||
}
|
||||
else {
|
||||
console.log(radio);
|
||||
let cfg = radio.config;
|
||||
// Bind up all the fields.
|
||||
let elems = document.querySelectorAll('[data-bind]');
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
let el = elems[i];
|
||||
switch (el.dataset.type) {
|
||||
case 'bool':
|
||||
el.checked = cfg[el.dataset.bind];
|
||||
break;
|
||||
case 'float':
|
||||
el.value = cfg[el.dataset.bind].fmt("#.##");
|
||||
break;
|
||||
default:
|
||||
el.value = cfg[el.dataset.bind];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function createCheckbox(f) { return `<input type="checkbox" id="id_${f.i}" name="${f.i}" data-type="${f.t}" data-bind="${f.bind || f.i}"></input><label for="${f.i}>${f.l || f.i}</label>`; }
|
||||
function createNumberField(f) { return `<input type="number" id="id_${f.i}" name="${f.i}" data-type="${f.t}" data-bind="${f.bind || f.i}"></input > <label for="${f.i}>${f.l || f.i}</label>`; }
|
||||
function createDropdown(f) {
|
||||
let dd = `<select id="id_${f.i}" name="${f.i}" data-type="${f.t}"" data-bind="${f.bind || f.i}">`;
|
||||
for (let i = 0; i < f.o.length; i++) {
|
||||
let o = f.o[i];
|
||||
dd += `<option value="${o.v}">${o.n}</option>`
|
||||
}
|
||||
dd += '</select>';
|
||||
dd += `<label for="${f.i}>${f.l || f.i}</label>`;
|
||||
return dd;
|
||||
}
|
||||
function createFields() {
|
||||
let shtml = '<div id="divAttrs">';
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let f = fields[i];
|
||||
shtml += '<div class="cfg-attr"><div class="cfg-attr-field">'
|
||||
switch (f.t) {
|
||||
case 'bool':
|
||||
shtml += createCheckbox(f);
|
||||
break;
|
||||
case 'uint8_t':
|
||||
case 'int8_t':
|
||||
case 'uint16_t':
|
||||
case 'float':
|
||||
case 'int8_t':
|
||||
shtml += typeof f.o === 'undefined' ? createNumberField(f) : createDropdown(f);
|
||||
break;
|
||||
}
|
||||
shtml += `</div><div class="cfg-attr-desc">${f.d}</div>`
|
||||
shtml += '</div>';
|
||||
}
|
||||
shtml += '</div>';
|
||||
document.getElementById('divFields').innerHTML = shtml;
|
||||
}
|
||||
function serviceError(el, err) {
|
||||
let msg = '';
|
||||
if (typeof err === 'string' && err.startsWith('{')) {
|
||||
let e = JSON.parse(err);
|
||||
if (typeof e !== 'undefined' && typeof e.desc === 'string') msg = e.desc;
|
||||
else msg = err;
|
||||
}
|
||||
else if (typeof err === 'string') {
|
||||
msg = err;
|
||||
}
|
||||
else if (typeof err === 'number') {
|
||||
switch (err) {
|
||||
case 404:
|
||||
msg = `404: Service not found`;
|
||||
break;
|
||||
default:
|
||||
msg = `${err}: Network HTTP Error`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (typeof err !== 'undefined') {
|
||||
console.log(err);
|
||||
if (typeof err.desc === 'string') msg = typeof err.desc !== 'undefined' ? err.desc : err.message;
|
||||
}
|
||||
return errorMessage(el, msg);
|
||||
}
|
||||
function errorMessage(el, msg) {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = '<div class="innerError">' + msg + '</div><button type="button" onclick="clearErrors();">Close</button></div>';
|
||||
div.classList.add('errorMessage');
|
||||
el.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
function getJSON(url, cb) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', baseUrl + url, true);
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
let status = xhr.status;
|
||||
cb(status === 200 ? null : status, xhr.response);
|
||||
}
|
||||
xhr.onerror = (evt) => {
|
||||
cb(xhr.status || 500, xhr.statusText);
|
||||
}
|
||||
xhr.send();
|
||||
}
|
||||
function putJSON(url, data, cb) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
console.log({ put: baseUrl + url, data: data });
|
||||
xhr.open('PUT', baseUrl + url, true);
|
||||
xhr.responseType = 'json';
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.onload = () => {
|
||||
let status = xhr.status;
|
||||
cb(status === 200 ? null : status, xhr.response);
|
||||
}
|
||||
xhr.onerror = (evt) => {
|
||||
cb(xhr.status || 500, xhr.statusText);
|
||||
}
|
||||
xhr.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function waitMessage(el) {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = '<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div></div>';
|
||||
div.classList.add('waitoverlay');
|
||||
el.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
function clearErrors() {
|
||||
let errors = document.querySelectorAll('div.errorMessage');
|
||||
if (errors && errors.length > 0) errors.forEach((el) => { el.remove(); });
|
||||
}
|
||||
function saveSettings() {
|
||||
let elems = document.querySelectorAll('[data-bind]');
|
||||
let obj = {};
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
let el = elems[i];
|
||||
switch (el.dataset.type) {
|
||||
case 'bool':
|
||||
obj[el.dataset.bind] = el.checked;
|
||||
break;
|
||||
case 'float':
|
||||
obj[el.dataset.bind] = parseFloat(el.value);
|
||||
break;
|
||||
default:
|
||||
obj[el.dataset.bind] = parseInt(el.value, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let overlay = waitMessage(document.getElementById('divFields'));
|
||||
putJSON('/setRadioConfig', { config: obj }, (err, radio) => {
|
||||
overlay.remove();
|
||||
if (err) {
|
||||
console.log(err);
|
||||
serviceError(document.getElementById('divFields'), err);
|
||||
}
|
||||
else {
|
||||
console.log(radio);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div syle="white-space:nowrap;">
|
||||
<label for="repairAddress">Repair Address</label>
|
||||
<input id="fldRepairAddress" name="repairAddress" type="number" style="margin-right: 10px; text-align: right; display: inline-block; width: 127px;" value="4624451" />
|
||||
<label for="repairRCode">Rolling Code</label>
|
||||
<input id="fldRepairRCode" name="repairRCode" type="number" style="margin-right:10px;text-align:right;display:inline-block;width:127px;" value="1641" />
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button id="btnRepairRemote" style="width:127px" onclick="runRepair = true; runRemoteRepair();">Repair Remote</button>
|
||||
<button id="btnStopRepairRemote" style="width:127px" onclick="stopRemoteRepair();">Stop Repair</button>
|
||||
</div>
|
||||
<div id="divFields"></div>
|
||||
<div class="button-container">
|
||||
<button id="btnSaveSetting" type="button" style="display:inline-block;width:44%" onclick="resetDefaults();">
|
||||
Reset Defaults
|
||||
</button>
|
||||
<button id="btnSaveSetting" type="button" style="display:inline-block;width:44%" onclick="saveSettings();">
|
||||
Save Radio Settings
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
createFields();
|
||||
loadConfig();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
982
data/index.html
982
data/index.html
File diff suppressed because it is too large
Load diff
2248
data/index.js
2248
data/index.js
File diff suppressed because it is too large
Load diff
53
data/login.html
Normal file
53
data/login.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="main.css?v=2.0.0" type="text/css" />
|
||||
<link rel="stylesheet" href="widgets.css?v=2.0.0" type="text/css" />
|
||||
<link rel="stylesheet" href="icons.css?v=2.0.0" type="text/css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<script type="text/javascript" src="index.js?v=2.0.0"></script>
|
||||
</head>
|
||||
<body onload="general.loadLogin();">
|
||||
<div id="divContainer" class="container" data-securitytype="0">
|
||||
<h1 style="text-align: center;"><img src="icon.png" style="width:127px;margin-left:1px;margin-top:-10px;" /></h1>
|
||||
<div id="divLoginPnl" class="login-content" style="position:relative;">
|
||||
<div style="max-width:270px;margin:0px auto;">
|
||||
<form id="frmLogin" action="/login" method="post" class="login-form">
|
||||
<input id="fldPin" type="hidden" name="pin">
|
||||
<div id="divPinSecurity" style="display:none;">
|
||||
<div class="field-group" style="text-align:center;">
|
||||
<div id="fldPinEntry" style="display:inline-block;" onkeydown="general.pinKeyPressed(event);">
|
||||
<input class="pin-digit" maxlength="1" data-bind="security.pin.d0" onfocus="general.pinDigitFocus(event);">
|
||||
<input class="pin-digit" maxlength="1" data-bind="security.pin.d1" onfocus="general.pinDigitFocus(event);">
|
||||
<input class="pin-digit" maxlength="1" data-bind="security.pin.d2" onfocus="general.pinDigitFocus(event);">
|
||||
<input class="pin-digit" maxlength="1" data-bind="security.pin.d3" onfocus="general.pinDigitFocus(event);">
|
||||
</div>
|
||||
<label for="fldPinEntry" style="margin-top:7px;">Enter Pin</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="divPasswordSecurity" style="display:none;">
|
||||
<div class="field-group">
|
||||
<input id="fldUsername" name="username" type="text" data-bind="security.username" length=32 placeholder="Username">
|
||||
<label for="fldUsername">Username</label>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<input id="fldPassword" name="password" type="password" data-bind="security.password" length=32 placeholder="Password">
|
||||
<label for="fldPassword">Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center;"><span id="spanLoginMessage" style="color:red"></span></div>
|
||||
<div class="button-container">
|
||||
<button id="btnLogin" type="button" value="Submit" onclick="general.login();">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
179
data/main.css
179
data/main.css
|
|
@ -4,6 +4,29 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #434343;
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: #eeeeee;
|
||||
margin-top: 77px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0px;
|
||||
color: #939393;
|
||||
font-size: 28px;
|
||||
}
|
||||
hr {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
|
||||
input[type="range"] {
|
||||
accent-color: #00bcd4;
|
||||
outline:none;
|
||||
|
|
@ -12,6 +35,9 @@ input[type="range"] {
|
|||
input[type="range"]:active {
|
||||
outline: dotted 1px silver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.button-outline {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
|
@ -48,34 +74,11 @@ input[type="range"] {
|
|||
transform: rotate(7deg);
|
||||
}
|
||||
}
|
||||
body {
|
||||
color: #434343;
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: #eeeeee;
|
||||
margin-top: 77px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0px;
|
||||
color: #939393;
|
||||
font-size: 28px;
|
||||
}
|
||||
form {
|
||||
margin-top:-18px;
|
||||
}
|
||||
|
||||
form .field-group {
|
||||
.field-group {
|
||||
box-sizing: border-box;
|
||||
clear: both;
|
||||
padding: 4px 0;
|
||||
|
|
@ -84,7 +87,7 @@ form .field-group {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
form .field-group1 {
|
||||
.field-group1 {
|
||||
box-sizing: border-box;
|
||||
clear: both;
|
||||
padding: 4px 0;
|
||||
|
|
@ -93,7 +96,7 @@ form .field-group1 {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
form .field-group1 > span {
|
||||
.field-group1 > span {
|
||||
color: #757575;
|
||||
display: inline-block;
|
||||
margin: 0 0 5px 0;
|
||||
|
|
@ -104,7 +107,7 @@ form .field-group1 {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
form .field-group > label {
|
||||
.field-group > label {
|
||||
display: block;
|
||||
margin: 0px;
|
||||
padding: 3px 0 0;
|
||||
|
|
@ -116,11 +119,11 @@ form .field-group > label {
|
|||
margin-top:-10px;
|
||||
color:#00bcd4;
|
||||
}
|
||||
form .field-group1 > div.field-group {
|
||||
.field-group1 > div.field-group {
|
||||
display:inline-block;
|
||||
width:auto;
|
||||
}
|
||||
::placeholder {
|
||||
*::placeholder {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
|
@ -156,9 +159,9 @@ select {
|
|||
margin-bottom:14px;
|
||||
}
|
||||
|
||||
form .field-group input[type=password],
|
||||
form .field-group input[type=number],
|
||||
form .field-group input[type=text] {
|
||||
.field-group input[type=password],
|
||||
.field-group input[type=number],
|
||||
.field-group input[type=text] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -214,7 +217,8 @@ a {
|
|||
bottom: 7px;
|
||||
border-bottom: 1px solid #00bcd4;
|
||||
}
|
||||
div.errorMessage {
|
||||
div.prompt-message,
|
||||
div.error-message {
|
||||
position:absolute;
|
||||
left:0px;
|
||||
top:0px;
|
||||
|
|
@ -229,17 +233,31 @@ div.errorMessage {
|
|||
align-items:center;
|
||||
align-content:center;
|
||||
font-size:32px;
|
||||
border-radius:5px;
|
||||
border-radius:27px;
|
||||
transition-duration:3s;
|
||||
}
|
||||
div.errorMessage > div {
|
||||
div.prompt-message,
|
||||
div.error-message > div {
|
||||
padding:10px;
|
||||
}
|
||||
div.error-message > .sub-message {
|
||||
font-size:14px;
|
||||
}
|
||||
div.socket-error {
|
||||
opacity: 1;
|
||||
font-size: 20px;
|
||||
min-height: 277px;
|
||||
z-index: 20001;
|
||||
}
|
||||
.prompt-message > .button-container {
|
||||
text-align:center;
|
||||
}
|
||||
.prompt-message > .button-container > button {
|
||||
width: calc(50% - 22px);
|
||||
margin-left:7px;
|
||||
margin-right:7px;
|
||||
display:inline-block;
|
||||
}
|
||||
div.instructions {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
|
|
@ -247,11 +265,11 @@ div.instructions {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: gray;
|
||||
opacity: .9;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
font-size: 20px;
|
||||
|
|
@ -279,6 +297,45 @@ div.instructions {
|
|||
border-bottom-width: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.subtab-container {
|
||||
margin-top:3px;
|
||||
margin-left: 3px;
|
||||
display: flex;
|
||||
width: calc(100% - 7px);
|
||||
margin-bottom: -1px;
|
||||
overflow: visible;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.subtab-container > span {
|
||||
display:inline-block;
|
||||
background:white;
|
||||
margin-bottom:-1px;
|
||||
padding-left: 10px;
|
||||
padding-right:10px;
|
||||
padding-top:5px;
|
||||
padding-bottom:5px;
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
cursor:pointer;
|
||||
background:ghostwhite;
|
||||
}
|
||||
.subtab-container > span.selected {
|
||||
border-bottom:none;
|
||||
background:white;
|
||||
}
|
||||
.subtab-content {
|
||||
display: block;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 25px;
|
||||
position: relative;
|
||||
}
|
||||
fieldset {
|
||||
display: block;
|
||||
border: 1px solid #ccc;
|
||||
|
|
@ -371,7 +428,7 @@ span.strength {
|
|||
button.disabled {
|
||||
opacity:.3;
|
||||
}
|
||||
div.waitoverlay {
|
||||
div.wait-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
|
@ -383,8 +440,9 @@ div.waitoverlay {
|
|||
background:rgba(227, 226, 230, 0.46);
|
||||
cursor:no-drop;
|
||||
z-index:20000;
|
||||
border-radius:27px;
|
||||
}
|
||||
div.waitoverlay > .lds-roller {
|
||||
div.wait-overlay > .lds-roller {
|
||||
z-index:1000;
|
||||
opacity:1;
|
||||
}
|
||||
|
|
@ -463,10 +521,10 @@ div.waitoverlay > .lds-roller {
|
|||
top: 63px;
|
||||
left: 17px;
|
||||
}
|
||||
|
||||
.lds-roller div:nth-child(8) {
|
||||
animation-delay: -0.288s;
|
||||
}
|
||||
|
||||
.lds-roller div:nth-child(8):after {
|
||||
top: 56px;
|
||||
left: 12px;
|
||||
|
|
@ -486,17 +544,6 @@ div.waitoverlay > .lds-roller {
|
|||
.radioPins > label {
|
||||
text-align:center;
|
||||
}
|
||||
.somfyShade {
|
||||
text-align:center;
|
||||
padding-bottom:4px;
|
||||
display:inline-table;
|
||||
padding-left:4px;
|
||||
padding-right:4px;
|
||||
}
|
||||
.somfyShade > div,
|
||||
.somfyShade > span {
|
||||
display:table-cell;
|
||||
}
|
||||
.button-outline {
|
||||
background-color: #00bcd4;
|
||||
border-radius:50%;
|
||||
|
|
@ -507,6 +554,7 @@ div.waitoverlay > .lds-roller {
|
|||
font-size:1.5em;
|
||||
color:white;
|
||||
}
|
||||
.group-name,
|
||||
.shade-name {
|
||||
text-align:left;
|
||||
width: 100%;
|
||||
|
|
@ -517,6 +565,7 @@ div.waitoverlay > .lds-roller {
|
|||
overflow:hidden;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.group-address,
|
||||
.shade-address {
|
||||
width:77px;
|
||||
padding-left:2px;
|
||||
|
|
@ -558,6 +607,7 @@ div.waitoverlay > .lds-roller {
|
|||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.somfyGroupCtl,
|
||||
.somfyShadeCtl {
|
||||
height:60px;
|
||||
border-bottom:dotted 2px gainsboro;
|
||||
|
|
@ -576,12 +626,14 @@ div.waitoverlay > .lds-roller {
|
|||
font-size: 48px;
|
||||
position:relative;
|
||||
}
|
||||
.somfyGroupCtl .group-name,
|
||||
.somfyShadeCtl .shade-name {
|
||||
display:inline-block;
|
||||
padding:7px;
|
||||
vertical-align:middle;
|
||||
width:100%;
|
||||
}
|
||||
.somfyGroupCtl .groupctl-name,
|
||||
.somfyShadeCtl .shadectl-name {
|
||||
font-size: 1.5em;
|
||||
color: silver;
|
||||
|
|
@ -590,18 +642,23 @@ div.waitoverlay > .lds-roller {
|
|||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
.somfyGroupCtl label,
|
||||
.somfyShadeCtl label {
|
||||
color:silver;
|
||||
}
|
||||
.somfyGroupCtl .groupctl-mypos,
|
||||
.somfyShadeCtl .shadectl-mypos {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.somfyGroupCtl .groupctl-buttons,
|
||||
.somfyShadeCtl .shadectl-buttons {
|
||||
margin-top:3px;
|
||||
float:right;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.somfyGroupCtl .groupctl-buttons .button-outline,
|
||||
.somfyShadeCtl .shadectl-buttons .button-outline {
|
||||
display: inline-block;
|
||||
padding: 7px;
|
||||
|
|
@ -675,30 +732,24 @@ div.eth-setting-line label {
|
|||
color:mediumspringgreen;
|
||||
}
|
||||
div.frame-log {
|
||||
position:absolute;
|
||||
top:0px;
|
||||
left:0px;
|
||||
width:100%;
|
||||
height:100%;
|
||||
border-radius:27px;
|
||||
border:solid 2px gray;
|
||||
background:gainsboro;
|
||||
}
|
||||
div.frame-list {
|
||||
position:relative;
|
||||
width: calc(100% - 14px);
|
||||
height: calc(100% - 147px);
|
||||
left: 7px;
|
||||
width: 100%;
|
||||
max-height:357px;
|
||||
background:white;
|
||||
min-height:127px;
|
||||
overflow-y:auto;
|
||||
border:solid 1px silver;
|
||||
border-bottom:solid 1px silver;
|
||||
}
|
||||
div.frame-header {
|
||||
border-top-left-radius:5px;
|
||||
border-top-right-radius:5px;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
background: #00bcd4;
|
||||
width: calc(100% - 14px);
|
||||
left: 7px;
|
||||
width: 100%;
|
||||
color:white;
|
||||
padding-top:4px;
|
||||
}
|
||||
|
|
|
|||
112
data/widgets.css
Normal file
112
data/widgets.css
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
|
||||
background-color: #ffffff;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
border-radius: 27px;
|
||||
}
|
||||
.main.container {
|
||||
min-height:327px;
|
||||
}
|
||||
|
||||
.header-message {
|
||||
text-align: center;
|
||||
background: gainsboro;
|
||||
color: gray;
|
||||
margin-bottom: 7px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.inst-overlay {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-content: flex-start;
|
||||
position: absolute;
|
||||
border-radius: 27px;
|
||||
background: gray;
|
||||
opacity: .9;
|
||||
padding: 10px;
|
||||
font-size: 20px;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
color: white;
|
||||
}
|
||||
.edit-grouplist,
|
||||
.edit-motorlist {
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
min-height: 147px;
|
||||
}
|
||||
.prompt-message .sub-message {
|
||||
font-size: 17px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.prompt-message .prompt-text {
|
||||
text-align:center;
|
||||
}
|
||||
.promp-message > .inner-error,
|
||||
.error-message > .inner-error {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.wizard[data-stepid="1"] .wizard-step:not([data-stepid="1"]) { display: none; }
|
||||
.wizard[data-stepid="2"] .wizard-step:not([data-stepid="2"]) { display: none; }
|
||||
.wizard[data-stepid="3"] .wizard-step:not([data-stepid="3"]) { display: none; }
|
||||
.wizard[data-stepid="4"] .wizard-step:not([data-stepid="4"]) { display: none; }
|
||||
.wizard[data-stepid="5"] .wizard-step:not([data-stepid="5"]) { display: none; }
|
||||
.wizard[data-stepid="6"] .wizard-step:not([data-stepid="6"]) { display: none; }
|
||||
.wizard[data-stepid="7"] .wizard-step:not([data-stepid="7"]) { display: none; }
|
||||
|
||||
.somfyGroup,
|
||||
.somfyShade {
|
||||
text-align: center;
|
||||
padding-bottom: 4px;
|
||||
display: inline-table;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.linked-shade > div,
|
||||
.linked-shade > span,
|
||||
.somfyGroup > div,
|
||||
.somfyGroup > span,
|
||||
.somfyShade > div,
|
||||
.somfyShade > span {
|
||||
display: table-cell;
|
||||
padding-left:4px;
|
||||
padding-right:4px;
|
||||
}
|
||||
.linked-shade {
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
display: inline-table;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.linked-shade > .linkedshade-name { width: 100%; }
|
||||
.pin-digit {
|
||||
margin: 0 0.3rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #00bcd4;
|
||||
border-radius:5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
}
|
||||
.login-content {
|
||||
display: block;
|
||||
padding: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue