- /*
-
- 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"
-
- #if KINGART_CURTAIN_SUPPORT
-
- #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
- #endif
-
- #ifndef KINGART_CURTAIN_BUFFER_SIZE
- #define KINGART_CURTAIN_BUFFER_SIZE 100 // Local UART buffer size
- #endif
-
- #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_INIT_CLOSE 1
- #define CURTAIN_INIT_OPEN 2
- #define CURTAIN_INIT_POSITION 3
-
- #define CURTAIN_POSITION_UNKNOWN -1
- // <--
-
- #define KINGART_DEBUG_MSG_P(...) do { if (_curtain_debug_flag) { DEBUG_MSG_P(__VA_ARGS__); } } while(0)
-
- 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 _curtain_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();
- KINGART_DEBUG_MSG_P(PSTR("[KA] UART OUT %s\n"), tx_buffer);
- }
-
- //------------------------------------------------------------------------------
- //Send a formatted message to MCU
- void _KACurtainSet(int button, int position = CURTAIN_POSITION_UNKNOWN) {
-
- if(_curtain_waiting_ack) {
- KINGART_DEBUG_MSG_P(PSTR("[KA] UART ACK not received : 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) { //The curtain stopped moving for the first time, set the position back to
- int init_position = getSetting("curtainBoot", 0);
- KINGART_DEBUG_MSG_P(PSTR("[KA] curtainBoot : %d, curtainBootPos : %d\n"), init_position, getSetting("curtainBootPos", 100));
- if (init_position == CURTAIN_INIT_CLOSE) {
- _KACurtainSet(CURTAIN_BUTTON_CLOSE);
- } else if (init_position == CURTAIN_INIT_OPEN) {
- _KACurtainSet(CURTAIN_BUTTON_OPEN);
- } else if (init_position == CURTAIN_INIT_POSITION) {
- int pos = getSetting("curtainBootPos", 100); //Set closed if we do not have initial position set.
- if (_curtain_position_set != pos) {
- _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, pos);
- }
- }
- _curtain_initial_position_set = true;
- _curtain_debug_flag = false; //Disable debug - user has could ask for it
- } else if(_curtain_position_set != CURTAIN_POSITION_UNKNOWN && _curtain_position_set != getSetting("curtainBootPos", _curtain_position_set)) {
- setSetting("curtainBootPos", _curtain_last_position); //Remeber last position in case of power loss
- }
- }
-
- //------------------------------------------------------------------------------
- //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();
- if (rc != KINGART_CURTAIN_TERMINATION) {
- _KACurtainBuffer[ndx] = rc;
- if (ndx < KINGART_CURTAIN_BUFFER_SIZE - 1) ndx++;
- } else {
- _KACurtainBuffer[ndx] = '\0';
- _KACurtainNewData = true;
- ndx = 0;
- }
- }
- if(_KACurtainNewData) {
- KINGART_DEBUG_MSG_P(PSTR("[KA] Serial 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
-
- RAW paylod format looks like:
- AT+UPDATE="switch":"on","setclose":13
- AT+UPDATE="switch":"off","setclose":38
- AT+UPDATE="switch":"pause","setclose":75
-
- The device is listening to MQTT topic {hostname}/curtain/set, to which you can send:
- - position value, in range from 0 to 100
- - "pause", to stop the movement.
- The device will determine the direction all by itself.
-
- # Set the Cover / Shutter / Blind / Curtain run time
-
- The factory default Open and Close run time for the switch is 50 seconds, and it must be set to
- an accurate run time for smooth working. Some motors do not have the resistance stop function,
- so when the Cover/Shutter/Blind/Curtain track open or close to the maximum length, but the motor keeps on running.
- This might cause damage on the motor and the switch, it also wastes a lot of energy. In order
- to protect the motor, this switch designed with a time setting function. After setting up the run time,
- the switch will automaticly stop when the track reaches its limits. The run time setting is also helpful
- for the accurate control when manually controlling the device via the touch panel.
-
- After installing the switch and connecting the switch for the very first time:
- - First, it will automatically close the Cover/Shutter/Blind/Curtain to the maximum.
- - Press and hold the touch interface pause button for around 4 seconds until the red background
- led lights up and starts blinking. Then, press the open touch button so start the opening process.
- - When cover is fully open, press the Open or Close button to stop the timer and save the calculated run time.
-
- 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() {
-
- // Need to send confirmation to the N76E003AT20 that message is received
- // ECH : TODO Check this is the case every time
- _KACurtainSend("AT+SEND=ok");
-
- //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);
- }
- }
- // 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 {
- KINGART_DEBUG_MSG_P(PSTR("[KA] ERROR : Serial 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();
- }
-
- //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 MQTT_SUPPORT
- const String pos = String(_curtain_last_position);
- mqttSend(MQTT_TOPIC_CURTAIN, pos.c_str());
- #endif // MQTT_SUPPORT
- }
-
- //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, CustomResetReason::Hardware);
- } else { //In any other case, update as it could be a move action
- curtainUpdateUI();
- }
- }
-
- // -----------------------------------------------------------------------------
- // MQTT Support
- // -----------------------------------------------------------------------------
-
- #if MQTT_SUPPORT
-
- //------------------------------------------------------------------------------
- void _curtainMQTTCallback(unsigned int type, const char * topic, char * payload) {
- if (type == MQTT_CONNECT_EVENT) {
- mqttSubscribe(MQTT_TOPIC_CURTAIN);
- } else if (type == MQTT_MESSAGE_EVENT) {
- // Match topic
- const String t = mqttMagnitude(const_cast<char*>(topic));
- if (t.equals(MQTT_TOPIC_CURTAIN)) {
- 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 = String(payload).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["curtainBoot"] = getSetting("curtainBoot", "0");
- root["curtainConfig"] = 1;
- }
-
- //------------------------------------------------------------------------------
- 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["get"] = _curtain_last_position;
- if(_curtain_position_set == CURTAIN_POSITION_UNKNOWN) {
- _curtain_position_set = _curtain_last_position;
- }
- state["set"] = _curtain_position_set;
- state["button"] = _curtain_last_button;
- state["moving"] = _curtain_moving;
- state["type"] = getSetting("curtainType", "0");
- }
-
- //------------------------------------------------------------------------------
- void _curtainWebSocketStatus(JsonObject& root) {
- _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<int>();
- _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set);
- } else if(data.containsKey("button")){
- _curtain_last_button = data["button"].as<int>();
- _KACurtainSet(_curtain_last_button);
- }
- }
- }
-
- void _curtainWebSocketOnVisible(JsonObject& root) {
- root["curtainVisible"] = 1;
- }
-
- #endif //WEB_SUPPORT
-
- // -----------------------------------------------------------------------------
- // SETUP & LOOP
- // -----------------------------------------------------------------------------
-
- //------------------------------------------------------------------------------
- void _KACurtainLoop() {
-
- 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
- }
-
- // -----------------------------------------------------------------------------
- // Public
- // -----------------------------------------------------------------------------
-
- //------------------------------------------------------------------------------
- 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
- 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);
-
- }
-
- //------------------------------------------------------------------------------
- void curtainSetPosition(unsigned char id, long value) {
- if (id > 1) return;
- _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, constrain(value, 0, 100));
- }
-
- unsigned char curtainCount() {
- return 1;
- }
-
- #endif // KINGART_CURTAIN_SUPPORT
|