diff --git a/ConfigFile.cpp b/ConfigFile.cpp index 5769a2a..b616393 100644 --- a/ConfigFile.cpp +++ b/ConfigFile.cpp @@ -7,9 +7,9 @@ extern Preferences pref; -#define SHADE_HDR_VER 15 +#define SHADE_HDR_VER 16 #define SHADE_HDR_SIZE 56 -#define SHADE_REC_SIZE 264 +#define SHADE_REC_SIZE 268 #define GROUP_REC_SIZE 184 #define TRANS_REC_SIZE 74 @@ -661,16 +661,21 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { shade->gpioUp = this->readUInt8(shade->gpioUp); shade->gpioDown = this->readUInt8(shade->gpioDown); } + if(this->header.version > 15) { + shade->gpioMy = this->readUInt8(shade->gpioMy); + } if(shade->getShadeId() == 255) shade->clear(); else if(shade->tiltType == tilt_types::tiltonly) { shade->myPos = shade->currentPos = shade->target = 100.0f; } pref.end(); - if(shade->proto == radio_proto::GPIO) { + if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { pinMode(shade->gpioUp, OUTPUT); pinMode(shade->gpioDown, OUTPUT); } + if(shade->proto == radio_proto::GP_Remote) + pinMode(shade->gpioMy, OUTPUT); return true; } bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { @@ -769,7 +774,8 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) { this->writeUInt8(shade->repeats); this->writeUInt8(shade->sortOrder); this->writeUInt8(shade->gpioUp); - this->writeUInt8(shade->gpioDown, CFG_REC_END); + this->writeUInt8(shade->gpioDown); + this->writeUInt8(shade->gpioMy, CFG_REC_END); return true; } bool ShadeConfigFile::writeSettingsRecord() { diff --git a/MQTT.cpp b/MQTT.cpp index b442ecf..f4e9591 100644 --- a/MQTT.cpp +++ b/MQTT.cpp @@ -4,6 +4,7 @@ #include "MQTT.h" #include "ConfigSettings.h" #include "Somfy.h" +#include "Network.h" WiFiClient tcpClient; PubSubClient mqttClient(tcpClient); @@ -13,6 +14,9 @@ static char g_content[MQTT_MAX_RESPONSE]; extern ConfigSettings settings; extern SomfyShadeController somfy; +extern Network net; + + bool MQTTClass::begin() { this->suspended = false; return true; @@ -197,6 +201,7 @@ bool MQTTClass::connect() { this->publish("host", settings.hostname, true); this->publish("firmware", settings.fwVersion, true); this->publish("serverId", settings.serverId, true); + this->publish("mac", net.mac.c_str()); somfy.publish(); this->subscribe("shades/+/target/set"); this->subscribe("shades/+/tiltTarget/set"); diff --git a/Somfy.cpp b/Somfy.cpp index c4ac2c6..7663986 100644 --- a/Somfy.cpp +++ b/Somfy.cpp @@ -801,7 +801,7 @@ bool SomfyShade::isInGroup() { return false; } void SomfyShade::setGPIOs() { - if(this->proto == radio_proto::GPIO) { + if(this->proto == radio_proto::GP_Relay) { // Determine whether the direction needs to be set. int8_t dir = this->direction; if(dir == 0 && this->tiltType == tilt_types::integrated) @@ -825,6 +825,63 @@ void SomfyShade::setGPIOs() { } this->gpioDir = dir; } + else if(this->proto == radio_proto::GP_Remote) { + if(millis() > this->gpioRelease) { + digitalWrite(this->gpioUp, LOW); + digitalWrite(this->gpioDown, LOW); + digitalWrite(this->gpioMy, LOW); + this->gpioRelease = 0; + } + } +} +void SomfyRemote::triggerGPIOs(somfy_frame_t &frame) { + if(this->proto == radio_proto::GP_Remote) { + int8_t dir = 0; + switch(frame.cmd) { + case somfy_commands::My: + digitalWrite(this->gpioUp, LOW); + digitalWrite(this->gpioDown, LOW); + digitalWrite(this->gpioMy, HIGH); + dir = 0; + if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false, MY: true\n"); + break; + case somfy_commands::Up: + digitalWrite(this->gpioMy, LOW); + digitalWrite(this->gpioDown, LOW); + digitalWrite(this->gpioUp, HIGH); + dir = -1; + Serial.printf("UP: true, DOWN: false, MY: false\n"); + break; + case somfy_commands::Toggle: + case somfy_commands::Down: + digitalWrite(this->gpioMy, LOW); + digitalWrite(this->gpioUp, LOW); + digitalWrite(this->gpioDown, HIGH); + dir = 1; + Serial.printf("UP: false, DOWN: true, MY: false\n"); + break; + case somfy_commands::MyUp: + digitalWrite(this->gpioDown, LOW); + digitalWrite(this->gpioMy, HIGH); + digitalWrite(this->gpioUp, HIGH); + Serial.printf("UP: true, DOWN: false, MY: true\n"); + break; + case somfy_commands::MyDown: + digitalWrite(this->gpioUp, LOW); + digitalWrite(this->gpioMy, HIGH); + digitalWrite(this->gpioDown, HIGH); + Serial.printf("UP: true, DOWN: false, MY: true\n"); + break; + case somfy_commands::MyUpDown: + digitalWrite(this->gpioUp, HIGH); + digitalWrite(this->gpioMy, HIGH); + digitalWrite(this->gpioDown, HIGH); + Serial.printf("UP: true, DOWN: true, MY: true\n"); + break; + } + this->gpioRelease = millis() + (frame.repeats * 200); + this->gpioDir = dir; + } } void SomfyShade::checkMovement() { const uint64_t curTime = millis(); @@ -2439,11 +2496,12 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) { int8_t ret = 0; if(obj.containsKey("proto")) { radio_proto proto = this->proto; - if(proto == radio_proto::GPIO) { + if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) { // Check to see if we are using the up and or down // GPIOs anywhere else. uint8_t upPin = obj.containsKey("gpioUp") ? obj["gpioUp"].as() : this->gpioUp; uint8_t downPin = obj.containsKey("gpioDown") ? obj["gpioDown"].as() : this->gpioDown; + uint8_t myPin = obj.containsKey("gpioMy") ? obj["gpioMy"].as() : this->gpioMy; if(somfy.transceiver.config.enabled) { if(somfy.transceiver.config.SCKPin == upPin || somfy.transceiver.config.SCKPin == downPin || somfy.transceiver.config.TXPin == upPin || somfy.transceiver.config.TXPin == downPin || @@ -2452,27 +2510,55 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) { somfy.transceiver.config.MISOPin == upPin || somfy.transceiver.config.MISOPin == downPin || somfy.transceiver.config.CSNPin == upPin || somfy.transceiver.config.CSNPin == downPin) ret = -10; // Pin in use with transceiver. + else if(proto == radio_proto::GP_Remote) { + if(somfy.transceiver.config.SCKPin == myPin || + somfy.transceiver.config.TXPin == myPin || + somfy.transceiver.config.RXPin == myPin || + somfy.transceiver.config.MOSIPin == myPin || + somfy.transceiver.config.MISOPin == myPin || + somfy.transceiver.config.CSNPin == myPin) + ret = -10; // Pin in use with transceiver. + } } if(settings.connType == conn_types::ethernet || settings.connType == conn_types::ethernetpref) { if((settings.Ethernet.CLKMode == 0 || settings.Ethernet.CLKMode == 1) && (upPin == 0 || downPin == 0)) ret = -11; // Pin in use with ethernet. + else if(proto == radio_proto::GP_Remote && ((settings.Ethernet.CLKMode == 0 || settings.Ethernet.CLKMode == 1) && myPin == 0)) + ret = -11; // Pin in use with ethernet. else if((settings.Ethernet.CLKMode == 2 && (upPin == 16 || downPin == 16)) || (settings.Ethernet.CLKMode == 3 && (upPin == 17 || downPin == 17))) ret = -11; + else if(proto == radio_proto::GP_Remote && (settings.Ethernet.CLKMode == 2 && myPin == 16 || settings.Ethernet.CLKMode == 3 && myPin == 17)) + ret = -11; else if(settings.Ethernet.PWRPin == upPin || settings.Ethernet.PWRPin == downPin || settings.Ethernet.MDCPin == upPin || settings.Ethernet.MDCPin == downPin || settings.Ethernet.MDIOPin == upPin || settings.Ethernet.MDIOPin == downPin) ret = -11; + else if(proto == radio_proto::GP_Remote && (settings.Ethernet.PWRPin == myPin || + settings.Ethernet.MDCPin == myPin || settings.Ethernet.MDIOPin == myPin)) + ret = -11; } if(ret == 0) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { SomfyShade *shade = &somfy.shades[i]; if(shade->getShadeId() == this->getShadeId() || shade->getShadeId() == 255) continue; - if(shade->proto == radio_proto::GPIO) { + if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { if(shade->gpioUp == upPin || shade->gpioDown == upPin || shade->gpioDown == upPin || shade->gpioDown == downPin) { ret = -12; break; } + else if(proto == radio_proto::GP_Remote && (shade->gpioUp == myPin || shade->gpioDown == myPin)) { + ret = -12; + break; + } + else if(shade->proto == radio_proto::GP_Remote && (shade->gpioMy == upPin || shade->gpioMy == downPin)) { + ret = -12; + break; + } + else if(shade->proto == radio_proto::GP_Remote && proto == radio_proto::GP_Remote && (shade->gpioMy == myPin)) { + ret = -12; + break; + } } } } @@ -2550,12 +2636,16 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) { } } if(obj.containsKey("flags")) this->flags = obj["flags"]; - if(this->proto == radio_proto::GPIO) { + if(this->proto == radio_proto::GP_Remote || this->proto == radio_proto::GP_Relay) { if(obj.containsKey("gpioUp")) this->gpioUp = obj["gpioUp"]; if(obj.containsKey("gpioDown")) this->gpioDown = obj["gpioDown"]; pinMode(this->gpioUp, OUTPUT); pinMode(this->gpioDown, OUTPUT); } + if(this->proto == radio_proto::GP_Remote) { + if(obj.containsKey("gpioMy")) this->gpioMy = obj["gpioMy"]; + pinMode(this->gpioMy, OUTPUT); + } } return err; } @@ -2612,6 +2702,7 @@ bool SomfyShade::toJSON(JsonObject &obj) { obj["sortOrder"] = this->sortOrder; obj["gpioUp"] = this->gpioUp; obj["gpioDown"] = this->gpioDown; + obj["gpioMy"] = this->gpioMy; SomfyRemote::toJSON(obj); JsonArray arr = obj.createNestedArray("linkedRemotes"); for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { @@ -2958,7 +3049,7 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) { // We have to set the processed to clear this if we are sending // another command. this->lastFrame.processed = false; - if(this->proto == radio_proto::GPIO) { + if(this->proto == radio_proto::GP_Relay) { Serial.print("CMD:"); Serial.print(translateSomfyCommand(this->lastFrame.cmd)); Serial.print(" ADDR:"); @@ -2967,6 +3058,16 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) { Serial.print(this->lastFrame.rollingCode); Serial.println(" SETTING GPIO"); } + else if(this->proto == radio_proto::GP_Remote) { + 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.println(" TRIGGER GPIO"); + this->triggerGPIOs(this->lastFrame); + } else { Serial.print("CMD:"); Serial.print(translateSomfyCommand(this->lastFrame.cmd)); @@ -2988,6 +3089,12 @@ bool SomfyRemote::isLastCommand(somfy_commands cmd) { return true; } void SomfyRemote::repeatFrame(uint8_t repeat) { + if(this->proto == radio_proto::GP_Relay) + return; + else if(this->proto == radio_proto::GP_Remote) { + this->triggerGPIOs(this->lastFrame); + return; + } somfy.transceiver.beginTransmit(); byte frm[10]; this->lastFrame.encodeFrame(frm); diff --git a/Somfy.h b/Somfy.h index 21e9ec3..369a623 100644 --- a/Somfy.h +++ b/Somfy.h @@ -26,7 +26,8 @@ enum class radio_proto : byte { // Ordinal byte 0-255 RTS = 0x00, RTW = 0x01, RTV = 0x02, - GPIO = 0x08 + GP_Relay = 0x08, + GP_Remote = 0x09 }; enum class somfy_commands : byte { Unknown0 = 0x0, @@ -185,8 +186,11 @@ class SomfyRemote { uint32_t m_remoteAddress = 0; public: radio_proto proto = radio_proto::RTS; + int8_t gpioDir = 0; uint8_t gpioUp = 0; uint8_t gpioDown = 0; + uint8_t gpioMy = 0; + uint32_t gpioRelease = 0; somfy_frame_t lastFrame; bool flipCommands = false; uint16_t lastRollingCode = 0; @@ -210,6 +214,8 @@ class SomfyRemote { void repeatFrame(uint8_t repeat); virtual uint16_t p_lastRollingCode(uint16_t code); somfy_commands transformCommand(somfy_commands cmd); + void triggerGPIOs(somfy_frame_t &frame); + }; class SomfyLinkedRemote : public SomfyRemote { public: @@ -236,7 +242,6 @@ class SomfyShade : public SomfyRemote { bool settingTiltPos = false; uint32_t awaitMy = 0; public: - int8_t gpioDir = 0; int8_t sortOrder = 0; bool flipPosition = false; shade_types shadeType = shade_types::roller; @@ -296,9 +301,9 @@ class SomfyShade : public SomfyRemote { void commitShadePosition(); void commitTiltPosition(); void commitMyPosition(); - void setGPIOs(); void clear(); int8_t transformPosition(float fpos); + void setGPIOs(); // State Setters int8_t p_direction(int8_t dir); diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin index 7baf9a8..723602b 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 7c952a0..057a205 100644 Binary files a/SomfyController.littlefs.bin and b/SomfyController.littlefs.bin differ diff --git a/data/index.html b/data/index.html index 5acbfec..ebfadbf 100644 --- a/data/index.html +++ b/data/index.html @@ -293,7 +293,8 @@ - + + @@ -304,17 +305,23 @@ -
-
+
+
-
+
+
+ + +
+
diff --git a/data/index.js b/data/index.js index 6f0c420..ce189ea 100644 --- a/data/index.js +++ b/data/index.js @@ -1857,10 +1857,11 @@ class Somfy { this.loadPins('input', document.getElementById('selTransRXPin')); //this.loadSomfy(); ui.toElement(document.getElementById('divTransceiverSettings'), { - transceiver: { config: { proto: 0, SCKPin: 18, CSNPin: 5, MOSIPin: 23, MISOPin: 19, TXPin: 12, RXPin: 13, frequency: 433.42, rxBandwidth: 97.96, type:56, deviation: 11.43, txPower: 10, enabled: false } } + transceiver: { config: { proto: 0, SCKPin: 18, CSNPin: 5, MOSIPin: 23, MISOPin: 19, TXPin: 12, RXPin: 13, frequency: 433.42, rxBandwidth: 97.96, type: 56, deviation: 11.43, txPower: 10, enabled: false } } }); this.loadPins('out', document.getElementById('selShadeGPIOUp')); this.loadPins('out', document.getElementById('selShadeGPIODown')); + this.loadPins('out', document.getElementById('selShadeGPIOMy')); this.initialized = true; } async loadSomfy() { @@ -1932,7 +1933,7 @@ class Somfy { if (valid) valid = fnValDup(trans.config, 'RXPin'); if (valid) { putJSONSync('/saveRadio', trans, (err, trans) => { - if (err) + if (err) ui.serviceError(err); else { document.getElementById('btnSaveRadio').classList.remove('disabled'); @@ -2112,7 +2113,7 @@ class Somfy { divCtl += `${shade.name}`; if (shade.tiltType === 3) divCtl += `${shade.myTiltPos > 0 ? shade.myTiltPos + '%' : '---'}` - else if(shade.shadeType !== 5 && shade.shadeType !== 9) { + else if (shade.shadeType !== 5 && shade.shadeType !== 9) { divCtl += `${shade.myPos > 0 ? shade.myPos + '%' : '---'}`; if (shade.myTiltPos > 0 && shade.tiltType !== 3) divCtl += `${shade.myTiltPos > 0 ? shade.myTiltPos + '%' : '---'}`; } @@ -2248,7 +2249,7 @@ class Somfy { let changed = false; let timerStart = null; let dragDiv = null; - let fnDragStart = function(e) { + let fnDragStart = function (e) { //console.log({ evt: 'dragStart', e: e, this: this }); if (typeof e.dataTransfer !== 'undefined') { e.dataTransfer.effectAllowed = 'move'; @@ -2317,7 +2318,7 @@ class Somfy { console.log({ evt: 'dragLeave', e: e, this: this }); this.classList.remove('over'); }; - let fnDrop = function(e) { + let fnDrop = function (e) { // Shift around the items. console.log({ evt: 'drop', e: e, this: this }); let elDrag = list.querySelector('.dragging'); @@ -2333,7 +2334,7 @@ class Somfy { } } }; - let fnDragEnd = function(e) { + let fnDragEnd = function (e) { console.log({ evt: 'dragEnd', e: e, this: this }); let elOver = list.querySelector('.over'); [].forEach.call(items, (item) => { item.classList.remove('over') }); @@ -2604,7 +2605,7 @@ class Somfy { case 9: case 10: case 11: - if(type !== 'inout' && type !== 'input') continue; + if (type !== 'inout' && type !== 'input') continue; break; case 37: case 38: @@ -2645,7 +2646,7 @@ class Somfy { for (let i = 0; i < flags.length; i++) { flags[i].style.display = state.sunSensor ? '' : 'none'; flags[i].setAttribute('data-on', state.flags & 0x01 === 0x01 ? 'true' : 'false'); - + } let divs = document.querySelectorAll(`.somfyShadeCtl[data-shadeid="${state.shadeId}"]`); for (let i = 0; i < divs.length; i++) { @@ -3110,12 +3111,18 @@ class Somfy { ui.errorMessage(document.getElementById('divSomfySettings'), 'Down Time must be a value between 0 and 4,294,967,295 milliseconds. This is the travel time to go from full open to full closed.'); valid = false; } - if (obj.proto === 8) { + if (obj.proto === 8 || obj.proto === 9) { if (obj.gpioUp === obj.gpioDown) { ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down GPIO selections must be unique.'); valid = false; } } + if (obj.proto === 9) { + if (obj.gpioMy === obj.gpioUp || obj.gpioMy === obj.gpioDown) { + ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down and my GPIO selections must be unique.'); + valid = false; + } + } if (valid) { if (isNaN(shadeId) || shadeId >= 255) { // We are adding. diff --git a/data/widgets.css b/data/widgets.css index c9a2446..67c17f1 100644 --- a/data/widgets.css +++ b/data/widgets.css @@ -141,25 +141,33 @@ #somfyShade #divShadeBitLength { display:inline-block; } -#somfyShade[data-proto="8"] #divGPIOControl { +#somfyShade[data-proto="8"] #divGPIOControl, +#somfyShade[data-proto="9"] #divGPIOControl { display: inline-block; width: auto; margin-top: -18px; } -#somfyShade[data-proto="8"] #divShadeBitLength { +#somfyShade[data-proto="8"] #divShadeBitLength, +#somfyShade[data-proto="9"] #divShadeBitLength { display: none; } #divGPIOControl { text-align:center; margin-left:7px; } + #divGPIOControl > div.field-group { + width: 70px; + display: inline-block; + } #somfyShade[data-proto="8"] #divStepSettings, +#somfyShade[data-proto="9"] #divStepSettings, +#somfyShade[data-proto="8"] #divGPIOMy, #somfyShade[data-bitlength="56"] #divStepSettings, #somfyShade[data-shadetype="5"] #divStepSettings, -#somfyShade[data-shadetype="6"] #divStepSettings, -#somfyShade[data-shadetype="9"] #divStepSettings { +#somfyShade[data-shadetype="6"] #divStepSettings { display: none; } + .group-draggable, .shade-draggable { height:32px;