mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 00:19:02 +02:00
Add light.brightness_changed trigger
This commit is contained in:
@@ -473,6 +473,29 @@
|
||||
"description": "Triggers when a light turns off.",
|
||||
"description_configured": "Triggers when a light turns off",
|
||||
"name": "When a light turns off"
|
||||
},
|
||||
"brightness_changed": {
|
||||
"description": "Triggers when the brightness of a light changes.",
|
||||
"description_configured": "Triggers when the brightness of a light changes",
|
||||
"fields": {
|
||||
"lower": {
|
||||
"description": "The minimum brightness value to trigger on. Only triggers when brightness is at or above this value.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"upper": {
|
||||
"description": "The maximum brightness value to trigger on. Only triggers when brightness is at or below this value.",
|
||||
"name": "Upper limit"
|
||||
},
|
||||
"above": {
|
||||
"description": "Only trigger when brightness is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when brightness is below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When the brightness of a light changes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
@@ -22,6 +23,12 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
CONF_LOWER = "lower"
|
||||
CONF_UPPER = "upper"
|
||||
CONF_ABOVE = "above"
|
||||
CONF_BELOW = "below"
|
||||
|
||||
TURNS_ON_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
@@ -34,6 +41,26 @@ TURNS_OFF_TRIGGER_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
BRIGHTNESS_CHANGED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Exclusive(CONF_LOWER, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_UPPER, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_ABOVE, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_BELOW, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LightTurnsOnTrigger(Trigger):
|
||||
"""Trigger for when a light turns on."""
|
||||
@@ -161,9 +188,98 @@ class LightTurnsOffTrigger(Trigger):
|
||||
)
|
||||
|
||||
|
||||
class LightBrightnessChangedTrigger(Trigger):
|
||||
"""Trigger for when a light's brightness changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, BRIGHTNESS_CHANGED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the light brightness changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
self._options = config.options or {}
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
lower_limit = self._options.get(CONF_LOWER)
|
||||
upper_limit = self._options.get(CONF_UPPER)
|
||||
above_limit = self._options.get(CONF_ABOVE)
|
||||
below_limit = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Get brightness values
|
||||
from_brightness = (
|
||||
from_state.attributes.get(ATTR_BRIGHTNESS) if from_state else None
|
||||
)
|
||||
to_brightness = to_state.attributes.get(ATTR_BRIGHTNESS)
|
||||
|
||||
# Only trigger if brightness value exists and has changed
|
||||
if to_brightness is None or from_brightness == to_brightness:
|
||||
return
|
||||
|
||||
# Apply threshold filters if configured
|
||||
if lower_limit is not None and to_brightness < lower_limit:
|
||||
return
|
||||
if upper_limit is not None and to_brightness > upper_limit:
|
||||
return
|
||||
if above_limit is not None and to_brightness <= above_limit:
|
||||
return
|
||||
if below_limit is not None and to_brightness >= below_limit:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
"from_brightness": from_brightness,
|
||||
"to_brightness": to_brightness,
|
||||
},
|
||||
f"brightness changed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turns_on": LightTurnsOnTrigger,
|
||||
"turns_off": LightTurnsOffTrigger,
|
||||
"brightness_changed": LightBrightnessChangedTrigger,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,37 @@ turns_off:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
|
||||
brightness_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
fields:
|
||||
lower:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
upper:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
|
||||
@@ -242,3 +242,409 @@ async def test_light_turns_off_trigger_ignores_unavailable(
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger fires when brightness changes."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
"from_brightness": "{{ trigger.from_brightness }}",
|
||||
"to_brightness": "{{ trigger.to_brightness }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change brightness - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
assert service_calls[0].data["from_brightness"] == "100"
|
||||
assert service_calls[0].data["to_brightness"] == "150"
|
||||
service_calls.clear()
|
||||
|
||||
# Change brightness again - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["from_brightness"] == "150"
|
||||
assert service_calls[0].data["to_brightness"] == "200"
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_same_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger does not fire when brightness is the same."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Update state but keep brightness the same - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_lower_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with lower limit."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"lower": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness below lower limit - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness at lower limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness above lower limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_upper_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with upper limit."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"upper": 150,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness above upper limit - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 180})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness at upper limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness below upper limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_above(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with above threshold."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"above": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness at threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness above threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 101})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness well above threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_below(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with below threshold."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"below": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness at threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness below threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 99})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness well below threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger ignores unavailable states."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change brightness after unavailable - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_from_no_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger fires when brightness is added."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state without brightness (on/off only light)
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
"from_brightness": "{{ trigger.from_brightness }}",
|
||||
"to_brightness": "{{ trigger.to_brightness }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Add brightness attribute - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["from_brightness"] == "None"
|
||||
assert service_calls[0].data["to_brightness"] == "100"
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_no_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger does not fire when brightness is not present."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state without brightness
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn light off and on without brightness - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
Reference in New Issue
Block a user