mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Review the whole binding
Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
parent
ed2a6d5700
commit
e12876ba11
@ -17,11 +17,14 @@ import java.io.IOException;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.gce.internal.model.M2MMessageParser;
|
import org.openhab.binding.gce.internal.model.M2MMessageParser;
|
||||||
|
import org.openhab.binding.gce.internal.model.PortDefinition;
|
||||||
import org.openhab.binding.gce.internal.model.StatusFile;
|
import org.openhab.binding.gce.internal.model.StatusFile;
|
||||||
import org.openhab.binding.gce.internal.model.StatusFileAccessor;
|
import org.openhab.binding.gce.internal.model.StatusFileAccessor;
|
||||||
import org.openhab.core.thing.ThingUID;
|
import org.openhab.core.thing.ThingUID;
|
||||||
@ -38,16 +41,18 @@ import org.xml.sax.SAXException;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class Ipx800DeviceConnector extends Thread {
|
public class Ipx800DeviceConnector extends Thread {
|
||||||
private static final int DEFAULT_SOCKET_TIMEOUT_MS = 5000;
|
private static final int DEFAULT_SOCKET_TIMEOUT_MS = 10000;
|
||||||
private static final int DEFAULT_RECONNECT_TIMEOUT_MS = 5000;
|
private static final int DEFAULT_RECONNECT_TIMEOUT_MS = 5000;
|
||||||
private static final int MAX_KEEPALIVE_FAILURE = 3;
|
private static final int MAX_KEEPALIVE_FAILURE = 3;
|
||||||
private static final String ENDL = "\r\n";
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(Ipx800DeviceConnector.class);
|
private final Logger logger = LoggerFactory.getLogger(Ipx800DeviceConnector.class);
|
||||||
|
private final Random randomizer = new Random();
|
||||||
|
|
||||||
private final String hostname;
|
private final String hostname;
|
||||||
private final int portNumber;
|
private final int portNumber;
|
||||||
private final M2MMessageParser messageParser;
|
private final M2MMessageParser parser;
|
||||||
private final StatusFileAccessor statusAccessor;
|
private final StatusFileAccessor statusAccessor;
|
||||||
|
private final Ipx800EventListener listener;
|
||||||
|
|
||||||
private Optional<Socket> socket = Optional.empty();
|
private Optional<Socket> socket = Optional.empty();
|
||||||
private Optional<BufferedReader> input = Optional.empty();
|
private Optional<BufferedReader> input = Optional.empty();
|
||||||
@ -60,19 +65,12 @@ public class Ipx800DeviceConnector extends Thread {
|
|||||||
super("OH-binding-" + uid);
|
super("OH-binding-" + uid);
|
||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
this.portNumber = portNumber;
|
this.portNumber = portNumber;
|
||||||
this.messageParser = new M2MMessageParser(this, listener);
|
this.listener = listener;
|
||||||
|
this.parser = new M2MMessageParser(listener);
|
||||||
this.statusAccessor = new StatusFileAccessor(hostname);
|
this.statusAccessor = new StatusFileAccessor(hostname);
|
||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void send(String message) {
|
|
||||||
output.ifPresentOrElse(out -> {
|
|
||||||
logger.debug("Sending '{}' to Ipx800", message);
|
|
||||||
out.write(message + ENDL);
|
|
||||||
out.flush();
|
|
||||||
}, () -> logger.warn("Trying to send '{}' while the output stream is closed.", message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the ipx800
|
* Connect to the ipx800
|
||||||
*
|
*
|
||||||
@ -84,39 +82,27 @@ public class Ipx800DeviceConnector extends Thread {
|
|||||||
logger.debug("Connecting to {}:{}...", hostname, portNumber);
|
logger.debug("Connecting to {}:{}...", hostname, portNumber);
|
||||||
Socket socket = new Socket(hostname, portNumber);
|
Socket socket = new Socket(hostname, portNumber);
|
||||||
socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MS);
|
socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MS);
|
||||||
socket.getInputStream().skip(socket.getInputStream().available());
|
// socket.getInputStream().skip(socket.getInputStream().available());
|
||||||
this.socket = Optional.of(socket);
|
this.socket = Optional.of(socket);
|
||||||
|
|
||||||
input = Optional.of(new BufferedReader(new InputStreamReader(socket.getInputStream())));
|
|
||||||
output = Optional.of(new PrintWriter(socket.getOutputStream(), true));
|
output = Optional.of(new PrintWriter(socket.getOutputStream(), true));
|
||||||
|
input = Optional.of(new BufferedReader(new InputStreamReader(socket.getInputStream())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect the device
|
* Disconnect the device
|
||||||
*/
|
*/
|
||||||
private void disconnect() {
|
private void disconnect() {
|
||||||
logger.debug("Disconnecting");
|
|
||||||
|
|
||||||
input.ifPresent(in -> {
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignore) {
|
|
||||||
}
|
|
||||||
input = Optional.empty();
|
|
||||||
});
|
|
||||||
|
|
||||||
output.ifPresent(PrintWriter::close);
|
|
||||||
output = Optional.empty();
|
|
||||||
|
|
||||||
socket.ifPresent(client -> {
|
socket.ifPresent(client -> {
|
||||||
try {
|
try {
|
||||||
|
logger.debug("Closing socket");
|
||||||
client.close();
|
client.close();
|
||||||
} catch (IOException ignore) {
|
} catch (IOException ignore) {
|
||||||
}
|
}
|
||||||
socket = Optional.empty();
|
socket = Optional.empty();
|
||||||
|
input = Optional.empty();
|
||||||
|
output = Optional.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Disconnected");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,23 +113,35 @@ public class Ipx800DeviceConnector extends Thread {
|
|||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void send(String message) {
|
||||||
|
output.ifPresentOrElse(out -> {
|
||||||
|
logger.debug("Sending '{}' to Ipx800", message);
|
||||||
|
out.println(message);
|
||||||
|
}, () -> logger.warn("Unable to send '{}' when the output stream is closed.", message));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an arbitrary keepalive command which cause the IPX to send an update.
|
* Send an arbitrary keepalive command which cause the IPX to send an update.
|
||||||
* If we don't receive the update maxKeepAliveFailure time, the connection is closed and reopened
|
* If we don't receive the update maxKeepAliveFailure time, the connection is closed and reopened
|
||||||
*/
|
*/
|
||||||
private void sendKeepalive() {
|
private void sendKeepalive() {
|
||||||
output.ifPresent(out -> {
|
output.ifPresentOrElse(out -> {
|
||||||
|
PortDefinition pd = PortDefinition.values()[randomizer.nextInt(PortDefinition.AS_SET.size())];
|
||||||
|
String command = "%s%d".formatted(pd.m2mCommand, randomizer.nextInt(pd.quantity) + 1);
|
||||||
|
|
||||||
if (waitingKeepaliveResponse) {
|
if (waitingKeepaliveResponse) {
|
||||||
failedKeepalive++;
|
failedKeepalive++;
|
||||||
logger.debug("Sending keepalive, attempt {}", failedKeepalive);
|
logger.debug("Sending keepalive {}, attempt {}", command, failedKeepalive);
|
||||||
} else {
|
} else {
|
||||||
failedKeepalive = 0;
|
failedKeepalive = 0;
|
||||||
logger.debug("Sending keepalive");
|
logger.debug("Sending keepalive {}", command);
|
||||||
}
|
}
|
||||||
out.println("GetIn01");
|
|
||||||
out.flush();
|
out.println(command);
|
||||||
|
parser.setExpectedResponse(command);
|
||||||
|
|
||||||
waitingKeepaliveResponse = true;
|
waitingKeepaliveResponse = true;
|
||||||
});
|
}, () -> logger.warn("Unable to send keepAlive when the output stream is closed."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -160,7 +158,7 @@ public class Ipx800DeviceConnector extends Thread {
|
|||||||
try {
|
try {
|
||||||
String command = in.readLine();
|
String command = in.readLine();
|
||||||
waitingKeepaliveResponse = false;
|
waitingKeepaliveResponse = false;
|
||||||
messageParser.unsolicitedUpdate(command);
|
parser.unsolicitedUpdate(command);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
handleException(e);
|
handleException(e);
|
||||||
}
|
}
|
||||||
@ -182,18 +180,43 @@ public class Ipx800DeviceConnector extends Thread {
|
|||||||
if (e instanceof SocketTimeoutException) {
|
if (e instanceof SocketTimeoutException) {
|
||||||
sendKeepalive();
|
sendKeepalive();
|
||||||
return;
|
return;
|
||||||
|
} else if (e instanceof SocketException) {
|
||||||
|
logger.debug("SocketException raised by streams while closing socket");
|
||||||
} else if (e instanceof IOException) {
|
} else if (e instanceof IOException) {
|
||||||
logger.warn("Communication error: '{}'. Will retry in {} ms", e, DEFAULT_RECONNECT_TIMEOUT_MS);
|
logger.warn("Communication error: '{}'. Will retry in {} ms", e, DEFAULT_RECONNECT_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
messageParser.errorOccurred(e);
|
listener.errorOccurred(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public M2MMessageParser getParser() {
|
|
||||||
return messageParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StatusFile readStatusFile() throws SAXException, IOException {
|
public StatusFile readStatusFile() throws SAXException, IOException {
|
||||||
return statusAccessor.read();
|
return statusAccessor.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set output of the device sending the corresponding command
|
||||||
|
*
|
||||||
|
* @param targetPort
|
||||||
|
* @param targetValue
|
||||||
|
*/
|
||||||
|
public void setOutput(String targetPort, int targetValue, boolean pulse) {
|
||||||
|
logger.debug("Sending {} to {}", targetValue, targetPort);
|
||||||
|
String command = "Set%02d%s%s".formatted(Integer.parseInt(targetPort), targetValue, pulse ? "p" : "");
|
||||||
|
send(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the counter value to 0
|
||||||
|
*
|
||||||
|
* @param targetCounter
|
||||||
|
*/
|
||||||
|
public void resetCounter(int targetCounter) {
|
||||||
|
logger.debug("Resetting counter {} to 0", targetCounter);
|
||||||
|
send("ResetCount%d".formatted(targetCounter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPLC() {
|
||||||
|
send("Reset");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,6 @@ import org.openhab.core.thing.type.ChannelKind;
|
|||||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
import org.openhab.core.types.State;
|
import org.openhab.core.types.State;
|
||||||
import org.openhab.core.types.UnDefType;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
@ -92,8 +91,7 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
PortData currentData = portDatas.get(port);
|
if (portDatas.get(port) instanceof PortData currentData && currentData.getValue() == 1
|
||||||
if (currentData != null && currentData.getValue() == 1
|
|
||||||
&& referenceTime.equals(currentData.getTimestamp())) {
|
&& referenceTime.equals(currentData.getTimestamp())) {
|
||||||
triggerChannel(eventChannelId, EVENT_LONG_PRESS);
|
triggerChannel(eventChannelId, EVENT_LONG_PRESS);
|
||||||
}
|
}
|
||||||
@ -133,29 +131,18 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
connector.start();
|
connector.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status != null) {
|
if (status instanceof StatusFile statusFile) {
|
||||||
for (PortDefinition portDefinition : PortDefinition.values()) {
|
PortDefinition.AS_SET.forEach(portDefinition -> statusFile.getPorts(portDefinition).forEach(
|
||||||
status.getMatchingNodes(portDefinition.nodeName).forEach(node -> {
|
(portNum, value) -> dataReceived("%s%d".formatted(portDefinition.portName, portNum), value)));
|
||||||
String sPortNum = node.getNodeName().replace(portDefinition.nodeName, "");
|
|
||||||
try {
|
|
||||||
int portNum = Integer.parseInt(sPortNum) + 1;
|
|
||||||
double value = Double
|
|
||||||
.parseDouble(node.getTextContent().replace("dn", "1").replace("up", "0"));
|
|
||||||
dataReceived("%s%d".formatted(portDefinition.portName, portNum), value);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
logger.warn(e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChannels(@Nullable StatusFile status) {
|
private void updateChannels(@Nullable StatusFile status) {
|
||||||
List<Channel> channels = new ArrayList<>(getThing().getChannels());
|
List<Channel> channels = new ArrayList<>(getThing().getChannels());
|
||||||
PortDefinition.AS_SET.forEach(portDefinition -> {
|
PortDefinition.AS_SET.forEach(portDefinition -> {
|
||||||
int nbElements = status != null ? status.getMaxNumberofNodeType(portDefinition) : portDefinition.quantity;
|
int nbElements = status != null ? status.getPorts(portDefinition).size() : portDefinition.quantity;
|
||||||
for (int i = 0; i < nbElements; i++) {
|
for (int i = 0; i < nbElements; i++) {
|
||||||
ChannelUID portChannelUID = createChannels(portDefinition, i, channels);
|
ChannelUID portChannelUID = createChannels(portDefinition, i, channels);
|
||||||
portDatas.put(portChannelUID.getId(), new PortData());
|
portDatas.put(portChannelUID.getId(), new PortData());
|
||||||
@ -181,7 +168,7 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
|
|
||||||
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
||||||
connector.dispose();
|
connector.dispose();
|
||||||
connector = null;
|
deviceConnector = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
portDatas.values().stream().forEach(PortData::dispose);
|
portDatas.values().stream().forEach(PortData::dispose);
|
||||||
@ -205,29 +192,25 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
ChannelUID mainChannelUID = new ChannelUID(groupUID, ndx);
|
ChannelUID mainChannelUID = new ChannelUID(groupUID, ndx);
|
||||||
ChannelTypeUID channelType = new ChannelTypeUID(BINDING_ID, advancedChannelTypeName);
|
ChannelTypeUID channelType = new ChannelTypeUID(BINDING_ID, advancedChannelTypeName);
|
||||||
switch (portDefinition) {
|
switch (portDefinition) {
|
||||||
case ANALOG:
|
case ANALOG -> {
|
||||||
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
|
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
|
||||||
.withLabel("Analog Input " + ndx).withType(channelType), channels);
|
.withLabel("Analog Input " + ndx).withType(channelType), channels);
|
||||||
addIfChannelAbsent(
|
addIfChannelAbsent(
|
||||||
ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-voltage"), "Number:ElectricPotential")
|
ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-voltage"), "Number:ElectricPotential")
|
||||||
.withType(new ChannelTypeUID(BINDING_ID, CHANNEL_VOLTAGE)).withLabel("Voltage " + ndx),
|
.withType(new ChannelTypeUID(BINDING_ID, CHANNEL_VOLTAGE)).withLabel("Voltage " + ndx),
|
||||||
channels);
|
channels);
|
||||||
break;
|
}
|
||||||
case CONTACT:
|
case CONTACT -> {
|
||||||
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.CONTACT)
|
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.CONTACT)
|
||||||
.withLabel("Contact " + ndx).withType(channelType), channels);
|
.withLabel("Contact " + ndx).withType(channelType), channels);
|
||||||
addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-event"), null)
|
addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-event"), null)
|
||||||
.withType(new ChannelTypeUID(BINDING_ID, TRIGGER_CONTACT + (portIndex < 8 ? "" : "Advanced")))
|
.withType(new ChannelTypeUID(BINDING_ID, TRIGGER_CONTACT + (portIndex < 8 ? "" : "Advanced")))
|
||||||
.withLabel("Contact " + ndx + " Event").withKind(ChannelKind.TRIGGER), channels);
|
.withLabel("Contact " + ndx + " Event").withKind(ChannelKind.TRIGGER), channels);
|
||||||
break;
|
}
|
||||||
case COUNTER:
|
case COUNTER -> addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
|
||||||
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
|
.withLabel("Counter " + ndx).withType(channelType), channels);
|
||||||
.withLabel("Counter " + ndx).withType(channelType), channels);
|
case RELAY -> addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH)
|
||||||
break;
|
.withLabel("Relay " + ndx).withType(channelType), channels);
|
||||||
case RELAY:
|
|
||||||
addIfChannelAbsent(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH)
|
|
||||||
.withLabel("Relay " + ndx).withType(channelType), channels);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-duration"), "Number:Time")
|
addIfChannelAbsent(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-duration"), "Number:Time")
|
||||||
@ -244,7 +227,7 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
|
|
||||||
private boolean ignoreCondition(double newValue, PortData portData, Configuration configuration,
|
private boolean ignoreCondition(double newValue, PortData portData, Configuration configuration,
|
||||||
PortDefinition portDefinition, Instant now) {
|
PortDefinition portDefinition, Instant now) {
|
||||||
if (!portData.isInitializing()) { // Always accept if portData is not initialized
|
if (portData.isInitialized()) { // Always accept if portData is not initialized
|
||||||
double prevValue = portData.getValue();
|
double prevValue = portData.getValue();
|
||||||
if (newValue == prevValue) { // Always reject if the value did not change
|
if (newValue == prevValue) { // Always reject if the value did not change
|
||||||
return true;
|
return true;
|
||||||
@ -265,68 +248,57 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
@Override
|
@Override
|
||||||
public void dataReceived(String port, double value) {
|
public void dataReceived(String port, double value) {
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
Channel channel = thing.getChannel(PortDefinition.asChannelId(port));
|
if (thing.getChannel(PortDefinition.asChannelId(port)) instanceof Channel channel) {
|
||||||
if (channel != null) {
|
|
||||||
String channelId = channel.getUID().getId();
|
String channelId = channel.getUID().getId();
|
||||||
String groupId = channel.getUID().getGroupId();
|
|
||||||
PortData portData = portDatas.get(channelId);
|
if (portDatas.get(channelId) instanceof PortData portData
|
||||||
if (portData != null && groupId != null) {
|
&& channel.getUID().getGroupId() instanceof String groupId) {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
long sinceLastChange = Duration.between(portData.getTimestamp(), now).toMillis();
|
|
||||||
Configuration configuration = channel.getConfiguration();
|
Configuration configuration = channel.getConfiguration();
|
||||||
PortDefinition portDefinition = PortDefinition.fromGroupId(groupId);
|
PortDefinition portDefinition = PortDefinition.fromGroupId(groupId);
|
||||||
if (ignoreCondition(value, portData, configuration, portDefinition, now)) {
|
if (ignoreCondition(value, portData, configuration, portDefinition, now)) {
|
||||||
logger.debug("Ignore condition met for port '{}' with data '{}'", port, value);
|
logger.trace("Ignore condition met for port '{}' with data '{}'", port, value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.debug("About to update port '{}' with data '{}'", port, value);
|
logger.debug("About to update port '{}' with data '{}'", port, value);
|
||||||
State state = UnDefType.NULL;
|
long sinceLastChange = Duration.between(portData.getTimestamp(), now).toMillis();
|
||||||
switch (portDefinition) {
|
State state = switch (portDefinition) {
|
||||||
case COUNTER:
|
case COUNTER -> new DecimalType(value);
|
||||||
state = new DecimalType(value);
|
case RELAY -> OnOffType.from(value == 1);
|
||||||
break;
|
case ANALOG -> {
|
||||||
case RELAY:
|
|
||||||
state = OnOffType.from(value == 1);
|
|
||||||
break;
|
|
||||||
case ANALOG:
|
|
||||||
state = new DecimalType(value);
|
|
||||||
updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
|
updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
|
||||||
new QuantityType<>(value * ANALOG_SAMPLING, Units.VOLT));
|
new QuantityType<>(value * ANALOG_SAMPLING, Units.VOLT));
|
||||||
break;
|
yield new DecimalType(value);
|
||||||
case CONTACT:
|
}
|
||||||
DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
|
case CONTACT -> {
|
||||||
portData.cancelPulsing();
|
portData.cancelPulsing();
|
||||||
state = value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
|
DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
|
||||||
switch ((OpenClosedType) state) {
|
|
||||||
case CLOSED:
|
if (value == 1) { // CLOSED
|
||||||
if (config.longPressTime != 0 && !portData.isInitializing()) {
|
if (config.longPressTime != 0 && portData.isInitialized()) {
|
||||||
scheduler.schedule(new LongPressEvaluator(channel, port, portData),
|
scheduler.schedule(new LongPressEvaluator(channel, port, portData),
|
||||||
config.longPressTime, TimeUnit.MILLISECONDS);
|
config.longPressTime, TimeUnit.MILLISECONDS);
|
||||||
} else if (config.pulsePeriod != 0) {
|
} else if (config.pulsePeriod != 0) {
|
||||||
portData.setPulsing(scheduler.scheduleWithFixedDelay(() -> {
|
portData.setPulsing(scheduler.scheduleWithFixedDelay(() -> {
|
||||||
triggerPushButtonChannel(channel, EVENT_PULSE);
|
triggerPushButtonChannel(channel, EVENT_PULSE);
|
||||||
}, config.pulsePeriod, config.pulsePeriod, TimeUnit.MILLISECONDS));
|
}, config.pulsePeriod, config.pulsePeriod, TimeUnit.MILLISECONDS));
|
||||||
if (config.pulseTimeout != 0) {
|
if (config.pulseTimeout != 0) {
|
||||||
scheduler.schedule(portData::cancelPulsing, config.pulseTimeout,
|
portData.setPulseCanceler(scheduler.schedule(portData::cancelPulsing,
|
||||||
TimeUnit.MILLISECONDS);
|
config.pulseTimeout, TimeUnit.MILLISECONDS));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case OPEN:
|
} else if (portData.isInitialized() && sinceLastChange < config.longPressTime) {
|
||||||
if (!portData.isInitializing() && config.longPressTime != 0
|
triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
|
||||||
&& sinceLastChange < config.longPressTime) {
|
|
||||||
triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (!portData.isInitializing()) {
|
if (portData.isInitialized()) {
|
||||||
triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
|
triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
|
||||||
}
|
}
|
||||||
break;
|
yield value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
updateIfLinked(channelId, state);
|
updateIfLinked(channelId, state);
|
||||||
if (!portData.isInitializing()) {
|
if (portData.isInitialized()) {
|
||||||
updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
|
updateIfLinked(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
|
||||||
new QuantityType<>(sinceLastChange / 1000, Units.SECOND));
|
new QuantityType<>(sinceLastChange / 1000, Units.SECOND));
|
||||||
}
|
}
|
||||||
@ -354,21 +326,18 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
logger.debug("Received channel: {}, command: {}", channelUID, command);
|
logger.debug("Received channel: {}, command: {}", channelUID, command);
|
||||||
|
|
||||||
Channel channel = thing.getChannel(channelUID.getId());
|
if (thing.getChannel(channelUID.getId()) instanceof Channel channel
|
||||||
String groupId = channelUID.getGroupId();
|
&& channelUID.getGroupId() instanceof String groupId //
|
||||||
|
&& command instanceof OnOffType onOffCommand //
|
||||||
if (channel == null || groupId == null) {
|
&& isValidPortId(channelUID) //
|
||||||
return;
|
&& PortDefinition.RELAY.equals(PortDefinition.fromGroupId(groupId))
|
||||||
}
|
|
||||||
if (command instanceof OnOffType onOffCommand && isValidPortId(channelUID)
|
|
||||||
&& PortDefinition.fromGroupId(groupId) == PortDefinition.RELAY
|
|
||||||
&& deviceConnector instanceof Ipx800DeviceConnector connector) {
|
&& deviceConnector instanceof Ipx800DeviceConnector connector) {
|
||||||
RelayOutputConfiguration config = channel.getConfiguration().as(RelayOutputConfiguration.class);
|
RelayOutputConfiguration config = channel.getConfiguration().as(RelayOutputConfiguration.class);
|
||||||
String id = channelUID.getIdWithoutGroup();
|
connector.setOutput(channelUID.getIdWithoutGroup(), OnOffType.ON.equals(onOffCommand) ? 1 : 0,
|
||||||
connector.getParser().setOutput(id, onOffCommand == OnOffType.ON ? 1 : 0, config.pulse);
|
config.pulse);
|
||||||
return;
|
} else {
|
||||||
|
logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
|
||||||
}
|
}
|
||||||
logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidPortId(ChannelUID channelUID) {
|
private boolean isValidPortId(ChannelUID channelUID) {
|
||||||
@ -377,13 +346,13 @@ public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventList
|
|||||||
|
|
||||||
public void resetCounter(int counter) {
|
public void resetCounter(int counter) {
|
||||||
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
||||||
connector.getParser().resetCounter(counter);
|
connector.resetCounter(counter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
if (deviceConnector instanceof Ipx800DeviceConnector connector) {
|
||||||
connector.getParser().resetPLC();
|
connector.resetPLC();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ package org.openhab.binding.gce.internal.model;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.gce.internal.handler.Ipx800DeviceConnector;
|
|
||||||
import org.openhab.binding.gce.internal.handler.Ipx800EventListener;
|
import org.openhab.binding.gce.internal.handler.Ipx800EventListener;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -33,22 +32,19 @@ public class M2MMessageParser {
|
|||||||
.compile("I=" + IO_DESCRIPTOR + "&O=" + IO_DESCRIPTOR + "&([AC]\\d{1,2}=\\d+&)*[^I]*");
|
.compile("I=" + IO_DESCRIPTOR + "&O=" + IO_DESCRIPTOR + "&([AC]\\d{1,2}=\\d+&)*[^I]*");
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(M2MMessageParser.class);
|
private final Logger logger = LoggerFactory.getLogger(M2MMessageParser.class);
|
||||||
private final Ipx800DeviceConnector connector;
|
|
||||||
private final Ipx800EventListener listener;
|
private final Ipx800EventListener listener;
|
||||||
|
|
||||||
private String expectedResponse = "";
|
private String expectedResponse = "";
|
||||||
|
|
||||||
public M2MMessageParser(Ipx800DeviceConnector connector, Ipx800EventListener listener) {
|
public M2MMessageParser(Ipx800EventListener listener) {
|
||||||
this.connector = connector;
|
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
public void unsolicitedUpdate(String data) {
|
public void unsolicitedUpdate(String data) {
|
||||||
if (IO_PATTERN.matcher(data).matches()) {
|
if ("OK".equals(data)) { // If OK, do nothing special
|
||||||
|
} else if ("? Bad command".equals(data)) {
|
||||||
|
logger.warn(data);
|
||||||
|
} else if (IO_PATTERN.matcher(data).matches()) {
|
||||||
PortDefinition portDefinition = PortDefinition.fromM2MCommand(expectedResponse);
|
PortDefinition portDefinition = PortDefinition.fromM2MCommand(expectedResponse);
|
||||||
decodeDataLine(portDefinition, data);
|
decodeDataLine(portDefinition, data);
|
||||||
} else if (VALIDATION_PATTERN.matcher(data).matches()) {
|
} else if (VALIDATION_PATTERN.matcher(data).matches()) {
|
||||||
@ -72,8 +68,9 @@ public class M2MMessageParser {
|
|||||||
}
|
}
|
||||||
} else if (!expectedResponse.isEmpty()) {
|
} else if (!expectedResponse.isEmpty()) {
|
||||||
setStatus(expectedResponse, Double.parseDouble(data));
|
setStatus(expectedResponse, Double.parseDouble(data));
|
||||||
|
} else {
|
||||||
|
logger.warn("Unable to handle data received: {}", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedResponse = "";
|
expectedResponse = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,47 +81,17 @@ public class M2MMessageParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setStatus(String port, double value) {
|
private void setStatus(String port, double value) {
|
||||||
logger.debug("Received {} : {}", port, value);
|
logger.debug("Received {} on port {}", value, port);
|
||||||
listener.dataReceived(port, value);
|
listener.dataReceived(port, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExpectedResponse(String expectedResponse) {
|
public void setExpectedResponse(String expectedResponse) {
|
||||||
if (expectedResponse.endsWith("s")) { // GetInputs or GetOutputs
|
if (expectedResponse.endsWith("s")) { // GetInputs or GetOutputs
|
||||||
this.expectedResponse = expectedResponse;
|
this.expectedResponse = expectedResponse;
|
||||||
} else { // GetAnx or GetCountx
|
return;
|
||||||
PortDefinition portType = PortDefinition.fromM2MCommand(expectedResponse);
|
|
||||||
this.expectedResponse = expectedResponse.replaceAll(portType.m2mCommand, portType.portName);
|
|
||||||
}
|
}
|
||||||
}
|
// GetAnx or GetCountx
|
||||||
|
PortDefinition portType = PortDefinition.fromM2MCommand(expectedResponse);
|
||||||
/**
|
this.expectedResponse = expectedResponse.replaceAll(portType.m2mCommand, portType.portName);
|
||||||
* Set output of the device sending the corresponding command
|
|
||||||
*
|
|
||||||
* @param targetPort
|
|
||||||
* @param targetValue
|
|
||||||
*/
|
|
||||||
public void setOutput(String targetPort, int targetValue, boolean pulse) {
|
|
||||||
logger.debug("Sending {} to {}", targetValue, targetPort);
|
|
||||||
String command = "Set%02d%s%s".formatted(Integer.parseInt(targetPort), targetValue, pulse ? "p" : "");
|
|
||||||
connector.send(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the counter value to 0
|
|
||||||
*
|
|
||||||
* @param targetCounter
|
|
||||||
*/
|
|
||||||
public void resetCounter(int targetCounter) {
|
|
||||||
logger.debug("Resetting counter {} to 0", targetCounter);
|
|
||||||
connector.send("ResetCount%d".formatted(targetCounter));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void errorOccurred(Exception e) {
|
|
||||||
logger.warn("Error received from connector : {}", e.getMessage());
|
|
||||||
listener.errorOccurred(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetPLC() {
|
|
||||||
connector.send("Reset");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
package org.openhab.binding.gce.internal.model;
|
package org.openhab.binding.gce.internal.model;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link PortData} is responsible for holding data regarding current status of a port.
|
* The {@link PortData} is responsible for holding data regarding current status of a port.
|
||||||
@ -27,15 +27,27 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
public class PortData {
|
public class PortData {
|
||||||
private double value = -1;
|
private double value = -1;
|
||||||
private Instant timestamp = Instant.now();
|
private Instant timestamp = Instant.now();
|
||||||
private Optional<ScheduledFuture<?>> pulsing = Optional.empty();
|
private @Nullable ScheduledFuture<?> pulsing;
|
||||||
|
private @Nullable ScheduledFuture<?> pulseCanceler;
|
||||||
|
|
||||||
public void cancelPulsing() {
|
public void cancelPulsing() {
|
||||||
pulsing.ifPresent(pulse -> pulse.cancel(true));
|
if (pulsing instanceof ScheduledFuture job) {
|
||||||
pulsing = Optional.empty();
|
job.cancel(true);
|
||||||
|
pulsing = null;
|
||||||
|
}
|
||||||
|
cancelCanceler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelCanceler() {
|
||||||
|
if (pulseCanceler instanceof ScheduledFuture job) {
|
||||||
|
job.cancel(true);
|
||||||
|
pulseCanceler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
cancelPulsing();
|
cancelPulsing();
|
||||||
|
cancelCanceler();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setData(double value, Instant timestamp) {
|
public void setData(double value, Instant timestamp) {
|
||||||
@ -53,10 +65,14 @@ public class PortData {
|
|||||||
|
|
||||||
public void setPulsing(ScheduledFuture<?> pulsing) {
|
public void setPulsing(ScheduledFuture<?> pulsing) {
|
||||||
cancelPulsing();
|
cancelPulsing();
|
||||||
this.pulsing = Optional.of(pulsing);
|
this.pulsing = pulsing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInitializing() {
|
public boolean isInitialized() {
|
||||||
return value == -1;
|
return value != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPulseCanceler(ScheduledFuture<?> pulseCanceler) {
|
||||||
|
this.pulseCanceler = pulseCanceler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ public enum PortDefinition {
|
|||||||
public final String m2mCommand; // associated M2M command
|
public final String m2mCommand; // associated M2M command
|
||||||
public final int quantity; // base number of ports
|
public final int quantity; // base number of ports
|
||||||
|
|
||||||
PortDefinition(String nodeName, String portName, String m2mCommand, int quantity) {
|
private PortDefinition(String nodeName, String portName, String m2mCommand, int quantity) {
|
||||||
this.nodeName = nodeName;
|
this.nodeName = nodeName;
|
||||||
this.portName = portName;
|
this.portName = portName;
|
||||||
this.m2mCommand = m2mCommand;
|
this.m2mCommand = m2mCommand;
|
||||||
|
@ -12,14 +12,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.gce.internal.model;
|
package org.openhab.binding.gce.internal.model;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.Map;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,12 +30,14 @@ import org.w3c.dom.NodeList;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class StatusFile {
|
public class StatusFile {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(StatusFile.class);
|
||||||
private final Element root;
|
private final Element root;
|
||||||
|
private final NodeList childs;
|
||||||
|
|
||||||
public StatusFile(Document doc) {
|
public StatusFile(Document doc) {
|
||||||
this.root = doc.getDocumentElement();
|
this.root = doc.getDocumentElement();
|
||||||
root.normalize();
|
root.normalize();
|
||||||
|
this.childs = root.getChildNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMac() {
|
public String getMac() {
|
||||||
@ -45,14 +48,20 @@ public class StatusFile {
|
|||||||
return root.getElementsByTagName("version").item(0).getTextContent();
|
return root.getElementsByTagName("version").item(0).getTextContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Node> getMatchingNodes(String criteria) {
|
public Map<Integer, Double> getPorts(PortDefinition portDefinition) {
|
||||||
NodeList nodeList = root.getChildNodes();
|
Map<Integer, Double> result = new HashMap<>();
|
||||||
return IntStream.range(0, nodeList.getLength()).boxed().map(nodeList::item)
|
|
||||||
.filter(node -> node.getNodeName().startsWith(criteria)).sorted(Comparator.comparing(Node::getNodeName))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxNumberofNodeType(PortDefinition portDefinition) {
|
String searched = portDefinition.nodeName;
|
||||||
return getMatchingNodes(portDefinition.nodeName).size();
|
|
||||||
|
IntStream.range(0, childs.getLength()).boxed().map(childs::item)
|
||||||
|
.filter(node -> node.getNodeName().startsWith(searched)).forEach(node -> {
|
||||||
|
try {
|
||||||
|
result.put(Integer.parseInt(node.getNodeName().replace(searched, "")) + 1,
|
||||||
|
Double.parseDouble(node.getTextContent().replace("dn", "1").replace("up", "0")));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warn(e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class takes care of interpreting the status.xml file
|
* This class takes care of providing the IPX status file
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
*/
|
*/
|
||||||
@ -34,7 +34,7 @@ public class StatusFileAccessor {
|
|||||||
private final String url;
|
private final String url;
|
||||||
|
|
||||||
public StatusFileAccessor(String hostname) {
|
public StatusFileAccessor(String hostname) {
|
||||||
this.url = String.format(URL_TEMPLATE, hostname);
|
this.url = URL_TEMPLATE.formatted(hostname);
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
factory.setXIncludeAware(false);
|
factory.setXIncludeAware(false);
|
||||||
factory.setExpandEntityReferences(false);
|
factory.setExpandEntityReferences(false);
|
||||||
@ -45,12 +45,11 @@ public class StatusFileAccessor {
|
|||||||
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||||
builder = factory.newDocumentBuilder();
|
builder = factory.newDocumentBuilder();
|
||||||
} catch (ParserConfigurationException e) {
|
} catch (ParserConfigurationException e) {
|
||||||
throw new IllegalArgumentException("Error initializing StatusFileInterpreter", e);
|
throw new IllegalArgumentException("Error initializing StatusFileAccessor", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StatusFile read() throws SAXException, IOException {
|
public StatusFile read() throws SAXException, IOException {
|
||||||
StatusFile document = new StatusFile(builder.parse(url));
|
return new StatusFile(builder.parse(url));
|
||||||
return document;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user