diff --git a/ConfigSettings.h b/ConfigSettings.h index 170ad78..dc75e45 100644 --- a/ConfigSettings.h +++ b/ConfigSettings.h @@ -3,7 +3,7 @@ #ifndef configsettings_h #define configsettings_h -#define FW_VERSION "v2.1.7" +#define FW_VERSION "v2.1.8" enum DeviceStatus { DS_OK = 0, DS_ERROR = 1, diff --git a/MQTT.cpp b/MQTT.cpp index 3b0534c..c8a476e 100644 --- a/MQTT.cpp +++ b/MQTT.cpp @@ -126,6 +126,24 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { if(val > 0) shade->sendCommand(somfy_commands::SunFlag); else shade->sendCommand(somfy_commands::Flag); } + else if(strncmp(command, "position", sizeof(command)) == 0) { + if(val >= 0 && val <= 100) { + shade->target = shade->currentPos = (float)val; + shade->emitState(); + } + } + else if(strncmp(command, "tiltPosition", sizeof(command)) == 0) { + if(val >= 0 && val <= 100) { + shade->tiltTarget = shade->currentTiltPos = (float)val; + shade->emitState(); + } + } + else if(strncmp(command, "sunny", sizeof(command)) == 0) { + if(val >= 0) shade->sendSensorCommand(-1, val, shade->repeats); + } + else if(strncmp(command, "windy", sizeof(command)) == 0) { + if(val >= 0) shade->sendSensorCommand(val, -1, shade->repeats); + } } } else if(strncmp(entityType, "groups", sizeof(entityType)) == 0) { @@ -146,6 +164,12 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { else group->sendCommand(somfy_commands::SunFlag); } + else if(strncmp(command, "sunny", sizeof(command)) == 0) { + if(val >= 0) group->sendSensorCommand(-1, val, group->repeats); + } + else if(strncmp(command, "windy", sizeof(command)) == 0) { + if(val >= 0) group->sendSensorCommand(val, -1, group->repeats); + } } } } @@ -172,7 +196,15 @@ bool MQTTClass::connect() { this->subscribe("shades/+/mypos/set"); this->subscribe("shades/+/myTiltPos/set"); this->subscribe("shades/+/sunFlag/set"); + this->subscribe("shades/+/sunny/set"); + this->subscribe("shades/+/windy/set"); + this->subscribe("shades/+/position/set"); + this->subscribe("shades/+/tiltPosition/set"); this->subscribe("groups/+/direction/set"); + this->subscribe("groups/+/sunFlag/set"); + this->subscribe("groups/+/sunny/set"); + this->subscribe("groups/+/windy/set"); + mqttClient.setCallback(MQTTClass::receive); this->lastConnect = millis(); return true; @@ -198,6 +230,14 @@ bool MQTTClass::disconnect() { this->unsubscribe("shades/+/myTiltPos/set"); this->unsubscribe("shades/+/sunFlag/set"); this->unsubscribe("groups/+/direction/set"); + this->unsubscribe("shades/+/sunny/set"); + this->unsubscribe("shades/+/windy/set"); + this->unsubscribe("shades/+/position/set"); + this->unsubscribe("shades/+/tiltPosition/set"); + this->unsubscribe("groups/+/direction/set"); + this->unsubscribe("groups/+/sunFlag/set"); + this->unsubscribe("groups/+/sunny/set"); + this->unsubscribe("groups/+/windy/set"); mqttClient.disconnect(); } return true; diff --git a/Somfy.cpp b/Somfy.cpp index cb78c41..927fa12 100644 --- a/Somfy.cpp +++ b/Somfy.cpp @@ -143,7 +143,6 @@ void somfy_frame_t::decodeFrame(byte* frame) { // We reuse this memory address so we must reset the processed // flag. This will ensure we can see frames on the first beat. this->processed = false; - Serial.println("Processed set to false"); // Pull in the data for an 80-bit step command. if(this->cmd == somfy_commands::StepDown) this->cmd = (somfy_commands)((decoded[1] >> 4) | ((decoded[8] & 0x08) << 4)); this->rollingCode = decoded[3] + (decoded[2] << 8); @@ -856,6 +855,7 @@ void SomfyShade::checkMovement() { && this->noSunStart && (curTime - this->noSunStart) >= SOMFY_NO_SUN_TIMEOUT) { + if(this->tiltType == tilt_types::tiltonly) this->tiltTarget = 0.0f; this->target = 0.0f; this->noSunDone = true; Serial.printf("[%u] No Sun -> done\r\n", this->shadeId); @@ -867,6 +867,7 @@ void SomfyShade::checkMovement() { && this->windStart && (curTime - this->windStart) >= SOMFY_WIND_TIMEOUT) { + if(this->tiltType == tilt_types::tiltonly) this->tiltTarget = 0.0f; this->target = 0.0f; this->windDone = true; Serial.printf("[%u] Wind -> done\r\n", this->shadeId); @@ -2635,6 +2636,32 @@ somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) { } return cmd; } +void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat) { + uint8_t flags = (this->flags >> 4) & 0x0F; + if(isWindy > 0) flags |= 0x01; + if(isSunny > 0) flags |= 0x02; + if(isWindy == 0) flags &= ~0x01; + if(isSunny == 0) flags &= ~0x02; + + // Now ship this off as an 80 bit command. + this->lastFrame.remoteAddress = this->getRemoteAddress(); + this->lastFrame.repeats = repeat; + this->lastFrame.bitLength = this->bitLength; + this->lastFrame.rollingCode = (uint16_t)flags; + this->lastFrame.encKey = 160; // Sensor commands are always encryption code 160. + this->lastFrame.cmd = somfy_commands::Sensor; + this->lastFrame.processed = false; + Serial.print("CMD:"); + Serial.print(translateSomfyCommand(this->lastFrame.cmd)); + Serial.print(" ADDR:"); + Serial.print(this->lastFrame.remoteAddress); + Serial.print(" RCODE:"); + Serial.print(this->lastFrame.rollingCode); + Serial.print(" REPEAT:"); + Serial.println(repeat); + somfy.sendFrame(this->lastFrame, repeat); + somfy.processFrame(this->lastFrame, true); +} void SomfyRemote::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) { this->lastFrame.rollingCode = this->getNextRollingCode(); diff --git a/Somfy.h b/Somfy.h index d5a5a4e..c8ab5de 100644 --- a/Somfy.h +++ b/Somfy.h @@ -201,6 +201,7 @@ class SomfyRemote { void setLight(bool bHasLight); virtual void sendCommand(somfy_commands cmd); virtual void sendCommand(somfy_commands cmd, uint8_t repeat); + void sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat); void repeatFrame(uint8_t repeat); somfy_commands transformCommand(somfy_commands cmd); }; diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin index 4f7d750..bbd7fe8 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 afafa8b..90b6937 100644 Binary files a/SomfyController.littlefs.bin and b/SomfyController.littlefs.bin differ diff --git a/Web.cpp b/Web.cpp index 9d973a4..1e00d26 100644 --- a/Web.cpp +++ b/Web.cpp @@ -696,6 +696,133 @@ void Web::handleDiscovery(WebServer &server) { else server.send(500, _encoding_text, "Invalid http method"); } +void Web::handleSetPositions(WebServer &server) { + webServer.sendCORSHeaders(server); + if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } + HTTPMethod method = apiServer.method(); + uint8_t shadeId = (server.hasArg("shadeId")) ? atoi(server.arg("shadeId").c_str()) : 255; + int8_t pos = (server.hasArg("position")) ? atoi(server.arg("position").c_str()) : -1; + int8_t tiltPos = (server.hasArg("tiltPosition")) ? atoi(server.arg("tiltPosition").c_str()) : -1; + if(server.hasArg("plain")) { + DynamicJsonDocument doc(512); + DeserializationError err = deserializeJson(doc, server.arg("plain")); + if (err) { + switch (err.code()) { + case DeserializationError::InvalidInput: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}")); + break; + case DeserializationError::NoMemory: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}")); + break; + default: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}")); + break; + } + } + else { + JsonObject obj = doc.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + if(obj.containsKey("position")) pos = obj["position"]; + if(obj.containsKey("tiltPosition")) tiltPos = obj["tiltPosition"]; + } + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + if(pos >= 0) shade->target = shade->currentPos = pos; + if(tiltPos >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = tiltPos; + shade->emitState(); + DynamicJsonDocument sdoc(2048); + 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\":\"An invalid shadeId was provided\"}")); + } + else { + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}")); + } +} +void Web::handleSetSensor(WebServer &server) { + webServer.sendCORSHeaders(server); + if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } + HTTPMethod method = apiServer.method(); + uint8_t shadeId = (server.hasArg("shadeId")) ? atoi(server.arg("shadeId").c_str()) : 255; + uint8_t groupId = (server.hasArg("groupId")) ? atoi(server.arg("groupId").c_str()) : 255; + int8_t sunny = (server.hasArg("sunny")) ? toBoolean(server.arg("sunny").c_str(), false) ? 1 : 0 : -1; + int8_t windy = (server.hasArg("windy")) ? atoi(server.arg("windy").c_str()) : -1; + int8_t repeat = (server.hasArg("repeat")) ? atoi(server.arg("repeat").c_str()) : -1; + if(server.hasArg("plain")) { + DynamicJsonDocument doc(512); + DeserializationError err = deserializeJson(doc, server.arg("plain")); + if (err) { + switch (err.code()) { + case DeserializationError::InvalidInput: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}")); + break; + case DeserializationError::NoMemory: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}")); + break; + default: + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}")); + break; + } + } + else { + JsonObject obj = doc.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"].as(); + if(obj.containsKey("groupId")) groupId = obj["groupId"].as(); + if(obj.containsKey("sunny")) { + if(obj["sunny"].is()) + sunny = obj["sunny"].as() ? 1 : 0; + else + sunny = obj["sunny"].as(); + } + if(obj.containsKey("windy")) { + if(obj["windy"].is()) + windy = obj["windy"].as() ? 1 : 0; + else + windy = obj["windy"].as(); + } + if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); + } + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + shade->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : shade->repeats); + shade->emitState(); + DynamicJsonDocument sdoc(2048); + 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\":\"An invalid shadeId was provided\"}")); + + } + else if(groupId != 255) { + SomfyGroup *group = somfy.getGroupById(groupId); + if(group) { + group->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : group->repeats); + group->emitState(); + DynamicJsonDocument sdoc(2048); + JsonObject sobj = sdoc.to(); + group->toJSON(sobj); + serializeJson(sdoc, g_content); + server.send(200, _encoding_json, g_content); + } + else + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid groupId was provided\"}")); + } + else { + server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}")); + } +} + void Web::handleNotFound(WebServer &server) { HTTPMethod method = server.method(); Serial.printf("Request %s 404-%d ", server.uri().c_str(), method); @@ -743,13 +870,16 @@ void Web::begin() { apiServer.on("/repeatCommand", []() { webServer.handleRepeatCommand(apiServer); }); apiServer.on("/shade", HTTP_GET, [] () { webServer.handleShade(apiServer); }); apiServer.on("/group", HTTP_GET, [] () { webServer.handleGroup(apiServer); }); - + apiServer.on("/setPositions", []() { webServer.handleSetPositions(apiServer); }); + apiServer.on("/setSensor", []() { webServer.handleSetSensor(apiServer); }); + // Web Interface server.on("/tiltCommand", []() { webServer.handleTiltCommand(server); }); server.on("/repeatCommand", []() { webServer.handleRepeatCommand(server); }); server.on("/shadeCommand", []() { webServer.handleShadeCommand(server); }); server.on("/groupCommand", []() { webServer.handleGroupCommand(server); }); - + server.on("/setPositions", []() { webServer.handleSetPositions(server); }); + server.on("/setSensor", []() { webServer.handleSetSensor(server); }); server.on("/upnp.xml", []() { SSDP.schema(server.client()); }); server.on("/", []() { webServer.handleStreamFile(server, "/index.html", _encoding_html); }); server.on("/login", []() { webServer.handleLogin(server); }); @@ -1623,14 +1753,15 @@ void Web::begin() { if (Update.hasError()) server.send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating firmware: \"}"); else - server.send(200, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Updating firmware: \"}"); + server.send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated firmware\"}"); rebootDelay.reboot = true; rebootDelay.rebootTime = millis() + 500; }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { webServer.uploadSuccess = false; - Serial.printf("Update: %s\n", upload.filename.c_str()); + Serial.printf("Update: %s - %d\n", upload.filename.c_str(), upload.totalSize); + //if(!Update.begin(upload.totalSize, U_SPIFFS)) { if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size Update.printError(Serial); } @@ -1639,10 +1770,16 @@ void Web::begin() { mqtt.end(); } } + else if(upload.status == UPLOAD_FILE_ABORTED) { + Serial.printf("Upload of %s aborted\n", upload.filename.c_str()); + Update.abort(); + } else if (upload.status == UPLOAD_FILE_WRITE) { /* flashing firmware to ESP*/ if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); + Serial.printf("Upload of %s aborted invalid size %d\n", upload.filename.c_str(), upload.currentSize); + Update.abort(); } } else if (upload.status == UPLOAD_FILE_END) { @@ -1683,14 +1820,18 @@ void Web::begin() { webServer.sendCORSHeaders(server); if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } server.sendHeader("Connection", "close"); - server.send(200, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Updating Application: \"}"); + if (Update.hasError()) + server.send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating application: \"}"); + else + server.send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated application\"}"); rebootDelay.reboot = true; rebootDelay.rebootTime = millis() + 500; }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { webServer.uploadSuccess = false; - Serial.printf("Update: %s\n", upload.filename.c_str()); + Serial.printf("Update: %s %d\n", upload.filename.c_str(), upload.totalSize); + //if(!Update.begin(upload.totalSize, U_SPIFFS)) { if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { //start with max available size and tell it we are updating the file system. Update.printError(Serial); } @@ -1699,10 +1840,17 @@ void Web::begin() { mqtt.end(); } } + else if(upload.status == UPLOAD_FILE_ABORTED) { + Serial.printf("Upload of %s aborted\n", upload.filename.c_str()); + Update.abort(); + somfy.commit(); + } else if (upload.status == UPLOAD_FILE_WRITE) { /* flashing littlefs to ESP*/ if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); + Serial.printf("Upload of %s aborted invalid size %d\n", upload.filename.c_str(), upload.currentSize); + Update.abort(); } } else if (upload.status == UPLOAD_FILE_END) { @@ -1712,6 +1860,7 @@ void Web::begin() { somfy.commit(); } else { + somfy.commit(); Update.printError(Serial); } } diff --git a/Web.h b/Web.h index 4d49c00..c24b5f8 100644 --- a/Web.h +++ b/Web.h @@ -22,6 +22,8 @@ class Web { void handleNotFound(WebServer &server); void handleShade(WebServer &server); void handleGroup(WebServer &server); + void handleSetPositions(WebServer &server); + void handleSetSensor(WebServer &server); void begin(); void loop(); void end(); diff --git a/data/appversion b/data/appversion index 9671f9a..b370e25 100644 --- a/data/appversion +++ b/data/appversion @@ -1 +1 @@ -2.1.7 \ No newline at end of file +2.1.8 \ No newline at end of file diff --git a/data/index.html b/data/index.html index 56c3ca3..6975adc 100644 --- a/data/index.html +++ b/data/index.html @@ -3,11 +3,11 @@ - - - + + + - +
@@ -611,6 +611,24 @@
off
+
+ + Sensor + + + + + + + + + + + +
+
send
+
+