Add dry contact support for IO Remote and Relays #165

This commit is contained in:
Robert Strouse 2023-10-10 11:41:21 -07:00
parent 87995bf707
commit bb36ed0f82
9 changed files with 194 additions and 122 deletions

View file

@ -492,6 +492,15 @@ bool EthernetSettings::toJSON(JsonObject &obj) {
obj["MDIOPin"] = this->MDIOPin; obj["MDIOPin"] = this->MDIOPin;
return true; return true;
} }
bool EthernetSettings::usesPin(uint8_t pin) {
if((this->CLKMode == 0 || this->CLKMode == 1) && pin == 0) return true;
else if(this->CLKMode == 2 && pin == 16) return true;
else if(this->CLKMode == 3 && pin == 17) return true;
else if(this->PWRPin == pin) return true;
else if(this->MDCPin == pin) return true;
else if(this->MDIOPin == pin) return true;
return false;
}
bool EthernetSettings::save() { bool EthernetSettings::save() {
pref.begin("ETH"); pref.begin("ETH");
pref.clear(); pref.clear();

View file

@ -76,6 +76,7 @@ class EthernetSettings: BaseSettings {
bool load(); bool load();
bool save(); bool save();
void print(); void print();
bool usesPin(uint8_t pin);
}; };
class IPSettings: BaseSettings { class IPSettings: BaseSettings {
public: public:

223
Somfy.cpp
View file

@ -806,24 +806,32 @@ void SomfyShade::setGPIOs() {
int8_t dir = this->direction; int8_t dir = this->direction;
if(dir == 0 && this->tiltType == tilt_types::integrated) if(dir == 0 && this->tiltType == tilt_types::integrated)
dir = this->tiltDirection; dir = this->tiltDirection;
switch(dir) { if(this->shadeType == shade_types::drycontact) {
case -1: digitalWrite(this->gpioDown, this->currentPos == 100 ? HIGH : LOW);
digitalWrite(this->gpioDown, LOW); this->gpioDir = this->currentPos == 100 ? 1 : -1;
digitalWrite(this->gpioUp, HIGH); }
if(dir != this->gpioDir) Serial.printf("UP: true, DOWN: false\n"); else {
break; switch(dir) {
case 1: case -1:
digitalWrite(this->gpioUp, LOW); digitalWrite(this->gpioDown, LOW);
digitalWrite(this->gpioDown, HIGH); digitalWrite(this->gpioUp, HIGH);
if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: true\n"); if(dir != this->gpioDir) Serial.printf("UP: true, DOWN: false\n");
break; this->gpioDir = dir;
default: break;
digitalWrite(this->gpioUp, LOW); case 1:
digitalWrite(this->gpioDown, LOW); digitalWrite(this->gpioUp, LOW);
if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false\n"); digitalWrite(this->gpioDown, HIGH);
break; if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: true\n");
this->gpioDir = dir;
break;
default:
digitalWrite(this->gpioUp, LOW);
digitalWrite(this->gpioDown, LOW);
if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false\n");
this->gpioDir = dir;
break;
}
} }
this->gpioDir = dir;
} }
else if(this->proto == radio_proto::GP_Remote) { else if(this->proto == radio_proto::GP_Remote) {
if(millis() > this->gpioRelease) { if(millis() > this->gpioRelease) {
@ -834,49 +842,62 @@ void SomfyShade::setGPIOs() {
} }
} }
} }
void SomfyRemote::triggerGPIOs(somfy_frame_t &frame) { void SomfyRemote::triggerGPIOs(somfy_frame_t &frame) { }
void SomfyShade::triggerGPIOs(somfy_frame_t &frame) {
if(this->proto == radio_proto::GP_Remote) { if(this->proto == radio_proto::GP_Remote) {
int8_t dir = 0; int8_t dir = 0;
switch(frame.cmd) { switch(frame.cmd) {
case somfy_commands::My: case somfy_commands::My:
digitalWrite(this->gpioUp, LOW); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioDown, LOW); digitalWrite(this->gpioUp, LOW);
digitalWrite(this->gpioMy, HIGH); digitalWrite(this->gpioDown, LOW);
dir = 0; digitalWrite(this->gpioMy, HIGH);
if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false, MY: true\n"); dir = 0;
if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false, MY: true\n");
}
break; break;
case somfy_commands::Up: case somfy_commands::Up:
digitalWrite(this->gpioMy, LOW); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioDown, LOW); digitalWrite(this->gpioMy, LOW);
digitalWrite(this->gpioUp, HIGH); digitalWrite(this->gpioDown, LOW);
dir = -1; digitalWrite(this->gpioUp, HIGH);
Serial.printf("UP: true, DOWN: false, MY: false\n"); dir = -1;
Serial.printf("UP: true, DOWN: false, MY: false\n");
}
break; break;
case somfy_commands::Toggle: case somfy_commands::Toggle:
case somfy_commands::Down: case somfy_commands::Down:
digitalWrite(this->gpioMy, LOW); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioUp, LOW); digitalWrite(this->gpioMy, LOW);
digitalWrite(this->gpioUp, LOW);
}
digitalWrite(this->gpioDown, HIGH); digitalWrite(this->gpioDown, HIGH);
dir = 1; dir = 1;
Serial.printf("UP: false, DOWN: true, MY: false\n"); Serial.printf("UP: false, DOWN: true, MY: false\n");
break; break;
case somfy_commands::MyUp: case somfy_commands::MyUp:
digitalWrite(this->gpioDown, LOW); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioMy, HIGH); digitalWrite(this->gpioDown, LOW);
digitalWrite(this->gpioUp, HIGH); digitalWrite(this->gpioMy, HIGH);
Serial.printf("UP: true, DOWN: false, MY: true\n"); digitalWrite(this->gpioUp, HIGH);
Serial.printf("UP: true, DOWN: false, MY: true\n");
}
break; break;
case somfy_commands::MyDown: case somfy_commands::MyDown:
digitalWrite(this->gpioUp, LOW); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioMy, HIGH); digitalWrite(this->gpioUp, LOW);
digitalWrite(this->gpioDown, HIGH); digitalWrite(this->gpioMy, HIGH);
Serial.printf("UP: true, DOWN: false, MY: true\n"); digitalWrite(this->gpioDown, HIGH);
Serial.printf("UP: false, DOWN: true, MY: true\n");
}
break; break;
case somfy_commands::MyUpDown: case somfy_commands::MyUpDown:
digitalWrite(this->gpioUp, HIGH); if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::garage1) {
digitalWrite(this->gpioMy, HIGH); digitalWrite(this->gpioUp, HIGH);
digitalWrite(this->gpioDown, HIGH); digitalWrite(this->gpioMy, HIGH);
Serial.printf("UP: true, DOWN: true, MY: true\n"); digitalWrite(this->gpioDown, HIGH);
Serial.printf("UP: true, DOWN: true, MY: true\n");
}
break; break;
} }
this->gpioRelease = millis() + (frame.repeats * 200); this->gpioRelease = millis() + (frame.repeats * 200);
@ -2492,8 +2513,50 @@ bool SomfyShade::save() {
return true; return true;
} }
bool SomfyGroup::save() { somfy.commit(); return true; } bool SomfyGroup::save() { somfy.commit(); return true; }
bool SomfyShade::usesPin(uint8_t pin) {
if(this->proto != radio_proto::GP_Remote && this->proto != radio_proto::GP_Relay) return false;
if(this->gpioDown == pin) return true;
else if(this->shadeType == shade_types::drycontact)
return this->gpioDown == pin;
else if(this->shadeType == shade_types::garage1) {
if(this->proto == radio_proto::GP_Relay && this->gpioUp == pin) return true;
}
else {
if(this->gpioUp == pin) return true;
else if(this->proto == radio_proto::GP_Remote && this->gpioMy == pin) return true;
}
return false;
}
int8_t SomfyShade::validateJSON(JsonObject &obj) { int8_t SomfyShade::validateJSON(JsonObject &obj) {
int8_t ret = 0; int8_t ret = 0;
shade_types type = this->shadeType;
if(obj.containsKey("shadeType")) {
if(obj["shadeType"].is<const char *>()) {
if(strncmp(obj["shadeType"].as<const char *>(), "roller", 7) == 0)
type = shade_types::roller;
else if(strncmp(obj["shadeType"].as<const char *>(), "ldrapery", 9) == 0)
type = shade_types::ldrapery;
else if(strncmp(obj["shadeType"].as<const char *>(), "rdrapery", 9) == 0)
type = shade_types::rdrapery;
else if(strncmp(obj["shadeType"].as<const char *>(), "cdrapery", 9) == 0)
type = shade_types::cdrapery;
else if(strncmp(obj["shadeType"].as<const char *>(), "garage1", 7) == 0)
type = shade_types::garage1;
else if(strncmp(obj["shadeType"].as<const char *>(), "garage3", 7) == 0)
type = shade_types::garage3;
else if(strncmp(obj["shadeType"].as<const char *>(), "blind", 5) == 0)
type = shade_types::blind;
else if(strncmp(obj["shadeType"].as<const char *>(), "awning", 7) == 0)
type = shade_types::awning;
else if(strncmp(obj["shadeType"].as<const char *>(), "shutter", 8) == 0)
type = shade_types::shutter;
else if(strncmp(obj["shadeType"].as<const char *>(), "drycontact", 11) == 0)
type = shade_types::drycontact;
}
else {
this->shadeType = static_cast<shade_types>(obj["shadeType"].as<uint8_t>());
}
}
if(obj.containsKey("proto")) { if(obj.containsKey("proto")) {
radio_proto proto = this->proto; radio_proto proto = this->proto;
if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) { if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) {
@ -2502,63 +2565,29 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) {
uint8_t upPin = obj.containsKey("gpioUp") ? obj["gpioUp"].as<uint8_t>() : this->gpioUp; uint8_t upPin = obj.containsKey("gpioUp") ? obj["gpioUp"].as<uint8_t>() : this->gpioUp;
uint8_t downPin = obj.containsKey("gpioDown") ? obj["gpioDown"].as<uint8_t>() : this->gpioDown; uint8_t downPin = obj.containsKey("gpioDown") ? obj["gpioDown"].as<uint8_t>() : this->gpioDown;
uint8_t myPin = obj.containsKey("gpioMy") ? obj["gpioMy"].as<uint8_t>() : this->gpioMy; uint8_t myPin = obj.containsKey("gpioMy") ? obj["gpioMy"].as<uint8_t>() : this->gpioMy;
if(type == shade_types::drycontact || (type == shade_types::garage1 && proto == radio_proto::GP_Remote)) upPin = myPin = 255;
if(proto == radio_proto::GP_Relay) myPin = 255;
if(somfy.transceiver.config.enabled) { if(somfy.transceiver.config.enabled) {
if(somfy.transceiver.config.SCKPin == upPin || somfy.transceiver.config.SCKPin == downPin || if((upPin != 255 && somfy.transceiver.usesPin(upPin)) ||
somfy.transceiver.config.TXPin == upPin || somfy.transceiver.config.TXPin == downPin || (downPin != 255 && somfy.transceiver.usesPin(downPin)) ||
somfy.transceiver.config.RXPin == upPin || somfy.transceiver.config.RXPin == downPin || (myPin != 255 && somfy.transceiver.usesPin(myPin)))
somfy.transceiver.config.MOSIPin == upPin || somfy.transceiver.config.MOSIPin == downPin || ret = -10;
somfy.transceiver.config.MISOPin == upPin || somfy.transceiver.config.MISOPin == downPin ||
somfy.transceiver.config.CSNPin == upPin || somfy.transceiver.config.CSNPin == downPin)
ret = -10; // Pin in use with transceiver.
else if(proto == radio_proto::GP_Remote) {
if(somfy.transceiver.config.SCKPin == myPin ||
somfy.transceiver.config.TXPin == myPin ||
somfy.transceiver.config.RXPin == myPin ||
somfy.transceiver.config.MOSIPin == myPin ||
somfy.transceiver.config.MISOPin == myPin ||
somfy.transceiver.config.CSNPin == myPin)
ret = -10; // Pin in use with transceiver.
}
} }
if(settings.connType == conn_types::ethernet || settings.connType == conn_types::ethernetpref) { if(settings.connType == conn_types::ethernet || settings.connType == conn_types::ethernetpref) {
if((settings.Ethernet.CLKMode == 0 || settings.Ethernet.CLKMode == 1) && (upPin == 0 || downPin == 0)) if((upPin != 255 && settings.Ethernet.usesPin(upPin)) ||
ret = -11; // Pin in use with ethernet. (downPin != 255 && somfy.transceiver.usesPin(downPin)) ||
else if(proto == radio_proto::GP_Remote && ((settings.Ethernet.CLKMode == 0 || settings.Ethernet.CLKMode == 1) && myPin == 0)) (myPin != 255 && somfy.transceiver.usesPin(myPin)))
ret = -11; // Pin in use with ethernet.
else if((settings.Ethernet.CLKMode == 2 && (upPin == 16 || downPin == 16)) ||
(settings.Ethernet.CLKMode == 3 && (upPin == 17 || downPin == 17)))
ret = -11;
else if(proto == radio_proto::GP_Remote && (settings.Ethernet.CLKMode == 2 && myPin == 16 || settings.Ethernet.CLKMode == 3 && myPin == 17))
ret = -11;
else if(settings.Ethernet.PWRPin == upPin || settings.Ethernet.PWRPin == downPin ||
settings.Ethernet.MDCPin == upPin || settings.Ethernet.MDCPin == downPin ||
settings.Ethernet.MDIOPin == upPin || settings.Ethernet.MDIOPin == downPin)
ret = -11;
else if(proto == radio_proto::GP_Remote && (settings.Ethernet.PWRPin == myPin ||
settings.Ethernet.MDCPin == myPin || settings.Ethernet.MDIOPin == myPin))
ret = -11; ret = -11;
} }
if(ret == 0) { if(ret == 0) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &somfy.shades[i]; SomfyShade *shade = &somfy.shades[i];
if(shade->getShadeId() == this->getShadeId() || shade->getShadeId() == 255) continue; if(shade->getShadeId() == this->getShadeId() || shade->getShadeId() == 255) continue;
if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { if((upPin != 255 && shade->usesPin(upPin)) ||
if(shade->gpioUp == upPin || shade->gpioDown == upPin || shade->gpioDown == upPin || shade->gpioDown == downPin) { (downPin != 255 && shade->usesPin(downPin)) ||
ret = -12; (myPin != 255 && shade->usesPin(myPin))){
break; ret = -12;
} break;
else if(proto == radio_proto::GP_Remote && (shade->gpioUp == myPin || shade->gpioDown == myPin)) {
ret = -12;
break;
}
else if(shade->proto == radio_proto::GP_Remote && (shade->gpioMy == upPin || shade->gpioMy == downPin)) {
ret = -12;
break;
}
else if(shade->proto == radio_proto::GP_Remote && proto == radio_proto::GP_Remote && (shade->gpioMy == myPin)) {
ret = -12;
break;
}
} }
} }
} }
@ -2600,6 +2629,8 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) {
this->shadeType = shade_types::awning; this->shadeType = shade_types::awning;
else if(strncmp(obj["shadeType"].as<const char *>(), "shutter", 8) == 0) else if(strncmp(obj["shadeType"].as<const char *>(), "shutter", 8) == 0)
this->shadeType = shade_types::shutter; this->shadeType = shade_types::shutter;
else if(strncmp(obj["shadeType"].as<const char *>(), "drycontact", 11) == 0)
this->shadeType = shade_types::drycontact;
} }
else { else {
this->shadeType = static_cast<shade_types>(obj["shadeType"].as<uint8_t>()); this->shadeType = static_cast<shade_types>(obj["shadeType"].as<uint8_t>());
@ -3708,6 +3739,18 @@ bool Transceiver::fromJSON(JsonObject& obj) {
} }
return true; return true;
} }
bool Transceiver::usesPin(uint8_t pin) {
if(this->config.enabled) {
if(this->config.SCKPin == pin ||
this->config.TXPin == pin ||
this->config.RXPin == pin ||
this->config.MOSIPin == pin ||
this->config.MISOPin == pin ||
this->config.CSNPin == pin)
return true;
}
return false;
}
bool Transceiver::save() { bool Transceiver::save() {
this->config.save(); this->config.save();
this->config.apply(); this->config.apply();

View file

@ -214,7 +214,7 @@ class SomfyRemote {
void repeatFrame(uint8_t repeat); void repeatFrame(uint8_t repeat);
virtual uint16_t p_lastRollingCode(uint16_t code); virtual uint16_t p_lastRollingCode(uint16_t code);
somfy_commands transformCommand(somfy_commands cmd); somfy_commands transformCommand(somfy_commands cmd);
void triggerGPIOs(somfy_frame_t &frame); virtual void triggerGPIOs(somfy_frame_t &frame);
}; };
class SomfyLinkedRemote : public SomfyRemote { class SomfyLinkedRemote : public SomfyRemote {
@ -304,7 +304,8 @@ class SomfyShade : public SomfyRemote {
void clear(); void clear();
int8_t transformPosition(float fpos); int8_t transformPosition(float fpos);
void setGPIOs(); void setGPIOs();
void triggerGPIOs(somfy_frame_t &frame);
bool usesPin(uint8_t pin);
// State Setters // State Setters
int8_t p_direction(int8_t dir); int8_t p_direction(int8_t dir);
int8_t p_tiltDirection(int8_t dir); int8_t p_tiltDirection(int8_t dir);
@ -450,6 +451,7 @@ class Transceiver {
void endFrequencyScan(); void endFrequencyScan();
void processFrequencyScan(bool received = false); void processFrequencyScan(bool received = false);
void emitFrequencyScan(uint8_t num = 255); void emitFrequencyScan(uint8_t num = 255);
bool usesPin(uint8_t pin);
}; };
class SomfyShadeController { class SomfyShadeController {
protected: protected:

Binary file not shown.

Binary file not shown.

View file

@ -293,8 +293,8 @@
<option value="0">RTS</option> <option value="0">RTS</option>
<option value="1">RTW</option> <option value="1">RTW</option>
<option value="2">RTV</option> <option value="2">RTV</option>
<option value="8">Relay</option> <option value="8">IO-Relay</option>
<option value="9">Remote</option> <option value="9">IO-Remote</option>
</select> </select>
<label for="selShadeProto">Protocol</label> <label for="selShadeProto">Protocol</label>
</div> </div>
@ -306,12 +306,12 @@
<label for="selShadeBitLength">Bit Length</label> <label for="selShadeBitLength">Bit Length</label>
</div> </div>
<div id="divGPIOControl" class="field-group"> <div id="divGPIOControl" class="field-group">
<div class="field-group" style=""> <div id="divGPIOUp" class="field-group" style="">
<select id="selShadeGPIOUp" data-bind="gpioUp" data-datatype="int" style="width:70px;"> <select id="selShadeGPIOUp" data-bind="gpioUp" data-datatype="int" style="width:70px;">
</select> </select>
<label for="selShadeGPIOUp">UP</label> <label for="selShadeGPIOUp">UP</label>
</div> </div>
<div class="field-group" style=""> <div id="divGPIODown" class="field-group" style="">
<select id="selShadeGPIODown" data-bind="gpioDown" data-datatype="int" style="width:70px;"> <select id="selShadeGPIODown" data-bind="gpioDown" data-datatype="int" style="width:70px;">
</select> </select>
<label for="selShadeGPIODown">Down</label> <label for="selShadeGPIODown">Down</label>

View file

@ -1,7 +1,7 @@
var errors = [ var errors = [
{ code: -10, desc: "Pin setting in use for Transceiver" }, { code: -10, desc: "Pin setting in use for Transceiver. Output pins cannot be re-used." },
{ code: -11, desc: "Pin setting in use for Ethernet Adapter" }, { code: -11, desc: "Pin setting in use for Ethernet Adapter. Output pins cannot be re-used." },
{ code: -12, desc: "Pin setting in use on another motor" } { code: -12, desc: "Pin setting in use on another motor. Output pins cannot be re-used." }
] ]
document.oncontextmenu = (event) => { document.oncontextmenu = (event) => {
if (event.target && event.target.tagName.toLowerCase() === 'input' && (event.target.type.toLowerCase() === 'text' || event.target.type.toLowerCase() === 'password')) if (event.target && event.target.tagName.toLowerCase() === 'input' && (event.target.type.toLowerCase() === 'text' || event.target.type.toLowerCase() === 'password'))
@ -3112,15 +3112,25 @@ class Somfy {
valid = false; valid = false;
} }
if (obj.proto === 8 || obj.proto === 9) { if (obj.proto === 8 || obj.proto === 9) {
if (obj.gpioUp === obj.gpioDown) { switch (obj.shadeType) {
ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down GPIO selections must be unique.'); case 5: // Garage 1-button
valid = false; if (obj.proto !== 9 && obj.gpioUp === obj.gpioDown) {
} ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down GPIO selections must be unique.');
} valid = false;
if (obj.proto === 9) { }
if (obj.gpioMy === obj.gpioUp || obj.gpioMy === obj.gpioDown) { break;
ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down and my GPIO selections must be unique.'); case 9: // Dry contact.
valid = false; break;
default:
if (obj.gpioUp === obj.gpioDown) {
ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down GPIO selections must be unique.');
valid = false;
}
else if (obj.proto === 9 && (obj.gpioMy === obj.gpioUp || obj.gpioMy === obj.gpioDown)) {
ui.errorMessage(document.getElementById('divSomfySettings'), 'For GPIO controlled motors the up and down and my GPIO selections must be unique.');
valid = false;
}
break;
} }
} }
if (valid) { if (valid) {

View file

@ -167,20 +167,27 @@
#somfyShade[data-shadetype="6"] #divStepSettings { #somfyShade[data-shadetype="6"] #divStepSettings {
display: none; display: none;
} }
#somfyShade[data-proto="9"][data-shadetype="5"] #divGPIOUp,
#somfyShade[data-proto="9"][data-shadetype="5"] #divGPIOMy,
#somfyShade[data-proto="8"][data-shadetype="5"] #divGPIOMy,
#somfyShade[data-proto="9"][data-shadetype="9"] #divGPIOUp,
#somfyShade[data-proto="9"][data-shadetype="9"] #divGPIOMy,
#somfyShade[data-proto="8"][data-shadetype="9"] #divGPIOUp {
display: none;
}
.group-draggable, .group-draggable,
.shade-draggable { .shade-draggable {
height:32px; height: 32px;
border-top:solid 2px transparent; border-top: solid 2px transparent;
cursor:grab; cursor: grab;
}
.group-draggable.dragging *,
.group-draggable.over *,
.shade-draggable.dragging *,
.shade-draggable.over * {
pointer-events: none;
}
.group-draggable.over,
.shade-draggable.over {
border-top: solid 2px var(--shade-color, '#00bcd4');
} }
.group-draggable.dragging *,
.group-draggable.over *,
.shade-draggable.dragging *,
.shade-draggable.over * {
pointer-events: none;
}
.group-draggable.over,
.shade-draggable.over {
border-top: solid 2px var(--shade-color, '#00bcd4');
}