Pre-v2.0.0 adds groups, groups, security, and tab interface #83 #53

This commit is contained in:
Robert Strouse 2023-07-01 09:20:23 -07:00
parent a0c24ceb07
commit d228a21c83
19 changed files with 4095 additions and 1781 deletions

View file

@ -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;

View file

@ -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

View file

@ -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);
}

View file

@ -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
View file

@ -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;

View file

@ -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
View file

@ -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
View file

@ -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.

960
Web.cpp

File diff suppressed because it is too large Load diff

14
Web.h
View file

@ -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

View file

@ -1 +1 @@
1.7.2
2.0.0

View file

@ -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>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

53
data/login.html Normal file
View 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>

View file

@ -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
View 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;
}