mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 16:37:52 +01:00
Add Alpha3 pump component (#3787)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
a391815921
commit
f3cdcc008a
@ -21,6 +21,7 @@ esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||
esphome/components/alarm_control_panel/* @grahambrown11
|
||||
esphome/components/alpha3/* @jan-hofmeier
|
||||
esphome/components/am43/* @buxtronix
|
||||
esphome/components/am43/cover/* @buxtronix
|
||||
esphome/components/am43/sensor/* @buxtronix
|
||||
|
1
esphome/components/alpha3/__init__.py
Normal file
1
esphome/components/alpha3/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@jan-hofmeier"]
|
189
esphome/components/alpha3/alpha3.cpp
Normal file
189
esphome/components/alpha3/alpha3.cpp
Normal file
@ -0,0 +1,189 @@
|
||||
#include "alpha3.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <lwip/sockets.h> //gives ntohl
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace alpha3 {
|
||||
|
||||
static const char *const TAG = "alpha3";
|
||||
|
||||
void Alpha3::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ALPHA3");
|
||||
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
||||
LOG_SENSOR(" ", "Head", this->head_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
}
|
||||
|
||||
void Alpha3::setup() {}
|
||||
|
||||
void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
|
||||
int16_t value_offset, sensor::Sensor *sensor, float factor) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
// we need to handle cases where a value is split over two packets
|
||||
const int16_t value_length = 4; // 32bit float
|
||||
// offset inside current response packet
|
||||
auto rel_offset = value_offset - response_offset;
|
||||
if (rel_offset <= -value_length)
|
||||
return; // aready passed the value completly
|
||||
if (rel_offset >= length)
|
||||
return; // value not in this packet
|
||||
|
||||
auto start_offset = std::max(0, rel_offset);
|
||||
auto end_offset = std::min((int16_t) (rel_offset + value_length), length);
|
||||
auto copy_length = end_offset - start_offset;
|
||||
auto buffer_offset = std::max(-rel_offset, 0);
|
||||
std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length);
|
||||
|
||||
if (rel_offset + value_length <= length) {
|
||||
// we have the whole value
|
||||
void *buffer = this->buffer_; // to prevent warnings when casting the pointer
|
||||
*((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian
|
||||
float fvalue = *((float *) buffer);
|
||||
sensor->publish_state(fvalue * factor);
|
||||
}
|
||||
}
|
||||
|
||||
bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
|
||||
return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH);
|
||||
}
|
||||
|
||||
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
if (this->response_offset_ >= this->response_length_) {
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
|
||||
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
||||
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
|
||||
return;
|
||||
}
|
||||
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
|
||||
response[0], response[1], response[2], response[3], response[4]);
|
||||
return;
|
||||
}
|
||||
this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum
|
||||
this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH;
|
||||
std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH);
|
||||
}
|
||||
|
||||
auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor,
|
||||
float factor) {
|
||||
this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor);
|
||||
};
|
||||
|
||||
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
|
||||
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1],
|
||||
this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5],
|
||||
this->response_type_[6], this->response_type_[7]);
|
||||
}
|
||||
this->response_offset_ += length;
|
||||
}
|
||||
|
||||
void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
this->response_offset_ = 0;
|
||||
this->response_length_ = 0;
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0)
|
||||
return;
|
||||
auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->node_state = espbt::ClientState::IDLE;
|
||||
if (this->flow_sensor_ != nullptr)
|
||||
this->flow_sensor_->publish_state(NAN);
|
||||
if (this->head_sensor_ != nullptr)
|
||||
this->head_sensor_->publish_state(NAN);
|
||||
if (this->power_sensor_ != nullptr)
|
||||
this->power_sensor_->publish_state(NAN);
|
||||
if (this->current_sensor_ != nullptr)
|
||||
this->current_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->speed_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(NAN);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
chr->handle);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
|
||||
}
|
||||
this->geni_handle_ = chr->handle;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
this->update();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle == this->geni_handle_) {
|
||||
this->handle_geni_response_(param->notify.value, param->notify.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) {
|
||||
uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31};
|
||||
this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head));
|
||||
delay(25); // need to wait between requests
|
||||
}
|
||||
if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr ||
|
||||
this->voltage_sensor_ != nullptr) {
|
||||
uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205};
|
||||
this->send_request_(geni_request_power, sizeof(geni_request_power));
|
||||
delay(25); // need to wait between requests
|
||||
}
|
||||
}
|
||||
} // namespace alpha3
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
73
esphome/components/alpha3/alpha3.h
Normal file
73
esphome/components/alpha3/alpha3.h
Normal file
@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace alpha3 {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
|
||||
espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
|
||||
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
|
||||
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
|
||||
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
|
||||
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;
|
||||
|
||||
static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24};
|
||||
static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0;
|
||||
static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4;
|
||||
|
||||
static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37};
|
||||
static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0;
|
||||
static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4;
|
||||
static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8;
|
||||
static const int16_t GENI_RESPONSE_POWER_OFFSET = 12;
|
||||
static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure
|
||||
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
|
||||
|
||||
class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
|
||||
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
|
||||
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; }
|
||||
void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *flow_sensor_{nullptr};
|
||||
sensor::Sensor *head_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *speed_sensor_{nullptr};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
uint16_t geni_handle_;
|
||||
int16_t response_length_;
|
||||
int16_t response_offset_;
|
||||
uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH];
|
||||
uint8_t buffer_[4];
|
||||
void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
|
||||
int16_t value_offset, sensor::Sensor *sensor, float factor);
|
||||
void handle_geni_response_(const uint8_t *response, uint16_t length);
|
||||
void send_request_(uint8_t *request, size_t len);
|
||||
bool is_current_response_type_(const uint8_t *response_type);
|
||||
};
|
||||
} // namespace alpha3
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
85
esphome/components/alpha3/sensor.py
Normal file
85
esphome/components/alpha3/sensor.py
Normal file
@ -0,0 +1,85 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_CURRENT,
|
||||
CONF_FLOW,
|
||||
CONF_HEAD,
|
||||
CONF_POWER,
|
||||
CONF_SPEED,
|
||||
CONF_VOLTAGE,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_METER,
|
||||
UNIT_CUBIC_METER_PER_HOUR,
|
||||
UNIT_REVOLUTIONS_PER_MINUTE,
|
||||
)
|
||||
|
||||
alpha3_ns = cg.esphome_ns.namespace("alpha3")
|
||||
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Alpha3),
|
||||
cv.Optional(CONF_FLOW): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_HEAD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("15s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
if CONF_FLOW in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FLOW])
|
||||
cg.add(var.set_flow_sensor(sens))
|
||||
|
||||
if CONF_HEAD in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HEAD])
|
||||
cg.add(var.set_head_sensor(sens))
|
||||
|
||||
if CONF_POWER in config:
|
||||
sens = await sensor.new_sensor(config[CONF_POWER])
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
|
||||
if CONF_CURRENT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_CURRENT])
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
|
||||
if CONF_SPEED in config:
|
||||
sens = await sensor.new_sensor(config[CONF_SPEED])
|
||||
cg.add(var.set_speed_sensor(sens))
|
||||
|
||||
if CONF_VOLTAGE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_VOLTAGE])
|
||||
cg.add(var.set_voltage_sensor(sens))
|
@ -262,6 +262,7 @@ CONF_FINGER_ID = "finger_id"
|
||||
CONF_FINGERPRINT_COUNT = "fingerprint_count"
|
||||
CONF_FLASH_LENGTH = "flash_length"
|
||||
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
|
||||
CONF_FLOW = "flow"
|
||||
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
||||
CONF_FOR = "for"
|
||||
CONF_FORCE_UPDATE = "force_update"
|
||||
@ -286,6 +287,7 @@ CONF_GPIO = "gpio"
|
||||
CONF_GREEN = "green"
|
||||
CONF_GROUP = "group"
|
||||
CONF_HARDWARE_UART = "hardware_uart"
|
||||
CONF_HEAD = "head"
|
||||
CONF_HEARTBEAT = "heartbeat"
|
||||
CONF_HEAT_ACTION = "heat_action"
|
||||
CONF_HEAT_DEADBAND = "heat_deadband"
|
||||
@ -891,6 +893,7 @@ UNIT_CENTIMETER = "cm"
|
||||
UNIT_COUNT_DECILITRE = "/dL"
|
||||
UNIT_COUNTS_PER_CUBIC_METER = "#/m³"
|
||||
UNIT_CUBIC_METER = "m³"
|
||||
UNIT_CUBIC_METER_PER_HOUR = "m³/h"
|
||||
UNIT_DECIBEL = "dB"
|
||||
UNIT_DECIBEL_MILLIWATT = "dBm"
|
||||
UNIT_DEGREE_PER_SECOND = "°/s"
|
||||
@ -928,6 +931,7 @@ UNIT_PERCENT = "%"
|
||||
UNIT_PH = "pH"
|
||||
UNIT_PULSES = "pulses"
|
||||
UNIT_PULSES_PER_MINUTE = "pulses/min"
|
||||
UNIT_REVOLUTIONS_PER_MINUTE = "RPM"
|
||||
UNIT_SECOND = "s"
|
||||
UNIT_STEPS = "steps"
|
||||
UNIT_VOLT = "V"
|
||||
|
@ -325,6 +325,7 @@ ble_client:
|
||||
accept: True
|
||||
- mac_address: C4:4F:33:11:22:33
|
||||
id: my_bedjet_ble_client
|
||||
|
||||
bedjet:
|
||||
- ble_client_id: my_bedjet_ble_client
|
||||
id: my_bedjet_client
|
||||
@ -1269,6 +1270,16 @@ sensor:
|
||||
pressure:
|
||||
name: "MPL3115A2 Pressure"
|
||||
update_interval: 10s
|
||||
- platform: alpha3
|
||||
ble_client_id: ble_foo
|
||||
flow:
|
||||
name: "Radiator Pump Flow"
|
||||
head:
|
||||
name: "Radiator Pump Head"
|
||||
power:
|
||||
name: "Radiator Pump Power"
|
||||
speed:
|
||||
name: "Radiator Pump Speed"
|
||||
- platform: ld2410
|
||||
moving_distance:
|
||||
name: "Moving distance (cm)"
|
||||
|
Loading…
Reference in New Issue
Block a user