Review the whole binding

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
clinique 2024-12-27 10:21:21 +01:00
parent ed2a6d5700
commit e12876ba11
7 changed files with 186 additions and 203 deletions

View File

@ -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");
}
} }

View File

@ -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();
} }
} }

View File

@ -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");
} }
} }

View File

@ -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;
} }
} }

View File

@ -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;

View File

@ -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;
} }
} }

View File

@ -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;
} }
} }