Browse Source

Kingart curtain switch UI support (#2250)

* 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

* Checked commit before PR. Added html gulp static files generated.

* Corrected a code refactor error which make the MQTT messages not handling position from received messages. Added comment to debug tab to show how to enable debug

* Use the maximum compression for gzip. A byte is a byte :)

* Update code/espurna/curtain_kingart.cpp

Co-authored-by: Max Prokhorov <prokhorov.max@outlook.com>

* Variable name prefix update
Added public set position function and curtain count

* Added Scheduler new type : curtain

* Add curtain schedule support and correct the 12h bug when adding a slot (00:00 after refresh). Default Hour is 12h

* Code cleaning.
Schedule support.
Light support is having an issue with new schedules corrected here.

* I let do better by the specialists

* Curtain init statues

* Coding style corrections

* Removed debug stuff from now - will be back later with the right way

* rebuild webui

* revert 435f1c5e03 schHour default, rebuild ui

Co-authored-by: Eric Chauvet <eric.chauvet@test-tree.com>
Co-authored-by: Max Prokhorov <prokhorov.max@outlook.com>
mcspr-patch-1
Eric Chauvet 4 years ago
committed by GitHub
parent
commit
21e75bef51
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 25273 additions and 22152 deletions
  1. +3
    -0
      code/espurna/board.cpp
  2. +2
    -0
      code/espurna/config/hardware.h
  3. +1
    -0
      code/espurna/config/types.h
  4. +10
    -0
      code/espurna/config/webui.h
  5. +384
    -73
      code/espurna/curtain_kingart.cpp
  6. +2
    -0
      code/espurna/curtain_kingart.h
  7. BIN
      code/espurna/data/index.all.html.gz
  8. BIN
      code/espurna/data/index.curtain.html.gz
  9. BIN
      code/espurna/data/index.light.html.gz
  10. BIN
      code/espurna/data/index.lightfox.html.gz
  11. BIN
      code/espurna/data/index.rfbridge.html.gz
  12. BIN
      code/espurna/data/index.rfm69.html.gz
  13. BIN
      code/espurna/data/index.sensor.html.gz
  14. BIN
      code/espurna/data/index.small.html.gz
  15. BIN
      code/espurna/data/index.thermostat.html.gz
  16. +36
    -5
      code/espurna/scheduler.cpp
  17. +3109
    -3065
      code/espurna/static/index.all.html.gz.h
  18. +2459
    -0
      code/espurna/static/index.curtain.html.gz.h
  19. +2851
    -2854
      code/espurna/static/index.light.html.gz.h
  20. +2435
    -2436
      code/espurna/static/index.lightfox.html.gz.h
  21. +2459
    -2461
      code/espurna/static/index.rfbridge.html.gz.h
  22. +3902
    -3908
      code/espurna/static/index.rfm69.html.gz.h
  23. +2510
    -2513
      code/espurna/static/index.sensor.html.gz.h
  24. +2392
    -2394
      code/espurna/static/index.small.html.gz.h
  25. +2433
    -2435
      code/espurna/static/index.thermostat.html.gz.h
  26. +2
    -0
      code/espurna/web.cpp
  27. +8
    -2
      code/gulpfile.js
  28. +34
    -1
      code/html/custom.css
  29. +138
    -3
      code/html/custom.js
  30. +103
    -2
      code/html/index.html

+ 3
- 0
code/espurna/board.cpp View File

@ -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


+ 2
- 0
code/espurna/config/hardware.h View File

@ -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


+ 1
- 0
code/espurna/config/types.h View File

@ -205,6 +205,7 @@
#define SCHEDULER_TYPE_SWITCH 1
#define SCHEDULER_TYPE_DIM 2
#define SCHEDULER_TYPE_CURTAIN 3
// -----------------------------------------------------------------------------
// IR


+ 10
- 0
code/espurna/config/webui.h View File

@ -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


+ 384
- 73
code/espurna/curtain_kingart.cpp View File

@ -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,166 @@ 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_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);
}
void _KACurtainReceiveUART() {
//------------------------------------------------------------------------------
//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();
@ -51,8 +201,15 @@ void _KACurtainReceiveUART() {
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
@ -87,112 +244,266 @@ 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);
}
}
// 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();
}
#endif // MQTT_SUPPORT
// Handle configuration button presses
if (buffer.indexOf("enterESPTOUCH") > 0) {
wifiStartAP();
} else if (buffer.indexOf("exitESPTOUCH") > 0) {
deferredReset(100, CUSTOM_RESET_HARDWARE);
}
} else {
KINGART_DEBUG_MSG_P(PSTR("[KA] ERROR : Serial unknown message : %s\n"), _KACurtainBuffer);
}
_KACurtainNewData = false;
//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();
}
}
// %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);
//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, 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<char*>(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 = 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() {
_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
}
// -----------------------------------------------------------------------------
// 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
#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);
}
//------------------------------------------------------------------------------
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

+ 2
- 0
code/espurna/curtain_kingart.h View File

@ -10,3 +10,5 @@ Copyright (C) 2019 by Albert Weterings
#include "espurna.h"
void kingartCurtainSetup();
void curtainSetPosition(unsigned char id, long value);
unsigned char curtainCount();

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.curtain.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.lightfox.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


BIN
code/espurna/data/index.thermostat.html.gz View File


+ 36
- 5
code/espurna/scheduler.cpp View File

@ -16,11 +16,20 @@ Adapted by Xose Pérez <xose dot perez at gmail dot com>
#include "ntp.h"
#include "relay.h"
#include "ws.h"
#include "curtain_kingart.h"
constexpr const int SchedulerDummySwitchId = 0xff;
int _sch_restore = 0;
unsigned char schedulableCount() {
return relayCount()
#ifdef CURTAIN_SUPPORT
+ curtainCount()
#endif
;
}
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
@ -30,13 +39,13 @@ bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
}
void _schWebSocketOnVisible(JsonObject& root) {
if (!relayCount()) return;
if (!schedulableCount()) return;
root["schVisible"] = 1;
}
void _schWebSocketOnConnected(JsonObject &root){
if (!relayCount()) return;
if (!schedulableCount()) return;
JsonObject &schedules = root.createNestedObject("schedules");
schedules["max"] = SCHEDULER_MAX_SCHEDULES;
@ -106,11 +115,16 @@ void _schConfigure() {
int sch_minute = getSetting({"schMinute", i}, 0);
bool sch_utc = getSetting({"schUTC", i}, false);
String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
int type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
const auto sch_type =
(SCHEDULER_TYPE_SWITCH == type) ? "switch" :
(SCHEDULER_TYPE_CURTAIN == type) ? "curtain" :
(SCHEDULER_TYPE_DIM == type) ? "channel" : "unknown";
DEBUG_MSG_P(
PSTR("[SCH] Schedule #%d: %s #%d to %d at %02d:%02d %s on %s%s\n"),
i, SCHEDULER_TYPE_SWITCH == sch_type ? "switch" : "channel", sch_switch,
i, sch_type, sch_switch,
sch_action, sch_hour, sch_minute, sch_utc ? "UTC" : "local time",
(char *) sch_weekdays.c_str(),
sch_enabled ? "" : " (disabled)"
@ -163,6 +177,13 @@ void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
lightUpdate(true, true);
}
#endif
#if CURTAIN_SUPPORT == 1
if (SCHEDULER_TYPE_CURTAIN == sch_type) {
DEBUG_MSG_P(PSTR("[SCH] Set curtain %d value to %d\n"), sch_switch, sch_action);
curtainSetPosition(sch_switch, sch_action);
}
#endif
}
#if NTP_LEGACY_SUPPORT
@ -255,6 +276,16 @@ void _schCheck(int relay, int daybefore) {
}
#endif
#if CURTAIN_SUPPORT == 1
if (SCHEDULER_TYPE_CURTAIN == sch_type && sch_switch == relay && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
minimum_restore_time = minutes_to_trigger;
saved_action = sch_action;
saved_sch = i;
}
#endif
if (minutes_to_trigger == 0 && relay == -1) {
_schAction(i, sch_action, sch_switch);
@ -311,7 +342,7 @@ void schSetup() {
static bool restore_once = true;
if (restore_once) {
for (unsigned char i = 0; i < relayCount(); i++) {
for (unsigned char i = 0; i < schedulableCount(); i++) {
if (getSetting({"relayLastSch", i}, 1 == SCHEDULER_RESTORE_LAST_SCHEDULE)) {
_schCheck(i, 0);
}


+ 3109
- 3065
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 2459
- 0
code/espurna/static/index.curtain.html.gz.h
File diff suppressed because it is too large
View File


+ 2851
- 2854
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 2435
- 2436
code/espurna/static/index.lightfox.html.gz.h
File diff suppressed because it is too large
View File


+ 2459
- 2461
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 3902
- 3908
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 2510
- 2513
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 2392
- 2394
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 2433
- 2435
code/espurna/static/index.thermostat.html.gz.h
File diff suppressed because it is too large
View File


+ 2
- 0
code/espurna/web.cpp View File

@ -30,6 +30,8 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#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


+ 8
- 2
code/gulpfile.js View File

@ -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
@ -166,7 +167,7 @@ var buildWebUI = function(module) {
minifyJS: true
})).
pipe(replace('pure-', 'p-')).
pipe(gzip()).
pipe(gzip({ gzipOptions: { level: 9 } })).
pipe(rename('index.' + module + '.html.gz')).
pipe(gulp.dest(dataFolder)).
pipe(toHeader('webui_image', true)).
@ -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'
)
);


+ 34
- 1
code/html/custom.css View File

@ -130,6 +130,35 @@ div.state {
color: #0F0;
}
/* -----------------------------------------------------------------------------
Curtains
-------------------------------------------------------------------------- */
/* removeIf(!curtain) */
.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;
}
/* endRemoveIf(!curtain) */
/* -----------------------------------------------------------------------------
Buttons
-------------------------------------------------------------------------- */
@ -190,7 +219,8 @@ div.state {
}
.button-add-switch-schedule,
.button-add-light-schedule {
.button-add-light-schedule,
.button-add-curtain-schedule {
background: rgb(0, 192, 0); /* green */
display: none;
}
@ -202,6 +232,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 */
}


+ 138
- 3
code/html/custom.js View File

@ -1099,7 +1099,18 @@ function addSchedule(values) {
var template = $("#scheduleTemplate").children();
var line = $(template).clone();
var type = (1 === values.schType) ? "switch" : "light";
var type = "none";
switch(values.schType) {
case 1:
type = "switch";
break;
case 2:
type = "light";
break;
case 3:
type = "curtain";
break;
}
template = $("#" + type + "ActionTemplate").children();
$(line).find("#schActionDiv").append(template.clone());
@ -1272,6 +1283,62 @@ function initMagnitudes(data) {
}
<!-- endRemoveIf(!sensor)-->
// -----------------------------------------------------------------------------
// Curtains
// -----------------------------------------------------------------------------
<!-- removeIf(!curtain)-->
//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});
});
}
function initCurtainConfig(data) {
var current = $("#curtainConfig > legend").length; // there is a legend per relay
if (current > 0) { return; }
// Populate the curtain select
$("select.iscurtain").append(
$("<option></option>")
.attr("value", "0")
.text("Curtain #" + "0")
);
}
<!-- endRemoveIf(!curtain)-->
// -----------------------------------------------------------------------------
// Lights
// -----------------------------------------------------------------------------
@ -1691,6 +1758,54 @@ function processData(data) {
if (key == "rpnNames") return;
// ---------------------------------------------------------------------
// Curtains
// ---------------------------------------------------------------------
<!-- removeIf(!curtain)-->
function applyCurtain(a, b) {
$("#curtainGetPicture").css('background', 'linear-gradient(' + a + ', black ' + b + '%, #a0d6ff ' + b + '%)');
}
if ("curtainState" === key) {
initCurtain();
switch(value.type) {
case '0': //Roller
default:
applyCurtain('180deg', value.get);
break;
case '1': //One side left to right
applyCurtain('90deg', value.get);
break;
case '2': //One side right to left
applyCurtain('270deg', value.get);
break;
case '3': //Two sides
$("#curtainGetPicture").css('background', 'linear-gradient(90deg, black ' + value.get/2 + '%, #a0d6ff ' + value.get/2 + '% ' + (100 - value.get/2) + '%, black ' + (100 - value.get/2) + '%)');
break;
}
$("#curtainSet").val(value.set);
if(!value.moving) {
$("button.curtain-button").css('background', 'rgb(66, 184, 221)');
} else {
if(!value.button)
$("button.button-curtain-pause").css('background', 'rgb(192, 0, 0)');
else if(value.button == 1) {
$("button.button-curtain-close").css('background', 'rgb(66, 184, 221)');
$("button.button-curtain-open").css('background', 'rgb(192, 0, 0)');
}
else if(value.button == 2) {
$("button.button-curtain-open").css('background', 'rgb(66, 184, 221)');
$("button.button-curtain-close").css('background', 'rgb(192, 0, 0)');
}
}
return;
}
<!-- endRemoveIf(!curtain)-->
// ---------------------------------------------------------------------
// Lights
// ---------------------------------------------------------------------
@ -1851,6 +1966,22 @@ function processData(data) {
return;
}
// ---------------------------------------------------------------------
// Curtain(s)
// ---------------------------------------------------------------------
<!-- removeIf(!curtain)-->
// Relay configuration
if ("curtainConfig" === key) {
initCurtainConfig(value);
return;
}
<!-- endRemoveIf(!curtain)-->
// ---------------------------------------------------------------------
// LEDs
// ---------------------------------------------------------------------
@ -2234,6 +2365,12 @@ $(function() {
});
<!-- endRemoveIf(!light)-->
<!-- removeIf(!curtain)-->
$(".button-add-curtain-schedule").on("click", function() {
addSchedule({schType: 3, schSwitch: -1});
});
<!-- endRemoveIf(!curtain)-->
$(".button-add-rpnrule").on('click', addRPNRule);
$(".button-add-rpntopic").on('click', addRPNTopic);
@ -2271,8 +2408,6 @@ $(function() {
// don't autoconnect when opening from filesystem
if (window.location.protocol === "file:") {
processData({"webMode": 0});
processData({"hlwVisible":1,"pwrVisible":1,"tmpCorrection":0,"humCorrection":0,"luxCorrection":0,"snsRead":5,"snsReport":10,"snsSave":2,"magnitudesConfig":{"index":[0,0,0,0,0,0,0,0],"type":[4,5,6,8,7,9,11,10],"units":["A","V","W","VAR","VA","%","J","kWh"],"description":["HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)","HLW8012 @ GPIO(5,14,13)"],"size":8}});
processData({"magnitudes":{"value":["0.079","218","37","0","17","100","96","0.001"],"error":[0,0,0,0,0,0,0,0],"info":[0,0,0,0,0,0,0,"Last saved: 2020-04-07 21:10:15"],"size":8}});
return;
}


+ 103
- 2
code/html/index.html View File

@ -96,6 +96,12 @@
<a href="#" class="pure-menu-link" data="panel-general">GENERAL</a>
</li>
<!-- removeIf(!curtain) -->
<li class="pure-menu-item module module-curtain">
<a href="#" class="pure-menu-link" data="panel-curtain">CURTAIN</a>
</li>
<!-- endRemoveIf(!curtain) -->
<!-- removeIf(!thermostat) -->
<li class="pure-menu-item module module-thermostat">
<a href="#" class="pure-menu-link" data="panel-thermostat">THERMOSTAT</a>
@ -228,6 +234,10 @@
<div id="relays"></div>
<!-- removeIf(!curtain) -->
<div id="curtains"></div>
<!-- endRemoveIf(!curtain) -->
<!-- removeIf(!light) -->
<div id="colors"></div>
<div id="cct"></div>
@ -484,6 +494,54 @@
</div>
</form>
<!-- removeIf(!curtain) -->
<form id="form-curtain" class="pure-form form-settings">
<div class="panel" id="panel-curtain">
<div class="header">
<h1>CURTAIN</h1>
<h2>Curtain configuration</h2>
</div>
<div class="page">
<fieldset>
<legend class="module module-curtain">General</legend>
<div class="pure-g module module-curtain">
<label class="pure-u-1 pure-u-lg-1-4">Curtain type</label>
<select class="pure-u-1 pure-u-lg-1-4" name="curtainType" tabindex="3">
<option value="0">Roller</option>
<option value="1">Closing right</option>
<option value="2">Closing left</option>
<option value="3">Two sides</option>
</select>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Define your curtain type. It adapts the graphical view in status menu.</div>
</div>
<div class="pure-g module module-curtain">
<label class="pure-u-1 pure-u-lg-1-4">Curtain bootup position</label>
<select class="pure-u-1 pure-u-lg-1-4" name="curtainBoot" tabindex="3">
<option value="0">Do nothing</option>
<option value="1">Closed</option>
<option value="2">Opened</option>
<option value="3">Last set position</option>
</select>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Define the initial position of the curtain after a reboot or power loss.</div>
</div>
<div id="curtainConfig"></div>
</fieldset>
</div>
</div>
</form>
<!-- endRemoveIf(!curtain) -->
<!-- removeIf(!light) -->
<form id="form-color" class="pure-form form-settings">
<div class="panel" id="panel-color">
@ -785,12 +843,15 @@
<fieldset>
<div id="schedules" class="group-settings" data-settings-max="10" data-settings-target="schSwitch" ></div>
<div id="schedules" class="group-settings" data-settings-max="10" data-settings-target="schSwitch schType" ></div>
<button type="button" class="pure-button button-add-switch-schedule module module-relay">Add switch schedule</button>
<!-- removeIf(!light) -->
<button type="button" class="pure-button button-add-light-schedule module module-color">Add channel schedule</button>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!curtain) -->
<button type="button" class="pure-button button-add-curtain-schedule module module-curtain">Add curtain schedule</button>
<!-- endRemoveIf(!curtain) -->
</fieldset>
@ -972,6 +1033,9 @@
<div class="pure-u-1 pure-u-lg-3-4 hint">
This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.<br />
- <strong>&lt;root&gt;/relay/#/set</strong> 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.<br />
<!-- removeIf(!curtain) -->
- <strong>&lt;root&gt;/curtain/set</strong> Set the curtain opening value (0-100), 0 means closed, 100 opened. "on", "off", "pause" pilots buttons.<br />
<!-- endRemoveIf(!curtain) -->
<!-- removeIf(!light) -->
<span class="module module-color">- <strong>&lt;root&gt;/rgb/set</strong> Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/hsv/set</strong> Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.<br /></span>
@ -1500,6 +1564,9 @@
<div class="pure-g module module-cmd">
<div class="pure-u-1 hint">
Write a command and click send to execute it on the device. The output will be shown in the debug text area below.
<!-- removeIf(!curtain) -->
Type "debug" to display MCU serial exchanges. Any other command is sent to MCU serial as is.
<!-- endRemoveIf(!curtain) -->
</div>
<input name="dbgcmd" class="pure-u-3-4" type="text" tabindex="2" />
<div class="pure-u-1-4 pure-u-lg-1-4"><button type="button" class="pure-button button-dbgcmd pure-u-23-24">Send</button></div>
@ -1890,7 +1957,7 @@
<label class="pure-u-1 pure-u-lg-1-4">When time is</label>
<div class="pure-u-1-4 pure-u-lg-1-5">
<input class="pure-u-2-3" name="schHour" type="number" min="0" step="1" max="23" value="12" />
<input class="pure-u-2-3" name="schHour" type="number" min="0" step="1" max="23" value="0" />
<div class="pure-u-1-4 hint center">&nbsp;h</div>
</div>
<div class="pure-u-1-4 pure-u-lg-1-5">
@ -1944,6 +2011,17 @@
</div>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!curtain) -->
<div id="curtainActionTemplate" class="template">
<label class="pure-u-1 pure-u-lg-1-4">Curtain position (0 = open to 100 = closed)</label>
<div class="pure-u-1 pure-u-lg-1-5">
<input class="pure-u-2-3" name="schAction" type="number" min="0" step="1" max="100" value="0" />
</div>
<select class="pure-u-1 pure-u-lg-1-5 iscurtain" name="schSwitch" ></select>
<input type="hidden" name="schType" value="3">
</div>
<!-- endRemoveIf(!curtain) -->
<div id="relayTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Switch #<span class="id"></span></label>
@ -2070,6 +2148,29 @@
</div>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!curtain) -->
<div id="curtainTemplate" class="template">
<div class="pure-g">
<div class="pure-u-1-1 curtain-div">
<div class="curtain-roller" id="curtainGetPicture"></div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-1-3 curtain-div"><button type="button" class="pure-button curtain-button button-curtain-close">CLOSE</button></div>
<div class="pure-u-1-3 curtain-div"><button type="button" class="pure-button curtain-button button-curtain-pause">PAUSE</button></div>
<div class="pure-u-1-3 curtain-div"><button type="button" class="pure-button curtain-button button-curtain-open">OPEN</button></div>
</div>
<div class="pure-g">
<input type="range" list="tickmarks" min="0" max="100" class="slider pure-u-1-1 reverse-range" id="curtainSet">
<datalist id="tickmarks">
<option value="0"></option>
<option value="50"></option>
<option value="100"></option>
</datalist>
</div>
</div>
<!-- endRemoveIf(!curtain) -->
<!-- removeIf(!sensor) -->
<div id="magnitudeTemplate" class="template">
<div class="pure-g">


Loading…
Cancel
Save