mirror of
https://github.com/esphome/esphome.git
synced 2026-02-04 13:19:39 -07:00
Compare commits
2 Commits
integratio
...
json_web_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840ad30880 | ||
|
|
823b5ac1ab |
@@ -104,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
|
|||||||
esphome/components/ccs811/* @habbie
|
esphome/components/ccs811/* @habbie
|
||||||
esphome/components/cd74hc4067/* @asoehlke
|
esphome/components/cd74hc4067/* @asoehlke
|
||||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||||
|
esphome/components/ch423/* @dwmw2
|
||||||
esphome/components/chsc6x/* @kkosik20
|
esphome/components/chsc6x/* @kkosik20
|
||||||
esphome/components/climate/* @esphome/core
|
esphome/components/climate/* @esphome/core
|
||||||
esphome/components/climate_ir/* @glmnet
|
esphome/components/climate_ir/* @glmnet
|
||||||
|
|||||||
103
esphome/components/ch423/__init__.py
Normal file
103
esphome/components/ch423/__init__.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c
|
||||||
|
from esphome.components.i2c import I2CBus
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_I2C_ID,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_INPUT,
|
||||||
|
CONF_INVERTED,
|
||||||
|
CONF_MODE,
|
||||||
|
CONF_NUMBER,
|
||||||
|
CONF_OPEN_DRAIN,
|
||||||
|
CONF_OUTPUT,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
CODEOWNERS = ["@dwmw2"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
ch423_ns = cg.esphome_ns.namespace("ch423")
|
||||||
|
|
||||||
|
CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice)
|
||||||
|
CH423GPIOPin = ch423_ns.class_(
|
||||||
|
"CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component)
|
||||||
|
)
|
||||||
|
|
||||||
|
CONF_CH423 = "ch423"
|
||||||
|
|
||||||
|
# Note that no address is configurable - each register in the CH423 has a dedicated i2c address
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.declare_id(CH423Component),
|
||||||
|
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
# Can't use register_i2c_device because there is no CONF_ADDRESS
|
||||||
|
parent = await cg.get_variable(config[CONF_I2C_ID])
|
||||||
|
cg.add(var.set_i2c_bus(parent))
|
||||||
|
|
||||||
|
|
||||||
|
# This is used as a final validation step so that modes have been fully transformed.
|
||||||
|
def pin_mode_check(pin_config, _):
|
||||||
|
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
|
||||||
|
raise cv.Invalid("CH423 only supports input on pins 0-7")
|
||||||
|
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
|
||||||
|
raise cv.Invalid("CH423 only supports open drain output on pins 8-23")
|
||||||
|
|
||||||
|
ch423_id = pin_config[CONF_CH423]
|
||||||
|
pin_num = pin_config[CONF_NUMBER]
|
||||||
|
is_output = pin_config[CONF_MODE][CONF_OUTPUT]
|
||||||
|
is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN]
|
||||||
|
|
||||||
|
# Track pin modes per CH423 instance in CORE.data
|
||||||
|
ch423_modes = CORE.data.setdefault(CONF_CH423, {})
|
||||||
|
if ch423_id not in ch423_modes:
|
||||||
|
ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None}
|
||||||
|
|
||||||
|
if pin_num < 8:
|
||||||
|
# GPIO pins (0-7): all must have same direction
|
||||||
|
if ch423_modes[ch423_id]["gpio_output"] is None:
|
||||||
|
ch423_modes[ch423_id]["gpio_output"] = is_output
|
||||||
|
elif ch423_modes[ch423_id]["gpio_output"] != is_output:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"CH423 GPIO pins (0-7) must all be configured as input or all as output"
|
||||||
|
)
|
||||||
|
# GPO pins (8-23): all must have same open-drain setting
|
||||||
|
elif ch423_modes[ch423_id]["gpo_open_drain"] is None:
|
||||||
|
ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain
|
||||||
|
elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CH423_PIN_SCHEMA = pins.gpio_base_schema(
|
||||||
|
CH423GPIOPin,
|
||||||
|
cv.int_range(min=0, max=23),
|
||||||
|
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_CH423): cv.use_id(CH423Component),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check)
|
||||||
|
async def ch423_pin_to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
parent = await cg.get_variable(config[CONF_CH423])
|
||||||
|
|
||||||
|
cg.add(var.set_parent(parent))
|
||||||
|
|
||||||
|
num = config[CONF_NUMBER]
|
||||||
|
cg.add(var.set_pin(num))
|
||||||
|
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||||
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
|
return var
|
||||||
148
esphome/components/ch423/ch423.cpp
Normal file
148
esphome/components/ch423/ch423.cpp
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#include "ch423.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
|
namespace esphome::ch423 {
|
||||||
|
|
||||||
|
static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1)
|
||||||
|
static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable
|
||||||
|
static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins
|
||||||
|
static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1)
|
||||||
|
static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down)
|
||||||
|
static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1)
|
||||||
|
static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1)
|
||||||
|
|
||||||
|
static const char *const TAG = "ch423";
|
||||||
|
|
||||||
|
void CH423Component::setup() {
|
||||||
|
// set outputs before mode
|
||||||
|
this->write_outputs_();
|
||||||
|
// Set system parameters and check for errors
|
||||||
|
bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_);
|
||||||
|
// Only read inputs if pins are configured for input (IO_OE not set)
|
||||||
|
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
|
||||||
|
success = this->read_inputs_();
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
ESP_LOGE(TAG, "CH423 not detected");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||||
|
this->status_has_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::loop() {
|
||||||
|
// Clear all the previously read flags.
|
||||||
|
this->pin_read_flags_ = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "CH423:");
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||||
|
if (pin < 8) {
|
||||||
|
if (flags & gpio::FLAG_OUTPUT) {
|
||||||
|
this->sys_params_ |= CH423_SYS_IO_OE;
|
||||||
|
}
|
||||||
|
} else if (pin >= 8 && pin < 24) {
|
||||||
|
if (flags & gpio::FLAG_OPEN_DRAIN) {
|
||||||
|
this->sys_params_ |= CH423_SYS_OD_EN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::digital_read(uint8_t pin) {
|
||||||
|
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
|
||||||
|
// Read values on first access or in case it's being read again in the same loop
|
||||||
|
this->read_inputs_();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pin_read_flags_ |= (1 << pin);
|
||||||
|
return (this->input_bits_ & (1 << pin)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::digital_write(uint8_t pin, bool value) {
|
||||||
|
if (value) {
|
||||||
|
this->output_bits_ |= (1 << pin);
|
||||||
|
} else {
|
||||||
|
this->output_bits_ &= ~(1 << pin);
|
||||||
|
}
|
||||||
|
this->write_outputs_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::read_inputs_() {
|
||||||
|
if (this->is_failed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// reading inputs requires IO_OE to be 0
|
||||||
|
if (this->sys_params_ & CH423_SYS_IO_OE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t result = this->read_reg_(CH423_REG_IO_RD);
|
||||||
|
this->input_bits_ = result;
|
||||||
|
this->status_clear_warning();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
|
||||||
|
bool CH423Component::write_reg_(uint8_t reg, uint8_t value) {
|
||||||
|
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
|
||||||
|
if (err != i2c::ERROR_OK) {
|
||||||
|
char buf[64];
|
||||||
|
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err);
|
||||||
|
this->status_set_warning(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->status_clear_warning();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CH423Component::read_reg_(uint8_t reg) {
|
||||||
|
uint8_t value;
|
||||||
|
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
|
||||||
|
if (err != i2c::ERROR_OK) {
|
||||||
|
char buf[64];
|
||||||
|
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err);
|
||||||
|
this->status_set_warning(buf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this->status_clear_warning();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::write_outputs_() {
|
||||||
|
bool success = true;
|
||||||
|
// Write IO7-IO0
|
||||||
|
success &= this->write_reg_(CH423_REG_IO, static_cast<uint8_t>(this->output_bits_));
|
||||||
|
// Write OC7-OC0
|
||||||
|
success &= this->write_reg_(CH423_REG_OCL, static_cast<uint8_t>(this->output_bits_ >> 8));
|
||||||
|
// Write OC15-OC8
|
||||||
|
success &= this->write_reg_(CH423_REG_OCH, static_cast<uint8_t>(this->output_bits_ >> 16));
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
float CH423Component::get_setup_priority() const { return setup_priority::IO; }
|
||||||
|
|
||||||
|
// Run our loop() method very early in the loop, so that we cache read values
|
||||||
|
// before other components call our digital_read() method.
|
||||||
|
float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
|
||||||
|
|
||||||
|
void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||||
|
bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
|
||||||
|
|
||||||
|
void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
|
||||||
|
size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||||
|
return snprintf(buffer, len, "EXIO%u via CH423", this->pin_);
|
||||||
|
}
|
||||||
|
void CH423GPIOPin::set_flags(gpio::Flags flags) {
|
||||||
|
flags_ = flags;
|
||||||
|
this->parent_->pin_mode(this->pin_, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::ch423
|
||||||
67
esphome/components/ch423/ch423.h
Normal file
67
esphome/components/ch423/ch423.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome::ch423 {
|
||||||
|
|
||||||
|
class CH423Component : public Component, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
CH423Component() = default;
|
||||||
|
|
||||||
|
/// Check i2c availability and setup masks
|
||||||
|
void setup() override;
|
||||||
|
/// Poll for input changes periodically
|
||||||
|
void loop() override;
|
||||||
|
/// Helper function to read the value of a pin.
|
||||||
|
bool digital_read(uint8_t pin);
|
||||||
|
/// Helper function to write the value of a pin.
|
||||||
|
void digital_write(uint8_t pin, bool value);
|
||||||
|
/// Helper function to set the pin mode of a pin.
|
||||||
|
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||||
|
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
float get_loop_priority() const override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool write_reg_(uint8_t reg, uint8_t value);
|
||||||
|
uint8_t read_reg_(uint8_t reg);
|
||||||
|
bool read_inputs_();
|
||||||
|
bool write_outputs_();
|
||||||
|
|
||||||
|
/// The mask to write as output state - 1 means HIGH, 0 means LOW
|
||||||
|
uint32_t output_bits_{0x00};
|
||||||
|
/// Flags to check if read previously during this loop
|
||||||
|
uint8_t pin_read_flags_{0x00};
|
||||||
|
/// Copy of last read values
|
||||||
|
uint8_t input_bits_{0x00};
|
||||||
|
/// System parameters
|
||||||
|
uint8_t sys_params_{0x00};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper class to expose a CH423 pin as a GPIO pin.
|
||||||
|
class CH423GPIOPin : public GPIOPin {
|
||||||
|
public:
|
||||||
|
void setup() override{};
|
||||||
|
void pin_mode(gpio::Flags flags) override;
|
||||||
|
bool digital_read() override;
|
||||||
|
void digital_write(bool value) override;
|
||||||
|
size_t dump_summary(char *buffer, size_t len) const override;
|
||||||
|
|
||||||
|
void set_parent(CH423Component *parent) { parent_ = parent; }
|
||||||
|
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||||
|
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||||
|
void set_flags(gpio::Flags flags);
|
||||||
|
|
||||||
|
gpio::Flags get_flags() const override { return this->flags_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
CH423Component *parent_{};
|
||||||
|
uint8_t pin_{};
|
||||||
|
bool inverted_{};
|
||||||
|
gpio::Flags flags_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::ch423
|
||||||
36
tests/components/ch423/common.yaml
Normal file
36
tests/components/ch423/common.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
ch423:
|
||||||
|
- id: ch423_hub
|
||||||
|
i2c_id: i2c_bus
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
id: ch423_input
|
||||||
|
name: CH423 Binary Sensor
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 1
|
||||||
|
mode: INPUT
|
||||||
|
inverted: true
|
||||||
|
- platform: gpio
|
||||||
|
id: ch423_input_2
|
||||||
|
name: CH423 Binary Sensor 2
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 0
|
||||||
|
mode: INPUT
|
||||||
|
inverted: false
|
||||||
|
output:
|
||||||
|
- platform: gpio
|
||||||
|
id: ch423_out_11
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 11
|
||||||
|
mode: OUTPUT_OPEN_DRAIN
|
||||||
|
inverted: true
|
||||||
|
- platform: gpio
|
||||||
|
id: ch423_out_23
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 23
|
||||||
|
mode: OUTPUT_OPEN_DRAIN
|
||||||
|
inverted: false
|
||||||
4
tests/components/ch423/test.esp32-idf.yaml
Normal file
4
tests/components/ch423/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
4
tests/components/ch423/test.esp8266-ard.yaml
Normal file
4
tests/components/ch423/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
4
tests/components/ch423/test.rp2040-ard.yaml
Normal file
4
tests/components/ch423/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
58
tests/unit_tests/components/test_ch423.py
Normal file
58
tests/unit_tests/components/test_ch423.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Tests for ch423 component validation."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from esphome import config, yaml_util
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
|
||||||
|
def test_ch423_mixed_gpio_modes_fails(tmp_path, capsys):
|
||||||
|
"""Test that mixing input/output on GPIO pins 0-7 fails validation."""
|
||||||
|
test_file = tmp_path / "test.yaml"
|
||||||
|
test_file.write_text("""
|
||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
sda: GPIO4
|
||||||
|
scl: GPIO5
|
||||||
|
|
||||||
|
ch423:
|
||||||
|
- id: ch423_hub
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
name: "CH423 Input 0"
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 0
|
||||||
|
mode: input
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: gpio
|
||||||
|
name: "CH423 Output 1"
|
||||||
|
pin:
|
||||||
|
ch423: ch423_hub
|
||||||
|
number: 1
|
||||||
|
mode: output
|
||||||
|
""")
|
||||||
|
|
||||||
|
parsed_yaml = yaml_util.load_yaml(test_file)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(yaml_util, "load_yaml", return_value=parsed_yaml),
|
||||||
|
patch.object(CORE, "config_path", test_file),
|
||||||
|
):
|
||||||
|
result = config.read_config({})
|
||||||
|
|
||||||
|
assert result is None, "Expected validation to fail with mixed GPIO modes"
|
||||||
|
|
||||||
|
# Check that the error message mentions the GPIO pin restriction
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert (
|
||||||
|
"GPIO pins (0-7) must all be configured as input or all as output"
|
||||||
|
in captured.out
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user