mirror of
https://github.com/arendst/Tasmota.git
synced 2025-04-24 06:47:17 +00:00
Zigbee auto-mapping
This commit is contained in:
parent
5892cffbda
commit
4ea69ce2ef
@ -20,6 +20,8 @@
|
||||
#ifdef USE_ZIGBEE
|
||||
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
void EZ_SendZDO(uint16_t shortaddr, uint16_t cmd, const unsigned char *payload, size_t payload_len, bool retry = true);
|
||||
|
||||
//
|
||||
// Trying to get a uniform LQI measure, we are aligning with the definition of ZNP
|
||||
// I.e. a linear projection from -87dBm to +10dB over 0..255
|
||||
@ -200,6 +202,44 @@ int32_t EZ_MessageSent(int32_t res, const class SBuffer &buf) {
|
||||
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
|
||||
/*********************************************************************************************\
|
||||
* Handle auto-mapping
|
||||
\*********************************************************************************************/
|
||||
// low-level sending of packet
|
||||
void Z_Send_State_or_Map(uint16_t shortaddr, uint8_t index, uint16_t zdo_cmd) {
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
SBuffer buf(10);
|
||||
buf.add8(Z_SREQ | Z_ZDO); // 25
|
||||
buf.add8(zdo_cmd); // 33
|
||||
buf.add16(shortaddr); // shortaddr
|
||||
buf.add8(index); // StartIndex = 0
|
||||
|
||||
ZigbeeZNPSend(buf.getBuffer(), buf.len());
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
|
||||
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
// ZDO message payload (see Zigbee spec 2.4.3.3.4)
|
||||
uint8_t buf[] = { index }; // index = 0
|
||||
|
||||
EZ_SendZDO(shortaddr, zdo_cmd, buf, sizeof(buf), false);
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
}
|
||||
|
||||
// This callback is registered to send ZbMap(s) to each device one at a time
|
||||
void Z_Map(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) {
|
||||
if (BAD_SHORTADDR != shortaddr) {
|
||||
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "sending `ZnMap 0x%04X`"), shortaddr);
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
Z_Send_State_or_Map(shortaddr, value, ZDO_MGMT_LQI_REQ);
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
Z_Send_State_or_Map(shortaddr, value, ZDO_Mgmt_Lqi_req);
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
} else {
|
||||
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZbMap done"));
|
||||
}
|
||||
}
|
||||
/*********************************************************************************************\
|
||||
* Parsers for incoming EZSP messages
|
||||
\*********************************************************************************************/
|
||||
@ -948,80 +988,7 @@ int32_t Z_UnbindRsp(int32_t res, const class SBuffer &buf) {
|
||||
// Handle MgMt Bind Rsp incoming message
|
||||
//
|
||||
int32_t Z_MgmtBindRsp(int32_t res, const class SBuffer &buf) {
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
uint16_t shortaddr = buf.get16(2);
|
||||
uint8_t status = buf.get8(4);
|
||||
uint8_t bind_total = buf.get8(5);
|
||||
uint8_t bind_start = buf.get8(6);
|
||||
uint8_t bind_len = buf.get8(7);
|
||||
const size_t prefix_len = 8;
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
uint16_t shortaddr = buf.get16(buf.len()-2);
|
||||
uint8_t status = buf.get8(0);
|
||||
uint8_t bind_total = buf.get8(1);
|
||||
uint8_t bind_start = buf.get8(2);
|
||||
uint8_t bind_len = buf.get8(3);
|
||||
const size_t prefix_len = 4;
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
|
||||
// device is reachable
|
||||
zigbee_devices.deviceWasReached(shortaddr);
|
||||
|
||||
const char * friendlyName = zigbee_devices.getFriendlyName(shortaddr);
|
||||
|
||||
Response_P(PSTR("{\"" D_JSON_ZIGBEE_BIND_STATE "\":{\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\""), shortaddr);
|
||||
if (friendlyName) {
|
||||
ResponseAppend_P(PSTR(",\"" D_JSON_ZIGBEE_NAME "\":\"%s\""), friendlyName);
|
||||
}
|
||||
ResponseAppend_P(PSTR(",\"" D_JSON_ZIGBEE_STATUS "\":%d"
|
||||
",\"" D_JSON_ZIGBEE_STATUS_MSG "\":\"%s\""
|
||||
",\"BindingsTotal\":%d"
|
||||
",\"BindingsStart\":%d"
|
||||
",\"Bindings\":["
|
||||
), status, getZigbeeStatusMessage(status).c_str(), bind_total, bind_start + 1);
|
||||
|
||||
uint32_t idx = prefix_len;
|
||||
for (uint32_t i = 0; i < bind_len; i++) {
|
||||
if (idx + 14 > buf.len()) { break; } // overflow, frame size is between 14 and 21
|
||||
|
||||
//uint64_t srcaddr = buf.get16(idx); // unused
|
||||
uint8_t srcep = buf.get8(idx + 8);
|
||||
uint16_t cluster = buf.get16(idx + 9);
|
||||
uint8_t addrmode = buf.get8(idx + 11);
|
||||
uint16_t group = 0x0000;
|
||||
uint64_t dstaddr = 0;
|
||||
uint8_t dstep = 0x00;
|
||||
if (Z_Addr_Group == addrmode) { // Group address mode
|
||||
group = buf.get16(idx + 12);
|
||||
idx += 14;
|
||||
} else if (Z_Addr_IEEEAddress == addrmode) { // IEEE address mode
|
||||
dstaddr = buf.get64(idx + 12);
|
||||
dstep = buf.get8(idx + 20);
|
||||
idx += 21;
|
||||
} else {
|
||||
//AddLog_P(LOG_LEVEL_INFO, PSTR("ZNP_MgmtBindRsp unknwon address mode %d"), addrmode);
|
||||
break; // abort for any other value since we don't know the length of the field
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
ResponseAppend_P(PSTR(","));
|
||||
}
|
||||
ResponseAppend_P(PSTR("{\"Cluster\":\"0x%04X\",\"Endpoint\":%d,"), cluster, srcep);
|
||||
if (Z_Addr_Group == addrmode) { // Group address mode
|
||||
ResponseAppend_P(PSTR("\"ToGroup\":%d}"), group);
|
||||
} else if (Z_Addr_IEEEAddress == addrmode) { // IEEE address mode
|
||||
char hex[20];
|
||||
Uint64toHex(dstaddr, hex, 64);
|
||||
ResponseAppend_P(PSTR("\"ToDevice\":\"0x%s\",\"ToEndpoint\":%d}"), hex, dstep);
|
||||
}
|
||||
}
|
||||
|
||||
ResponseAppend_P(PSTR("]}}"));
|
||||
|
||||
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEE_BIND_STATE));
|
||||
|
||||
return -1;
|
||||
return Z_Mgmt_Lqi_Bind_Rsp(res, buf, false);
|
||||
}
|
||||
|
||||
// Return false, true or null (if unknown)
|
||||
@ -1056,23 +1023,25 @@ const char * Z_DeviceType(uint32_t value) {
|
||||
}
|
||||
|
||||
//
|
||||
// Handle MgMt Bind Rsp incoming message
|
||||
// Combined code for MgmtLqiRsp and MgmtBindRsp
|
||||
//
|
||||
int32_t Z_MgmtLqiRsp(int32_t res, const class SBuffer &buf) {
|
||||
// If the response has a follow-up, send more requests automatically
|
||||
//
|
||||
int32_t Z_Mgmt_Lqi_Bind_Rsp(int32_t res, const class SBuffer &buf, boolean lqi) {
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
uint16_t shortaddr = buf.get16(2);
|
||||
uint8_t status = buf.get8(4);
|
||||
uint8_t lqi_total = buf.get8(5);
|
||||
uint8_t lqi_start = buf.get8(6);
|
||||
uint8_t lqi_len = buf.get8(7);
|
||||
uint8_t total = buf.get8(5);
|
||||
uint8_t start = buf.get8(6);
|
||||
uint8_t len = buf.get8(7);
|
||||
const size_t prefix_len = 8;
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
uint16_t shortaddr = buf.get16(buf.len()-2);
|
||||
uint8_t status = buf.get8(0);
|
||||
uint8_t lqi_total = buf.get8(1);
|
||||
uint8_t lqi_start = buf.get8(2);
|
||||
uint8_t lqi_len = buf.get8(3);
|
||||
uint8_t total = buf.get8(1);
|
||||
uint8_t start = buf.get8(2);
|
||||
uint8_t len = buf.get8(3);
|
||||
const size_t prefix_len = 4;
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
|
||||
@ -1081,62 +1050,123 @@ int32_t Z_MgmtLqiRsp(int32_t res, const class SBuffer &buf) {
|
||||
|
||||
const char * friendlyName = zigbee_devices.getFriendlyName(shortaddr);
|
||||
|
||||
Response_P(PSTR("{\"" D_JSON_ZIGBEE_MAP "\":{\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\""), shortaddr);
|
||||
Response_P(PSTR("{\"%s\":{\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\""),
|
||||
lqi ? PSTR(D_JSON_ZIGBEE_MAP) : PSTR(D_JSON_ZIGBEE_BIND_STATE), shortaddr);
|
||||
if (friendlyName) {
|
||||
ResponseAppend_P(PSTR(",\"" D_JSON_ZIGBEE_NAME "\":\"%s\""), friendlyName);
|
||||
}
|
||||
ResponseAppend_P(PSTR(",\"" D_JSON_ZIGBEE_STATUS "\":%d"
|
||||
",\"" D_JSON_ZIGBEE_STATUS_MSG "\":\"%s\""
|
||||
",\"MapTotal\":%d"
|
||||
",\"MapStart\":%d"
|
||||
",\"Map\":["
|
||||
), status, getZigbeeStatusMessage(status).c_str(), lqi_total, lqi_start + 1);
|
||||
",\"Total\":%d"
|
||||
",\"Start\":%d"
|
||||
",\"%s\":["
|
||||
), status, getZigbeeStatusMessage(status).c_str(), total, start + 1,
|
||||
lqi ? PSTR("Map") : PSTR("Bindings"));
|
||||
|
||||
uint32_t idx = prefix_len;
|
||||
for (uint32_t i = 0; i < lqi_len; i++) {
|
||||
if (idx + 22 > buf.len()) { break; } // size 22 for EZSP
|
||||
if (lqi) {
|
||||
uint32_t idx = prefix_len;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
if (idx + 22 > buf.len()) { break; } // size 22 for EZSP
|
||||
|
||||
//uint64_t extpanid = buf.get16(idx); // unused
|
||||
// uint64_t m_longaddr = buf.get64(idx + 8);
|
||||
uint16_t m_shortaddr = buf.get16(idx + 16);
|
||||
uint8_t m_dev_type = buf.get8(idx + 18);
|
||||
uint8_t m_permitjoin = buf.get8(idx + 19);
|
||||
uint8_t m_depth = buf.get8(idx + 20);
|
||||
uint8_t m_lqi = buf.get8(idx + 21);
|
||||
idx += 22;
|
||||
//uint64_t extpanid = buf.get16(idx); // unused
|
||||
// uint64_t m_longaddr = buf.get64(idx + 8);
|
||||
uint16_t m_shortaddr = buf.get16(idx + 16);
|
||||
uint8_t m_dev_type = buf.get8(idx + 18);
|
||||
uint8_t m_permitjoin = buf.get8(idx + 19);
|
||||
uint8_t m_depth = buf.get8(idx + 20);
|
||||
uint8_t m_lqi = buf.get8(idx + 21);
|
||||
idx += 22;
|
||||
|
||||
if (i > 0) {
|
||||
ResponseAppend_P(PSTR(","));
|
||||
if (i > 0) {
|
||||
ResponseAppend_P(PSTR(","));
|
||||
}
|
||||
ResponseAppend_P(PSTR("{\"Device\":\"0x%04X\","), m_shortaddr);
|
||||
|
||||
const char * friendlyName = zigbee_devices.getFriendlyName(m_shortaddr);
|
||||
if (friendlyName) {
|
||||
ResponseAppend_P(PSTR("\"Name\":\"%s\","), friendlyName);
|
||||
}
|
||||
ResponseAppend_P(PSTR("\"DeviceType\":\"%s\","
|
||||
"\"RxOnWhenIdle\":%s,"
|
||||
"\"Relationship\":\"%s\","
|
||||
"\"PermitJoin\":%s,"
|
||||
"\"Depth\":%d,"
|
||||
"\"LinkQuality\":%d"
|
||||
"}"
|
||||
),
|
||||
Z_DeviceType(m_dev_type & 0x03),
|
||||
TrueFalseNull((m_dev_type & 0x0C) >> 2),
|
||||
Z_DeviceRelationship((m_dev_type & 0x70) >> 4),
|
||||
TrueFalseNull(m_permitjoin & 0x02),
|
||||
m_depth,
|
||||
m_lqi);
|
||||
}
|
||||
ResponseAppend_P(PSTR("{\"Device\":\"0x%04X\","), m_shortaddr);
|
||||
|
||||
const char * friendlyName = zigbee_devices.getFriendlyName(m_shortaddr);
|
||||
if (friendlyName) {
|
||||
ResponseAppend_P(PSTR("\"Name\":\"%s\","), friendlyName);
|
||||
ResponseAppend_P(PSTR("]}}"));
|
||||
} else { // Bind
|
||||
|
||||
uint32_t idx = prefix_len;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
if (idx + 14 > buf.len()) { break; } // overflow, frame size is between 14 and 21
|
||||
|
||||
//uint64_t srcaddr = buf.get16(idx); // unused
|
||||
uint8_t srcep = buf.get8(idx + 8);
|
||||
uint16_t cluster = buf.get16(idx + 9);
|
||||
uint8_t addrmode = buf.get8(idx + 11);
|
||||
uint16_t group = 0x0000;
|
||||
uint64_t dstaddr = 0;
|
||||
uint8_t dstep = 0x00;
|
||||
if (Z_Addr_Group == addrmode) { // Group address mode
|
||||
group = buf.get16(idx + 12);
|
||||
idx += 14;
|
||||
} else if (Z_Addr_IEEEAddress == addrmode) { // IEEE address mode
|
||||
dstaddr = buf.get64(idx + 12);
|
||||
dstep = buf.get8(idx + 20);
|
||||
idx += 21;
|
||||
} else {
|
||||
//AddLog_P(LOG_LEVEL_INFO, PSTR("ZNP_MgmtBindRsp unknwon address mode %d"), addrmode);
|
||||
break; // abort for any other value since we don't know the length of the field
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
ResponseAppend_P(PSTR(","));
|
||||
}
|
||||
ResponseAppend_P(PSTR("{\"Cluster\":\"0x%04X\",\"Endpoint\":%d,"), cluster, srcep);
|
||||
if (Z_Addr_Group == addrmode) { // Group address mode
|
||||
ResponseAppend_P(PSTR("\"ToGroup\":%d}"), group);
|
||||
} else if (Z_Addr_IEEEAddress == addrmode) { // IEEE address mode
|
||||
char hex[20];
|
||||
Uint64toHex(dstaddr, hex, 64);
|
||||
ResponseAppend_P(PSTR("\"ToDevice\":\"0x%s\",\"ToEndpoint\":%d}"), hex, dstep);
|
||||
}
|
||||
}
|
||||
ResponseAppend_P(PSTR("\"DeviceType\":\"%s\","
|
||||
"\"RxOnWhenIdle\":%s,"
|
||||
"\"Relationship\":\"%s\","
|
||||
"\"PermitJoin\":%s,"
|
||||
"\"Depth\":%d,"
|
||||
"\"LinkQuality\":%d"
|
||||
"}"
|
||||
),
|
||||
Z_DeviceType(m_dev_type & 0x03),
|
||||
TrueFalseNull((m_dev_type & 0x0C) >> 2),
|
||||
Z_DeviceRelationship((m_dev_type & 0x70) >> 4),
|
||||
TrueFalseNull(m_permitjoin & 0x02),
|
||||
m_depth,
|
||||
m_lqi);
|
||||
|
||||
ResponseAppend_P(PSTR("]}}"));
|
||||
}
|
||||
|
||||
ResponseAppend_P(PSTR("]}}"));
|
||||
|
||||
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEE_MAP));
|
||||
|
||||
// Check if there are more values waiting, if so re-send a new request to get other values
|
||||
if (start + len < total) {
|
||||
// there are more values to read
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
Z_Send_State_or_Map(shortaddr, start + len, lqi ? ZDO_MGMT_LQI_REQ : ZDO_MGMT_BIND_REQ);
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
Z_Send_State_or_Map(shortaddr, start + len, lqi ? ZDO_Mgmt_Lqi_req : ZDO_Mgmt_Bind_req);
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle MgMt Bind Rsp incoming message
|
||||
//
|
||||
int32_t Z_MgmtLqiRsp(int32_t res, const class SBuffer &buf) {
|
||||
return Z_Mgmt_Lqi_Bind_Rsp(res, buf, true);
|
||||
}
|
||||
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
//
|
||||
// Handle Parent Annonce Rsp incoming message
|
||||
@ -1572,7 +1602,7 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) {
|
||||
* Send ZDO Message
|
||||
\*********************************************************************************************/
|
||||
|
||||
void EZ_SendZDO(uint16_t shortaddr, uint16_t cmd, const unsigned char *payload, size_t payload_len) {
|
||||
void EZ_SendZDO(uint16_t shortaddr, uint16_t cmd, const unsigned char *payload, size_t payload_len, bool retry) {
|
||||
SBuffer buf(payload_len + 22);
|
||||
uint8_t seq = zigbee_devices.getNextSeqNumber(0x0000);
|
||||
|
||||
@ -1587,7 +1617,11 @@ void EZ_SendZDO(uint16_t shortaddr, uint16_t cmd, const unsigned char *payload,
|
||||
buf.add16(cmd); // ZDO cmd in cluster
|
||||
buf.add8(0); // srcEp
|
||||
buf.add8(0); // dstEp
|
||||
buf.add16(EMBER_APS_OPTION_ENABLE_ROUTE_DISCOVERY | EMBER_APS_OPTION_RETRY); // APS frame
|
||||
if (retry) {
|
||||
buf.add16(EMBER_APS_OPTION_ENABLE_ROUTE_DISCOVERY | EMBER_APS_OPTION_RETRY); // APS frame
|
||||
} else {
|
||||
buf.add16(EMBER_APS_OPTION_ENABLE_ROUTE_DISCOVERY); // APS frame
|
||||
}
|
||||
buf.add16(0x0000); // groupId
|
||||
buf.add8(seq);
|
||||
// end of ApsFrame
|
||||
|
@ -986,24 +986,7 @@ void CmndZbBindState_or_Map(uint16_t zdo_cmd) {
|
||||
if (BAD_SHORTADDR == shortaddr) { ResponseCmndChar_P(PSTR("Unknown device")); return; }
|
||||
uint8_t index = XdrvMailbox.index - 1; // change default 1 to 0
|
||||
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
SBuffer buf(10);
|
||||
buf.add8(Z_SREQ | Z_ZDO); // 25
|
||||
buf.add8(zdo_cmd); // 33
|
||||
buf.add16(shortaddr); // shortaddr
|
||||
buf.add8(index); // StartIndex = 0
|
||||
|
||||
ZigbeeZNPSend(buf.getBuffer(), buf.len());
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
|
||||
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
// ZDO message payload (see Zigbee spec 2.4.3.3.4)
|
||||
uint8_t buf[] = { index }; // index = 0
|
||||
|
||||
EZ_SendZDO(shortaddr, zdo_cmd, buf, sizeof(buf));
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
|
||||
Z_Send_State_or_Map(shortaddr, index, zdo_cmd);
|
||||
ResponseCmndDone();
|
||||
}
|
||||
|
||||
@ -1025,12 +1008,28 @@ void CmndZbBindState(void) {
|
||||
// `ZbMap<x>` as index if it does not fit. If default, `1` starts at the beginning
|
||||
//
|
||||
void CmndZbMap(void) {
|
||||
if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
|
||||
RemoveSpace(XdrvMailbox.data);
|
||||
|
||||
if (strlen(XdrvMailbox.data) == 0) {
|
||||
// defer sending ZbMap to each device
|
||||
const static uint32_t DELAY_ZBMAP = 2000; // wait for 1s between commands
|
||||
uint32_t wait_ms = DELAY_ZBMAP;
|
||||
zigbee_devices.setTimer(0x0000, 0, 0 /*wait_ms*/, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map);
|
||||
for (const auto & device : zigbee_devices.getDevices()) {
|
||||
zigbee_devices.setTimer(device.shortaddr, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map);
|
||||
wait_ms += DELAY_ZBMAP;
|
||||
}
|
||||
zigbee_devices.setTimer(BAD_SHORTADDR, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map);
|
||||
ResponseCmndDone();
|
||||
} else {
|
||||
#ifdef USE_ZIGBEE_ZNP
|
||||
CmndZbBindState_or_Map(ZDO_MGMT_LQI_REQ);
|
||||
CmndZbBindState_or_Map(ZDO_MGMT_LQI_REQ);
|
||||
#endif // USE_ZIGBEE_ZNP
|
||||
#ifdef USE_ZIGBEE_EZSP
|
||||
CmndZbBindState_or_Map(ZDO_Mgmt_Lqi_req);
|
||||
CmndZbBindState_or_Map(ZDO_Mgmt_Lqi_req);
|
||||
#endif // USE_ZIGBEE_EZSP
|
||||
}
|
||||
}
|
||||
|
||||
// Probe a specific device to get its endpoints and supported clusters
|
||||
|
Loading…
x
Reference in New Issue
Block a user