[deconz] Add Pairing/Scene actions, new devices and improve code (#14622)
* port changes * update instructions * Incorporate review comments from #14134 * new improvements (mostly Java 17 changes) * further improvements Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
@@ -60,7 +60,7 @@ import com.google.gson.GsonBuilder;
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@MockitoSettings(strictness = Strictness.WARN)
|
||||
@NonNullByDefault
|
||||
public class DeconzTest {
|
||||
private @NonNullByDefault({}) Gson gson;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.deconz;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ALL_ON;
|
||||
import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ANY_ON;
|
||||
import static org.openhab.binding.deconz.internal.BindingConstants.THING_TYPE_LIGHTGROUP;
|
||||
import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
|
||||
import org.openhab.binding.deconz.internal.dto.GroupMessage;
|
||||
import org.openhab.binding.deconz.internal.handler.GroupThingHandler;
|
||||
import org.openhab.binding.deconz.internal.types.GroupType;
|
||||
import org.openhab.binding.deconz.internal.types.GroupTypeDeserializer;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* This class provides tests for deconz light groups
|
||||
*
|
||||
* @author Christoph Weitkamp - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@NonNullByDefault
|
||||
public class LightGroupTest {
|
||||
private @NonNullByDefault({}) Gson gson;
|
||||
|
||||
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
|
||||
private @Mock @NonNullByDefault({}) DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
|
||||
|
||||
@BeforeEach
|
||||
public void initialize() {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
|
||||
gson = gsonBuilder.create();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lightGroupUpdateTest() throws IOException {
|
||||
GroupMessage lightMessage = DeconzTest.getObjectFromJson("group.json", GroupMessage.class, gson);
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "lightgroup");
|
||||
ChannelUID channelUIDAllOn = new ChannelUID(thingUID, CHANNEL_ALL_ON);
|
||||
ChannelUID channelUIDAnyOn = new ChannelUID(thingUID, CHANNEL_ANY_ON);
|
||||
|
||||
Thing group = ThingBuilder.create(THING_TYPE_LIGHTGROUP, thingUID)
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDAllOn, CoreItemFactory.SWITCH).build())
|
||||
.withChannel(ChannelBuilder.create(channelUIDAnyOn, CoreItemFactory.SWITCH).build()).build();
|
||||
GroupThingHandler groupThingHandler = new GroupThingHandler(group, gson, commandDescriptionProvider);
|
||||
groupThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
groupThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAllOn), eq(OnOffType.OFF));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAnyOn), eq(OnOffType.OFF));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package org.openhab.binding.deconz;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.openhab.binding.deconz.internal.BindingConstants.*;
|
||||
import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
@@ -77,34 +78,36 @@ public class LightsTest {
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
|
||||
ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID)
|
||||
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
|
||||
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build())
|
||||
.withChannel(ChannelBuilder.create(channelUIDCt, "Number").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider);
|
||||
lightThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
lightThingHandler.messageReceived("", lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21")));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500")));
|
||||
lightThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("21")));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDCt), eq(new DecimalType("2500")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorTemperatureLightStateDescriptionProviderTest() {
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
|
||||
ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
|
||||
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put(PROPERTY_CT_MAX, "500");
|
||||
properties.put(PROPERTY_CT_MIN, "200");
|
||||
properties.put(PROPERTY_THING_TYPE_VERSION, "1");
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties)
|
||||
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
|
||||
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
|
||||
.withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build())
|
||||
.withChannel(ChannelBuilder.create(channelUIDCt, "Number").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider) {
|
||||
// avoid warning when initializing
|
||||
@@ -116,7 +119,7 @@ public class LightsTest {
|
||||
|
||||
lightThingHandler.initialize();
|
||||
|
||||
Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUID_ct), any());
|
||||
Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUIDCt), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -125,16 +128,17 @@ public class LightsTest {
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
|
||||
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider);
|
||||
lightThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
lightThingHandler.messageReceived("", lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("38")));
|
||||
lightThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("38")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -143,16 +147,17 @@ public class LightsTest {
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
|
||||
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider);
|
||||
lightThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
lightThingHandler.messageReceived("", lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("100")));
|
||||
lightThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("100")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -161,16 +166,17 @@ public class LightsTest {
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
|
||||
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider);
|
||||
lightThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
lightThingHandler.messageReceived("", lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("0")));
|
||||
lightThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("0")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -179,15 +185,16 @@ public class LightsTest {
|
||||
assertNotNull(lightMessage);
|
||||
|
||||
ThingUID thingUID = new ThingUID("deconz", "light");
|
||||
ChannelUID channelUID_pos = new ChannelUID(thingUID, CHANNEL_POSITION);
|
||||
ChannelUID channelUIDPos = new ChannelUID(thingUID, CHANNEL_POSITION);
|
||||
|
||||
Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID)
|
||||
.withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build();
|
||||
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
|
||||
.withChannel(ChannelBuilder.create(channelUIDPos, "Rollershutter").build()).build();
|
||||
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
|
||||
commandDescriptionProvider);
|
||||
lightThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
lightThingHandler.messageReceived("", lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_pos), eq(new PercentType("41")));
|
||||
lightThingHandler.messageReceived(lightMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDPos), eq(new PercentType("41")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
@@ -82,7 +83,7 @@ public class SensorsTest {
|
||||
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
|
||||
sensorThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
sensorThingHandler.messageReceived("", sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON));
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ public class SensorsTest {
|
||||
sensorThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
// ACT
|
||||
sensorThingHandler.messageReceived("", sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
|
||||
// ASSERT
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good")));
|
||||
@@ -120,10 +121,10 @@ public class SensorsTest {
|
||||
sensorThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
// ACT
|
||||
sensorThingHandler.messageReceived("", sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
|
||||
// ASSERT
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new DecimalType(129)));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new QuantityType<>("129 ppb")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -144,15 +145,23 @@ public class SensorsTest {
|
||||
SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson);
|
||||
sensorThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
sensorThingHandler.messageReceived("", sensorMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID),
|
||||
eq(new QuantityType<>(100.0, Units.PERCENT)));
|
||||
sensorMessage = DeconzTest.getObjectFromJson("thermostat-undef.json", SensorMessage.class, gson);
|
||||
assertNotNull(sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), eq(UnDefType.UNDEF));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID),
|
||||
eq(new QuantityType<>(25, SIUnits.CELSIUS)));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID),
|
||||
eq(new StringType(ThermostatMode.AUTO.name())));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID),
|
||||
eq(new QuantityType<>(16.5, SIUnits.CELSIUS)));
|
||||
|
||||
sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson);
|
||||
assertNotNull(sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID),
|
||||
eq(new QuantityType<>(99, Units.PERCENT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -174,7 +183,7 @@ public class SensorsTest {
|
||||
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
|
||||
sensorThingHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
sensorThingHandler.messageReceived("", sensorMessage);
|
||||
sensorThingHandler.messageReceived(sensorMessage);
|
||||
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelFireUID), eq(OnOffType.OFF));
|
||||
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelBatteryLevelUID), eq(new DecimalType(98)));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"battery": 98,
|
||||
"on": true,
|
||||
"pending" : [],
|
||||
"reachable": false
|
||||
"reachable": true
|
||||
},
|
||||
"ep": 1,
|
||||
"etag": "717549a99371f3ea1a5f0b40f1537094",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"action": {
|
||||
"alert": "none",
|
||||
"bri": 127,
|
||||
"colormode": "hs",
|
||||
"ct": 0,
|
||||
"effect": "none",
|
||||
"hue": 0,
|
||||
"on": false,
|
||||
"sat": 127,
|
||||
"scene": null,
|
||||
"xy": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"devicemembership": [
|
||||
"3"
|
||||
],
|
||||
"etag": "586d2448a818aa7f6f3baa4907f43468",
|
||||
"id": "1",
|
||||
"lights": [],
|
||||
"name": "RM01",
|
||||
"scenes": [],
|
||||
"state": {
|
||||
"all_on": false,
|
||||
"any_on": false
|
||||
},
|
||||
"type": "LightGroup"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"battery": 85,
|
||||
"displayflipped": null,
|
||||
"heatsetpoint": 2500,
|
||||
"locked": null,
|
||||
"mode": "auto",
|
||||
"offset": 0,
|
||||
"on": true,
|
||||
"reachable": true
|
||||
},
|
||||
"ep": 1,
|
||||
"etag": "717549a99371f3ea1a5f0b40f1537094",
|
||||
"lastseen": "2020-05-31T20:24:55.819",
|
||||
"manufacturername": "Eurotronic",
|
||||
"modelid": "SPZB0001",
|
||||
"name": "Test Thermostat",
|
||||
"state": {
|
||||
"lastupdated": "2020-05-31T20:24:55.819",
|
||||
"on": true,
|
||||
"temperature": 1650,
|
||||
"valve": 255
|
||||
},
|
||||
"swversion": "20191014",
|
||||
"type": "ZHAThermostat",
|
||||
"uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"lastupdated": "2020-05-31T20:24:55.819",
|
||||
"on": true,
|
||||
"temperature": 1650,
|
||||
"valve": 255
|
||||
"valve": 99
|
||||
},
|
||||
"swversion": "20191014",
|
||||
"type": "ZHAThermostat",
|
||||
|
||||
Reference in New Issue
Block a user