diff --git a/ConfigSettings.h b/ConfigSettings.h index 99165ff..06eeee5 100644 --- a/ConfigSettings.h +++ b/ConfigSettings.h @@ -3,7 +3,7 @@ #ifndef configsettings_h #define configsettings_h -#define FW_VERSION "v1.3.0" +#define FW_VERSION "v1.3.1" enum DeviceStatus { DS_OK = 0, DS_ERROR = 1, diff --git a/Network.cpp b/Network.cpp index d4de417..027e6d1 100644 --- a/Network.cpp +++ b/Network.cpp @@ -308,7 +308,7 @@ bool Network::connectWiFi() { return false; } bool Network::connect() { - if(settings.connType != conn_types::wifi && !this->wifiFallback) + if(settings.connType != conn_types::wifi && settings.connType != conn_types::unset && !this->wifiFallback) return this->connectWired(); return this->connectWiFi(); } diff --git a/Somfy.cpp b/Somfy.cpp index 01aab4d..b93a962 100644 --- a/Somfy.cpp +++ b/Somfy.cpp @@ -24,6 +24,7 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to #endif #define SETMY_REPEATS 15 +#define TILT_REPEATS 7 int sort_asc(const void *cmp1, const void *cmp2) { int a = *((uint8_t *)cmp1); @@ -346,6 +347,9 @@ bool SomfyShade::unlinkRemote(uint32_t address) { void SomfyShade::checkMovement() { int8_t currDir = this->direction; uint8_t currPos = this->position; + int8_t currTiltDir = this->tiltDirection; + uint8_t currTiltPos = this->tiltPosition; + if(this->direction > 0) { if(this->downTime == 0) { this->direction = 0; @@ -437,6 +441,69 @@ void SomfyShade::checkMovement() { this->seekingPos = false; } } + if(this->tiltDirection > 0) { + int32_t msFrom0 = (int32_t)floor(this->startTiltPos * this->tiltTime); + msFrom0 += (millis() - this->tiltStart); + msFrom0 = min((int32_t)this->tiltTime, msFrom0); + if(msFrom0 >= this->tiltTime) { + this->currentTiltPos = 1.0; + this->tiltDirection = 0; + } + else { + this->currentTiltPos = min(max((float)0.0, (float)msFrom0 / (float)this->tiltTime), (float)1.0); + if(this->currentTiltPos >= 1) { + this->tiltDirection = 0; + this->currentTiltPos = 1.0; + } + } + this->tiltPosition = floor(this->currentTiltPos * 100); + if(this->seekingTiltPos && this->tiltPosition >= this->tiltTarget) { + Serial.print("Stopping Shade Tilt:"); + Serial.print(this->name); + Serial.print(" at "); + Serial.print(this->tiltPosition); + Serial.print("% target "); + Serial.print(this->tiltTarget); + Serial.println("%"); + this->sendCommand(somfy_commands::My); + this->tiltDirection = 0; + this->seekingTiltPos = false; + } + } + else if(this->tiltDirection < 0) { + if(this->tiltTime == 0) { + this->tiltDirection = 0; + this->currentTiltPos = 0; + } + else { + int32_t msFrom100 = (int32_t)this->tiltTime - (int32_t)floor(this->startTiltPos * this->tiltTime); + msFrom100 += (millis() - this->tiltStart); + msFrom100 = min((int32_t)this->tiltTime, msFrom100); + if(msFrom100 >= this->tiltTime) { + this->currentTiltPos = 0.0; + this->tiltDirection = 0; + } + this->currentTiltPos = (float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)this->tiltTime), (float)1.0); + // If we are at the top of the shade then set the movement to 0. + if(this->currentTiltPos <= 0.0) { + this->tiltDirection = 0; + this->currentTiltPos = 0; + } + } + this->tiltPosition = floor(this->currentTiltPos * 100); + if(this->seekingTiltPos && this->tiltPosition <= this->tiltTarget) { + Serial.print("Stopping Shade Tilt:"); + Serial.print(this->name); + Serial.print(" at "); + Serial.print(this->tiltPosition); + Serial.print("% target "); + Serial.print(this->tiltTarget); + Serial.println("%"); + this->sendCommand(somfy_commands::My); + this->tiltDirection = 0; + this->seekingTiltPos = false; + } + } if(currDir != this->direction && this->direction == 0) { char shadeKey[15]; snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); @@ -463,11 +530,22 @@ void SomfyShade::checkMovement() { pref.end(); } } - if(currDir != this->direction || currPos != this->position) { + if(currTiltDir != this->tiltDirection && this->tiltDirection == 0) { + char shadeKey[15]; + snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); + Serial.print("Writing current shade tilt position: "); + Serial.println(this->currentTiltPos, 4); + pref.begin(shadeKey); + pref.putFloat("currentTiltPos", this->currentTiltPos); + pref.end(); + } + if(currDir != this->direction || currPos != this->position || currTiltDir != this->tiltDirection || currTiltPos != this->tiltPosition) { // We need to emit on the socket that our state has changed. this->position = floor(this->currentPos * 100.0); + this->tiltPosition = floor(this->currentTiltPos * 100.0); this->emitState(); } + } void SomfyShade::load() { char shadeKey[15]; @@ -487,6 +565,12 @@ void SomfyShade::load() { this->position = (uint8_t)floor(this->currentPos * 100); this->target = this->position; this->myPos = pref.getUShort("myPos", this->myPos); + this->hasTilt = pref.getBool("hasTilt", false); + this->shadeType = static_cast(pref.getChar("shadeType", static_cast(this->shadeType))); + this->tiltTime = pref.getUShort("tiltTime", 3000); + this->currentTiltPos = pref.getFloat("currentTiltPos", 0); + this->tiltPosition = (uint8_t)floor(this->currentTiltPos * 100); + this->tiltTarget = this->tiltPosition; pref.getBytes("linkedAddr", linkedAddresses, sizeof(linkedAddresses)); pref.end(); Serial.print("shadeId:"); @@ -527,14 +611,28 @@ void SomfyShade::publish() { snprintf(topic, sizeof(topic), "shades/%u/lastRollingCode", this->shadeId); mqtt.publish(topic, this->lastRollingCode); snprintf(topic, sizeof(topic), "shades/%u/mypos", this->shadeId); - mqtt.publish(topic, this->myPos); - + mqtt.publish(topic, this->hasTilt ? "true" : "false"); + snprintf(topic, sizeof(topic), "shades/%u/shadeType", this->shadeId); + mqtt.publish(topic, static_cast(this->shadeType)); + if(this->hasTilt) { + snprintf(topic, sizeof(topic), "shades/%u/tiltDirection", this->shadeId); + mqtt.publish(topic, this->tiltDirection); + snprintf(topic, sizeof(topic), "shades/%u/tiltPosition", this->shadeId); + mqtt.publish(topic, this->tiltPosition); + snprintf(topic, sizeof(topic), "shades/%u/tiltTarget", this->shadeId); + mqtt.publish(topic, this->tiltTarget); + } } } void SomfyShade::emitState(const char *evt) { this->emitState(255, evt); } void SomfyShade::emitState(uint8_t num, const char *evt) { - char buf[220]; - snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d}", this->shadeId, this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos); + char buf[320]; + if(this->hasTilt) + snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"type\":%u,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d,\"hasTilt\":%s,\"tiltDirection\":%d,\"tiltTarget\":%d,\"tiltPosition\":%d}", + this->shadeId, static_cast(this->shadeType), this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos, this->hasTilt ? "true" : "false", this->tiltDirection, this->tiltTarget, this->tiltPosition); + else + snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"type\":%u,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d,\"hasTilt\":%s}", + this->shadeId, static_cast(this->shadeType), this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos, this->hasTilt ? "true" : "false"); if(num >= 255) sockEmit.sendToClients(evt, buf); else sockEmit.sendToClient(num, evt, buf); if(mqtt.connected()) { @@ -549,18 +647,53 @@ void SomfyShade::emitState(uint8_t num, const char *evt) { mqtt.publish(topic, this->lastRollingCode); snprintf(topic, sizeof(topic), "shades/%u/mypos", this->shadeId); mqtt.publish(topic, this->myPos); + snprintf(topic, sizeof(topic), "shades/%u/hasTilt", this->hasTilt); + mqtt.publish(topic, this->hasTilt ? "true" : "false"); + if(this->hasTilt) { + snprintf(topic, sizeof(topic), "shades/%u/tiltPosition", this->shadeId); + mqtt.publish(topic, this->tiltPosition); + snprintf(topic, sizeof(topic), "shades/%u/tiltTarget", this->shadeId); + mqtt.publish(topic, this->tiltTarget); + } } } +bool SomfyShade::isIdle() { return this->direction == 0 && this->tiltDirection == 0; } void SomfyShade::processWaitingFrame() { if(this->shadeId == 255) { this->lastFrame.await = 0; return; } if(this->lastFrame.processed) return; - if(this->lastFrame.await > 0 && (millis() > this->lastFrame.await || this->lastFrame.repeats >= SETMY_REPEATS)) { + if(this->lastFrame.await > 0 && (millis() > this->lastFrame.await)) { switch(this->lastFrame.cmd) { + case somfy_commands::Down: + case somfy_commands::Up: + if(this->hasTilt) { // Theoretically this should get here unless it does have a tilt. + if(this->lastFrame.repeats >= TILT_REPEATS) { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->seekingTiltPos = false; + this->tiltTarget = dir > 0 ? 100 : 0; + this->setTiltMovement(dir); + this->lastFrame.processed = true; + Serial.print(this->name); + Serial.print(" Processing tilt "); + Serial.print(translateSomfyCommand(this->lastFrame.cmd)); + Serial.print(" after "); + Serial.print(this->lastFrame.repeats); + Serial.println(" repeats"); + } + else { + int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1; + this->seekingPos = false; + this->target = dir > 0 ? 100 : 0; + this->setMovement(dir); + this->lastFrame.processed = true; + } + if(this->lastFrame.repeats > TILT_REPEATS + 2) this->lastFrame.processed = true; + } + break; case somfy_commands::My: - if(this->lastFrame.repeats >= SETMY_REPEATS && this->direction == 0) { + if(this->lastFrame.repeats >= SETMY_REPEATS && this->isIdle()) { if(this->myPos == this->position) // We are clearing it. this->myPos = 255; else @@ -609,24 +742,61 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { this->lastFrame.copy(frame); int8_t dir = 0; // If the frame came from the radio it cannot be seeking a position. This means that the target will be set. - if(!internal) this->seekingPos = false; + if(!internal) this->seekingTiltPos = this->seekingPos = false; + // At this point we are not processing the combo buttons // will need to see what the shade does when you press both. switch(frame.cmd) { case somfy_commands::Up: - dir = -1; - if(!internal) this->target = 0; - this->lastFrame.processed = true; + if(this->hasTilt) { + // Wait another half seccond just in case we are potentially processing a tilt. + if(!internal) this->lastFrame.await = millis() + 500; + else if(this->lastFrame.repeats >= TILT_REPEATS) { + // This is an internal tilt command. + Serial.println("Processing Tilt UP..."); + this->setTiltMovement(-1); + return; + } + else { + dir = -1; + if(!internal) this->target = 0; + this->lastFrame.processed = true; + } + } + else { + dir = -1; + if(!internal) this->target = 0; + this->lastFrame.processed = true; + } break; case somfy_commands::Down: - dir = 1; - if(!internal) this->target = 100; - this->lastFrame.processed = true; + if(this->hasTilt) { + // Wait another half seccond just in case we are potentially processing a tilt. + if(!internal) this->lastFrame.await = millis() + 500; + else if(this->lastFrame.repeats >= TILT_REPEATS) { + // This is an internal tilt command. + Serial.println("Processing Tilt DOWN..."); + this->setTiltMovement(1); + return; + } + else { + dir = 1; + if(!internal) this->target = 100; + this->lastFrame.processed = true; + } + } + else { + dir = 1; + if(!internal) this->target = 100; + this->lastFrame.processed = true; + } break; case somfy_commands::My: dir = 0; - if(this->direction == 0) { + if(this->isIdle()) { if(!internal) { + // This frame is coming from a remote. We are potentially setting + // the my position. this->lastFrame.await = millis() + 500; } else if(myPos >= 0 && this->myPos <= 100) { @@ -654,6 +824,34 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { } this->setMovement(dir); } +void SomfyShade::setTiltMovement(int8_t dir) { + int8_t currDir = this->tiltDirection; + if(dir == 0) { + // The shade tilt is stopped. + this->startTiltPos = this->currentTiltPos; + this->tiltStart = 0; + this->tiltDirection = dir; + if(currDir != dir) { + char shadeKey[15]; + snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); + Serial.print("Writing current shade position:"); + Serial.println(this->currentTiltPos, 4); + pref.begin(shadeKey); + pref.putFloat("currentTiltPos", this->currentTiltPos); + pref.end(); + } + } + else if(this->direction != dir) { + this->tiltStart = millis(); + this->startTiltPos = this->currentTiltPos; + this->tiltDirection = dir; + } + if(this->tiltDirection != currDir) { + this->tiltPosition = floor(this->currentTiltPos * 100.0); + this->emitState(); + } +} + void SomfyShade::setMovement(int8_t dir) { int8_t currDir = this->direction; if(dir == 0) { @@ -738,7 +936,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) { this->seekingPos = false; } else if(cmd == somfy_commands::My) { - if(this->direction == 0 && this->myPos >= 0 && this->myPos <= 100) { + if(this->isIdle() && this->myPos >= 0 && this->myPos <= 100) { this->moveToMyPosition(); return; } @@ -749,6 +947,44 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) { } SomfyRemote::sendCommand(cmd, repeat); } +void SomfyShade::sendTiltCommand(somfy_commands cmd) { + if(cmd == somfy_commands::Up) { + this->tiltTarget = 0; + this->seekingTiltPos = false; + SomfyRemote::sendCommand(cmd, TILT_REPEATS); + } + else if(cmd == somfy_commands::Down) { + this->tiltTarget = 100; + this->seekingTiltPos = false; + SomfyRemote::sendCommand(cmd, TILT_REPEATS); + } + else if(cmd == somfy_commands::My) { + this->tiltTarget = this->tiltPosition; + this->seekingTiltPos = false; + SomfyRemote::sendCommand(cmd); + } +} +void SomfyShade::moveToTiltTarget(uint8_t target) { + int8_t newDir = 0; + somfy_commands cmd = somfy_commands::My; + if(target < this->tiltPosition) + cmd = somfy_commands::Up; + else if(target > this->tiltPosition) + cmd = somfy_commands::Down; + Serial.print("Moving Tilt to "); + Serial.print(target); + Serial.print("% from "); + Serial.print(this->tiltPosition); + Serial.print("% using "); + Serial.println(translateSomfyCommand(cmd)); + this->tiltTarget = target; + if(target > 0 && target < 100) this->seekingTiltPos = true; + else this->seekingTiltPos = false; + if(cmd != somfy_commands::My) + SomfyRemote::sendCommand(cmd, TILT_REPEATS); + else + SomfyRemote::sendCommand(cmd); +} void SomfyShade::moveToTarget(uint8_t target) { int8_t newDir = 0; somfy_commands cmd = somfy_commands::My; @@ -772,12 +1008,16 @@ bool SomfyShade::save() { snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->getShadeId()); pref.begin(shadeKey); pref.putString("name", this->name); + pref.putBool("hasTilt", this->hasTilt); pref.putBool("paired", this->paired); pref.putUShort("upTime", this->upTime); pref.putUShort("downTime", this->downTime); + pref.putUShort("tiltTime", this->tiltTime); pref.putULong("remoteAddress", this->getRemoteAddress()); pref.putFloat("currentPos", this->currentPos); + pref.putFloat("currentTiltPos", this->currentTiltPos); pref.putUShort("myPos", this->myPos); + pref.putChar("shadeType", static_cast(this->shadeType)); uint32_t linkedAddresses[SOMFY_MAX_LINKED_REMOTES]; memset(linkedAddresses, 0x00, sizeof(linkedAddresses)); uint8_t j = 0; @@ -794,6 +1034,21 @@ bool SomfyShade::fromJSON(JsonObject &obj) { if(obj.containsKey("upTime")) this->upTime = obj["upTime"]; if(obj.containsKey("downTime")) this->downTime = obj["downTime"]; if(obj.containsKey("remoteAddress")) this->setRemoteAddress(obj["remoteAddress"]); + if(obj.containsKey("tiltTime")) this->tiltTime = obj["tiltTime"]; + if(obj.containsKey("hasTilt")) this->hasTilt = obj["hasTilt"]; + if(obj.containsKey("shadeType")) { + if(obj["shadeType"].is()) { + if(strncmp(obj["shadeType"].as(), "roller", 7) == 0) + this->shadeType = shade_types::roller; + else if(strncmp(obj["shadeType"].as(), "drapery", 8) == 0) + this->shadeType = shade_types::drapery; + else if(strncmp(obj["shadeType"].as(), "blind", 5) == 0) + this->shadeType = shade_types::blind; + } + else { + this->shadeType = static_cast(obj["shadeType"].as()); + } + } if(obj.containsKey("linkedAddresses")) { uint32_t linkedAddresses[SOMFY_MAX_LINKED_REMOTES]; memset(linkedAddresses, 0x00, sizeof(linkedAddresses)); @@ -822,9 +1077,16 @@ bool SomfyShade::toJSON(JsonObject &obj) { obj["remotePrefId"] = this->getRemotePrefId(); obj["lastRollingCode"] = this->lastRollingCode; obj["position"] = this->position; + obj["tiltPosition"] = this->tiltPosition; + obj["tiltDirection"] = this->tiltDirection; + obj["tiltTime"] = this->tiltTime; + obj["tiltTarget"] = this->tiltTarget; obj["target"] = this->target; obj["myPos"] = this->myPos; obj["direction"] = this->direction; + obj["hasTilt"] = this->hasTilt; + obj["tiltTime"] = this->tiltTime; + obj["shadeType"] = static_cast(this->shadeType); SomfyRemote::toJSON(obj); JsonArray arr = obj.createNestedArray("linkedRemotes"); for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { @@ -967,6 +1229,7 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) { frame.rollingCode = this->getNextRollingCode(); frame.remoteAddress = this->getRemoteAddress(); frame.cmd = cmd; + frame.repeats = repeat; somfy.sendFrame(frame, repeat); somfy.processFrame(frame, true); } diff --git a/Somfy.h b/Somfy.h index 4a799d3..ee5daba 100644 --- a/Somfy.h +++ b/Somfy.h @@ -15,6 +15,11 @@ enum class somfy_commands : byte { SunFlag = 0x9, Flag = 0xA }; +enum class shade_types : byte { + roller = 0x00, + blind = 0x01, + drapery = 0x02 +}; String translateSomfyCommand(const somfy_commands cmd); somfy_commands translateSomfyCommand(const String& string); @@ -64,19 +69,28 @@ class SomfyShade : public SomfyRemote { protected: uint8_t shadeId = 255; uint64_t moveStart = 0; + uint64_t tiltStart = 0; float startPos = 0.0; + float startTiltPos = 0.00; bool seekingPos = false; + bool seekingTiltPos = false; bool seekingMyPos = false; bool settingMyPos = false; uint32_t awaitMy = 0; public: + shade_types shadeType = shade_types::roller; + bool hasTilt = false; void load(); somfy_frame_t lastFrame; float currentPos = 0.0; + float currentTiltPos = 0.0; //uint16_t movement = 0; int8_t direction = 0; // 0 = stopped, 1=down, -1=up. + int8_t tiltDirection = 0; // 0=stopped, 1=clockwise, -1=counter clockwise + uint8_t tiltPosition = 0; uint8_t position = 0; uint8_t target = 0; + uint8_t tiltTarget = 0; uint8_t myPos = 255; SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES]; bool paired = false; @@ -86,13 +100,18 @@ class SomfyShade : public SomfyRemote { void setShadeId(uint8_t id) { shadeId = id; } uint8_t getShadeId() { return shadeId; } uint16_t upTime = 10000; - uint16_t downTime = 1000; + uint16_t downTime = 10000; + uint16_t tiltTime = 5000; bool save(); + bool isIdle(); void checkMovement(); void processFrame(somfy_frame_t &frame, bool internal = false); + void setTiltMovement(int8_t dir); void setMovement(int8_t dir); void setTarget(uint8_t target); void moveToTarget(uint8_t target); + void moveToTiltTarget(uint8_t target); + void sendTiltCommand(somfy_commands cmd); void sendCommand(somfy_commands cmd, uint8_t repeat = 1); bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0); bool unlinkRemote(uint32_t remoteAddress); diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin index 59ef102..4121925 100644 Binary files a/SomfyController.ino.esp32.bin and b/SomfyController.ino.esp32.bin differ diff --git a/SomfyController.littlefs.bin b/SomfyController.littlefs.bin index a5f20fc..c72c401 100644 Binary files a/SomfyController.littlefs.bin and b/SomfyController.littlefs.bin differ diff --git a/Web.cpp b/Web.cpp index 5bb9891..272648b 100644 --- a/Web.cpp +++ b/Web.cpp @@ -394,7 +394,7 @@ void Web::begin() { // We are updating an existing shade. if (server.hasArg("plain")) { Serial.println("Updating a shade"); - DynamicJsonDocument doc(256); + DynamicJsonDocument doc(512); DeserializationError err = deserializeJson(doc, server.arg("plain")); if (err) { switch (err.code()) { @@ -416,7 +416,7 @@ void Web::begin() { if (shade) { shade->fromJSON(obj); shade->save(); - DynamicJsonDocument sdoc(256); + DynamicJsonDocument sdoc(512); JsonObject sobj = sdoc.to(); shade->toJSON(sobj); serializeJson(sdoc, g_content); @@ -437,7 +437,7 @@ void Web::begin() { // We are updating an existing shade. if (server.hasArg("plain")) { Serial.println("Updating a shade"); - DynamicJsonDocument doc(256); + DynamicJsonDocument doc(512); DeserializationError err = deserializeJson(doc, server.arg("plain")); if (err) { switch (err.code()) { @@ -459,7 +459,7 @@ void Web::begin() { if (shade) { shade->fromJSON(obj); shade->save(); - DynamicJsonDocument sdoc(256); + DynamicJsonDocument sdoc(512); JsonObject sobj = sdoc.to(); shade->toJSON(sobj); serializeJson(sdoc, g_content); @@ -473,6 +473,70 @@ void Web::begin() { else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); } }); + server.on("/tiltCommand", []() { + webServer.sendCORSHeaders(); + HTTPMethod method = server.method(); + uint8_t shadeId = 255; + uint8_t target = 255; + somfy_commands command = somfy_commands::My; + if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { + if (server.hasArg("shadeId")) { + shadeId = atoi(server.arg("shadeId").c_str()); + if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command")); + else if(server.hasArg("target")) target = atoi(server.arg("target").c_str()); + } + else if (server.hasArg("plain")) { + Serial.println("Sending Shade Tilt Command"); + DynamicJsonDocument doc(256); + DeserializationError err = deserializeJson(doc, server.arg("plain")); + if (err) { + switch (err.code()) { + case DeserializationError::InvalidInput: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}")); + break; + case DeserializationError::NoMemory: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}")); + break; + default: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}")); + break; + } + return; + } + else { + JsonObject obj = doc.as(); + if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); + if (obj.containsKey("command")) { + String scmd = obj["command"]; + command = translateSomfyCommand(scmd); + } + else if(obj.containsKey("target")) { + target = obj["target"].as(); + } + } + } + else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); + } + SomfyShade* shade = somfy.getShadeById(shadeId); + if (shade) { + Serial.print("Received:"); + Serial.println(server.arg("plain")); + // Send the command to the shade. + if(target >= 0 && target <= 100) + shade->moveToTiltTarget(target); + else + shade->sendTiltCommand(command); + DynamicJsonDocument sdoc(256); + JsonObject sobj = sdoc.to(); + shade->toJSON(sobj); + serializeJson(sdoc, g_content); + server.send(200, _encoding_json, g_content); + } + else { + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); + } + }); server.on("/shadeCommand", []() { webServer.sendCORSHeaders(); HTTPMethod method = server.method(); @@ -1210,7 +1274,6 @@ void Web::begin() { serializeJson(doc, g_content); server.send(200, _encoding_json, g_content); }); - server.on("/connectmqtt", []() { DynamicJsonDocument doc(512); DeserializationError err = deserializeJson(doc, server.arg("plain")); diff --git a/data/icons.css b/data/icons.css index e82e783..e3ca825 100644 --- a/data/icons.css +++ b/data/icons.css @@ -561,7 +561,7 @@ i.icss-window-shade { border-bottom: .1em solid transparent; box-shadow: inset 0 1em, 0 0em 0 -.1em; top: -.15em; - left: -.1em; + left: -.09em; } i.icss-window-shade:after { @@ -576,6 +576,52 @@ i.icss-window-shade { background-size: 0.05em 0.05em; background-color: rgba(71, 212, 255, 0); } +i.icss-window-blind { + width: 1.1em; + height: .75em; + background-color: transparent; + border: .05em solid transparent; + border-width: 0 .1em; + box-shadow: inset 0 0 0 .01em, inset 0 .01em 0 .07em, 0 .07em 0; + margin: .2em 0 .07em; +} + + i.icss-window-blind:before { + width: 1.1em; + height: .2em; + border-bottom: .1em solid transparent; + box-shadow: inset 0 1em, 0 0em 0 -.1em; + top: -.1em; + left: -.09em; + } + + i.icss-window-blind:after { + width: calc(100% - .13em); + height: var(--shade-position, 0%); + left: calc(0.077em - .2px); + top: 0.025em; + border-bottom: solid 0.025em gray; + background-image: repeating-linear-gradient(var(--shade-color, currentColor), var(--shade-color, currentColor) 1px, white 4px); + background-color: rgba(71, 212, 255, 0); + } +i.icss-window-tilt { + position:absolute; + width:100%; + height:100%; + left:0px; + top:0px; + color:dimgray; + background:transparent; +} + i.icss-window-tilt:after { + content: attr(data-tiltposition)'%'; + width:100%; + top: calc(50% - .25em); + text-align:center; + font-weight:bold; + font-size:.3em; + } + i.icss-upload { width: 1em; height: .6em; diff --git a/data/index.html b/data/index.html index fd866ea..2f91841 100644 --- a/data/index.html +++ b/data/index.html @@ -3,10 +3,10 @@ - - + + - +
@@ -200,7 +200,7 @@

ESPSomfy RTS
-
+
+
- +

-
+
'; @@ -278,16 +300,15 @@ async function initSockets() { } }; socket.onclose = (evt) => { - procWifiStrength({ ssid: '', channel: -1, strength: -100 }); - procEthernet({ connected: '', speed: 0, fullduplex: false }); - if (document.getElementsByClassName('socket-wait') === 0) + wifi.procWifiStrength({ ssid: '', channel: -1, strength: -100 }); + wifi.procEthernet({ connected: '', speed: 0, fullduplex: false }); + if (document.getElementsByClassName('socket-wait').length === 0) waitMessage(document.getElementById('divContainer')).classList.add('socket-wait'); if (evt.wasClean) { console.log({ msg: 'close-clean', evt: evt }); connectFailed = 0; - tConnect = setTimeout(async () => { await reopenSocket(); }, 10000); - console.log('Reconnecting socket in 10 seconds'); - + tConnect = setTimeout(async () => { await reopenSocket(); }, 7000); + console.log('Reconnecting socket in 7 seconds'); } else { console.log({ msg: 'close-died', reason: evt.reason, evt: evt, sock: socket }); @@ -334,7 +355,7 @@ async function reopenSocket() { await initSockets(); } class General { - appVersion = 'v1.3.0'; + appVersion = 'v1.3.1'; reloadApp = false; async init() { this.setAppVersion(); @@ -563,7 +584,7 @@ class Wifi { { val: 2, label: 'Olimex ESP32-POE', clk: 3, ct: 0, addr: 0, pwr: 12, mdc: 23, mdio: 18 }, { val: 3, label: 'Olimex ESP32-EVB', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 23, mdio: 18 }, { val: 4, label: 'LILYGO T-Internet POE', clk: 3, ct: 0, addr: 0, pwr: 16, mdc: 23, mdio: 18 }, - { val: 5, label: 'wESP32 v7+', clk: 0, ct: 3, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, + { val: 5, label: 'wESP32 v7+', clk: 0, ct: 2, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, { val: 6, label: 'wESP32 < v7', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 16, mdio: 17 } ]; ethClockModes = [{ val: 0, label: 'GPIO0 IN' }, { val: 1, label: 'GPIO0 OUT' }, { val: 2, label: 'GPIO16 OUT' }, { val: 3, label: 'GPIO17 OUT' }]; @@ -984,7 +1005,7 @@ class Somfy { let divCtl = ''; for (let i = 0; i < shades.length; i++) { let shade = shades[i]; - divCfg += `
`; + divCfg += `
`; divCfg += `
`; //divCfg += ``; divCfg += `${shade.name}`; @@ -992,96 +1013,111 @@ class Somfy { divCfg += `
`; divCfg += '
'; - divCtl += `
`; - divCtl += `
`; - divCtl += `
`; + divCtl += `
`; + divCtl += ``; + divCtl += shade.hasTilt ? `
` : '
'; divCtl += `
`; divCtl += `${shade.name}`; divCtl += `${shade.myPos !== 255 ? shade.myPos + '%' : '---'}` divCtl += '
' divCtl += `
`; - divCtl += `
`; - divCtl += `
my
`; - divCtl += `
`; + divCtl += `
`; + divCtl += `
my
`; + divCtl += `
`; divCtl += '
'; } document.getElementById('divShadeList').innerHTML = divCfg; let shadeControls = document.getElementById('divShadeControls'); shadeControls.innerHTML = divCtl; // Attach the timer for setting the My Position for the shade. - let btns = shadeControls.querySelectorAll('div.my-button'); + let btns = shadeControls.querySelectorAll('div.cmd-button'); for (let i = 0; i < btns.length; i++) { btns[i].addEventListener('mouseup', (event) => { console.log(this); console.log(event); console.log('mouseup'); + let cmd = event.currentTarget.getAttribute('data-cmd'); + let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10); if (this.btnTimer) { clearTimeout(this.btnTimer); this.btnTimer = null; + if (new Date().getTime() - this.btnDown > 2000) event.preventDefault(); + else this.sendCommand(shadeId, cmd); } - let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10); - if (new Date().getTime() - this.btnDown > 2000) { - event.preventDefault(); - } - else { - this.sendCommand(shadeId, 'my'); - } - + else this.sendCommand(shadeId, cmd); }, true); btns[i].addEventListener('mousedown', (event) => { - if (this.btnTimer) return; + if (this.btnTimer) { + clearTimeout(this.btnTimer); + this.btnTimer = null; + } console.log(this); console.log(event); - console.log('mousedown'); - + let elShade = event.currentTarget.closest('div.somfyShadeCtl'); + let cmd = event.currentTarget.getAttribute('data-cmd'); let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10); let el = event.currentTarget.closest('.somfyShadeCtl'); this.btnDown = new Date().getTime(); - if (parseInt(el.getAttribute('data-direction'), 10) === 0) { - this.btnTimer = setTimeout(() => { - // Open up the set My Position dialog. We will allow the user to change the position to match - // the desired position. - this.openSetMyPosition(shadeId); - - }, 2000); + if (cmd === 'my') { + if (parseInt(el.getAttribute('data-direction'), 10) === 0) { + this.btnTimer = setTimeout(() => { + // Open up the set My Position dialog. We will allow the user to change the position to match + // the desired position. + this.openSetMyPosition(shadeId); + }, 2000); + } } - - }, true); - btns[i].addEventListener('touchstart', (event) => { - let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10); - let el = event.currentTarget.closest('.somfyShadeCtl'); - console.log('touchstart'); - - this.btnDown = new Date().getTime(); - if (parseInt(el.getAttribute('data-direction'), 10) === 0) { + else if (makeBool(elShade.getAttribute('data-tilt'))) { this.btnTimer = setTimeout(() => { - // Open up the set My Position dialog. We will allow the user to change the position to match - // the desired position. - this.openSetMyPosition(shadeId); - + this.sendTiltCommand(shadeId, cmd); }, 2000); } }, true); - /* - btns[i].addEventListener('touchend', (event) => { - event.preventDefault(); // Make sure the idiot - console.log(this); - console.log(event); + btns[i].addEventListener('touchstart', (event) => { if (this.btnTimer) { clearTimeout(this.btnTimer); this.btnTimer = null; } + console.log(this); + console.log(event); + console.log('touchstart'); + let elShade = event.currentTarget.closest('div.somfyShadeCtl'); + let cmd = event.currentTarget.getAttribute('data-cmd'); let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10); - if (new Date().getTime() - this.btnDown > 2000) { - event.preventDefault(); - } - else { - this.sendCommand(shadeId, 'my'); + let el = event.currentTarget.closest('.somfyShadeCtl'); + this.btnDown = new Date().getTime(); + if (parseInt(el.getAttribute('data-direction'), 10) === 0) { + if (cmd === 'my') { + this.btnTimer = setTimeout(() => { + // Open up the set My Position dialog. We will allow the user to change the position to match + // the desired position. + this.openSetMyPosition(shadeId); + }, 2000); + } + else { + if (makeBool(elShade.getAttribute('data-tilt'))) { + this.btnTimer = setTimeout(() => { + this.sendTiltCommand(shadeId, cmd); + }, 2000); + } + } } }, true); - */ } }; closeShadePositioners() { @@ -1196,12 +1232,23 @@ class Somfy { for (let i = 0; i < icons.length; i++) { icons[i].style.setProperty('--shade-position', `${state.position}%`); } + if (state.hasTilt) { + let tilts = document.querySelectorAll(`.icss-window-tilt[data-shadeid="${state.shadeId}"]`); + for (let i = 0; i < tilts.length; i++) { + tilts[i].setAttribute('data-tiltposition', `${state.tiltPosition}`); + } + } let divs = document.querySelectorAll(`.somfyShadeCtl[data-shadeid="${state.shadeId}"]`); for (let i = 0; i < divs.length; i++) { divs[i].setAttribute('data-direction', state.direction); divs[i].setAttribute('data-position', state.position); divs[i].setAttribute('data-target', state.target); divs[i].setAttribute('data-mypos', state.mypos); + if (state.hasTilt) { + divs[i].setAttribute('data-tiltdirection', state.tiltDirection); + divs[i].setAttribute('data-tiltposition', state.tiltPosition); + divs[i].setAttribute('data-tilttarget', state.tiltTarget); + } let span = divs[i].querySelector('#spanMyPos'); if (span) span.innerHTML = typeof state.mypos !== 'undefined' && state.mypos !== 255 ? `${state.mypos}%` : '---'; } @@ -1235,6 +1282,26 @@ class Somfy { document.getElementById('somfyTransceiver').style.display = 'none'; document.getElementById('somfyMain').style.display = ''; }; + onShadeTypeChanged(el) { + let sel = document.getElementById('selShadeType'); + let tilt = document.getElementById('cbHasTilt').checked; + let ico = document.getElementById('icoShade'); + switch (parseInt(sel.value, 10)) { + case 1: + document.getElementById('divTiltSettings').style.display = ''; + if (ico.classList.contains('icss-window-shade')) ico.classList.remove('icss-window-shade'); + if (!ico.classList.contains('icss-window-blind')) ico.classList.add('icss-window-blind'); + break; + default: + if (ico.classList.contains('icss-window-blind')) ico.classList.remove('icss-window-blind'); + if (!ico.classList.contains('icss-window-shade')) ico.classList.add('icss-window-shade'); + document.getElementById('divTiltSettings').style.display = 'none'; + tilt = false; + break; + } + document.getElementById('fldTiltTime').parentElement.style.display = tilt ? 'inline-block' : 'none'; + document.querySelector('#divSomfyButtons i.icss-window-tilt').style.display = tilt ? '' : 'none'; + }; openEditShade(shadeId) { console.log('Opening Edit Shade'); if (typeof shadeId === 'undefined') { @@ -1281,13 +1348,28 @@ class Somfy { document.getElementById('somfyShade').style.display = ''; document.getElementById('btnSaveShade').style.display = 'inline-block'; document.getElementById('btnLinkRemote').style.display = ''; + document.getElementById('selShadeType').value = shade.shadeType; document.getElementsByName('shadeAddress')[0].value = shade.remoteAddress; document.getElementsByName('shadeName')[0].value = shade.name; document.getElementsByName('shadeUpTime')[0].value = shade.upTime; document.getElementsByName('shadeDownTime')[0].value = shade.downTime; + document.getElementById('fldTiltTime').value = shade.tiltTime; + document.getElementById('cbHasTilt').checked = shade.hasTilt; + this.onShadeTypeChanged(document.getElementById('selShadeType')); let ico = document.getElementById('icoShade'); + switch (shade.shadeType) { + case 1: + ico.classList.remove('icss-window-shade'); + ico.classList.add('icss-window-blind'); + break; + } + let tilt = ico.parentElement.querySelector('i.icss-window-tilt'); + tilt.style.display = shade.hasTilt ? '' : 'none'; + tilt.setAttribute('data-tiltposition', shade.tiltPosition); ico.style.setProperty('--shade-position', `${shade.position}%`); + ico.style.setProperty('--tilt-position', `${shade.tiltPosition}%`); ico.setAttribute('data-shadeid', shade.shadeId); + document.getElementById('btnSetRollingCode').style.display = 'inline-block'; if (shade.paired) { document.getElementById('btnUnpairShade').style.display = 'inline-block'; } @@ -1319,8 +1401,14 @@ class Somfy { remoteAddress: parseInt(document.getElementsByName('shadeAddress')[0].value, 10), name: document.getElementsByName('shadeName')[0].value, upTime: parseInt(document.getElementsByName('shadeUpTime')[0].value, 10), - downTime: parseInt(document.getElementsByName('shadeDownTime')[0].value, 10) + downTime: parseInt(document.getElementsByName('shadeDownTime')[0].value, 10), + shadeType: parseInt(document.getElementById('selShadeType').value, 10), + tiltTime: parseInt(document.getElementById('fldTiltTime').value, 10) }; + if (obj.shadeType == 1) { + obj.hasTilt = document.getElementById('cbHasTilt').checked; + } + else obj.hasTilt = false; let valid = true; if (valid && (isNaN(obj.remoteAddress) || obj.remoteAddress < 1 || obj.remoteAddress > 16777215)) { errorMessage(document.getElementById('fsSomfySettings'), 'The remote address must be a number between 1 and 16777215. This number must be unique for all shades.'); @@ -1338,8 +1426,6 @@ class Somfy { errorMessage(document.getElementById('fsSomfySettings'), 'Down Time must be a value between 0 and 65,355 milliseconds. This is the travel time to go from full open to full closed.'); valid = false; } - - console.log(obj); if (valid) { let overlay = waitMessage(document.getElementById('fsSomfySettings')); if (isNaN(shadeId) || shadeId >= 255) { @@ -1358,13 +1444,14 @@ class Somfy { else { document.getElementById('btnPairShade').style.display = 'inline-block'; } - document.getElementById('btnSetRollingCode').style.display = ''; + document.getElementById('btnSetRollingCode').style.display = 'inline-block'; }); } else { obj.shadeId = shadeId; + console.log(obj); putJSON('/saveShade', obj, (err, shade) => { console.log(shade); // We are updating. @@ -1554,7 +1641,7 @@ class Somfy { return div; }; sendCommand(shadeId, command) { - let data = { shadeId: shadeId }; + console.log(`Sending Shade command ${shadeId}-${command}`); if (isNaN(parseInt(command, 10))) putJSON('/shadeCommand', { shadeId: shadeId, command: command }, (err, shade) => { }); @@ -1562,6 +1649,16 @@ class Somfy { putJSON('/shadeCommand', { shadeId: shadeId, target: parseInt(command, 10) }, (err, shade) => { }); }; + sendTiltCommand(shadeId, command) { + console.log(`Sending Tilt command ${shadeId}-${command}`); + if (isNaN(parseInt(command, 10))) + putJSON('/tiltCommand', { shadeId: shadeId, command: command }, (err, shade) => { + }); + else + putJSON('/tiltCommand', { shadeId: shadeId, target: parseInt(command, 10) }, (err, shade) => { + }); + }; + linkRemote(shadeId) { let div = document.createElement('div'); let html = `
`; @@ -1606,8 +1703,15 @@ class Somfy { positioner.querySelector(`.shade-target`).innerHTML = el.value; somfy.sendCommand(shadeId, el.value); } - } + processShadeTiltTarget(el, shadeId) { + let positioner = document.querySelector(`.shade-positioner[data-shadeid="${shadeId}"]`); + if (positioner) { + positioner.querySelector(`.shade-tilt-target`).innerHTML = el.value; + somfy.sendTiltCommand(shadeId, el.value); + } + } + openSetPosition(shadeId) { console.log('Opening Shade Positioner'); if (typeof shadeId === 'undefined') { @@ -1628,6 +1732,11 @@ class Somfy { let html = `
${shadeName}
`; html += ``; html += ``; + if (makeBool(shade.getAttribute('data-tilt'))) { + let currTiltPos = parseInt(shade.getAttribute('data-tilttarget'), 10); + html += ``; + html += ``; + } html += `
`; let div = document.createElement('div'); div.setAttribute('class', 'shade-positioner'); diff --git a/data/main.css b/data/main.css index 840cd6b..3b7fe43 100644 --- a/data/main.css +++ b/data/main.css @@ -564,6 +564,7 @@ div.waitoverlay > .lds-roller { vertical-align: middle; margin-top: -5px; font-size: 48px; + position:relative; } .somfyShadeCtl .shade-name { display:inline-block; @@ -597,15 +598,20 @@ div.waitoverlay > .lds-roller { } .shade-positioner { - position:absolute; - width:100%; - background-color:gainsboro; - color:gray; - min-height:60px; - top:0px; - padding-left:7px; - padding-right:7px; - z-index:100; + position: absolute; + width: 100%; + background-color: oldlace; + color: gray; + min-height: 60px; + top: 0px; + padding-left: 7px; + padding-right: 7px; + padding-bottom: 7px; + z-index: 100; + margin-top: -7px; + box-shadow: 4px 4px 4px gray; + border: solid 1px silver; + border-radius: 5px; } .shade-positioner .shade-name { display:block; @@ -623,6 +629,7 @@ div.waitoverlay > .lds-roller { font-size:1em; margin-top:-3px; margin-left:27px; + color:gray; } .shade-positioner label > span:last-child { float: right;