mirror of
https://github.com/anklimov/lighthub
synced 2025-12-07 12:19:49 +03:00
modbus retry issue, core fixes (cold restore issues)
This commit is contained in:
@@ -409,6 +409,7 @@ debugSerial<<F("Txt2Cmd:")<<cmd<<endl;
|
|||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
short Item::cmd2changeActivity(int lastActivity, short defaultCmd)
|
short Item::cmd2changeActivity(int lastActivity, short defaultCmd)
|
||||||
{
|
{
|
||||||
if (isActive())
|
if (isActive())
|
||||||
@@ -417,24 +418,27 @@ short Item::cmd2changeActivity(int lastActivity, short defaultCmd)
|
|||||||
else if (lastActivity) return CMD_OFF;
|
else if (lastActivity) return CMD_OFF;
|
||||||
else return defaultCmd;
|
else return defaultCmd;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode, char *subItem) {
|
int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode, char *subItem) {
|
||||||
|
|
||||||
debugSerial<<F("RAM=")<<freeRam()<<F(" Item=")<<itemArr->name<<F(" Sub=")<<subItem<<F(" Suff=")<<suffixCode<<F(" Cmd=")<<cmd<<F(" Par= ");
|
debugSerial<<F("RAM=")<<freeRam()<<F(" Item=")<<itemArr->name<<F(" Sub=")<<subItem<<F(" Suff=")<<suffixCode<<F(" Cmd=")<<cmd<<F(" Par=(");
|
||||||
if (!itemArr) return -1;
|
if (!itemArr) return -1;
|
||||||
int Par[MAXCTRLPAR] = {0, 0, 0};
|
int Par[MAXCTRLPAR] = {0, 0, 0};
|
||||||
if (Parameters)
|
if (Parameters)
|
||||||
for (short i=0;i<n && i<MAXCTRLPAR;i++){
|
for (short i=0;i<n && i<MAXCTRLPAR;i++){
|
||||||
Par[i] = Parameters[i];
|
Par[i] = Parameters[i];
|
||||||
debugSerial<<F("<")<<Par[i]<<F("> ");
|
debugSerial<<F("<")<<Par[i]<<F(">");
|
||||||
}
|
}
|
||||||
debugSerial<<endl;
|
debugSerial<<F(")")<<endl;
|
||||||
|
|
||||||
|
/*
|
||||||
if (itemType == CH_GROUP && !send)
|
if (itemType == CH_GROUP && !send)
|
||||||
{
|
{
|
||||||
debugSerial<<F("Skip Grp")<<endl;
|
debugSerial<<F("Skip Grp")<<endl;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
int iaddr = getArg();
|
int iaddr = getArg();
|
||||||
int chActive =isActive();
|
int chActive =isActive();
|
||||||
@@ -475,56 +479,32 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
|
|
||||||
if (driver) return driver->Ctrl(cmd, n, Parameters, send, suffixCode, subItem);
|
if (driver) return driver->Ctrl(cmd, n, Parameters, send, suffixCode, subItem);
|
||||||
// Legacy code
|
// Legacy code
|
||||||
bool instantON = false;
|
bool toExecute = (chActive>0); //if channel is already active - unconditionally propogate changes
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 0: // old style SET - with turning ON
|
case 0: // old style SET - with turning ON
|
||||||
instantON = true;
|
toExecute = true;
|
||||||
case CMD_SET: // new style SET - w/o turning ON
|
case CMD_SET: // new style SET - w/o turning ON
|
||||||
|
|
||||||
if (itemType !=CH_THERMO) setCmd(CMD_SET); //prevent ON thermostat by semp set ????
|
//if (/*itemType !=CH_THERMO && */send) setCmd(CMD_SET); //prevent ON thermostat by semp set ????
|
||||||
|
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
|
|
||||||
case CH_RGBW: //only if configured VAL array
|
case CH_RGBW: //only if configured VAL array
|
||||||
if (!Par[1] && (n == 3)) itemType = CH_WHITE;
|
if (!Par[1] && (n == 3)) itemType = CH_WHITE;
|
||||||
case CH_RGB:
|
case CH_RGB:
|
||||||
/*
|
|
||||||
if (n == 3) { // Full triplet passed
|
|
||||||
st.h = Par[0];
|
|
||||||
st.s = Par[1];
|
|
||||||
st.v = Par[2];
|
|
||||||
setVal(st.aslong);
|
|
||||||
} else
|
|
||||||
{ //Uncomplete triplet passed
|
|
||||||
st.aslong = getVal(); // restore CSV triplet
|
|
||||||
|
|
||||||
switch (n) {
|
|
||||||
case 1:
|
|
||||||
st.v = Par[0]; // Just volume passed // override volume
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
st.h = Par[0];
|
|
||||||
st.s = Par[1];
|
|
||||||
}
|
|
||||||
setVal(st.aslong); // Save back
|
|
||||||
Par[0] = st.h;
|
|
||||||
Par[1] = st.s;
|
|
||||||
Par[2] = st.v;
|
|
||||||
n = 3;
|
|
||||||
}
|
|
||||||
setCmd(cmd2changeActivity(chActive,cmd));
|
|
||||||
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED); // Send back triplet ?
|
|
||||||
break; */
|
|
||||||
case CH_GROUP: //Save for groups as well
|
case CH_GROUP: //Save for groups as well
|
||||||
st.aslong = getVal();
|
st.aslong = getVal();
|
||||||
switch (n)
|
switch (n)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
|
|
||||||
st.v = Par[0]; //Volume only
|
st.v = Par[0]; //Volume only
|
||||||
|
if (st.hsv_flag)
|
||||||
|
{
|
||||||
Par[0] = st.h;
|
Par[0] = st.h;
|
||||||
Par[1] = st.s;
|
Par[1] = st.s;
|
||||||
Par[2] = st.v;
|
Par[2] = st.v;
|
||||||
n = 3;
|
n = 3;}
|
||||||
break;
|
break;
|
||||||
case 2: // Just hue and saturation
|
case 2: // Just hue and saturation
|
||||||
st.h = Par[0];
|
st.h = Par[0];
|
||||||
@@ -540,28 +520,36 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
st.hsv_flag = 1;
|
st.hsv_flag = 1;
|
||||||
}
|
}
|
||||||
setVal(st.aslong);
|
setVal(st.aslong);
|
||||||
if (chActive && !st.v) setCmd(CMD_OFF);
|
if (toExecute)
|
||||||
if (!chActive && st.v) setCmd(CMD_ON);
|
{ //
|
||||||
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED);//// Send back volume for grp?
|
if (chActive && !st.v) setCmd(CMD_OFF);
|
||||||
|
if (!chActive && st.v) setCmd(CMD_ON);
|
||||||
|
SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED);
|
||||||
|
}
|
||||||
|
else SendStatus(SEND_PARAMETERS | SEND_DEFFERED);
|
||||||
break;
|
break;
|
||||||
case CH_PWM:
|
case CH_PWM:
|
||||||
case CH_VC:
|
case CH_VC:
|
||||||
case CH_DIMMER:
|
case CH_DIMMER:
|
||||||
case CH_MODBUS:
|
case CH_MODBUS:
|
||||||
case CH_VCTEMP:
|
|
||||||
|
|
||||||
setVal(Par[0]); // Store value
|
setVal(Par[0]); // Store value
|
||||||
setCmd(cmd2changeActivity(chActive,cmd));
|
// setCmd(cmd2changeActivity(chActive,cmd));
|
||||||
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED); // Send back parameter for channel above this line
|
if (toExecute)
|
||||||
|
{ // Not restoring, working
|
||||||
|
if (chActive && !Par[0]) setCmd(CMD_OFF);
|
||||||
|
if (!chActive && Par[0]) setCmd(CMD_ON);
|
||||||
|
SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED); // Send back parameter for channel above this line
|
||||||
|
}
|
||||||
|
else SendStatus(SEND_PARAMETERS | SEND_DEFFERED);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CH_VCTEMP: // moved
|
||||||
case CH_THERMO: ///? wasnt send before/ now will ///
|
case CH_THERMO: ///? wasnt send before/ now will ///
|
||||||
setVal(Par[0]); // Store value
|
setVal(Par[0]); // Store value
|
||||||
if (send) SendStatus(SEND_PARAMETERS | SEND_DEFFERED); // Send back parameter for channel above this line
|
if (send) SendStatus(SEND_PARAMETERS | SEND_DEFFERED); // Send back parameter for channel above this line
|
||||||
|
|
||||||
}//itemtype
|
}//itemtype
|
||||||
|
|
||||||
if (getCmd() == CMD_SET && itemType !=CH_GROUP && !chActive>0) return 1; // Parameters are stored, no further action required
|
if (! toExecute && itemType !=CH_GROUP) return 1; // Parameters are stored, no further action required
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CMD_XON:
|
case CMD_XON:
|
||||||
@@ -595,11 +583,14 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS );
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS );
|
||||||
break;
|
break;
|
||||||
} // if forcewhite
|
} // if forcewhite
|
||||||
|
setCmd(cmd);
|
||||||
if (chActive>0) {SendStatus(SEND_COMMAND);return 1;}
|
if (chActive>0 && send)
|
||||||
|
{
|
||||||
|
SendStatus(SEND_COMMAND);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
short params = 0;
|
short params = 0;
|
||||||
setCmd(cmd);
|
|
||||||
//retrive stored values
|
//retrive stored values
|
||||||
st.aslong = getVal();
|
st.aslong = getVal();
|
||||||
|
|
||||||
@@ -628,7 +619,7 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
Par[1] = st.s;
|
Par[1] = st.s;
|
||||||
Par[2] = st.v;
|
Par[2] = st.v;
|
||||||
params = 3;
|
params = 3;
|
||||||
SendStatus(SEND_COMMAND | SEND_PARAMETERS); // Send restored triplet. In any cases
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS); // ???Send restored triplet. In any cases
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CH_VCTEMP:
|
case CH_VCTEMP:
|
||||||
@@ -638,7 +629,7 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
case CH_VC:
|
case CH_VC:
|
||||||
Par[0] = st.aslong;
|
Par[0] = st.aslong;
|
||||||
params = 1;
|
params = 1;
|
||||||
SendStatus(SEND_COMMAND | SEND_PARAMETERS); // Send restored parameter, even if send=false - no problem, loop will be supressed at next hop
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS); // ???Send restored parameter, even if send=false - no problem, loop will be supressed at next hop
|
||||||
break;
|
break;
|
||||||
case CH_THERMO:
|
case CH_THERMO:
|
||||||
Par[0] = st.aslong;
|
Par[0] = st.aslong;
|
||||||
@@ -656,7 +647,7 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
Par[0] = 20; //20 degrees celsium - safe temperature
|
Par[0] = 20; //20 degrees celsium - safe temperature
|
||||||
params = 1;
|
params = 1;
|
||||||
setVal(20);
|
setVal(20);
|
||||||
SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED);
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS); //??? // deffered ???
|
||||||
break;
|
break;
|
||||||
case CH_RGBW:
|
case CH_RGBW:
|
||||||
case CH_RGB:
|
case CH_RGB:
|
||||||
@@ -669,19 +660,19 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
st.s = Par[1];
|
st.s = Par[1];
|
||||||
st.v = Par[2];
|
st.v = Par[2];
|
||||||
setVal(st.aslong);
|
setVal(st.aslong);
|
||||||
SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED);
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS ); // deffered ???
|
||||||
break;
|
break;
|
||||||
case CH_RELAY:
|
case CH_RELAY:
|
||||||
Par[0] = 100;
|
Par[0] = 100;
|
||||||
params = 1;
|
params = 1;
|
||||||
setVal(100);
|
setVal(100);
|
||||||
if (send) SendStatus(SEND_COMMAND | SEND_DEFFERED);
|
if (send) SendStatus(SEND_COMMAND); // deffered ???
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Par[0] = 100;
|
Par[0] = 100;
|
||||||
params = 1;
|
params = 1;
|
||||||
setVal(100);
|
setVal(100);
|
||||||
SendStatus(SEND_COMMAND | SEND_PARAMETERS | SEND_DEFFERED);
|
if (send) SendStatus(SEND_COMMAND | SEND_PARAMETERS); //??? // deffered ???
|
||||||
}
|
}
|
||||||
} // default handler
|
} // default handler
|
||||||
for (short i = 0; i < params; i++) {
|
for (short i = 0; i < params; i++) {
|
||||||
@@ -790,24 +781,12 @@ int Item::Ctrl(short cmd, short n, int *Parameters, boolean send, int suffixCode
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef _modbus
|
#ifdef _modbus
|
||||||
case CH_MODBUS: {
|
case CH_MODBUS:
|
||||||
short numpar=0;
|
modbusDimmerSet(Par[0]);
|
||||||
if ((itemArg->type == aJson_Array) && ((numpar = aJson.getArraySize(itemArg)) >= 2)) {
|
break;
|
||||||
int _addr = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_ADDR)->valueint;
|
|
||||||
int _reg = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_REG)->valueint;
|
|
||||||
int _mask = -1;
|
|
||||||
if (numpar >= (MODBUS_CMD_ARG_MASK+1)) _mask = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_MASK)->valueint;
|
|
||||||
int _maxval = 0x3f;
|
|
||||||
if (numpar >= (MODBUS_CMD_ARG_MAX_SCALE+1)) _maxval = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_MAX_SCALE)->valueint;
|
|
||||||
int _regType = MODBUS_HOLDING_REG_TYPE;
|
|
||||||
if (numpar >= (MODBUS_CMD_ARG_REG_TYPE+1)) _regType = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_REG_TYPE)->valueint;
|
|
||||||
if (_maxval) modbusDimmerSet(_addr, _reg, _regType, _mask, map(Par[0], 0, 100, 0, _maxval));
|
|
||||||
else modbusDimmerSet(_addr, _reg, _regType, _mask, Par[0]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
case CH_GROUP://Group
|
case CH_GROUP://Group
|
||||||
{
|
{
|
||||||
if (itemArg->type == aJson_Array) {
|
if (itemArg->type == aJson_Array) {
|
||||||
@@ -894,7 +873,7 @@ int Item::isActive() {
|
|||||||
int val = 0;
|
int val = 0;
|
||||||
|
|
||||||
if (!isValid()) return -1;
|
if (!isValid()) return -1;
|
||||||
//debugSerial<<itemArr->name);
|
debugSerial<<itemArr->name;
|
||||||
|
|
||||||
|
|
||||||
int cmd = getCmd();
|
int cmd = getCmd();
|
||||||
@@ -908,12 +887,12 @@ int Item::isActive() {
|
|||||||
case CMD_AUTO:
|
case CMD_AUTO:
|
||||||
case CMD_HEAT:
|
case CMD_HEAT:
|
||||||
case CMD_COOL:
|
case CMD_COOL:
|
||||||
//debugSerial<<" active");
|
debugSerial<<F(" active\n");
|
||||||
return 1;
|
return 1;
|
||||||
case CMD_OFF:
|
case CMD_OFF:
|
||||||
case CMD_HALT:
|
case CMD_HALT:
|
||||||
case -1: ///// No last command
|
case -1: ///// No last command
|
||||||
//debugSerial<<" inactive");
|
debugSerial<<F(" inactive\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,17 +903,18 @@ int Item::isActive() {
|
|||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
case CH_GROUP: //make recursive calculation - is it some active in group
|
case CH_GROUP: //make recursive calculation - is it some active in group
|
||||||
if (itemArg->type == aJson_Array) {
|
if (itemArg->type == aJson_Array) {
|
||||||
debugSerial<<F("Grp check:\n");
|
debugSerial<<F(" Grp:");
|
||||||
aJsonObject *i = itemArg->child;
|
aJsonObject *i = itemArg->child;
|
||||||
while (i) {
|
while (i) {
|
||||||
Item it(i->valuestring);
|
Item it(i->valuestring);
|
||||||
|
|
||||||
if (it.isValid() && it.isActive()) {
|
if (it.isValid() && it.isActive()) {
|
||||||
debugSerial<<F("Active\n");
|
debugSerial<<F(" active\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
i = i->next;
|
i = i->next;
|
||||||
} //while
|
} //while
|
||||||
|
debugSerial<<F(" inactive\n");
|
||||||
return 0;
|
return 0;
|
||||||
} //if
|
} //if
|
||||||
break;
|
break;
|
||||||
@@ -954,9 +934,13 @@ int Item::isActive() {
|
|||||||
case CH_VCTEMP:
|
case CH_VCTEMP:
|
||||||
case CH_PWM:
|
case CH_PWM:
|
||||||
val = st.aslong;
|
val = st.aslong;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
debugSerial<<F(" unknown\n");
|
||||||
|
return 0;
|
||||||
} //switch
|
} //switch
|
||||||
//debugSerial<<F(":="));
|
debugSerial<<F(" value is ");
|
||||||
//debugSerial<<val);
|
debugSerial<<val<<endl;
|
||||||
if (val) return 1; else return 0;
|
if (val) return 1; else return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,6 +1010,32 @@ POLL 2101x10
|
|||||||
[22:27:29] => poll: 0A 03 08 34 00 0A 87 18
|
[22:27:29] => poll: 0A 03 08 34 00 0A 87 18
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
#ifdef _modbus
|
||||||
|
int Item::modbusDimmerSet(uint16_t value)
|
||||||
|
{
|
||||||
|
switch (getCmd())
|
||||||
|
{
|
||||||
|
case CMD_OFF:
|
||||||
|
case CMD_HALT:
|
||||||
|
value=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
short numpar=0;
|
||||||
|
if ((itemArg->type == aJson_Array) && ((numpar = aJson.getArraySize(itemArg)) >= 2)) {
|
||||||
|
int _addr = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_ADDR)->valueint;
|
||||||
|
int _reg = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_REG)->valueint;
|
||||||
|
int _mask = -1;
|
||||||
|
if (numpar >= (MODBUS_CMD_ARG_MASK+1)) _mask = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_MASK)->valueint;
|
||||||
|
int _maxval = 0x3f;
|
||||||
|
if (numpar >= (MODBUS_CMD_ARG_MAX_SCALE+1)) _maxval = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_MAX_SCALE)->valueint;
|
||||||
|
int _regType = MODBUS_HOLDING_REG_TYPE;
|
||||||
|
if (numpar >= (MODBUS_CMD_ARG_REG_TYPE+1)) _regType = aJson.getArrayItem(itemArg, MODBUS_CMD_ARG_REG_TYPE)->valueint;
|
||||||
|
if (_maxval) modbusDimmerSet(_addr, _reg, _regType, _mask, map(value, 0, 100, 0, _maxval));
|
||||||
|
else modbusDimmerSet(_addr, _reg, _regType, _mask, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Item::mb_fail() {
|
void Item::mb_fail() {
|
||||||
debugSerial<<F("Modbus op failed\n");
|
debugSerial<<F("Modbus op failed\n");
|
||||||
@@ -1244,7 +1254,8 @@ boolean Item::checkModbusRetry() {
|
|||||||
int val = getVal();
|
int val = getVal();
|
||||||
debugSerial<<F("Retrying CMD\n");
|
debugSerial<<F("Retrying CMD\n");
|
||||||
clearFlag(SEND_RETRY); // Clean retry flag
|
clearFlag(SEND_RETRY); // Clean retry flag
|
||||||
Ctrl(cmd,1,&val); // Execute command again
|
//Ctrl(cmd,1,&val); // Execute command again
|
||||||
|
modbusDimmerSet(val);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ class Item
|
|||||||
int VacomSetFan (int8_t val, int8_t cmd=0);
|
int VacomSetFan (int8_t val, int8_t cmd=0);
|
||||||
int VacomSetHeat(int addr, int8_t val, int8_t cmd=0);
|
int VacomSetHeat(int addr, int8_t val, int8_t cmd=0);
|
||||||
int modbusDimmerSet(int addr, uint16_t _reg, int _regType, int _mask, uint16_t value);
|
int modbusDimmerSet(int addr, uint16_t _reg, int _regType, int _mask, uint16_t value);
|
||||||
|
int modbusDimmerSet(uint16_t value);
|
||||||
void mb_fail();
|
void mb_fail();
|
||||||
int isActive();
|
int isActive();
|
||||||
void Parse();
|
void Parse();
|
||||||
|
|||||||
Reference in New Issue
Block a user