Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
20 KiB

  1. /*
  2. KingArt Cover/Shutter/Blind/Curtain support for ESPURNA
  3. Based on xdrv_19_ps16dz.dimmer.ino, PS_16_DZ dimmer support for Tasmota
  4. Copyright (C) 2019 by Albert Weterings
  5. Based on curtain_kingart.ino Albert Weterings
  6. Copyright (C) 2020 - Eric Chauvet
  7. */
  8. #include "curtain_kingart.h"
  9. #if KINGART_CURTAIN_SUPPORT
  10. #include "mqtt.h"
  11. #include "ntp.h"
  12. #include "ntp_timelib.h"
  13. #include "settings.h"
  14. #include "ws.h"
  15. #ifndef KINGART_CURTAIN_PORT
  16. #define KINGART_CURTAIN_PORT Serial // Hardware serial port by default
  17. #endif
  18. #ifndef KINGART_CURTAIN_BUFFER_SIZE
  19. #define KINGART_CURTAIN_BUFFER_SIZE 100 // Local UART buffer size
  20. #endif
  21. #define KINGART_CURTAIN_TERMINATION '\e' // Termination character after each message
  22. #define KINGART_CURTAIN_BAUDRATE 19200 // Serial speed is fixed for the device
  23. // --> Let see after if we define a curtain generic switch, use these for now
  24. #define CURTAIN_BUTTON_UNKNOWN -1
  25. #define CURTAIN_BUTTON_PAUSE 0
  26. #define CURTAIN_BUTTON_OPEN 1
  27. #define CURTAIN_BUTTON_CLOSE 2
  28. #define CURTAIN_INIT_CLOSE 1
  29. #define CURTAIN_INIT_OPEN 2
  30. #define CURTAIN_INIT_POSITION 3
  31. #define CURTAIN_POSITION_UNKNOWN -1
  32. // <--
  33. #define KINGART_DEBUG_MSG_P(...) do { if (_curtain_debug_flag) { DEBUG_MSG_P(__VA_ARGS__); } } while(0)
  34. char _KACurtainBuffer[KINGART_CURTAIN_BUFFER_SIZE];
  35. bool _KACurtainNewData = false;
  36. // Status vars - for curtain move detection :
  37. int _curtain_position = CURTAIN_POSITION_UNKNOWN;
  38. int _curtain_last_position = CURTAIN_POSITION_UNKNOWN;
  39. int _curtain_button = CURTAIN_BUTTON_UNKNOWN;
  40. int _curtain_last_button = CURTAIN_BUTTON_UNKNOWN;
  41. unsigned long last_uptime = 0;
  42. 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)
  43. bool _curtain_waiting_ack = false; //Avoid too fast MQTT commands
  44. bool _curtain_ignore_next_position = false; //Avoid a bug (see (*1)
  45. 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
  46. // Calculated behaviour depending on KA switch, MQTT and UI actions
  47. bool _curtain_moving = true;
  48. //Enable more traces, true as a default and stopped when curtain is setup.
  49. bool _curtain_debug_flag = true;
  50. #if WEB_SUPPORT
  51. bool _curtain_report_ws = true; //This will init curtain control and flag the web ui update
  52. #endif // WEB_SUPPORT
  53. //------------------------------------------------------------------------------
  54. void curtainUpdateUI() {
  55. #if WEB_SUPPORT
  56. _curtain_report_ws = true;
  57. #endif // WEB_SUPPORT
  58. }
  59. //------------------------------------------------------------------------------
  60. int setButtonFromSwitchText(String & text) {
  61. if(text == "on")
  62. return CURTAIN_BUTTON_OPEN;
  63. else if(text == "off")
  64. return CURTAIN_BUTTON_CLOSE;
  65. else if(text == "pause")
  66. return CURTAIN_BUTTON_PAUSE;
  67. else
  68. return CURTAIN_BUTTON_UNKNOWN;
  69. }
  70. // -----------------------------------------------------------------------------
  71. // Private
  72. // -----------------------------------------------------------------------------
  73. //------------------------------------------------------------------------------
  74. //This check that wa got latest and new stats from the AT+RESULT message
  75. bool _KAValidStatus() {
  76. return _curtain_button != CURTAIN_BUTTON_UNKNOWN &&
  77. _curtain_last_button != CURTAIN_BUTTON_UNKNOWN &&
  78. _curtain_position != CURTAIN_POSITION_UNKNOWN &&
  79. _curtain_last_position != CURTAIN_POSITION_UNKNOWN;
  80. }
  81. //------------------------------------------------------------------------------
  82. //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()
  83. void _KASetMoving() {
  84. last_uptime = millis() + 1000; //Let the returned curtain position to be refreshed to know of the curtain is still moving
  85. _curtain_moving = true;
  86. }
  87. //------------------------------------------------------------------------------
  88. //Send a buffer to serial
  89. void _KACurtainSend(const char * tx_buffer) {
  90. KINGART_CURTAIN_PORT.print(tx_buffer);
  91. KINGART_CURTAIN_PORT.print(KINGART_CURTAIN_TERMINATION);
  92. KINGART_CURTAIN_PORT.flush();
  93. KINGART_DEBUG_MSG_P(PSTR("[KA] UART OUT %s\n"), tx_buffer);
  94. }
  95. //------------------------------------------------------------------------------
  96. //Send a formatted message to MCU
  97. void _KACurtainSet(int button, int position = CURTAIN_POSITION_UNKNOWN) {
  98. if(_curtain_waiting_ack) {
  99. KINGART_DEBUG_MSG_P(PSTR("[KA] UART ACK not received : Request ignored!\n"));
  100. return;
  101. }
  102. char tx_buffer[80] = {0};
  103. if(button != CURTAIN_BUTTON_UNKNOWN && position != CURTAIN_POSITION_UNKNOWN) {
  104. snprintf_P(
  105. tx_buffer, sizeof(tx_buffer),
  106. PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\",\"setclose\":%d"),
  107. now(), millis() % 1000,
  108. (button == CURTAIN_BUTTON_PAUSE ? "pause" : (button == CURTAIN_BUTTON_OPEN ? "on" : "off")), position
  109. );
  110. } else if(button == CURTAIN_BUTTON_UNKNOWN) {
  111. snprintf_P(
  112. tx_buffer, sizeof(tx_buffer),
  113. PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"setclose\":%d"),
  114. now(), millis() % 1000,
  115. position
  116. );
  117. } else {
  118. snprintf_P(
  119. tx_buffer, sizeof(tx_buffer),
  120. PSTR("AT+UPDATE=\"sequence\":\"%d%03u\",\"switch\":\"%s\""),
  121. now(), millis() % 1000,
  122. (button == CURTAIN_BUTTON_PAUSE ? "pause" : (button == CURTAIN_BUTTON_OPEN ? "on" : "off"))
  123. );
  124. }
  125. _KACurtainSend(tx_buffer);
  126. _curtain_waiting_ack = true;
  127. }
  128. //------------------------------------------------------------------------------
  129. //Stop moving will set the real curtain position to the GUI/MQTT
  130. void _KAStopMoving() {
  131. _curtain_moving = false;
  132. if( _curtain_position != CURTAIN_POSITION_UNKNOWN)
  133. _curtain_position_set = _curtain_position;
  134. else if( _curtain_last_position != CURTAIN_POSITION_UNKNOWN)
  135. _curtain_position_set = _curtain_last_position;
  136. if (!_curtain_initial_position_set) { //The curtain stopped moving for the first time, set the position back to
  137. int init_position = getSetting("curtainBoot", 0);
  138. KINGART_DEBUG_MSG_P(PSTR("[KA] curtainBoot : %d, curtainBootPos : %d\n"), init_position, getSetting("curtainBootPos", 100));
  139. if (init_position == CURTAIN_INIT_CLOSE) {
  140. _KACurtainSet(CURTAIN_BUTTON_CLOSE);
  141. } else if (init_position == CURTAIN_INIT_OPEN) {
  142. _KACurtainSet(CURTAIN_BUTTON_OPEN);
  143. } else if (init_position == CURTAIN_INIT_POSITION) {
  144. int pos = getSetting("curtainBootPos", 100); //Set closed if we do not have initial position set.
  145. if (_curtain_position_set != pos) {
  146. _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, pos);
  147. }
  148. }
  149. _curtain_initial_position_set = true;
  150. _curtain_debug_flag = false; //Disable debug - user has could ask for it
  151. } else if(_curtain_position_set != CURTAIN_POSITION_UNKNOWN && _curtain_position_set != getSetting("curtainBootPos", _curtain_position_set)) {
  152. setSetting("curtainBootPos", _curtain_last_position); //Remeber last position in case of power loss
  153. }
  154. }
  155. //------------------------------------------------------------------------------
  156. //Receive a buffer from serial
  157. bool _KACurtainReceiveUART() {
  158. static unsigned char ndx = 0;
  159. while (KINGART_CURTAIN_PORT.available() > 0 && !_KACurtainNewData) {
  160. char rc = KINGART_CURTAIN_PORT.read();
  161. if (rc != KINGART_CURTAIN_TERMINATION) {
  162. _KACurtainBuffer[ndx] = rc;
  163. if (ndx < KINGART_CURTAIN_BUFFER_SIZE - 1) ndx++;
  164. } else {
  165. _KACurtainBuffer[ndx] = '\0';
  166. _KACurtainNewData = true;
  167. ndx = 0;
  168. }
  169. }
  170. if(_KACurtainNewData) {
  171. KINGART_DEBUG_MSG_P(PSTR("[KA] Serial received : %s\n"), _KACurtainBuffer);
  172. _KACurtainNewData = false;
  173. return true;
  174. }
  175. return false;
  176. }
  177. /*
  178. Buttons on the device will move Cover/Shutter/Blind/Curtain up/open or down/close On the end of
  179. every movement the unit reports the last action and posiston over MQTT topic {hostname}/curtain
  180. RAW paylod format looks like:
  181. AT+UPDATE="switch":"on","setclose":13
  182. AT+UPDATE="switch":"off","setclose":38
  183. AT+UPDATE="switch":"pause","setclose":75
  184. The device is listening to MQTT topic {hostname}/curtain/set, to which you can send:
  185. - position value, in range from 0 to 100
  186. - "pause", to stop the movement.
  187. The device will determine the direction all by itself.
  188. # Set the Cover / Shutter / Blind / Curtain run time
  189. The factory default Open and Close run time for the switch is 50 seconds, and it must be set to
  190. an accurate run time for smooth working. Some motors do not have the resistance stop function,
  191. so when the Cover/Shutter/Blind/Curtain track open or close to the maximum length, but the motor keeps on running.
  192. This might cause damage on the motor and the switch, it also wastes a lot of energy. In order
  193. to protect the motor, this switch designed with a time setting function. After setting up the run time,
  194. the switch will automaticly stop when the track reaches its limits. The run time setting is also helpful
  195. for the accurate control when manually controlling the device via the touch panel.
  196. After installing the switch and connecting the switch for the very first time:
  197. - First, it will automatically close the Cover/Shutter/Blind/Curtain to the maximum.
  198. - Press and hold the touch interface pause button for around 4 seconds until the red background
  199. led lights up and starts blinking. Then, press the open touch button so start the opening process.
  200. - When cover is fully open, press the Open or Close button to stop the timer and save the calculated run time.
  201. To configure the device:
  202. - Press up/down for 5 seconds to bring device into AP mode. After pressing up/down again, device will restart in normal mode.
  203. */
  204. //------------------------------------------------------------------------------
  205. void _KACurtainResult() {
  206. // Need to send confirmation to the N76E003AT20 that message is received
  207. // ECH : TODO Check this is the case every time
  208. _KACurtainSend("AT+SEND=ok");
  209. //Init receive stats : The buffer which may contain : "setclose":INT(0-100) or "switch":["on","off","pause"]
  210. const String buffer(_KACurtainBuffer);
  211. _curtain_button = CURTAIN_BUTTON_UNKNOWN;
  212. _curtain_position = CURTAIN_POSITION_UNKNOWN;
  213. if(buffer.indexOf("AT+RESULT") == 0) { //AT+RESULT is an acquitment of our command (MQTT or GUI)
  214. //Set the status on what we kown
  215. if( ( _curtain_last_button == CURTAIN_BUTTON_OPEN && _curtain_last_position == 0 ) ||
  216. ( _curtain_last_button == CURTAIN_BUTTON_CLOSE && _curtain_last_position == 100 ) ||
  217. _curtain_last_button == CURTAIN_BUTTON_PAUSE) { //The curtain is max opened, closed or pause
  218. _KAStopMoving();
  219. } else { //Else it is probably moving
  220. _KASetMoving();
  221. /*
  222. (*1) ATTENTION THERE :
  223. Send immediatly a AT+START - we need to purge the first response.
  224. It will return us the right direction of the switch but the position
  225. we set instead of the real on. We take care of the switch response but
  226. we ignore the position.
  227. */
  228. _KACurtainSend("AT+START");
  229. _curtain_ignore_next_position = true;
  230. }
  231. //Time to update UI
  232. curtainUpdateUI();
  233. _curtain_waiting_ack = false;
  234. return;
  235. } else if(buffer.indexOf("AT+UPDATE") == 0) { //AT+UPDATE is a response from the switch itself or AT+SEND query
  236. // Get switch status from MCU
  237. int switch_idx = buffer.indexOf("switch");
  238. if (switch_idx > 0) {
  239. String switch_text = buffer.substring(switch_idx + strlen("switch") + 3, buffer.length());
  240. int leftovers = switch_text.indexOf('"');
  241. if (leftovers > 0) { //We must find leftover as it is text
  242. switch_text = switch_text.substring(0, leftovers);
  243. _curtain_button = setButtonFromSwitchText(switch_text);
  244. }
  245. }
  246. // Get position from MCU
  247. int setclose_idx = buffer.indexOf("setclose");
  248. if (setclose_idx > 0) {
  249. String position = buffer.substring(setclose_idx + strlen("setclose") + 2, buffer.length());
  250. int leftovers = position.indexOf(',');
  251. if (leftovers > 0) { // Not found if finishing by setclose
  252. position = position.substring(0, leftovers);
  253. }
  254. if(_curtain_ignore_next_position) { // (*1)
  255. _curtain_ignore_next_position = false;
  256. } else {
  257. _curtain_position = position.toInt();
  258. }
  259. }
  260. } else {
  261. KINGART_DEBUG_MSG_P(PSTR("[KA] ERROR : Serial unknown message : %s\n"), _KACurtainBuffer);
  262. }
  263. //Check if curtain is moving or not
  264. if( _curtain_button == CURTAIN_BUTTON_PAUSE ) { //This is returned from MCU and tells us than last status is pause or full opened or closed
  265. _KAStopMoving();
  266. } else if(_curtain_moving ) {
  267. if(_KAValidStatus()) {
  268. if(_curtain_last_button != _curtain_button) //Direction change? Reset the timer to know
  269. _KASetMoving();
  270. else if(_curtain_last_position == _curtain_position) //Same direction, same position - curtain is not moving anymore
  271. _KAStopMoving();
  272. }
  273. } else { //Not paused, not moving, and we received an AT+UPDATE -> This means that we are moving
  274. _KASetMoving();
  275. }
  276. //Update last position and transmit to MQTT (GUI is at the end)
  277. if(_curtain_position != CURTAIN_POSITION_UNKNOWN && _curtain_last_position != _curtain_position) {
  278. _curtain_last_position = _curtain_position;
  279. #if MQTT_SUPPORT
  280. const String pos = String(_curtain_last_position);
  281. mqttSend(MQTT_TOPIC_CURTAIN, pos.c_str());
  282. #endif // MQTT_SUPPORT
  283. }
  284. //Reset last button to make the algorithm work and set last button state
  285. if(!_curtain_moving) {
  286. _curtain_last_button = CURTAIN_BUTTON_UNKNOWN;
  287. } else if (_curtain_button != CURTAIN_BUTTON_UNKNOWN) {
  288. _curtain_last_button = _curtain_button;
  289. }
  290. // Handle configuration button presses
  291. if (buffer.indexOf("enterESPTOUCH") > 0) {
  292. wifiStartAP();
  293. } else if (buffer.indexOf("exitESPTOUCH") > 0) {
  294. deferredReset(100, CustomResetReason::Hardware);
  295. } else { //In any other case, update as it could be a move action
  296. curtainUpdateUI();
  297. }
  298. }
  299. // -----------------------------------------------------------------------------
  300. // MQTT Support
  301. // -----------------------------------------------------------------------------
  302. #if MQTT_SUPPORT
  303. //------------------------------------------------------------------------------
  304. void _curtainMQTTCallback(unsigned int type, const char * topic, char * payload) {
  305. if (type == MQTT_CONNECT_EVENT) {
  306. mqttSubscribe(MQTT_TOPIC_CURTAIN);
  307. } else if (type == MQTT_MESSAGE_EVENT) {
  308. // Match topic
  309. const String t = mqttMagnitude(const_cast<char*>(topic));
  310. if (t.equals(MQTT_TOPIC_CURTAIN)) {
  311. if (strcmp(payload, "pause") == 0) {
  312. _KACurtainSet(CURTAIN_BUTTON_PAUSE);
  313. } else if (strcmp(payload, "on") == 0) {
  314. _KACurtainSet(CURTAIN_BUTTON_OPEN);
  315. } else if (strcmp(payload, "off") == 0) {
  316. _KACurtainSet(CURTAIN_BUTTON_CLOSE);
  317. } else {
  318. _curtain_position_set = String(payload).toInt();
  319. _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set);
  320. }
  321. }
  322. }
  323. }
  324. #endif // MQTT_SUPPORT
  325. // -----------------------------------------------------------------------------
  326. // WEB Support
  327. // -----------------------------------------------------------------------------
  328. #if WEB_SUPPORT
  329. //------------------------------------------------------------------------------
  330. void _curtainWebSocketOnConnected(JsonObject& root) {
  331. root["curtainType"] = getSetting("curtainType", "0");
  332. root["curtainBoot"] = getSetting("curtainBoot", "0");
  333. root["curtainConfig"] = 1;
  334. }
  335. //------------------------------------------------------------------------------
  336. bool _curtainWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  337. if (strncmp(key, "curtain", strlen("curtain")) == 0) return true;
  338. return false;
  339. }
  340. //------------------------------------------------------------------------------
  341. void _curtainWebSocketUpdate(JsonObject& root) {
  342. JsonObject& state = root.createNestedObject("curtainState");
  343. state["get"] = _curtain_last_position;
  344. if(_curtain_position_set == CURTAIN_POSITION_UNKNOWN) {
  345. _curtain_position_set = _curtain_last_position;
  346. }
  347. state["set"] = _curtain_position_set;
  348. state["button"] = _curtain_last_button;
  349. state["moving"] = _curtain_moving;
  350. state["type"] = getSetting("curtainType", "0");
  351. }
  352. //------------------------------------------------------------------------------
  353. void _curtainWebSocketStatus(JsonObject& root) {
  354. _curtainWebSocketUpdate(root);
  355. }
  356. //------------------------------------------------------------------------------
  357. void _curtainWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  358. if (strcmp(action, "curtainAction") == 0) {
  359. if (data.containsKey("position")) {
  360. _curtain_position_set = data["position"].as<int>();
  361. _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set);
  362. } else if(data.containsKey("button")){
  363. _curtain_last_button = data["button"].as<int>();
  364. _KACurtainSet(_curtain_last_button);
  365. }
  366. }
  367. }
  368. void _curtainWebSocketOnVisible(JsonObject& root) {
  369. root["curtainVisible"] = 1;
  370. }
  371. #endif //WEB_SUPPORT
  372. // -----------------------------------------------------------------------------
  373. // SETUP & LOOP
  374. // -----------------------------------------------------------------------------
  375. //------------------------------------------------------------------------------
  376. void _KACurtainLoop() {
  377. if(_KACurtainReceiveUART()) {
  378. _KACurtainResult();
  379. } else if(_curtain_moving) { //When curtain move and no messages, get position every 600ms with AT+START
  380. unsigned long uptime = millis();
  381. long diff = uptime - last_uptime;
  382. if(diff >= 600) {
  383. _KACurtainSend("AT+START");
  384. last_uptime = uptime;
  385. }
  386. }
  387. #if WEB_SUPPORT
  388. if (_curtain_report_ws) { //Launch a websocket update
  389. wsPost(_curtainWebSocketUpdate);
  390. _curtain_report_ws = false;
  391. }
  392. #endif
  393. }
  394. // -----------------------------------------------------------------------------
  395. // Public
  396. // -----------------------------------------------------------------------------
  397. //------------------------------------------------------------------------------
  398. void kingartCurtainSetup() {
  399. // Init port to receive and send messages
  400. KINGART_CURTAIN_PORT.begin(KINGART_CURTAIN_BAUDRATE);
  401. #if MQTT_SUPPORT
  402. // Register MQTT callback only when supported
  403. mqttRegister(_curtainMQTTCallback);
  404. #endif // MQTT_SUPPORT
  405. #if WEB_SUPPORT
  406. // Websockets
  407. wsRegister()
  408. .onVisible(_curtainWebSocketOnVisible)
  409. .onConnected(_curtainWebSocketOnConnected)
  410. .onKeyCheck(_curtainWebSocketOnKeyCheck)
  411. .onAction(_curtainWebSocketOnAction)
  412. .onData(_curtainWebSocketUpdate);
  413. #endif
  414. // Register loop to poll the UART for new messages
  415. espurnaRegisterLoop(_KACurtainLoop);
  416. }
  417. //------------------------------------------------------------------------------
  418. void curtainSetPosition(unsigned char id, long value) {
  419. if (id > 1) return;
  420. _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, constrain(value, 0, 100));
  421. }
  422. unsigned char curtainCount() {
  423. return 1;
  424. }
  425. #endif // KINGART_CURTAIN_SUPPORT