mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-29 13:46:37 +00:00
HA discovery via MQTT for BLE MI sensor devices
This commit is contained in:
parent
688c0b1a9f
commit
f69f000354
@ -320,7 +320,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui
|
|||||||
void BLEPostAdvert(ble_advertisment_t *Advertisment);
|
void BLEPostAdvert(ble_advertisment_t *Advertisment);
|
||||||
static void BLEPostMQTTSeenDevices(int type);
|
static void BLEPostMQTTSeenDevices(int type);
|
||||||
|
|
||||||
static void BLEShow(bool json);
|
static void BLEShowStats();
|
||||||
static void BLEPostMQTT(bool json);
|
static void BLEPostMQTT(bool json);
|
||||||
static void BLEStartOperationTask();
|
static void BLEStartOperationTask();
|
||||||
|
|
||||||
@ -390,7 +390,7 @@ uint8_t BLEAliasListTrigger = 0;
|
|||||||
// triggers send for ALL operations known about
|
// triggers send for ALL operations known about
|
||||||
uint8_t BLEPostMQTTTrigger = 0;
|
uint8_t BLEPostMQTTTrigger = 0;
|
||||||
int BLEMaxAge = 60*10; // 10 minutes
|
int BLEMaxAge = 60*10; // 10 minutes
|
||||||
int BLEAddressFilter = 3;
|
int BLEAddressFilter = 0;
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
@ -2181,6 +2181,7 @@ static void BLEEverySecond(bool restart){
|
|||||||
|
|
||||||
if (BLEPublishDevices){
|
if (BLEPublishDevices){
|
||||||
BLEPostMQTTSeenDevices(BLEPublishDevices);
|
BLEPostMQTTSeenDevices(BLEPublishDevices);
|
||||||
|
BLEShowStats();
|
||||||
BLEPublishDevices = 0;
|
BLEPublishDevices = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3209,26 +3210,15 @@ static void mainThreadOpCallbacks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void BLEShowStats(){
|
||||||
static void BLEShow(bool json)
|
uint32_t totalCount = BLEAdvertisment.totalCount;
|
||||||
{
|
uint32_t deviceCount = seenDevices.size();
|
||||||
if (json){
|
ResponseTime_P(PSTR(""));
|
||||||
#ifdef BLE_ESP32_DEBUG
|
ResponseAppend_P(PSTR(",\"BLE\":{\"scans\":%u,\"adverts\":%u,\"devices\":%u,\"resets\":%u}}"), BLEScanCount, totalCount, deviceCount, BLEResets);
|
||||||
if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: show json %d"),json);
|
MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), 0);
|
||||||
#endif
|
|
||||||
uint32_t totalCount = BLEAdvertisment.totalCount;
|
|
||||||
uint32_t deviceCount = seenDevices.size();
|
|
||||||
|
|
||||||
ResponseAppend_P(PSTR(",\"BLE\":{\"scans\":%u,\"adverts\":%u,\"devices\":%u,\"resets\":%u}"), BLEScanCount, totalCount, deviceCount, BLEResets);
|
|
||||||
}
|
|
||||||
#ifdef USE_WEBSERVER
|
|
||||||
else {
|
|
||||||
//WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size());
|
|
||||||
}
|
|
||||||
#endif // USE_WEBSERVER
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*void BLEAliasMqttList(){
|
/*void BLEAliasMqttList(){
|
||||||
ResponseTime_P(PSTR(",\"BLEAlias\":["));
|
ResponseTime_P(PSTR(",\"BLEAlias\":["));
|
||||||
for (int i = 0; i < aliases.size(); i++){
|
for (int i = 0; i < aliases.size(); i++){
|
||||||
@ -3495,7 +3485,6 @@ bool Xdrv52(uint8_t function)
|
|||||||
result = DecodeCommand(BLE_ESP32::kBLE_Commands, BLE_ESP32::BLE_Commands);
|
result = DecodeCommand(BLE_ESP32::kBLE_Commands, BLE_ESP32::BLE_Commands);
|
||||||
break;
|
break;
|
||||||
case FUNC_JSON_APPEND:
|
case FUNC_JSON_APPEND:
|
||||||
BLE_ESP32::BLEShow(1);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// next second, we will publish to our MQTT topic.
|
// next second, we will publish to our MQTT topic.
|
||||||
@ -3510,10 +3499,6 @@ bool Xdrv52(uint8_t function)
|
|||||||
case FUNC_WEB_ADD_HANDLER:
|
case FUNC_WEB_ADD_HANDLER:
|
||||||
WebServer_on(PSTR("/" WEB_HANDLE_BLE), BLE_ESP32::HandleBleConfiguration);
|
WebServer_on(PSTR("/" WEB_HANDLE_BLE), BLE_ESP32::HandleBleConfiguration);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FUNC_WEB_SENSOR:
|
|
||||||
BLE_ESP32::BLEShow(0);
|
|
||||||
break;
|
|
||||||
#endif // USE_WEBSERVER
|
#endif // USE_WEBSERVER
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -79,6 +79,7 @@ void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pD
|
|||||||
struct {
|
struct {
|
||||||
uint16_t perPage = 4;
|
uint16_t perPage = 4;
|
||||||
uint8_t mqttCurrentSlot = 0;
|
uint8_t mqttCurrentSlot = 0;
|
||||||
|
uint8_t mqttCurrentSingleSlot = 0;
|
||||||
uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start
|
uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start
|
||||||
int secondsCounter = 0; // counts up in MI32EverySecond to period
|
int secondsCounter = 0; // counts up in MI32EverySecond to period
|
||||||
int secondsCounter2 = 0; // counts up in MI32EverySecond to period
|
int secondsCounter2 = 0; // counts up in MI32EverySecond to period
|
||||||
@ -255,6 +256,8 @@ struct mi_sensor_t{
|
|||||||
uint8_t type; //MI_Flora = 1; MI_MI-HT_V1=2; MI_LYWSD02=3; MI_LYWSD03=4; MI_CGG1=5; MI_CGD1=6
|
uint8_t type; //MI_Flora = 1; MI_MI-HT_V1=2; MI_LYWSD02=3; MI_LYWSD03=4; MI_CGG1=5; MI_CGD1=6
|
||||||
uint8_t needkey; // tells http to display needkey message with link
|
uint8_t needkey; // tells http to display needkey message with link
|
||||||
uint8_t lastCnt; //device generated counter of the packet
|
uint8_t lastCnt; //device generated counter of the packet
|
||||||
|
uint8_t nextDiscoveryData; // used to lkimit discovery to one MQTT per sec
|
||||||
|
|
||||||
uint8_t shallSendMQTT;
|
uint8_t shallSendMQTT;
|
||||||
uint8_t MAC[6];
|
uint8_t MAC[6];
|
||||||
union {
|
union {
|
||||||
@ -1880,6 +1883,9 @@ void MI32EverySecond(bool restart){
|
|||||||
|
|
||||||
MI32ShowSomeSensors();
|
MI32ShowSomeSensors();
|
||||||
|
|
||||||
|
MI32DiscoveryOneMISensor();
|
||||||
|
MI32ShowOneMISensor();
|
||||||
|
|
||||||
// read a battery if
|
// read a battery if
|
||||||
// MI32.batteryreader.slot < filled and !MI32.batteryreader.active
|
// MI32.batteryreader.slot < filled and !MI32.batteryreader.active
|
||||||
readOneBat();
|
readOneBat();
|
||||||
@ -1906,10 +1912,12 @@ void MI32EverySecond(bool restart){
|
|||||||
AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Kick off tele sending"));
|
AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Kick off tele sending"));
|
||||||
MI32.mqttCurrentSlot = 0;
|
MI32.mqttCurrentSlot = 0;
|
||||||
MI32.secondsCounter2 = 0;
|
MI32.secondsCounter2 = 0;
|
||||||
|
MI32.mqttCurrentSingleSlot = 0;
|
||||||
} else {
|
} else {
|
||||||
AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot);
|
AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot);
|
||||||
MI32.mqttCurrentSlot = 0;
|
MI32.mqttCurrentSlot = 0;
|
||||||
MI32.secondsCounter2 = 0;
|
MI32.secondsCounter2 = 0;
|
||||||
|
MI32.mqttCurrentSingleSlot = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MI32.secondsCounter2++;
|
MI32.secondsCounter2++;
|
||||||
@ -2302,15 +2310,18 @@ void MI32TimeoutSensors(){
|
|||||||
|
|
||||||
|
|
||||||
// this assumes that we're adding to a ResponseTime_P
|
// this assumes that we're adding to a ResponseTime_P
|
||||||
void MI32GetOneSensorJson(int slot){
|
void MI32GetOneSensorJson(int slot, int hidename){
|
||||||
mi_sensor_t *p;
|
mi_sensor_t *p;
|
||||||
p = &MIBLEsensors[slot];
|
p = &MIBLEsensors[slot];
|
||||||
|
|
||||||
ResponseAppend_P(PSTR(",\"%s-%02x%02x%02x\":{"),
|
// remove hyphen - make it difficult to configure HASS
|
||||||
kMI32DeviceType[p->type-1],
|
if (!hidename) {
|
||||||
p->MAC[3], p->MAC[4], p->MAC[5]);
|
ResponseAppend_P(PSTR("\"%s%02x%02x%02x\":{"),
|
||||||
|
kMI32DeviceType[p->type-1],
|
||||||
|
p->MAC[3], p->MAC[4], p->MAC[5]);
|
||||||
|
}
|
||||||
|
|
||||||
ResponseAppend_P(PSTR("\"MAC\":\"%02x%02x%02x%02x%02x%02x\""),
|
ResponseAppend_P(PSTR("\"mac\":\"%02x%02x%02x%02x%02x%02x\""),
|
||||||
p->MAC[0], p->MAC[1], p->MAC[2],
|
p->MAC[0], p->MAC[1], p->MAC[2],
|
||||||
p->MAC[3], p->MAC[4], p->MAC[5]);
|
p->MAC[3], p->MAC[4], p->MAC[5]);
|
||||||
|
|
||||||
@ -2449,7 +2460,9 @@ void MI32GetOneSensorJson(int slot){
|
|||||||
}
|
}
|
||||||
if (MI32.option.showRSSI) ResponseAppend_P(PSTR(",\"RSSI\":%d"), p->RSSI);
|
if (MI32.option.showRSSI) ResponseAppend_P(PSTR(",\"RSSI\":%d"), p->RSSI);
|
||||||
|
|
||||||
ResponseAppend_P(PSTR("}"));
|
if (!hidename) {
|
||||||
|
ResponseAppend_P(PSTR("}"));
|
||||||
|
}
|
||||||
p->eventType.raw = 0;
|
p->eventType.raw = 0;
|
||||||
p->shallSendMQTT = 0;
|
p->shallSendMQTT = 0;
|
||||||
|
|
||||||
@ -2486,7 +2499,8 @@ void MI32ShowSomeSensors(){
|
|||||||
ResponseTime_P(PSTR(""));
|
ResponseTime_P(PSTR(""));
|
||||||
int cnt = 0;
|
int cnt = 0;
|
||||||
for (; (MI32.mqttCurrentSlot < numsensors) && (cnt < 4); MI32.mqttCurrentSlot++, cnt++) {
|
for (; (MI32.mqttCurrentSlot < numsensors) && (cnt < 4); MI32.mqttCurrentSlot++, cnt++) {
|
||||||
MI32GetOneSensorJson(MI32.mqttCurrentSlot);
|
ResponseAppend_P(PSTR(","));
|
||||||
|
MI32GetOneSensorJson(MI32.mqttCurrentSlot, 0);
|
||||||
int mlen = strlen(TasmotaGlobal.mqtt_data);
|
int mlen = strlen(TasmotaGlobal.mqtt_data);
|
||||||
|
|
||||||
// if we ran out of room, leave here.
|
// if we ran out of room, leave here.
|
||||||
@ -2511,6 +2525,183 @@ void MI32ShowSomeSensors(){
|
|||||||
#endif //USE_HOME_ASSISTANT
|
#endif //USE_HOME_ASSISTANT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// starts a completely fresh MQTT message.
|
||||||
|
// sends ONE sensor on a dedicated topic NOT related to this TAS
|
||||||
|
// triggered by setting MI32.mqttCurrentSingleSlot = 0
|
||||||
|
void MI32ShowOneMISensor(){
|
||||||
|
// don't detect half-added ones here
|
||||||
|
int numsensors = MIBLEsensors.size();
|
||||||
|
if (MI32.mqttCurrentSingleSlot >= numsensors){
|
||||||
|
// if we got to the end of the sensors, then don't send more
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_HOME_ASSISTANT
|
||||||
|
if(Settings.flag.hass_discovery){
|
||||||
|
|
||||||
|
ResponseTime_P(PSTR(","));
|
||||||
|
MI32GetOneSensorJson(MI32.mqttCurrentSingleSlot, 1);
|
||||||
|
mi_sensor_t *p;
|
||||||
|
p = &MIBLEsensors[MI32.mqttCurrentSingleSlot];
|
||||||
|
|
||||||
|
ResponseAppend_P(PSTR("}"));
|
||||||
|
|
||||||
|
char idstr[32];
|
||||||
|
const char *alias = BLE_ESP32::getAlias(p->MAC);
|
||||||
|
const char *id = idstr;
|
||||||
|
if (alias && *alias){
|
||||||
|
id = alias;
|
||||||
|
} else {
|
||||||
|
sprintf(idstr, PSTR("%s%02x%02x%02x"),
|
||||||
|
kMI32DeviceType[p->type-1],
|
||||||
|
p->MAC[3], p->MAC[4], p->MAC[5]);
|
||||||
|
}
|
||||||
|
char SensorTopic[60];
|
||||||
|
sprintf(SensorTopic, "tele/tasmota_ble/%s",
|
||||||
|
id);
|
||||||
|
|
||||||
|
MqttPublish(SensorTopic);
|
||||||
|
//AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data);
|
||||||
|
}
|
||||||
|
#endif //USE_HOME_ASSISTANT
|
||||||
|
MI32.mqttCurrentSingleSlot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// starts a completely fresh MQTT message.
|
||||||
|
// sends ONE sensor's worth of HA discovery msg
|
||||||
|
#define MI_HA_DISCOVERY_TEMPLATE PSTR("{\"availability\":[],\"device\": \
|
||||||
|
{\"identifiers\":[\"BLE%s\"],\
|
||||||
|
\"name\":\"%s\",\
|
||||||
|
\"manufacturer\":\"tas\",\
|
||||||
|
\"model\":\"%s\",\
|
||||||
|
\"via_device\":\"%s\"\
|
||||||
|
}, \
|
||||||
|
\"dev_cla\":\"%s\",\
|
||||||
|
\"expire_after\":600,\
|
||||||
|
\"json_attr_t\":\"%s\",\
|
||||||
|
\"name\":\"%s_%s\",\
|
||||||
|
\"state_topic\":\"%s\",\
|
||||||
|
\"uniq_id\":\"%s_%s\",\
|
||||||
|
\"unit_of_meas\":\"%s\",\
|
||||||
|
\"val_tpl\":\"{{ value_json.%s }}\"}")
|
||||||
|
void MI32DiscoveryOneMISensor(){
|
||||||
|
// don't detect half-added ones here
|
||||||
|
int numsensors = MIBLEsensors.size();
|
||||||
|
if (MI32.mqttCurrentSingleSlot >= numsensors){
|
||||||
|
// if we got to the end of the sensors, then don't send more
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_HOME_ASSISTANT
|
||||||
|
if(Settings.flag.hass_discovery){
|
||||||
|
mi_sensor_t *p;
|
||||||
|
p = &MIBLEsensors[MI32.mqttCurrentSingleSlot];
|
||||||
|
|
||||||
|
|
||||||
|
// careful - a missing comma causes a crash!!!!
|
||||||
|
// because of the way we loop?
|
||||||
|
const char *classes[] = {
|
||||||
|
"temperature",
|
||||||
|
"Temperature",
|
||||||
|
"°C",
|
||||||
|
"humidity",
|
||||||
|
"Humidity",
|
||||||
|
"%",
|
||||||
|
"temperature",
|
||||||
|
"DewPoint",
|
||||||
|
"°C",
|
||||||
|
"battery",
|
||||||
|
"Battery",
|
||||||
|
"%",
|
||||||
|
"signal_strength",
|
||||||
|
"RSSI",
|
||||||
|
"dB"
|
||||||
|
};
|
||||||
|
|
||||||
|
int datacount = (sizeof(classes)/sizeof(*classes))/3;
|
||||||
|
|
||||||
|
if (p->nextDiscoveryData >= datacount){
|
||||||
|
p->nextDiscoveryData = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//int i = p->nextDiscoveryData*3;
|
||||||
|
for (int i = 0; i < datacount*3; i += 3){
|
||||||
|
if (!classes[i] || !classes[i+1] || !classes[i+2]){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char idstr[32];
|
||||||
|
const char *alias = BLE_ESP32::getAlias(p->MAC);
|
||||||
|
const char *id = idstr;
|
||||||
|
if (alias && *alias){
|
||||||
|
id = alias;
|
||||||
|
} else {
|
||||||
|
sprintf(idstr, PSTR("%s%02x%02x%02x"),
|
||||||
|
kMI32DeviceType[p->type-1],
|
||||||
|
p->MAC[3], p->MAC[4], p->MAC[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
char SensorTopic[60];
|
||||||
|
sprintf(SensorTopic, "tele/tasmota_ble/%s",
|
||||||
|
id);
|
||||||
|
|
||||||
|
|
||||||
|
ResponseClear();
|
||||||
|
|
||||||
|
/*
|
||||||
|
{"availability":[],"device":{"identifiers":["TasmotaBLEa4c1387fc1e1"],"manufacturer":"simon","model":"someBLEsensor","name":"TASBLEa4c1387fc1e1","sw_version":"0.0.0"},"dev_cla":"temperature","json_attr_t":"tele/tasmota_esp32/SENSOR","name":"TASLYWSD037fc1e1Temp","state_topic":"tele/tasmota_esp32/SENSOR","uniq_id":"Tasmotaa4c1387fc1e1temp","unit_of_meas":"°C","val_tpl":"{{ value_json.LYWSD037fc1e1.Temperature }}"}
|
||||||
|
{"availability":[],"device":{"identifiers":["TasmotaBLEa4c1387fc1e1"],
|
||||||
|
"name":"TASBLEa4c1387fc1e1"},"dev_cla":"temperature",
|
||||||
|
"json_attr_t":"tele/tasmota_esp32/SENSOR",
|
||||||
|
"name":"TASLYWSD037fc1e1Temp","state_topic": "tele/tasmota_esp32/SENSOR",
|
||||||
|
"uniq_id":"Tasmotaa4c1387fc1e1temp","unit_of_meas":"°C",
|
||||||
|
"val_tpl":"{{ value_json.LYWSD037fc1e1.Temperature }}"}
|
||||||
|
*/
|
||||||
|
|
||||||
|
ResponseAppend_P(MI_HA_DISCOVERY_TEMPLATE,
|
||||||
|
//"{\"identifiers\":[\"BLE%s\"],"
|
||||||
|
id,
|
||||||
|
//"\"name\":\"%s\"},"
|
||||||
|
id,
|
||||||
|
//\"model\":\"%s\",
|
||||||
|
kMI32DeviceType[p->type-1],
|
||||||
|
//\"via_device\":\"%s\"
|
||||||
|
NetworkHostname(),
|
||||||
|
//"\"dev_cla\":\"%s\","
|
||||||
|
classes[i],
|
||||||
|
//"\"json_attr_t\":\"%s\"," - the topic the sensor publishes on
|
||||||
|
SensorTopic,
|
||||||
|
//"\"name\":\"%s_%s\"," - the name of this DATA
|
||||||
|
id, classes[i+1],
|
||||||
|
//"\"state_topic\":\"%s\"," - the topic the sensor publishes on?
|
||||||
|
SensorTopic,
|
||||||
|
//"\"uniq_id\":\"%s_%s\"," - unique for this data,
|
||||||
|
id, classes[i+1],
|
||||||
|
//"\"unit_of_meas\":\"%s\"," - the measure of this type of data
|
||||||
|
classes[i+2],
|
||||||
|
//"\"val_tpl\":\"{{ value_json.%s }}") // e.g. Temperature
|
||||||
|
classes[i+1]
|
||||||
|
//
|
||||||
|
);
|
||||||
|
|
||||||
|
char DiscoveryTopic[80];
|
||||||
|
sprintf(DiscoveryTopic, "homeassistant/sensor/%s/%s/config",
|
||||||
|
id, classes[i+1]);
|
||||||
|
|
||||||
|
MqttPublish(DiscoveryTopic);
|
||||||
|
p->nextDiscoveryData++;
|
||||||
|
//vTaskDelay(100/ portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
} // end if hass discovery
|
||||||
|
//AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data);
|
||||||
|
#endif //USE_HOME_ASSISTANT
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////
|
///////////////////////////////////////////////
|
||||||
// starts a completely fresh MQTT message.
|
// starts a completely fresh MQTT message.
|
||||||
// sends up to 4 sensors pe5r msg
|
// sends up to 4 sensors pe5r msg
|
||||||
@ -2535,7 +2726,8 @@ void MI32ShowTriggeredSensors(){
|
|||||||
if(p->shallSendMQTT==0) continue;
|
if(p->shallSendMQTT==0) continue;
|
||||||
|
|
||||||
cnt++;
|
cnt++;
|
||||||
MI32GetOneSensorJson(sensor);
|
ResponseAppend_P(PSTR(","));
|
||||||
|
MI32GetOneSensorJson(sensor, 0);
|
||||||
int mlen = strlen(TasmotaGlobal.mqtt_data);
|
int mlen = strlen(TasmotaGlobal.mqtt_data);
|
||||||
|
|
||||||
// if we ran out of room, leave here.
|
// if we ran out of room, leave here.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user