From f1cc8a860d677c54d8c2afc00c9df75027d7687c Mon Sep 17 00:00:00 2001 From: Eric Chauvet Date: Tue, 12 May 2020 02:27:57 +0200 Subject: [PATCH] Completed kingart curtain switch support: Added Web UI controls and view in status Added power up behaviour (nothing, close, open, last position) Added curtain style (Roller, etc...) for UI status and to be used in MQTT --- code/espurna/board.cpp | 3 + code/espurna/config/hardware.h | 2 + code/espurna/config/webui.h | 10 + code/espurna/curtain_kingart.cpp | 434 +++++++++++++++++++++++++------ code/espurna/web.cpp | 2 + code/gulpfile.js | 8 +- code/html/custom.css | 28 ++ code/html/custom.js | 86 ++++++ code/html/index.html | 85 ++++++ 9 files changed, 584 insertions(+), 74 deletions(-) diff --git a/code/espurna/board.cpp b/code/espurna/board.cpp index fdb3af1d..c1095921 100644 --- a/code/espurna/board.cpp +++ b/code/espurna/board.cpp @@ -177,6 +177,9 @@ PROGMEM const char espurna_webui[] = #if WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT "THERMOSTAT" #endif + #if WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN + "CURTAIN" + #endif #if WEBUI_IMAGE == WEBUI_IMAGE_FULL "FULL" #endif diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index 6b0c64b2..f5f3c241 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -4595,6 +4595,8 @@ #define MANUFACTURER "KINGART" #define DEVICE "CURTAIN_SWITCH" + #define CURTAIN_SUPPORT 1 + // LEDs #define LED1_PIN 13 #define LED1_PIN_INVERSE 1 diff --git a/code/espurna/config/webui.h b/code/espurna/config/webui.h index 2407e850..b01894f7 100644 --- a/code/espurna/config/webui.h +++ b/code/espurna/config/webui.h @@ -11,6 +11,7 @@ #define WEBUI_IMAGE_RFM69 8 #define WEBUI_IMAGE_LIGHTFOX 16 #define WEBUI_IMAGE_THERMOSTAT 32 +#define WEBUI_IMAGE_CURTAIN 64 #define WEBUI_IMAGE_FULL 15 #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE @@ -65,6 +66,15 @@ #endif #endif +#if CURTAIN_SUPPORT == 1 + #ifndef WEBUI_IMAGE + #define WEBUI_IMAGE WEBUI_IMAGE_CURTAIN + #else + #undef WEBUI_IMAGE + #define WEBUI_IMAGE WEBUI_IMAGE_FULL + #endif +#endif + #ifndef WEBUI_IMAGE #define WEBUI_IMAGE WEBUI_IMAGE_SMALL #endif diff --git a/code/espurna/curtain_kingart.cpp b/code/espurna/curtain_kingart.cpp index d46398e1..73e81d97 100644 --- a/code/espurna/curtain_kingart.cpp +++ b/code/espurna/curtain_kingart.cpp @@ -5,6 +5,9 @@ KingArt Cover/Shutter/Blind/Curtain support for ESPURNA Based on xdrv_19_ps16dz.dimmer.ino, PS_16_DZ dimmer support for Tasmota Copyright (C) 2019 by Albert Weterings +Based on curtain_kingart.ino Albert Weterings +Copyright (C) 2020 - Eric Chauvet + */ #include "curtain_kingart.h" @@ -13,6 +16,8 @@ Copyright (C) 2019 by Albert Weterings #include "ntp.h" #include "mqtt.h" +#include "settings.h" +#include "ws.h" #ifndef KINGART_CURTAIN_PORT #define KINGART_CURTAIN_PORT Serial // Hardware serial port by default @@ -24,21 +29,157 @@ Copyright (C) 2019 by Albert Weterings #define KINGART_CURTAIN_TERMINATION '\e' // Termination character after each message #define KINGART_CURTAIN_BAUDRATE 19200 // Serial speed is fixed for the device - + +// --> Let see after if we define a curtain generic switch, use these for now +#define CURTAIN_BUTTON_UNKNOWN -1 +#define CURTAIN_BUTTON_PAUSE 0 +#define CURTAIN_BUTTON_OPEN 1 +#define CURTAIN_BUTTON_CLOSE 2 + +#define CURTAIN_POSITION_UNKNOWN -1 +// <-- + char _KACurtainBuffer[KINGART_CURTAIN_BUFFER_SIZE]; bool _KACurtainNewData = false; + +// Status vars - for curtain move detection : +int _curtain_position = CURTAIN_POSITION_UNKNOWN; +int _curtain_last_position = CURTAIN_POSITION_UNKNOWN; +int _curtain_button = CURTAIN_BUTTON_UNKNOWN; +int _curtain_last_button = CURTAIN_BUTTON_UNKNOWN; +unsigned long last_uptime = 0; + +int _curtain_position_set = CURTAIN_POSITION_UNKNOWN; //Last position asked to be set (not the real position, the real query - updated when the curtain stops moving) +bool _curtain_waiting_ack = false; //Avoid too fast MQTT commands +bool _curtain_ignore_next_position = false; //Avoid a bug (see (*1) +bool _curtain_initial_position_set = false; //Flag to detect if we manage to set the curtain back to its position before power off or reboot + +// Calculated behaviour depending on KA switch, MQTT and UI actions +bool _curtain_moving = true; + +//Enable more traces, true as a default and stopped when curtain is setup. +bool _debug_flag = true; + +#if WEB_SUPPORT +bool _curtain_report_ws = true; //This will init curtain control and flag the web ui update +#endif // WEB_SUPPORT + +//------------------------------------------------------------------------------ +void curtainUpdateUI() { +#if WEB_SUPPORT + _curtain_report_ws = true; +#endif // WEB_SUPPORT +} + +//------------------------------------------------------------------------------ +int setButtonFromSwitchText(String & text) { + if(text == "on") + return CURTAIN_BUTTON_OPEN; + else if(text == "off") + return CURTAIN_BUTTON_CLOSE; + else if(text == "pause") + return CURTAIN_BUTTON_PAUSE; + else + return CURTAIN_BUTTON_UNKNOWN; +} // ----------------------------------------------------------------------------- // Private // ----------------------------------------------------------------------------- +//------------------------------------------------------------------------------ +//This check that wa got latest and new stats from the AT+RESULT message +bool _KAValidStatus() { + return _curtain_button != CURTAIN_BUTTON_UNKNOWN && + _curtain_last_button != CURTAIN_BUTTON_UNKNOWN && + _curtain_position != CURTAIN_POSITION_UNKNOWN && + _curtain_last_position != CURTAIN_POSITION_UNKNOWN; +} + + +//------------------------------------------------------------------------------ +//We consider that the curtain is moving. A timer is set to get the position of the curtain sending AT+START messages in the loop() +void _KASetMoving() { + last_uptime = millis() + 1000; //Let the returned curtain position to be refreshed to know of the curtain is still moving + _curtain_moving = true; +} + +//------------------------------------------------------------------------------ +//Send a buffer to serial void _KACurtainSend(const char * tx_buffer) { KINGART_CURTAIN_PORT.print(tx_buffer); KINGART_CURTAIN_PORT.print(KINGART_CURTAIN_TERMINATION); KINGART_CURTAIN_PORT.flush(); + if(_debug_flag) DEBUG_MSG_P(PSTR("[KAUART] Send : %s\n"), tx_buffer); } - -void _KACurtainReceiveUART() { + +//------------------------------------------------------------------------------ +//Send a formatted message to MCU +void _KACurtainSet(int button, int position = CURTAIN_POSITION_UNKNOWN) { + + if(_curtain_waiting_ack) { + DEBUG_MSG_P(PSTR("[KAUART] MCU BUSY : Request ignored!\n")); + return; + } + + char tx_buffer[80] = {0}; + if(button != CURTAIN_BUTTON_UNKNOWN && position != CURTAIN_POSITION_UNKNOWN) { + snprintf_P( + tx_buffer, sizeof(tx_buffer), + PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\",\"setclose\":%d"), + now(), millis() % 1000, + (button == CURTAIN_BUTTON_PAUSE ? "pause" : (button == CURTAIN_BUTTON_OPEN ? "on" : "off")), position + ); + } else if(button == CURTAIN_BUTTON_UNKNOWN) { + snprintf_P( + tx_buffer, sizeof(tx_buffer), + PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"setclose\":%d"), + now(), millis() % 1000, + position + ); + } else { + snprintf_P( + tx_buffer, sizeof(tx_buffer), + PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\""), + now(), millis() % 1000, + (button == CURTAIN_BUTTON_PAUSE ? "pause" : (button == CURTAIN_BUTTON_OPEN ? "on" : "off")) + ); + } + _KACurtainSend(tx_buffer); + _curtain_waiting_ack = true; +} + +//------------------------------------------------------------------------------ +//Stop moving will set the real curtain position to the GUI/MQTT +void _KAStopMoving() { + _curtain_moving = false; + if( _curtain_position != CURTAIN_POSITION_UNKNOWN) + _curtain_position_set = _curtain_position; + else if( _curtain_last_position != CURTAIN_POSITION_UNKNOWN) + _curtain_position_set = _curtain_last_position; + + if(_curtain_initial_position_set == false) {//The curtain stopped moving for the first time, set the position back to + int init_position = getSetting("curtainInitialBehaviour", 0); + DEBUG_MSG_P(PSTR("[CURTAIN] curtainInitialBehaviour : %d, curtainInitialPosition : %d\n"), init_position, getSetting("curtainInitialPosition", 100)); + if (init_position == 1) + _KACurtainSet(CURTAIN_BUTTON_CLOSE); + else if (init_position == 2) + _KACurtainSet(CURTAIN_BUTTON_OPEN); + else if (init_position == 3) { + int pos = getSetting("curtainInitialPosition", 100); //Set closed if we do not have initial position set. + if (_curtain_position_set != pos) { + _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, pos); + } else + DEBUG_MSG_P(PSTR("[CURTAIN] No need to update bootup position\n")); + } + _curtain_initial_position_set = true; + _debug_flag = false; //Disable debug - user has could ask for it + } +} + +//------------------------------------------------------------------------------ +//Receive a buffer from serial +bool _KACurtainReceiveUART() { static unsigned char ndx = 0; while (KINGART_CURTAIN_PORT.available() > 0 && !_KACurtainNewData) { char rc = KINGART_CURTAIN_PORT.read(); @@ -51,8 +192,15 @@ void _KACurtainReceiveUART() { ndx = 0; } } + if(_KACurtainNewData) { + if(_debug_flag) DEBUG_MSG_P(PSTR("[KAUART] Received : %s\n"), _KACurtainBuffer); + _KACurtainNewData = false; + return true; + } + return false; } + /* Buttons on the device will move Cover/Shutter/Blind/Curtain up/open or down/close On the end of every movement the unit reports the last action and posiston over MQTT topic {hostname}/curtain @@ -87,108 +235,248 @@ To configure the device: - Press up/down for 5 seconds to bring device into AP mode. After pressing up/down again, device will restart in normal mode. */ +//------------------------------------------------------------------------------ void _KACurtainResult() { - if (_KACurtainNewData) { - // Need to send confiramtion to the N76E003AT20 that message is received - _KACurtainSend("AT+SEND=ok"); + // Need to send confirmation to the N76E003AT20 that message is received + // ECH : TODO Check this is the case every time + _KACurtainSend("AT+SEND=ok"); - // We don't handle "setclose" any other way, simply redirect payload value - const String buffer(_KACurtainBuffer); - #if MQTT_SUPPORT - int setclose_idx = buffer.indexOf("setclose"); - if (setclose_idx > 0) { - auto position = buffer.substring(setclose_idx + strlen("setclose") + 2, buffer.length()); - int leftovers = position.indexOf(','); - if (leftovers > 0) { - position = position.substring(0, leftovers); - } - mqttSend(MQTT_TOPIC_CURTAIN, position.c_str()); + //Init receive stats : The buffer which may contain : "setclose":INT(0-100) or "switch":["on","off","pause"] + const String buffer(_KACurtainBuffer); + _curtain_button = CURTAIN_BUTTON_UNKNOWN; + _curtain_position = CURTAIN_POSITION_UNKNOWN; + + + if(buffer.indexOf("AT+RESULT") == 0) { //AT+RESULT is an acquitment of our command (MQTT or GUI) + //Set the status on what we kown + if( _curtain_last_button == CURTAIN_BUTTON_OPEN && _curtain_last_position == 0 || + _curtain_last_button == CURTAIN_BUTTON_CLOSE && _curtain_last_position == 100 || + _curtain_last_button == CURTAIN_BUTTON_PAUSE) //The curtain is max opened, closed or pause + _KAStopMoving(); + else { //Else it is probably moving + _KASetMoving(); + /* + (*1) ATTENTION THERE : + Send immediatly a AT+START - we need to purge the first response. + It will return us the right direction of the switch but the position + we set instead of the real on. We take care of the switch response but + we ignore the position. + */ + _KACurtainSend("AT+START"); + _curtain_ignore_next_position = true; + } + //Time to update UI + curtainUpdateUI(); + _curtain_waiting_ack = false; + return; + } else if(buffer.indexOf("AT+UPDATE") == 0) { //AT+UPDATE is a response from the switch itself or AT+SEND query + // Get switch status from MCU + int switch_idx = buffer.indexOf("switch"); + if (switch_idx > 0) { + String switch_text = buffer.substring(switch_idx + strlen("switch") + 3, buffer.length()); + int leftovers = switch_text.indexOf('"'); + if (leftovers > 0) { //We must find leftover as it is text + switch_text = switch_text.substring(0, leftovers); + _curtain_button = setButtonFromSwitchText(switch_text); } - #endif // MQTT_SUPPORT - - // Handle configuration button presses - if (buffer.indexOf("enterESPTOUCH") > 0) { - wifiStartAP(); - } else if (buffer.indexOf("exitESPTOUCH") > 0) { - deferredReset(100, CUSTOM_RESET_HARDWARE); + + } + // Get position from MCU + int setclose_idx = buffer.indexOf("setclose"); + if (setclose_idx > 0) { + String position = buffer.substring(setclose_idx + strlen("setclose") + 2, buffer.length()); + int leftovers = position.indexOf(','); + if (leftovers > 0) // Not found if finishing by setclose + position = position.substring(0, leftovers); + if(_curtain_ignore_next_position) // (*1) + _curtain_ignore_next_position = false; + else + _curtain_position = position.toInt(); } + } else { + DEBUG_MSG_P(PSTR("[KAUART] ERROR : Unknown message : %s\n"), _KACurtainBuffer); + } + + //Check if curtain is moving or not + if( _curtain_button == CURTAIN_BUTTON_PAUSE ) { //This is returned from MCU and tells us than last status is pause or full opened or closed + _KAStopMoving(); + } else if(_curtain_moving ) { + if(_KAValidStatus()) { + if(_curtain_last_button != _curtain_button) //Direction change? Reset the timer to know + _KASetMoving(); + else if(_curtain_last_position == _curtain_position) //Same direction, same position - curtain is not moving anymore + _KAStopMoving(); + } + } else //Not paused, not moving, and we received an AT+UPDATE -> This means that we are moving + _KASetMoving(); - _KACurtainNewData = false; + //Update last position and transmit to MQTT (GUI is at the end) + if(_curtain_position != CURTAIN_POSITION_UNKNOWN && _curtain_last_position != _curtain_position) { + _curtain_last_position = _curtain_position; + if(_curtain_initial_position_set) //Update initial position - TDOD : maybe only when move is finished to avoid to write too frequently + setSetting("curtainInitialPosition", _curtain_last_position); //Remeber last position in case of power loss + #if MQTT_SUPPORT + const String pos = String(_curtain_last_position); + mqttSend(MQTT_TOPIC_CURTAIN, pos.c_str()); + #endif // MQTT_SUPPORT } -} - -// %d = now() / time_t / NTP timestamp in seconds -// %03u = millis() / uint32_t / we need last 3 digits -// %s = char strings for various actions - -// Tell N76E003AT20 to stop moving and report current position -void _KACurtainPause(const char * message) { - char tx_buffer[80] = {0}; - snprintf_P( - tx_buffer, sizeof(tx_buffer), - PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\""), - now(), millis() % 1000, - message - ); - _KACurtainSend(tx_buffer); -} - -// Tell N76E003AT20 to go to position X (based on X N76E003AT20 decides to go up or down) -void _KACurtainSetclose(const char * message) { - char tx_buffer[80] = {0}; - snprintf_P( - tx_buffer, sizeof(tx_buffer), - PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\",\"setclose\":%s"), - now(), millis() % 1000, - "off", message - ); - _KACurtainSend(tx_buffer); -} - -#if MQTT_SUPPORT -void _KACurtainActionSelect(const char * message) { - if (strcmp(message, "pause") == 0) { - _KACurtainPause(message); - } else { - _KACurtainSetclose(message); + //Reset last button to make the algorithm work and set last button state + if(!_curtain_moving) + _curtain_last_button = CURTAIN_BUTTON_UNKNOWN; + else if (_curtain_button != CURTAIN_BUTTON_UNKNOWN) + _curtain_last_button = _curtain_button; + + // Handle configuration button presses + if (buffer.indexOf("enterESPTOUCH") > 0) { + wifiStartAP(); + } else if (buffer.indexOf("exitESPTOUCH") > 0) { + deferredReset(100, CUSTOM_RESET_HARDWARE); + } else { //In any other case, update as it could be a move action + curtainUpdateUI(); } } -void _KACurtainCallback(unsigned int type, const char * topic, char * payload) { +// ----------------------------------------------------------------------------- +// MQTT Support +// ----------------------------------------------------------------------------- + +#if MQTT_SUPPORT + +//------------------------------------------------------------------------------ +void _curtainMQTTCallback(unsigned int type, const char * topic, char * payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_CURTAIN); - } - if (type == MQTT_MESSAGE_EVENT) { + } else if (type == MQTT_MESSAGE_EVENT) { // Match topic const String t = mqttMagnitude(const_cast(topic)); if (t.equals(MQTT_TOPIC_CURTAIN)) { - _KACurtainActionSelect(payload); + if (strcmp(payload, "pause") == 0) + _KACurtainSet(CURTAIN_BUTTON_PAUSE); + else if (strcmp(payload, "on") == 0) + _KACurtainSet(CURTAIN_BUTTON_OPEN); + else if (strcmp(payload, "off") == 0) + _KACurtainSet(CURTAIN_BUTTON_CLOSE); + else { + _curtain_position_set = t.toInt(); + _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set); + + } } } } #endif // MQTT_SUPPORT - + +// ----------------------------------------------------------------------------- +// WEB Support +// ----------------------------------------------------------------------------- + +#if WEB_SUPPORT + +//------------------------------------------------------------------------------ +void _curtainWebSocketOnConnected(JsonObject& root) { + root["curtainType"] = getSetting("curtainType", "0"); + root["curtainInitialBehaviour"] = getSetting("curtainInitialBehaviour", "0"); +} + +//------------------------------------------------------------------------------ +bool _curtainWebSocketOnKeyCheck(const char * key, JsonVariant& value) { + if (strncmp(key, "curtain", strlen("curtain")) == 0) return true; + return false; +} + +//------------------------------------------------------------------------------ +void _curtainWebSocketUpdate(JsonObject& root) { + JsonObject& state = root.createNestedObject("curtainState"); + state["curtainGet"] = _curtain_last_position; + if(_curtain_position_set == CURTAIN_POSITION_UNKNOWN) + _curtain_position_set = _curtain_last_position; + state["curtainSet"] = _curtain_position_set; + state["curtainButton"] = _curtain_last_button; + state["curtainMoving"] = _curtain_moving ? "Moving" : "Idle"; + state["curtainType"] = getSetting("curtainType", "0"); +} + +//------------------------------------------------------------------------------ +void _curtainWebSocketStatus(JsonObject& root) { + root["curtainVisible"] = 1; + _curtainWebSocketUpdate(root); +} + +//------------------------------------------------------------------------------ +void _curtainWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { + + if (strcmp(action, "curtainAction") == 0) { + if (data.containsKey("position")) { + _curtain_position_set = data["position"].as(); + _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set); + } else if(data.containsKey("button")){ + _curtain_last_button = data["button"].as(); + _KACurtainSet(_curtain_last_button); + } + } else if (strcmp(action, "dbgcmd") == 0) { //Directly send our buffer to the KA serial + if(data["command"] == "debug") { + _debug_flag = true; + } else + _KACurtainSend(data["command"]); + } +} + +void _curtainWebSocketOnVisible(JsonObject& root) { + root["curtainVisible"] = 1; +} + +#endif //WEB_SUPPORT + // ----------------------------------------------------------------------------- // SETUP & LOOP // ----------------------------------------------------------------------------- - + +//------------------------------------------------------------------------------ void _KACurtainLoop() { - _KACurtainReceiveUART(); - _KACurtainResult(); + + if(_KACurtainReceiveUART()) + _KACurtainResult(); + else if(_curtain_moving) { //When curtain move and no messages, get position every 600ms with AT+START + unsigned long uptime = millis(); + long diff = uptime - last_uptime; + if(diff >= 600) { + _KACurtainSend("AT+START"); + last_uptime = uptime; + } + } + +#if WEB_SUPPORT + if (_curtain_report_ws) { //Launch a websocket update + wsPost(_curtainWebSocketUpdate); + _curtain_report_ws = false; + } + #endif } +//------------------------------------------------------------------------------ void kingartCurtainSetup() { // Init port to receive and send messages KINGART_CURTAIN_PORT.begin(KINGART_CURTAIN_BAUDRATE); - + +#if MQTT_SUPPORT // Register MQTT callback only when supported - #if MQTT_SUPPORT - mqttRegister(_KACurtainCallback); - #endif // MQTT_SUPPORT + mqttRegister(_curtainMQTTCallback); +#endif // MQTT_SUPPORT + + +#if WEB_SUPPORT + // Websockets + wsRegister() + .onVisible(_curtainWebSocketOnVisible) + .onConnected(_curtainWebSocketOnConnected) + .onKeyCheck(_curtainWebSocketOnKeyCheck) + .onAction(_curtainWebSocketOnAction) + .onData(_curtainWebSocketUpdate); +#endif // Register loop to poll the UART for new messages espurnaRegisterLoop(_KACurtainLoop); diff --git a/code/espurna/web.cpp b/code/espurna/web.cpp index e4991743..0dc38755 100644 --- a/code/espurna/web.cpp +++ b/code/espurna/web.cpp @@ -30,6 +30,8 @@ Copyright (C) 2016-2019 by Xose PĂ©rez #include "static/index.lightfox.html.gz.h" #elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT #include "static/index.thermostat.html.gz.h" +#elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN + #include "static/index.curtain.html.gz.h" #elif WEBUI_IMAGE == WEBUI_IMAGE_FULL #include "static/index.all.html.gz.h" #endif diff --git a/code/gulpfile.js b/code/gulpfile.js index 714ca018..89fce351 100644 --- a/code/gulpfile.js +++ b/code/gulpfile.js @@ -118,7 +118,8 @@ var buildWebUI = function(module) { 'rfbridge': false, 'rfm69': false, 'thermostat': false, - 'lightfox': false + 'lightfox': false, + 'curtain': false }; // Note: only build these when specified as module arg @@ -218,6 +219,10 @@ gulp.task('webui_thermostat', function() { return buildWebUI('thermostat'); }); +gulp.task('webui_curtain', function() { + return buildWebUI('curtain'); +}); + gulp.task('webui_all', function() { return buildWebUI('all'); }); @@ -231,6 +236,7 @@ gulp.task('webui', 'webui_rfm69', 'webui_lightfox', 'webui_thermostat', + 'webui_curtain', 'webui_all' ) ); diff --git a/code/html/custom.css b/code/html/custom.css index 75636cc8..76d211ad 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -130,6 +130,31 @@ div.state { color: #0F0; } +/* ----------------------------------------------------------------------------- + Curtains + -------------------------------------------------------------------------- */ + + .curtain-div { + text-align: center; +} + +.curtain-roller { + width: 300px; + height: 200px; + display: inline-block; +} + +.curtain-button{ + margin-left: 10px; + margin-right: 10px; + text-align: center; + +} + +.reverse-range { + direction: rtl; +} + /* ----------------------------------------------------------------------------- Buttons -------------------------------------------------------------------------- */ @@ -202,6 +227,9 @@ div.state { background: rgb(255, 128, 0); /* orange */ } +.button-curtain-open, +.button-curtain-pause, +.button-curtain-close, .button-generate-password { background: rgb(66, 184, 221); /* blue */ } diff --git a/code/html/custom.js b/code/html/custom.js index 0ed38770..c5b85f7b 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -1272,6 +1272,49 @@ function initMagnitudes(data) { } +// ----------------------------------------------------------------------------- +// Curtains +// ----------------------------------------------------------------------------- + + + +//Create the controls for one curtain. It is called when curtain is updated (so created the first time) +//Let this there as we plan to have more than one curtain per switch +function initCurtain(data) { + + var current = $("#curtains > div").length; + if (current > 0) { return; } + + // add curtain template (prepare multi switches) + var template = $("#curtainTemplate").children(); + var line = $(template).clone(); + // init curtain button + $(line).find(".button-curtain-open").on("click", function() { + sendAction("curtainAction", {button: 1}); + $(this).css('background', 'red'); + }); + $(line).find(".button-curtain-pause").on("click", function() { + sendAction("curtainAction", {button: 0}); + $(this).css('background', 'red'); + }); + $(line).find(".button-curtain-close").on("click", function() { + sendAction("curtainAction", {button: 2}); + $(this).css('background', 'red'); + }); + line.appendTo("#curtains"); + + // init curtain slider + $("#curtainSet").on("change", function() { + var value = $(this).val(); + var parent = $(this).parents(".pure-g"); + $("span", parent).html(value); + sendAction("curtainAction", {position: value}); + }); + +} + + + // ----------------------------------------------------------------------------- // Lights // ----------------------------------------------------------------------------- @@ -1691,6 +1734,49 @@ function processData(data) { if (key == "rpnNames") return; + // --------------------------------------------------------------------- + // Curtains + // --------------------------------------------------------------------- + + + + if ("curtainState" === key) { + initCurtain(); + switch(value.curtainType) { + case '0': //Roller + default: + $("#curtainGetPicture").css('background', 'linear-gradient(180deg, black ' + value.curtainGet + '%, #a0d6ff ' + value.curtainGet + '%)'); + break; + case '1': //One side left to right + $("#curtainGetPicture").css('background', 'linear-gradient(90deg, black ' + value.curtainGet + '%, #a0d6ff ' + value.curtainGet + '%)'); + break; + case '2': //One side right to left + $("#curtainGetPicture").css('background', 'linear-gradient(270deg, black ' + value.curtainGet + '%, #a0d6ff ' + value.curtainGet + '%)'); + break; + case '3': //Two sides + $("#curtainGetPicture").css('background', 'linear-gradient(90deg, black ' + value.curtainGet/2 + '%, #a0d6ff ' + value.curtainGet/2 + '% ' + (100 - value.curtainGet/2) + '%, black ' + (100 - value.curtainGet/2) + '%)'); + break; + } + $("#curtainSet").val(value.curtainSet); //Update sliders + if(value.curtainMoving == "Idle") { //When idle, all buttons are off (blue) + $("button.curtain-button").css('background', 'rgb(66, 184, 221)'); + } else { //If moving, adapt the color depending on the button active + if(value.curtainButton == 1) { + $("button.button-curtain-close").css('background', 'rgb(66, 184, 221)'); //Close back to blue + $("button.button-curtain-open").css('background', 'rgb(192, 0, 0)'); + } + else if(value.curtainButton == 0) + $("button.button-curtain-pause").css('background', 'rgb(192, 0, 0)'); + else if(value.curtainButton == 2) { + $("button.button-curtain-open").css('background', 'rgb(66, 184, 221)'); //Open back to blue + $("button.button-curtain-close").css('background', 'rgb(192, 0, 0)'); + + } + } + return; + } + + // --------------------------------------------------------------------- // Lights // --------------------------------------------------------------------- diff --git a/code/html/index.html b/code/html/index.html index 9addcad9..ee95bbc2 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -96,6 +96,12 @@ GENERAL + +
  • + CURTAIN +
  • + +
  • THERMOSTAT @@ -228,6 +234,10 @@
    + +
    + +
    @@ -484,6 +494,55 @@ + + +
    +
    + +
    +

    CURTAIN

    +

    Curtain configuration

    +
    + +
    + +
    + + General + +
    + + +
    +
    Define your curtain type. It adapts the graphical view in status menu.
    +
    + +
    + + +
    +
    Define the initial position of the curtain after a reboot or power loss.
    +
    + +
    + +
    + +
    +
    +
    + +
    @@ -972,6 +1031,9 @@
    This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.
    - <root>/relay/#/set Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the switch ID (starting from 0). If the board has only one switch it will be 0.
    + + - <root>/curtain/set Set the curtain opening value (0-100), 0 means closed, 100 opened. "on", "off", "pause" pilots buttons.
    + - <root>/rgb/set Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).
    - <root>/hsv/set Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.
    @@ -2070,6 +2132,29 @@
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    + +