mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2025-12-13 02:52:11 +01:00
Added blinds as a shade type
This commit is contained in:
parent
0f2f30bf4d
commit
c9f3e79453
11 changed files with 657 additions and 127 deletions
|
|
@ -3,7 +3,7 @@
|
|||
#ifndef configsettings_h
|
||||
#define configsettings_h
|
||||
|
||||
#define FW_VERSION "v1.3.0"
|
||||
#define FW_VERSION "v1.3.1"
|
||||
enum DeviceStatus {
|
||||
DS_OK = 0,
|
||||
DS_ERROR = 1,
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ bool Network::connectWiFi() {
|
|||
return false;
|
||||
}
|
||||
bool Network::connect() {
|
||||
if(settings.connType != conn_types::wifi && !this->wifiFallback)
|
||||
if(settings.connType != conn_types::wifi && settings.connType != conn_types::unset && !this->wifiFallback)
|
||||
return this->connectWired();
|
||||
return this->connectWiFi();
|
||||
}
|
||||
|
|
|
|||
283
Somfy.cpp
283
Somfy.cpp
|
|
@ -24,6 +24,7 @@ uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to
|
|||
#endif
|
||||
|
||||
#define SETMY_REPEATS 15
|
||||
#define TILT_REPEATS 7
|
||||
|
||||
int sort_asc(const void *cmp1, const void *cmp2) {
|
||||
int a = *((uint8_t *)cmp1);
|
||||
|
|
@ -346,6 +347,9 @@ bool SomfyShade::unlinkRemote(uint32_t address) {
|
|||
void SomfyShade::checkMovement() {
|
||||
int8_t currDir = this->direction;
|
||||
uint8_t currPos = this->position;
|
||||
int8_t currTiltDir = this->tiltDirection;
|
||||
uint8_t currTiltPos = this->tiltPosition;
|
||||
|
||||
if(this->direction > 0) {
|
||||
if(this->downTime == 0) {
|
||||
this->direction = 0;
|
||||
|
|
@ -437,6 +441,69 @@ void SomfyShade::checkMovement() {
|
|||
this->seekingPos = false;
|
||||
}
|
||||
}
|
||||
if(this->tiltDirection > 0) {
|
||||
int32_t msFrom0 = (int32_t)floor(this->startTiltPos * this->tiltTime);
|
||||
msFrom0 += (millis() - this->tiltStart);
|
||||
msFrom0 = min((int32_t)this->tiltTime, msFrom0);
|
||||
if(msFrom0 >= this->tiltTime) {
|
||||
this->currentTiltPos = 1.0;
|
||||
this->tiltDirection = 0;
|
||||
}
|
||||
else {
|
||||
this->currentTiltPos = min(max((float)0.0, (float)msFrom0 / (float)this->tiltTime), (float)1.0);
|
||||
if(this->currentTiltPos >= 1) {
|
||||
this->tiltDirection = 0;
|
||||
this->currentTiltPos = 1.0;
|
||||
}
|
||||
}
|
||||
this->tiltPosition = floor(this->currentTiltPos * 100);
|
||||
if(this->seekingTiltPos && this->tiltPosition >= this->tiltTarget) {
|
||||
Serial.print("Stopping Shade Tilt:");
|
||||
Serial.print(this->name);
|
||||
Serial.print(" at ");
|
||||
Serial.print(this->tiltPosition);
|
||||
Serial.print("% target ");
|
||||
Serial.print(this->tiltTarget);
|
||||
Serial.println("%");
|
||||
this->sendCommand(somfy_commands::My);
|
||||
this->tiltDirection = 0;
|
||||
this->seekingTiltPos = false;
|
||||
}
|
||||
}
|
||||
else if(this->tiltDirection < 0) {
|
||||
if(this->tiltTime == 0) {
|
||||
this->tiltDirection = 0;
|
||||
this->currentTiltPos = 0;
|
||||
}
|
||||
else {
|
||||
int32_t msFrom100 = (int32_t)this->tiltTime - (int32_t)floor(this->startTiltPos * this->tiltTime);
|
||||
msFrom100 += (millis() - this->tiltStart);
|
||||
msFrom100 = min((int32_t)this->tiltTime, msFrom100);
|
||||
if(msFrom100 >= this->tiltTime) {
|
||||
this->currentTiltPos = 0.0;
|
||||
this->tiltDirection = 0;
|
||||
}
|
||||
this->currentTiltPos = (float)1.0 - min(max((float)0.0, (float)msFrom100 / (float)this->tiltTime), (float)1.0);
|
||||
// If we are at the top of the shade then set the movement to 0.
|
||||
if(this->currentTiltPos <= 0.0) {
|
||||
this->tiltDirection = 0;
|
||||
this->currentTiltPos = 0;
|
||||
}
|
||||
}
|
||||
this->tiltPosition = floor(this->currentTiltPos * 100);
|
||||
if(this->seekingTiltPos && this->tiltPosition <= this->tiltTarget) {
|
||||
Serial.print("Stopping Shade Tilt:");
|
||||
Serial.print(this->name);
|
||||
Serial.print(" at ");
|
||||
Serial.print(this->tiltPosition);
|
||||
Serial.print("% target ");
|
||||
Serial.print(this->tiltTarget);
|
||||
Serial.println("%");
|
||||
this->sendCommand(somfy_commands::My);
|
||||
this->tiltDirection = 0;
|
||||
this->seekingTiltPos = false;
|
||||
}
|
||||
}
|
||||
if(currDir != this->direction && this->direction == 0) {
|
||||
char shadeKey[15];
|
||||
snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId);
|
||||
|
|
@ -463,11 +530,22 @@ void SomfyShade::checkMovement() {
|
|||
pref.end();
|
||||
}
|
||||
}
|
||||
if(currDir != this->direction || currPos != this->position) {
|
||||
if(currTiltDir != this->tiltDirection && this->tiltDirection == 0) {
|
||||
char shadeKey[15];
|
||||
snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId);
|
||||
Serial.print("Writing current shade tilt position: ");
|
||||
Serial.println(this->currentTiltPos, 4);
|
||||
pref.begin(shadeKey);
|
||||
pref.putFloat("currentTiltPos", this->currentTiltPos);
|
||||
pref.end();
|
||||
}
|
||||
if(currDir != this->direction || currPos != this->position || currTiltDir != this->tiltDirection || currTiltPos != this->tiltPosition) {
|
||||
// We need to emit on the socket that our state has changed.
|
||||
this->position = floor(this->currentPos * 100.0);
|
||||
this->tiltPosition = floor(this->currentTiltPos * 100.0);
|
||||
this->emitState();
|
||||
}
|
||||
|
||||
}
|
||||
void SomfyShade::load() {
|
||||
char shadeKey[15];
|
||||
|
|
@ -487,6 +565,12 @@ void SomfyShade::load() {
|
|||
this->position = (uint8_t)floor(this->currentPos * 100);
|
||||
this->target = this->position;
|
||||
this->myPos = pref.getUShort("myPos", this->myPos);
|
||||
this->hasTilt = pref.getBool("hasTilt", false);
|
||||
this->shadeType = static_cast<shade_types>(pref.getChar("shadeType", static_cast<uint8_t>(this->shadeType)));
|
||||
this->tiltTime = pref.getUShort("tiltTime", 3000);
|
||||
this->currentTiltPos = pref.getFloat("currentTiltPos", 0);
|
||||
this->tiltPosition = (uint8_t)floor(this->currentTiltPos * 100);
|
||||
this->tiltTarget = this->tiltPosition;
|
||||
pref.getBytes("linkedAddr", linkedAddresses, sizeof(linkedAddresses));
|
||||
pref.end();
|
||||
Serial.print("shadeId:");
|
||||
|
|
@ -527,14 +611,28 @@ void SomfyShade::publish() {
|
|||
snprintf(topic, sizeof(topic), "shades/%u/lastRollingCode", this->shadeId);
|
||||
mqtt.publish(topic, this->lastRollingCode);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/mypos", this->shadeId);
|
||||
mqtt.publish(topic, this->myPos);
|
||||
|
||||
mqtt.publish(topic, this->hasTilt ? "true" : "false");
|
||||
snprintf(topic, sizeof(topic), "shades/%u/shadeType", this->shadeId);
|
||||
mqtt.publish(topic, static_cast<uint8_t>(this->shadeType));
|
||||
if(this->hasTilt) {
|
||||
snprintf(topic, sizeof(topic), "shades/%u/tiltDirection", this->shadeId);
|
||||
mqtt.publish(topic, this->tiltDirection);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/tiltPosition", this->shadeId);
|
||||
mqtt.publish(topic, this->tiltPosition);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/tiltTarget", this->shadeId);
|
||||
mqtt.publish(topic, this->tiltTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
void SomfyShade::emitState(const char *evt) { this->emitState(255, evt); }
|
||||
void SomfyShade::emitState(uint8_t num, const char *evt) {
|
||||
char buf[220];
|
||||
snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d}", this->shadeId, this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos);
|
||||
char buf[320];
|
||||
if(this->hasTilt)
|
||||
snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"type\":%u,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d,\"hasTilt\":%s,\"tiltDirection\":%d,\"tiltTarget\":%d,\"tiltPosition\":%d}",
|
||||
this->shadeId, static_cast<uint8_t>(this->shadeType), this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos, this->hasTilt ? "true" : "false", this->tiltDirection, this->tiltTarget, this->tiltPosition);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "{\"shadeId\":%d,\"type\":%u,\"remoteAddress\":%d,\"name\":\"%s\",\"direction\":%d,\"position\":%d,\"target\":%d,\"mypos\":%d,\"hasTilt\":%s}",
|
||||
this->shadeId, static_cast<uint8_t>(this->shadeType), this->getRemoteAddress(), this->name, this->direction, this->position, this->target, this->myPos, this->hasTilt ? "true" : "false");
|
||||
if(num >= 255) sockEmit.sendToClients(evt, buf);
|
||||
else sockEmit.sendToClient(num, evt, buf);
|
||||
if(mqtt.connected()) {
|
||||
|
|
@ -549,18 +647,53 @@ void SomfyShade::emitState(uint8_t num, const char *evt) {
|
|||
mqtt.publish(topic, this->lastRollingCode);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/mypos", this->shadeId);
|
||||
mqtt.publish(topic, this->myPos);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/hasTilt", this->hasTilt);
|
||||
mqtt.publish(topic, this->hasTilt ? "true" : "false");
|
||||
if(this->hasTilt) {
|
||||
snprintf(topic, sizeof(topic), "shades/%u/tiltPosition", this->shadeId);
|
||||
mqtt.publish(topic, this->tiltPosition);
|
||||
snprintf(topic, sizeof(topic), "shades/%u/tiltTarget", this->shadeId);
|
||||
mqtt.publish(topic, this->tiltTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool SomfyShade::isIdle() { return this->direction == 0 && this->tiltDirection == 0; }
|
||||
void SomfyShade::processWaitingFrame() {
|
||||
if(this->shadeId == 255) {
|
||||
this->lastFrame.await = 0;
|
||||
return;
|
||||
}
|
||||
if(this->lastFrame.processed) return;
|
||||
if(this->lastFrame.await > 0 && (millis() > this->lastFrame.await || this->lastFrame.repeats >= SETMY_REPEATS)) {
|
||||
if(this->lastFrame.await > 0 && (millis() > this->lastFrame.await)) {
|
||||
switch(this->lastFrame.cmd) {
|
||||
case somfy_commands::Down:
|
||||
case somfy_commands::Up:
|
||||
if(this->hasTilt) { // Theoretically this should get here unless it does have a tilt.
|
||||
if(this->lastFrame.repeats >= TILT_REPEATS) {
|
||||
int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1;
|
||||
this->seekingTiltPos = false;
|
||||
this->tiltTarget = dir > 0 ? 100 : 0;
|
||||
this->setTiltMovement(dir);
|
||||
this->lastFrame.processed = true;
|
||||
Serial.print(this->name);
|
||||
Serial.print(" Processing tilt ");
|
||||
Serial.print(translateSomfyCommand(this->lastFrame.cmd));
|
||||
Serial.print(" after ");
|
||||
Serial.print(this->lastFrame.repeats);
|
||||
Serial.println(" repeats");
|
||||
}
|
||||
else {
|
||||
int8_t dir = this->lastFrame.cmd == somfy_commands::Up ? -1 : 1;
|
||||
this->seekingPos = false;
|
||||
this->target = dir > 0 ? 100 : 0;
|
||||
this->setMovement(dir);
|
||||
this->lastFrame.processed = true;
|
||||
}
|
||||
if(this->lastFrame.repeats > TILT_REPEATS + 2) this->lastFrame.processed = true;
|
||||
}
|
||||
break;
|
||||
case somfy_commands::My:
|
||||
if(this->lastFrame.repeats >= SETMY_REPEATS && this->direction == 0) {
|
||||
if(this->lastFrame.repeats >= SETMY_REPEATS && this->isIdle()) {
|
||||
if(this->myPos == this->position) // We are clearing it.
|
||||
this->myPos = 255;
|
||||
else
|
||||
|
|
@ -609,24 +742,61 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
|
|||
this->lastFrame.copy(frame);
|
||||
int8_t dir = 0;
|
||||
// If the frame came from the radio it cannot be seeking a position. This means that the target will be set.
|
||||
if(!internal) this->seekingPos = false;
|
||||
if(!internal) this->seekingTiltPos = this->seekingPos = false;
|
||||
|
||||
// At this point we are not processing the combo buttons
|
||||
// will need to see what the shade does when you press both.
|
||||
switch(frame.cmd) {
|
||||
case somfy_commands::Up:
|
||||
if(this->hasTilt) {
|
||||
// Wait another half seccond just in case we are potentially processing a tilt.
|
||||
if(!internal) this->lastFrame.await = millis() + 500;
|
||||
else if(this->lastFrame.repeats >= TILT_REPEATS) {
|
||||
// This is an internal tilt command.
|
||||
Serial.println("Processing Tilt UP...");
|
||||
this->setTiltMovement(-1);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
dir = -1;
|
||||
if(!internal) this->target = 0;
|
||||
this->lastFrame.processed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
dir = -1;
|
||||
if(!internal) this->target = 0;
|
||||
this->lastFrame.processed = true;
|
||||
}
|
||||
break;
|
||||
case somfy_commands::Down:
|
||||
if(this->hasTilt) {
|
||||
// Wait another half seccond just in case we are potentially processing a tilt.
|
||||
if(!internal) this->lastFrame.await = millis() + 500;
|
||||
else if(this->lastFrame.repeats >= TILT_REPEATS) {
|
||||
// This is an internal tilt command.
|
||||
Serial.println("Processing Tilt DOWN...");
|
||||
this->setTiltMovement(1);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
dir = 1;
|
||||
if(!internal) this->target = 100;
|
||||
this->lastFrame.processed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
dir = 1;
|
||||
if(!internal) this->target = 100;
|
||||
this->lastFrame.processed = true;
|
||||
}
|
||||
break;
|
||||
case somfy_commands::My:
|
||||
dir = 0;
|
||||
if(this->direction == 0) {
|
||||
if(this->isIdle()) {
|
||||
if(!internal) {
|
||||
// This frame is coming from a remote. We are potentially setting
|
||||
// the my position.
|
||||
this->lastFrame.await = millis() + 500;
|
||||
}
|
||||
else if(myPos >= 0 && this->myPos <= 100) {
|
||||
|
|
@ -654,6 +824,34 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
|
|||
}
|
||||
this->setMovement(dir);
|
||||
}
|
||||
void SomfyShade::setTiltMovement(int8_t dir) {
|
||||
int8_t currDir = this->tiltDirection;
|
||||
if(dir == 0) {
|
||||
// The shade tilt is stopped.
|
||||
this->startTiltPos = this->currentTiltPos;
|
||||
this->tiltStart = 0;
|
||||
this->tiltDirection = dir;
|
||||
if(currDir != dir) {
|
||||
char shadeKey[15];
|
||||
snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId);
|
||||
Serial.print("Writing current shade position:");
|
||||
Serial.println(this->currentTiltPos, 4);
|
||||
pref.begin(shadeKey);
|
||||
pref.putFloat("currentTiltPos", this->currentTiltPos);
|
||||
pref.end();
|
||||
}
|
||||
}
|
||||
else if(this->direction != dir) {
|
||||
this->tiltStart = millis();
|
||||
this->startTiltPos = this->currentTiltPos;
|
||||
this->tiltDirection = dir;
|
||||
}
|
||||
if(this->tiltDirection != currDir) {
|
||||
this->tiltPosition = floor(this->currentTiltPos * 100.0);
|
||||
this->emitState();
|
||||
}
|
||||
}
|
||||
|
||||
void SomfyShade::setMovement(int8_t dir) {
|
||||
int8_t currDir = this->direction;
|
||||
if(dir == 0) {
|
||||
|
|
@ -738,7 +936,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
|
|||
this->seekingPos = false;
|
||||
}
|
||||
else if(cmd == somfy_commands::My) {
|
||||
if(this->direction == 0 && this->myPos >= 0 && this->myPos <= 100) {
|
||||
if(this->isIdle() && this->myPos >= 0 && this->myPos <= 100) {
|
||||
this->moveToMyPosition();
|
||||
return;
|
||||
}
|
||||
|
|
@ -749,6 +947,44 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
|
|||
}
|
||||
SomfyRemote::sendCommand(cmd, repeat);
|
||||
}
|
||||
void SomfyShade::sendTiltCommand(somfy_commands cmd) {
|
||||
if(cmd == somfy_commands::Up) {
|
||||
this->tiltTarget = 0;
|
||||
this->seekingTiltPos = false;
|
||||
SomfyRemote::sendCommand(cmd, TILT_REPEATS);
|
||||
}
|
||||
else if(cmd == somfy_commands::Down) {
|
||||
this->tiltTarget = 100;
|
||||
this->seekingTiltPos = false;
|
||||
SomfyRemote::sendCommand(cmd, TILT_REPEATS);
|
||||
}
|
||||
else if(cmd == somfy_commands::My) {
|
||||
this->tiltTarget = this->tiltPosition;
|
||||
this->seekingTiltPos = false;
|
||||
SomfyRemote::sendCommand(cmd);
|
||||
}
|
||||
}
|
||||
void SomfyShade::moveToTiltTarget(uint8_t target) {
|
||||
int8_t newDir = 0;
|
||||
somfy_commands cmd = somfy_commands::My;
|
||||
if(target < this->tiltPosition)
|
||||
cmd = somfy_commands::Up;
|
||||
else if(target > this->tiltPosition)
|
||||
cmd = somfy_commands::Down;
|
||||
Serial.print("Moving Tilt to ");
|
||||
Serial.print(target);
|
||||
Serial.print("% from ");
|
||||
Serial.print(this->tiltPosition);
|
||||
Serial.print("% using ");
|
||||
Serial.println(translateSomfyCommand(cmd));
|
||||
this->tiltTarget = target;
|
||||
if(target > 0 && target < 100) this->seekingTiltPos = true;
|
||||
else this->seekingTiltPos = false;
|
||||
if(cmd != somfy_commands::My)
|
||||
SomfyRemote::sendCommand(cmd, TILT_REPEATS);
|
||||
else
|
||||
SomfyRemote::sendCommand(cmd);
|
||||
}
|
||||
void SomfyShade::moveToTarget(uint8_t target) {
|
||||
int8_t newDir = 0;
|
||||
somfy_commands cmd = somfy_commands::My;
|
||||
|
|
@ -772,12 +1008,16 @@ bool SomfyShade::save() {
|
|||
snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->getShadeId());
|
||||
pref.begin(shadeKey);
|
||||
pref.putString("name", this->name);
|
||||
pref.putBool("hasTilt", this->hasTilt);
|
||||
pref.putBool("paired", this->paired);
|
||||
pref.putUShort("upTime", this->upTime);
|
||||
pref.putUShort("downTime", this->downTime);
|
||||
pref.putUShort("tiltTime", this->tiltTime);
|
||||
pref.putULong("remoteAddress", this->getRemoteAddress());
|
||||
pref.putFloat("currentPos", this->currentPos);
|
||||
pref.putFloat("currentTiltPos", this->currentTiltPos);
|
||||
pref.putUShort("myPos", this->myPos);
|
||||
pref.putChar("shadeType", static_cast<uint8_t>(this->shadeType));
|
||||
uint32_t linkedAddresses[SOMFY_MAX_LINKED_REMOTES];
|
||||
memset(linkedAddresses, 0x00, sizeof(linkedAddresses));
|
||||
uint8_t j = 0;
|
||||
|
|
@ -794,6 +1034,21 @@ bool SomfyShade::fromJSON(JsonObject &obj) {
|
|||
if(obj.containsKey("upTime")) this->upTime = obj["upTime"];
|
||||
if(obj.containsKey("downTime")) this->downTime = obj["downTime"];
|
||||
if(obj.containsKey("remoteAddress")) this->setRemoteAddress(obj["remoteAddress"]);
|
||||
if(obj.containsKey("tiltTime")) this->tiltTime = obj["tiltTime"];
|
||||
if(obj.containsKey("hasTilt")) this->hasTilt = obj["hasTilt"];
|
||||
if(obj.containsKey("shadeType")) {
|
||||
if(obj["shadeType"].is<const char *>()) {
|
||||
if(strncmp(obj["shadeType"].as<const char *>(), "roller", 7) == 0)
|
||||
this->shadeType = shade_types::roller;
|
||||
else if(strncmp(obj["shadeType"].as<const char *>(), "drapery", 8) == 0)
|
||||
this->shadeType = shade_types::drapery;
|
||||
else if(strncmp(obj["shadeType"].as<const char *>(), "blind", 5) == 0)
|
||||
this->shadeType = shade_types::blind;
|
||||
}
|
||||
else {
|
||||
this->shadeType = static_cast<shade_types>(obj["shadeType"].as<uint8_t>());
|
||||
}
|
||||
}
|
||||
if(obj.containsKey("linkedAddresses")) {
|
||||
uint32_t linkedAddresses[SOMFY_MAX_LINKED_REMOTES];
|
||||
memset(linkedAddresses, 0x00, sizeof(linkedAddresses));
|
||||
|
|
@ -822,9 +1077,16 @@ bool SomfyShade::toJSON(JsonObject &obj) {
|
|||
obj["remotePrefId"] = this->getRemotePrefId();
|
||||
obj["lastRollingCode"] = this->lastRollingCode;
|
||||
obj["position"] = this->position;
|
||||
obj["tiltPosition"] = this->tiltPosition;
|
||||
obj["tiltDirection"] = this->tiltDirection;
|
||||
obj["tiltTime"] = this->tiltTime;
|
||||
obj["tiltTarget"] = this->tiltTarget;
|
||||
obj["target"] = this->target;
|
||||
obj["myPos"] = this->myPos;
|
||||
obj["direction"] = this->direction;
|
||||
obj["hasTilt"] = this->hasTilt;
|
||||
obj["tiltTime"] = this->tiltTime;
|
||||
obj["shadeType"] = static_cast<uint8_t>(this->shadeType);
|
||||
SomfyRemote::toJSON(obj);
|
||||
JsonArray arr = obj.createNestedArray("linkedRemotes");
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) {
|
||||
|
|
@ -967,6 +1229,7 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) {
|
|||
frame.rollingCode = this->getNextRollingCode();
|
||||
frame.remoteAddress = this->getRemoteAddress();
|
||||
frame.cmd = cmd;
|
||||
frame.repeats = repeat;
|
||||
somfy.sendFrame(frame, repeat);
|
||||
somfy.processFrame(frame, true);
|
||||
}
|
||||
|
|
|
|||
21
Somfy.h
21
Somfy.h
|
|
@ -15,6 +15,11 @@ enum class somfy_commands : byte {
|
|||
SunFlag = 0x9,
|
||||
Flag = 0xA
|
||||
};
|
||||
enum class shade_types : byte {
|
||||
roller = 0x00,
|
||||
blind = 0x01,
|
||||
drapery = 0x02
|
||||
};
|
||||
|
||||
String translateSomfyCommand(const somfy_commands cmd);
|
||||
somfy_commands translateSomfyCommand(const String& string);
|
||||
|
|
@ -64,19 +69,28 @@ class SomfyShade : public SomfyRemote {
|
|||
protected:
|
||||
uint8_t shadeId = 255;
|
||||
uint64_t moveStart = 0;
|
||||
uint64_t tiltStart = 0;
|
||||
float startPos = 0.0;
|
||||
float startTiltPos = 0.00;
|
||||
bool seekingPos = false;
|
||||
bool seekingTiltPos = false;
|
||||
bool seekingMyPos = false;
|
||||
bool settingMyPos = false;
|
||||
uint32_t awaitMy = 0;
|
||||
public:
|
||||
shade_types shadeType = shade_types::roller;
|
||||
bool hasTilt = false;
|
||||
void load();
|
||||
somfy_frame_t lastFrame;
|
||||
float currentPos = 0.0;
|
||||
float currentTiltPos = 0.0;
|
||||
//uint16_t movement = 0;
|
||||
int8_t direction = 0; // 0 = stopped, 1=down, -1=up.
|
||||
int8_t tiltDirection = 0; // 0=stopped, 1=clockwise, -1=counter clockwise
|
||||
uint8_t tiltPosition = 0;
|
||||
uint8_t position = 0;
|
||||
uint8_t target = 0;
|
||||
uint8_t tiltTarget = 0;
|
||||
uint8_t myPos = 255;
|
||||
SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
|
||||
bool paired = false;
|
||||
|
|
@ -86,13 +100,18 @@ class SomfyShade : public SomfyRemote {
|
|||
void setShadeId(uint8_t id) { shadeId = id; }
|
||||
uint8_t getShadeId() { return shadeId; }
|
||||
uint16_t upTime = 10000;
|
||||
uint16_t downTime = 1000;
|
||||
uint16_t downTime = 10000;
|
||||
uint16_t tiltTime = 5000;
|
||||
bool save();
|
||||
bool isIdle();
|
||||
void checkMovement();
|
||||
void processFrame(somfy_frame_t &frame, bool internal = false);
|
||||
void setTiltMovement(int8_t dir);
|
||||
void setMovement(int8_t dir);
|
||||
void setTarget(uint8_t target);
|
||||
void moveToTarget(uint8_t target);
|
||||
void moveToTiltTarget(uint8_t target);
|
||||
void sendTiltCommand(somfy_commands cmd);
|
||||
void sendCommand(somfy_commands cmd, uint8_t repeat = 1);
|
||||
bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0);
|
||||
bool unlinkRemote(uint32_t remoteAddress);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
73
Web.cpp
73
Web.cpp
|
|
@ -394,7 +394,7 @@ void Web::begin() {
|
|||
// We are updating an existing shade.
|
||||
if (server.hasArg("plain")) {
|
||||
Serial.println("Updating a shade");
|
||||
DynamicJsonDocument doc(256);
|
||||
DynamicJsonDocument doc(512);
|
||||
DeserializationError err = deserializeJson(doc, server.arg("plain"));
|
||||
if (err) {
|
||||
switch (err.code()) {
|
||||
|
|
@ -416,7 +416,7 @@ void Web::begin() {
|
|||
if (shade) {
|
||||
shade->fromJSON(obj);
|
||||
shade->save();
|
||||
DynamicJsonDocument sdoc(256);
|
||||
DynamicJsonDocument sdoc(512);
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
shade->toJSON(sobj);
|
||||
serializeJson(sdoc, g_content);
|
||||
|
|
@ -437,7 +437,7 @@ void Web::begin() {
|
|||
// We are updating an existing shade.
|
||||
if (server.hasArg("plain")) {
|
||||
Serial.println("Updating a shade");
|
||||
DynamicJsonDocument doc(256);
|
||||
DynamicJsonDocument doc(512);
|
||||
DeserializationError err = deserializeJson(doc, server.arg("plain"));
|
||||
if (err) {
|
||||
switch (err.code()) {
|
||||
|
|
@ -459,7 +459,7 @@ void Web::begin() {
|
|||
if (shade) {
|
||||
shade->fromJSON(obj);
|
||||
shade->save();
|
||||
DynamicJsonDocument sdoc(256);
|
||||
DynamicJsonDocument sdoc(512);
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
shade->toJSON(sobj);
|
||||
serializeJson(sdoc, g_content);
|
||||
|
|
@ -473,6 +473,70 @@ void Web::begin() {
|
|||
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}"));
|
||||
}
|
||||
});
|
||||
server.on("/tiltCommand", []() {
|
||||
webServer.sendCORSHeaders();
|
||||
HTTPMethod method = server.method();
|
||||
uint8_t shadeId = 255;
|
||||
uint8_t target = 255;
|
||||
somfy_commands command = somfy_commands::My;
|
||||
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
|
||||
if (server.hasArg("shadeId")) {
|
||||
shadeId = atoi(server.arg("shadeId").c_str());
|
||||
if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command"));
|
||||
else if(server.hasArg("target")) target = atoi(server.arg("target").c_str());
|
||||
}
|
||||
else if (server.hasArg("plain")) {
|
||||
Serial.println("Sending Shade Tilt Command");
|
||||
DynamicJsonDocument doc(256);
|
||||
DeserializationError err = deserializeJson(doc, server.arg("plain"));
|
||||
if (err) {
|
||||
switch (err.code()) {
|
||||
case DeserializationError::InvalidInput:
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}"));
|
||||
break;
|
||||
case DeserializationError::NoMemory:
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}"));
|
||||
break;
|
||||
default:
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}"));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
JsonObject obj = doc.as<JsonObject>();
|
||||
if (obj.containsKey("shadeId")) shadeId = obj["shadeId"];
|
||||
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
|
||||
if (obj.containsKey("command")) {
|
||||
String scmd = obj["command"];
|
||||
command = translateSomfyCommand(scmd);
|
||||
}
|
||||
else if(obj.containsKey("target")) {
|
||||
target = obj["target"].as<uint8_t>();
|
||||
}
|
||||
}
|
||||
}
|
||||
else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}"));
|
||||
}
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if (shade) {
|
||||
Serial.print("Received:");
|
||||
Serial.println(server.arg("plain"));
|
||||
// Send the command to the shade.
|
||||
if(target >= 0 && target <= 100)
|
||||
shade->moveToTiltTarget(target);
|
||||
else
|
||||
shade->sendTiltCommand(command);
|
||||
DynamicJsonDocument sdoc(256);
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
shade->toJSON(sobj);
|
||||
serializeJson(sdoc, g_content);
|
||||
server.send(200, _encoding_json, g_content);
|
||||
}
|
||||
else {
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
});
|
||||
server.on("/shadeCommand", []() {
|
||||
webServer.sendCORSHeaders();
|
||||
HTTPMethod method = server.method();
|
||||
|
|
@ -1210,7 +1274,6 @@ void Web::begin() {
|
|||
serializeJson(doc, g_content);
|
||||
server.send(200, _encoding_json, g_content);
|
||||
});
|
||||
|
||||
server.on("/connectmqtt", []() {
|
||||
DynamicJsonDocument doc(512);
|
||||
DeserializationError err = deserializeJson(doc, server.arg("plain"));
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ i.icss-window-shade {
|
|||
border-bottom: .1em solid transparent;
|
||||
box-shadow: inset 0 1em, 0 0em 0 -.1em;
|
||||
top: -.15em;
|
||||
left: -.1em;
|
||||
left: -.09em;
|
||||
}
|
||||
|
||||
i.icss-window-shade:after {
|
||||
|
|
@ -576,6 +576,52 @@ i.icss-window-shade {
|
|||
background-size: 0.05em 0.05em;
|
||||
background-color: rgba(71, 212, 255, 0);
|
||||
}
|
||||
i.icss-window-blind {
|
||||
width: 1.1em;
|
||||
height: .75em;
|
||||
background-color: transparent;
|
||||
border: .05em solid transparent;
|
||||
border-width: 0 .1em;
|
||||
box-shadow: inset 0 0 0 .01em, inset 0 .01em 0 .07em, 0 .07em 0;
|
||||
margin: .2em 0 .07em;
|
||||
}
|
||||
|
||||
i.icss-window-blind:before {
|
||||
width: 1.1em;
|
||||
height: .2em;
|
||||
border-bottom: .1em solid transparent;
|
||||
box-shadow: inset 0 1em, 0 0em 0 -.1em;
|
||||
top: -.1em;
|
||||
left: -.09em;
|
||||
}
|
||||
|
||||
i.icss-window-blind:after {
|
||||
width: calc(100% - .13em);
|
||||
height: var(--shade-position, 0%);
|
||||
left: calc(0.077em - .2px);
|
||||
top: 0.025em;
|
||||
border-bottom: solid 0.025em gray;
|
||||
background-image: repeating-linear-gradient(var(--shade-color, currentColor), var(--shade-color, currentColor) 1px, white 4px);
|
||||
background-color: rgba(71, 212, 255, 0);
|
||||
}
|
||||
i.icss-window-tilt {
|
||||
position:absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
left:0px;
|
||||
top:0px;
|
||||
color:dimgray;
|
||||
background:transparent;
|
||||
}
|
||||
i.icss-window-tilt:after {
|
||||
content: attr(data-tiltposition)'%';
|
||||
width:100%;
|
||||
top: calc(50% - .25em);
|
||||
text-align:center;
|
||||
font-weight:bold;
|
||||
font-size:.3em;
|
||||
}
|
||||
|
||||
i.icss-upload {
|
||||
width: 1em;
|
||||
height: .6em;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="main.css?v=1.3.0" type="text/css" />
|
||||
<link rel="stylesheet" href="icons.css?v=1.3.0" type="text/css" />
|
||||
<link rel="stylesheet" href="main.css?v=1.3.1" type="text/css" />
|
||||
<link rel="stylesheet" href="icons.css?v=1.3.1" type="text/css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<script type="text/javascript" src="index.js?v=1.3.0"></script>
|
||||
<script type="text/javascript" src="index.js?v=1.3.1"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="divContainer" class="container" style="user-select:none;position:relative;">
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
<form method="post" action="/radioSettings">
|
||||
<hr />
|
||||
<div id="divShadeSection">
|
||||
<div id="divShadeList" style="overflow-y:auto;max-height:400px;"></div>
|
||||
<div id="divShadeList" style="overflow-y: auto; max-height: 400px; padding-top: 2px; padding-bottom: 2px;"></div>
|
||||
<div class="button-container">
|
||||
<button id="btnAddShade" type="button" onclick="somfy.openEditShade();">
|
||||
Add Shade
|
||||
|
|
@ -217,15 +217,26 @@
|
|||
<form method="post" action="/shadesettings">
|
||||
<div style="display:inline-block;float:right;position:relative;margin-top:-14px;"><span id="spanShadeId">*</span>/<span id="spanMaxShades">5</span></div>
|
||||
<div>
|
||||
<div class="field-group" style="width:127px;display:inline-block;">
|
||||
<input id="fldShadeAddress" name="shadeAddress" type="number" length=5 placeholder="Address" style="width:100%;">
|
||||
<div class="field-group" style="width:127px;display:inline-block;margin-top:-20px;float:left;">
|
||||
<div class="field-group">
|
||||
<select id="selShadeType" name="shadeType" style="width:100%;" onchange="somfy.onShadeTypeChanged(this);">
|
||||
<option value="0">Roller Shade</option>
|
||||
<option value="1">Blind</option>
|
||||
<option value="2">Drapery</option>
|
||||
</select>
|
||||
<label for="selShadeType">Type</label>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<input id="fldShadeAddress" name="shadeAddress" type="number" length=5 placeholder="Address" style="width:100%;text-align:right;">
|
||||
<label for="fldShadeAddress">Remote Address</label>
|
||||
</div>
|
||||
<div id="divSomfyButtons" style="float:right">
|
||||
<div style="display:inline-block;padding-right:7px;"><i id="icoShade" class="somfy-shade-icon icss-window-shade" data-shadeid="0" style="--shade-position:0%;vertical-align:middle;font-size:32px;"></i></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'up');" style="display:inline-block;padding:7px;cursor:pointer;"><i class="icss-somfy-up"></i></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'my');" style="display:inline-block;font-size:2em;padding:10px;cursor:pointer;"><span>my</span></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'down');" style="display:inline-block;padding:7px;cursor:pointer;"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>
|
||||
|
||||
</div>
|
||||
<div id="divSomfyButtons" style="float:right;margin-top:10px;position:relative">
|
||||
<div style="display:inline-block;margin-right:7px;position:relative;font-size:48px;"><i id="icoShade" class="somfy-shade-icon icss-window-shade" data-shadeid="0" style="--shade-position:0%;vertical-align:middle;"></i><i class="icss-window-tilt" data-tiltposition="0" style="display:none;"></i></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'up');" style="display:inline-block;padding:7px;cursor:pointer;vertical-align:middle;margin:0px;"><i class="icss-somfy-up"></i></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'my');" style="display: inline-block; font-size: 2em; padding: 10px; vertical-align: middle; cursor: pointer;margin:0px;"><span>my</span></div>
|
||||
<div class="button-outline" onclick="somfy.sendCommand(parseInt(document.getElementById('spanShadeId').innerText, 10), 'down');" style="display: inline-block; padding: 7px; cursor: pointer; vertical-align: middle;margin:0px;"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
|
|
@ -234,30 +245,42 @@
|
|||
</div>
|
||||
<div>
|
||||
<div class="field-group" style="display:inline-block;max-width:127px;margin-right:17px;">
|
||||
<input id="fldShadeUpTime" name="shadeUpTime" type="number" length=5 placeholder="milliseconds" style="width:100%;text-align:right;">
|
||||
<input id="fldShadeUpTime" name="shadeUpTime" type="number" length=5 placeholder="milliseconds" style="width:100%;text-align:right;"/>
|
||||
<label for="fldShadeUpTime">Up Time (ms)</label>
|
||||
</div>
|
||||
<div class="field-group" style="display:inline-block;max-width:127px;">
|
||||
<input id="fldShadeDownTime" name="shadeDownTime" type="number" length=5 placeholder="milliseconds" style="width:100%;text-align:right;">
|
||||
<input id="fldShadeDownTime" name="shadeDownTime" type="number" length=5 placeholder="milliseconds" style="width:100%;text-align:right;"/>
|
||||
<label for="fldShadeDownTime">Down Time (ms)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="divTiltSettings" style="display:none;">
|
||||
<div class="field-group" style="display:inline-block; margin-right:17px;width:127px;vertical-align:middle;">
|
||||
<input id="cbHasTilt" type="checkbox" onchange="somfy.onShadeTypeChanged(this);" />
|
||||
<label for="cbHasTilt" style="display:inline-block;cursor:pointer;">Has Tilt</label>
|
||||
</div>
|
||||
<div class="field-group" style="display:inline-block;width:127px;vertical-align:middle;">
|
||||
<input id="fldTiltTime" name="shadeTiltTime" type="number" length=5 placeholder="milliseconds" style="width:100%;text-align:right;" />
|
||||
<label for="fldTiltTime">Tilt Time (ms)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container" style="text-align:center;">
|
||||
<button id="btnSaveShade" type="button" onclick="somfy.saveShade();" style="display:inline-block;width:47%;">
|
||||
Save Shade
|
||||
</button>
|
||||
<button id="btnPairShade" type="button" onclick="somfy.pairShade(parseInt(document.getElementById('spanShadeId').innerText, 10));" style="display:inline-block;width:47%;">
|
||||
Pair Shade
|
||||
</button>
|
||||
<button id="btnUnpairShade" type="button" onclick="somfy.unpairShade(parseInt(document.getElementById('spanShadeId').innerText, 10));" style="display:inline-block;width:47%;">
|
||||
Unpair Shade
|
||||
</button>
|
||||
<button id="btnSetRollingCode" type="button" onclick="somfy.openSetRollingCode(parseInt(document.getElementById('spanShadeId').innerText, 10));" style="display:inline-block;width:47%;">
|
||||
Set Rolling Code
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-container" style="margin-top:-10px;padding-left:7px;padding-right:7px;">
|
||||
<button id="btnChangeRollingCode" type="button" onclick="somfy.openSetRollingCode(parseInt(document.getElementById('spanShadeId').innerText, 10));">Set Rolling Code</button>
|
||||
<button id="btnSaveShade" type="button" onclick="somfy.saveShade();">
|
||||
Save Shade
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="divLinkedRemoteList" style="overflow-y:auto;max-height:77px;"></div>
|
||||
<div id="divLinkedRemoteList" style="overflow-y:auto;max-height:77px;padding-top:2px;padding-bottom:2px;"></div>
|
||||
<div class="button-container">
|
||||
<button id="btnLinkRemote" type="button" onclick="somfy.linkRemote(parseInt(document.getElementById('spanShadeId').innerText, 10));">
|
||||
Link Remote
|
||||
|
|
|
|||
257
data/index.js
257
data/index.js
|
|
@ -97,6 +97,28 @@ Number.prototype.fmt = function (format, empty) {
|
|||
if (rd.length === 0 && rw.length === 0) return '';
|
||||
return pfx + rw + rd + sfx;
|
||||
};
|
||||
function makeBool(val) {
|
||||
if (typeof (val) === 'boolean') return val;
|
||||
if (typeof (val) === 'undefined') return false;
|
||||
if (typeof (val) === 'number') return val >= 1;
|
||||
if (typeof (val) === 'string') {
|
||||
if (val === '') return false;
|
||||
switch (val.toLowerCase().trim()) {
|
||||
case 'on':
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'y':
|
||||
return true;
|
||||
case 'off':
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'n':
|
||||
return false;
|
||||
}
|
||||
if (!isNaN(parseInt(val, 10))) return parseInt(val, 10) >= 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function waitMessage(el) {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = '<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div></div>';
|
||||
|
|
@ -278,16 +300,15 @@ async function initSockets() {
|
|||
}
|
||||
};
|
||||
socket.onclose = (evt) => {
|
||||
procWifiStrength({ ssid: '', channel: -1, strength: -100 });
|
||||
procEthernet({ connected: '', speed: 0, fullduplex: false });
|
||||
if (document.getElementsByClassName('socket-wait') === 0)
|
||||
wifi.procWifiStrength({ ssid: '', channel: -1, strength: -100 });
|
||||
wifi.procEthernet({ connected: '', speed: 0, fullduplex: false });
|
||||
if (document.getElementsByClassName('socket-wait').length === 0)
|
||||
waitMessage(document.getElementById('divContainer')).classList.add('socket-wait');
|
||||
if (evt.wasClean) {
|
||||
console.log({ msg: 'close-clean', evt: evt });
|
||||
connectFailed = 0;
|
||||
tConnect = setTimeout(async () => { await reopenSocket(); }, 10000);
|
||||
console.log('Reconnecting socket in 10 seconds');
|
||||
|
||||
tConnect = setTimeout(async () => { await reopenSocket(); }, 7000);
|
||||
console.log('Reconnecting socket in 7 seconds');
|
||||
}
|
||||
else {
|
||||
console.log({ msg: 'close-died', reason: evt.reason, evt: evt, sock: socket });
|
||||
|
|
@ -334,7 +355,7 @@ async function reopenSocket() {
|
|||
await initSockets();
|
||||
}
|
||||
class General {
|
||||
appVersion = 'v1.3.0';
|
||||
appVersion = 'v1.3.1';
|
||||
reloadApp = false;
|
||||
async init() {
|
||||
this.setAppVersion();
|
||||
|
|
@ -563,7 +584,7 @@ class Wifi {
|
|||
{ val: 2, label: 'Olimex ESP32-POE', clk: 3, ct: 0, addr: 0, pwr: 12, mdc: 23, mdio: 18 },
|
||||
{ val: 3, label: 'Olimex ESP32-EVB', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 23, mdio: 18 },
|
||||
{ val: 4, label: 'LILYGO T-Internet POE', clk: 3, ct: 0, addr: 0, pwr: 16, mdc: 23, mdio: 18 },
|
||||
{ val: 5, label: 'wESP32 v7+', clk: 0, ct: 3, addr: 0, pwr: -1, mdc: 16, mdio: 17 },
|
||||
{ val: 5, label: 'wESP32 v7+', clk: 0, ct: 2, addr: 0, pwr: -1, mdc: 16, mdio: 17 },
|
||||
{ val: 6, label: 'wESP32 < v7', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 16, mdio: 17 }
|
||||
];
|
||||
ethClockModes = [{ val: 0, label: 'GPIO0 IN' }, { val: 1, label: 'GPIO0 OUT' }, { val: 2, label: 'GPIO16 OUT' }, { val: 3, label: 'GPIO17 OUT' }];
|
||||
|
|
@ -984,7 +1005,7 @@ class Somfy {
|
|||
let divCtl = '';
|
||||
for (let i = 0; i < shades.length; i++) {
|
||||
let shade = shades[i];
|
||||
divCfg += `<div class="somfyShade" data-shadeid="${shade.shadeId}" data-remoteaddress="${shade.remoteAddress}">`;
|
||||
divCfg += `<div class="somfyShade" data-shadeid="${shade.shadeId}" data-remoteaddress="${shade.remoteAddress}" data-tilt="${shade.hasTilt}" data-shadetype="${shade.shadeType}">`;
|
||||
divCfg += `<div class="button-outline" onclick="somfy.openEditShade(${shade.shadeId});"><i class="icss-edit"></i></div>`;
|
||||
//divCfg += `<i class="shade-icon" data-position="${shade.position || 0}%"></i>`;
|
||||
divCfg += `<span class="shade-name">${shade.name}</span>`;
|
||||
|
|
@ -992,96 +1013,111 @@ class Somfy {
|
|||
divCfg += `<div class="button-outline" onclick="somfy.deleteShade(${shade.shadeId});"><i class="icss-trash"></i></div>`;
|
||||
divCfg += '</div>';
|
||||
|
||||
divCtl += `<div class="somfyShadeCtl" data-shadeId="${shade.shadeId}" data-direction="${shade.direction}" data-remoteaddress="${shade.remoteAddress}" data-position="${shade.position}" data-target="${shade.target}" data-mypos="${shade.myPos}">`;
|
||||
divCtl += `<div class="shade-icon" data-shadeid="${shade.shadeId}">`;
|
||||
divCtl += `<i class="somfy-shade-icon icss-window-shade" data-shadeid="${shade.shadeId}" style="--shade-position:${shade.position}%;vertical-align:top;" onclick="event.stopPropagation(); console.log(event); somfy.openSetPosition(${shade.shadeId});"></i></div >`;
|
||||
divCtl += `<div class="somfyShadeCtl" data-shadeId="${shade.shadeId}" data-direction="${shade.direction}" data-remoteaddress="${shade.remoteAddress}" data-position="${shade.position}" data-target="${shade.target}" data-mypos="${shade.myPos}" data-shadetype="${shade.shadeType}" data-tilt="${shade.hasTilt}"`;
|
||||
if (shade.hasTilt) {
|
||||
divCtl += ` data-tiltposition="${shade.tiltPosition}" data-tiltdirection="${shade.tiltDirection}" data-tilttarget="${shade.tiltTarget}"`
|
||||
}
|
||||
divCtl += `><div class="shade-icon" data-shadeid="${shade.shadeId}" onclick="event.stopPropagation(); console.log(event); somfy.openSetPosition(${shade.shadeId});">`;
|
||||
divCtl += `<i class="somfy-shade-icon`;
|
||||
switch (shade.shadeType) {
|
||||
case 1:
|
||||
divCtl += ' icss-window-blind';
|
||||
break;
|
||||
default:
|
||||
divCtl += ' icss-window-shade'
|
||||
break;
|
||||
}
|
||||
divCtl += `" data-shadeid="${shade.shadeId}" style="--shade-position:${shade.position}%;vertical-align: top;"></i>`;
|
||||
divCtl += shade.hasTilt ? `<i class="icss-window-tilt" data-shadeid="${shade.shadeId}" data-tiltposition="${shade.tiltPosition}"></i></div>` : '</div>';
|
||||
divCtl += `<div class="shade-name">`;
|
||||
divCtl += `<span class="shadectl-name">${shade.name}</span>`;
|
||||
divCtl += `<span class="shadectl-mypos"><label>My: </label><span id="spanMyPos">${shade.myPos !== 255 ? shade.myPos + '%' : '---'}</span>`
|
||||
divCtl += '</div>'
|
||||
|
||||
divCtl += `<div class="shadectl-buttons">`;
|
||||
divCtl += `<div class="button-outline" onclick="somfy.sendCommand(${shade.shadeId}, 'up');"><i class="icss-somfy-up"></i></div>`;
|
||||
divCtl += `<div class="button-outline my-button" data-shadeid="${shade.shadeId}" style="font-size:2em;padding:10px;"><span>my</span></div>`;
|
||||
divCtl += `<div class="button-outline" onclick="somfy.sendCommand(${shade.shadeId}, 'down');"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>`;
|
||||
divCtl += `<div class="button-outline cmd-button" data-cmd="up" data-shadeid="${shade.shadeId}"><i class="icss-somfy-up"></i></div>`;
|
||||
divCtl += `<div class="button-outline cmd-button my-button" data-cmd="my" data-shadeid="${shade.shadeId}" style="font-size:2em;padding:10px;"><span>my</span></div>`;
|
||||
divCtl += `<div class="button-outline cmd-button" data-cmd="down" data-shadeid="${shade.shadeId}"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>`;
|
||||
divCtl += '</div></div>';
|
||||
}
|
||||
document.getElementById('divShadeList').innerHTML = divCfg;
|
||||
let shadeControls = document.getElementById('divShadeControls');
|
||||
shadeControls.innerHTML = divCtl;
|
||||
// Attach the timer for setting the My Position for the shade.
|
||||
let btns = shadeControls.querySelectorAll('div.my-button');
|
||||
let btns = shadeControls.querySelectorAll('div.cmd-button');
|
||||
for (let i = 0; i < btns.length; i++) {
|
||||
btns[i].addEventListener('mouseup', (event) => {
|
||||
console.log(this);
|
||||
console.log(event);
|
||||
console.log('mouseup');
|
||||
let cmd = event.currentTarget.getAttribute('data-cmd');
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
if (this.btnTimer) {
|
||||
clearTimeout(this.btnTimer);
|
||||
this.btnTimer = null;
|
||||
if (new Date().getTime() - this.btnDown > 2000) event.preventDefault();
|
||||
else this.sendCommand(shadeId, cmd);
|
||||
}
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
if (new Date().getTime() - this.btnDown > 2000) {
|
||||
event.preventDefault();
|
||||
}
|
||||
else {
|
||||
this.sendCommand(shadeId, 'my');
|
||||
}
|
||||
|
||||
else this.sendCommand(shadeId, cmd);
|
||||
}, true);
|
||||
btns[i].addEventListener('mousedown', (event) => {
|
||||
if (this.btnTimer) return;
|
||||
console.log(this);
|
||||
console.log(event);
|
||||
|
||||
console.log('mousedown');
|
||||
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
let el = event.currentTarget.closest('.somfyShadeCtl');
|
||||
this.btnDown = new Date().getTime();
|
||||
if (parseInt(el.getAttribute('data-direction'), 10) === 0) {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
// Open up the set My Position dialog. We will allow the user to change the position to match
|
||||
// the desired position.
|
||||
this.openSetMyPosition(shadeId);
|
||||
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
}, true);
|
||||
btns[i].addEventListener('touchstart', (event) => {
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
let el = event.currentTarget.closest('.somfyShadeCtl');
|
||||
console.log('touchstart');
|
||||
|
||||
this.btnDown = new Date().getTime();
|
||||
if (parseInt(el.getAttribute('data-direction'), 10) === 0) {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
// Open up the set My Position dialog. We will allow the user to change the position to match
|
||||
// the desired position.
|
||||
this.openSetMyPosition(shadeId);
|
||||
|
||||
}, 2000);
|
||||
}
|
||||
}, true);
|
||||
/*
|
||||
btns[i].addEventListener('touchend', (event) => {
|
||||
event.preventDefault(); // Make sure the idiot
|
||||
console.log(this);
|
||||
console.log(event);
|
||||
if (this.btnTimer) {
|
||||
clearTimeout(this.btnTimer);
|
||||
this.btnTimer = null;
|
||||
}
|
||||
console.log(this);
|
||||
console.log(event);
|
||||
console.log('mousedown');
|
||||
let elShade = event.currentTarget.closest('div.somfyShadeCtl');
|
||||
let cmd = event.currentTarget.getAttribute('data-cmd');
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
if (new Date().getTime() - this.btnDown > 2000) {
|
||||
event.preventDefault();
|
||||
let el = event.currentTarget.closest('.somfyShadeCtl');
|
||||
this.btnDown = new Date().getTime();
|
||||
if (cmd === 'my') {
|
||||
if (parseInt(el.getAttribute('data-direction'), 10) === 0) {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
// Open up the set My Position dialog. We will allow the user to change the position to match
|
||||
// the desired position.
|
||||
this.openSetMyPosition(shadeId);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
else if (makeBool(elShade.getAttribute('data-tilt'))) {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
this.sendTiltCommand(shadeId, cmd);
|
||||
}, 2000);
|
||||
}
|
||||
}, true);
|
||||
btns[i].addEventListener('touchstart', (event) => {
|
||||
if (this.btnTimer) {
|
||||
clearTimeout(this.btnTimer);
|
||||
this.btnTimer = null;
|
||||
}
|
||||
console.log(this);
|
||||
console.log(event);
|
||||
console.log('touchstart');
|
||||
let elShade = event.currentTarget.closest('div.somfyShadeCtl');
|
||||
let cmd = event.currentTarget.getAttribute('data-cmd');
|
||||
let shadeId = parseInt(event.currentTarget.getAttribute('data-shadeid'), 10);
|
||||
let el = event.currentTarget.closest('.somfyShadeCtl');
|
||||
this.btnDown = new Date().getTime();
|
||||
if (parseInt(el.getAttribute('data-direction'), 10) === 0) {
|
||||
if (cmd === 'my') {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
// Open up the set My Position dialog. We will allow the user to change the position to match
|
||||
// the desired position.
|
||||
this.openSetMyPosition(shadeId);
|
||||
}, 2000);
|
||||
}
|
||||
else {
|
||||
if (makeBool(elShade.getAttribute('data-tilt'))) {
|
||||
this.btnTimer = setTimeout(() => {
|
||||
this.sendTiltCommand(shadeId, cmd);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.sendCommand(shadeId, 'my');
|
||||
}
|
||||
}, true);
|
||||
*/
|
||||
}
|
||||
};
|
||||
closeShadePositioners() {
|
||||
|
|
@ -1196,12 +1232,23 @@ class Somfy {
|
|||
for (let i = 0; i < icons.length; i++) {
|
||||
icons[i].style.setProperty('--shade-position', `${state.position}%`);
|
||||
}
|
||||
if (state.hasTilt) {
|
||||
let tilts = document.querySelectorAll(`.icss-window-tilt[data-shadeid="${state.shadeId}"]`);
|
||||
for (let i = 0; i < tilts.length; i++) {
|
||||
tilts[i].setAttribute('data-tiltposition', `${state.tiltPosition}`);
|
||||
}
|
||||
}
|
||||
let divs = document.querySelectorAll(`.somfyShadeCtl[data-shadeid="${state.shadeId}"]`);
|
||||
for (let i = 0; i < divs.length; i++) {
|
||||
divs[i].setAttribute('data-direction', state.direction);
|
||||
divs[i].setAttribute('data-position', state.position);
|
||||
divs[i].setAttribute('data-target', state.target);
|
||||
divs[i].setAttribute('data-mypos', state.mypos);
|
||||
if (state.hasTilt) {
|
||||
divs[i].setAttribute('data-tiltdirection', state.tiltDirection);
|
||||
divs[i].setAttribute('data-tiltposition', state.tiltPosition);
|
||||
divs[i].setAttribute('data-tilttarget', state.tiltTarget);
|
||||
}
|
||||
let span = divs[i].querySelector('#spanMyPos');
|
||||
if (span) span.innerHTML = typeof state.mypos !== 'undefined' && state.mypos !== 255 ? `${state.mypos}%` : '---';
|
||||
}
|
||||
|
|
@ -1235,6 +1282,26 @@ class Somfy {
|
|||
document.getElementById('somfyTransceiver').style.display = 'none';
|
||||
document.getElementById('somfyMain').style.display = '';
|
||||
};
|
||||
onShadeTypeChanged(el) {
|
||||
let sel = document.getElementById('selShadeType');
|
||||
let tilt = document.getElementById('cbHasTilt').checked;
|
||||
let ico = document.getElementById('icoShade');
|
||||
switch (parseInt(sel.value, 10)) {
|
||||
case 1:
|
||||
document.getElementById('divTiltSettings').style.display = '';
|
||||
if (ico.classList.contains('icss-window-shade')) ico.classList.remove('icss-window-shade');
|
||||
if (!ico.classList.contains('icss-window-blind')) ico.classList.add('icss-window-blind');
|
||||
break;
|
||||
default:
|
||||
if (ico.classList.contains('icss-window-blind')) ico.classList.remove('icss-window-blind');
|
||||
if (!ico.classList.contains('icss-window-shade')) ico.classList.add('icss-window-shade');
|
||||
document.getElementById('divTiltSettings').style.display = 'none';
|
||||
tilt = false;
|
||||
break;
|
||||
}
|
||||
document.getElementById('fldTiltTime').parentElement.style.display = tilt ? 'inline-block' : 'none';
|
||||
document.querySelector('#divSomfyButtons i.icss-window-tilt').style.display = tilt ? '' : 'none';
|
||||
};
|
||||
openEditShade(shadeId) {
|
||||
console.log('Opening Edit Shade');
|
||||
if (typeof shadeId === 'undefined') {
|
||||
|
|
@ -1281,13 +1348,28 @@ class Somfy {
|
|||
document.getElementById('somfyShade').style.display = '';
|
||||
document.getElementById('btnSaveShade').style.display = 'inline-block';
|
||||
document.getElementById('btnLinkRemote').style.display = '';
|
||||
document.getElementById('selShadeType').value = shade.shadeType;
|
||||
document.getElementsByName('shadeAddress')[0].value = shade.remoteAddress;
|
||||
document.getElementsByName('shadeName')[0].value = shade.name;
|
||||
document.getElementsByName('shadeUpTime')[0].value = shade.upTime;
|
||||
document.getElementsByName('shadeDownTime')[0].value = shade.downTime;
|
||||
document.getElementById('fldTiltTime').value = shade.tiltTime;
|
||||
document.getElementById('cbHasTilt').checked = shade.hasTilt;
|
||||
this.onShadeTypeChanged(document.getElementById('selShadeType'));
|
||||
let ico = document.getElementById('icoShade');
|
||||
switch (shade.shadeType) {
|
||||
case 1:
|
||||
ico.classList.remove('icss-window-shade');
|
||||
ico.classList.add('icss-window-blind');
|
||||
break;
|
||||
}
|
||||
let tilt = ico.parentElement.querySelector('i.icss-window-tilt');
|
||||
tilt.style.display = shade.hasTilt ? '' : 'none';
|
||||
tilt.setAttribute('data-tiltposition', shade.tiltPosition);
|
||||
ico.style.setProperty('--shade-position', `${shade.position}%`);
|
||||
ico.style.setProperty('--tilt-position', `${shade.tiltPosition}%`);
|
||||
ico.setAttribute('data-shadeid', shade.shadeId);
|
||||
document.getElementById('btnSetRollingCode').style.display = 'inline-block';
|
||||
if (shade.paired) {
|
||||
document.getElementById('btnUnpairShade').style.display = 'inline-block';
|
||||
}
|
||||
|
|
@ -1319,8 +1401,14 @@ class Somfy {
|
|||
remoteAddress: parseInt(document.getElementsByName('shadeAddress')[0].value, 10),
|
||||
name: document.getElementsByName('shadeName')[0].value,
|
||||
upTime: parseInt(document.getElementsByName('shadeUpTime')[0].value, 10),
|
||||
downTime: parseInt(document.getElementsByName('shadeDownTime')[0].value, 10)
|
||||
downTime: parseInt(document.getElementsByName('shadeDownTime')[0].value, 10),
|
||||
shadeType: parseInt(document.getElementById('selShadeType').value, 10),
|
||||
tiltTime: parseInt(document.getElementById('fldTiltTime').value, 10)
|
||||
};
|
||||
if (obj.shadeType == 1) {
|
||||
obj.hasTilt = document.getElementById('cbHasTilt').checked;
|
||||
}
|
||||
else obj.hasTilt = false;
|
||||
let valid = true;
|
||||
if (valid && (isNaN(obj.remoteAddress) || obj.remoteAddress < 1 || obj.remoteAddress > 16777215)) {
|
||||
errorMessage(document.getElementById('fsSomfySettings'), 'The remote address must be a number between 1 and 16777215. This number must be unique for all shades.');
|
||||
|
|
@ -1338,8 +1426,6 @@ class Somfy {
|
|||
errorMessage(document.getElementById('fsSomfySettings'), 'Down Time must be a value between 0 and 65,355 milliseconds. This is the travel time to go from full open to full closed.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
console.log(obj);
|
||||
if (valid) {
|
||||
let overlay = waitMessage(document.getElementById('fsSomfySettings'));
|
||||
if (isNaN(shadeId) || shadeId >= 255) {
|
||||
|
|
@ -1358,13 +1444,14 @@ class Somfy {
|
|||
else {
|
||||
document.getElementById('btnPairShade').style.display = 'inline-block';
|
||||
}
|
||||
document.getElementById('btnSetRollingCode').style.display = '';
|
||||
document.getElementById('btnSetRollingCode').style.display = 'inline-block';
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
obj.shadeId = shadeId;
|
||||
console.log(obj);
|
||||
putJSON('/saveShade', obj, (err, shade) => {
|
||||
console.log(shade);
|
||||
// We are updating.
|
||||
|
|
@ -1554,7 +1641,7 @@ class Somfy {
|
|||
return div;
|
||||
};
|
||||
sendCommand(shadeId, command) {
|
||||
let data = { shadeId: shadeId };
|
||||
console.log(`Sending Shade command ${shadeId}-${command}`);
|
||||
if (isNaN(parseInt(command, 10)))
|
||||
putJSON('/shadeCommand', { shadeId: shadeId, command: command }, (err, shade) => {
|
||||
});
|
||||
|
|
@ -1562,6 +1649,16 @@ class Somfy {
|
|||
putJSON('/shadeCommand', { shadeId: shadeId, target: parseInt(command, 10) }, (err, shade) => {
|
||||
});
|
||||
};
|
||||
sendTiltCommand(shadeId, command) {
|
||||
console.log(`Sending Tilt command ${shadeId}-${command}`);
|
||||
if (isNaN(parseInt(command, 10)))
|
||||
putJSON('/tiltCommand', { shadeId: shadeId, command: command }, (err, shade) => {
|
||||
});
|
||||
else
|
||||
putJSON('/tiltCommand', { shadeId: shadeId, target: parseInt(command, 10) }, (err, shade) => {
|
||||
});
|
||||
};
|
||||
|
||||
linkRemote(shadeId) {
|
||||
let div = document.createElement('div');
|
||||
let html = `<div id="divLinking" class="instructions" data-type="link-remote" data-shadeid="${shadeId}">`;
|
||||
|
|
@ -1606,8 +1703,15 @@ class Somfy {
|
|||
positioner.querySelector(`.shade-target`).innerHTML = el.value;
|
||||
somfy.sendCommand(shadeId, el.value);
|
||||
}
|
||||
|
||||
}
|
||||
processShadeTiltTarget(el, shadeId) {
|
||||
let positioner = document.querySelector(`.shade-positioner[data-shadeid="${shadeId}"]`);
|
||||
if (positioner) {
|
||||
positioner.querySelector(`.shade-tilt-target`).innerHTML = el.value;
|
||||
somfy.sendTiltCommand(shadeId, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
openSetPosition(shadeId) {
|
||||
console.log('Opening Shade Positioner');
|
||||
if (typeof shadeId === 'undefined') {
|
||||
|
|
@ -1628,6 +1732,11 @@ class Somfy {
|
|||
let html = `<div class="shade-name">${shadeName}</div>`;
|
||||
html += `<input id="slidShadeTarget" name="shadeTarget" type="range" min="0" max="100" step="1" value="${currPos}" onchange="somfy.processShadeTarget(this, ${shadeId});" oninput="document.getElementById('spanShadeTarget').innerHTML = this.value;" />`;
|
||||
html += `<label for="slidShadeTarget"><span>Target Position </span><span><span id="spanShadeTarget" class="shade-target">${currPos}</span><span>%</span></span></label>`;
|
||||
if (makeBool(shade.getAttribute('data-tilt'))) {
|
||||
let currTiltPos = parseInt(shade.getAttribute('data-tilttarget'), 10);
|
||||
html += `<input id="slidShadeTiltTarget" name="shadeTarget" type="range" min="0" max="100" step="1" value="${currTiltPos}" onchange="somfy.processShadeTiltTarget(this, ${shadeId});" oninput="document.getElementById('spanShadeTiltTarget').innerHTML = this.value;" />`;
|
||||
html += `<label for="slidShadeTiltTarget"><span>Target Tilt Position </span><span><span id="spanShadeTiltTarget" class="shade-tilt-target">${currTiltPos}</span><span>%</span></span></label>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
let div = document.createElement('div');
|
||||
div.setAttribute('class', 'shade-positioner');
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ div.waitoverlay > .lds-roller {
|
|||
vertical-align: middle;
|
||||
margin-top: -5px;
|
||||
font-size: 48px;
|
||||
position:relative;
|
||||
}
|
||||
.somfyShadeCtl .shade-name {
|
||||
display:inline-block;
|
||||
|
|
@ -597,15 +598,20 @@ div.waitoverlay > .lds-roller {
|
|||
}
|
||||
|
||||
.shade-positioner {
|
||||
position:absolute;
|
||||
width:100%;
|
||||
background-color:gainsboro;
|
||||
color:gray;
|
||||
min-height:60px;
|
||||
top:0px;
|
||||
padding-left:7px;
|
||||
padding-right:7px;
|
||||
z-index:100;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background-color: oldlace;
|
||||
color: gray;
|
||||
min-height: 60px;
|
||||
top: 0px;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
padding-bottom: 7px;
|
||||
z-index: 100;
|
||||
margin-top: -7px;
|
||||
box-shadow: 4px 4px 4px gray;
|
||||
border: solid 1px silver;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.shade-positioner .shade-name {
|
||||
display:block;
|
||||
|
|
@ -623,6 +629,7 @@ div.waitoverlay > .lds-roller {
|
|||
font-size:1em;
|
||||
margin-top:-3px;
|
||||
margin-left:27px;
|
||||
color:gray;
|
||||
}
|
||||
.shade-positioner label > span:last-child {
|
||||
float: right;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue