SUPPORTED_THING_TYPES_UIDS = Collections.singleton(IPXV3_THING_TYPE);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ return IPXV3_THING_TYPE.equals(thingTypeUID) ? new Ipx800v3Handler(thing) : null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/IIpx800Actions.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/IIpx800Actions.java
new file mode 100644
index 0000000000000..03cd5d15edc8f
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/IIpx800Actions.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.action;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link IIpx800Actions} defines the interface for all thing actions supported by the binding.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public interface IIpx800Actions {
+ public void resetCounter(Integer counter);
+
+ public void reset(@Nullable Integer placeholder);
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/Ipx800Actions.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/Ipx800Actions.java
new file mode 100644
index 0000000000000..cfc07b3ee9697
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/action/Ipx800Actions.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.action;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.openhab.binding.gce.internal.handler.Ipx800v3Handler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {Ipx800Actions } defines rule actions for the GCE binding.
+ *
+ * Note: The static method invokeMethodOf handles the case where
+ * the test actions instanceof Ipx800Actions fails. This test can fail
+ * due to an issue in openHAB core v2.5.0 where the {@link Ipx800Actions} class
+ * can be loaded by a different classloader than the actions instance.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@ThingActionsScope(name = "gce")
+@NonNullByDefault
+public class Ipx800Actions implements ThingActions, IIpx800Actions {
+ private final Logger logger = LoggerFactory.getLogger(Ipx800Actions.class);
+
+ protected @Nullable Ipx800v3Handler handler;
+
+ public Ipx800Actions() {
+ logger.debug("IPX800 actions service instanciated");
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof Ipx800v3Handler) {
+ this.handler = (Ipx800v3Handler) handler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return this.handler;
+ }
+
+ @Override
+ @RuleAction(label = "GCE : Reset counter", description = "Resets to 0 value of a given counter")
+ public void resetCounter(
+ @ActionInput(name = "counter", label = "Counter", required = true, description = "Id of the counter", type = "java.lang.Integer") Integer counter) {
+ logger.debug("IPX800 action 'resetCounter' called");
+ Ipx800v3Handler theHandler = this.handler;
+ if (theHandler != null) {
+ theHandler.resetCounter(counter);
+ } else {
+ logger.warn("Method call resetCounter failed because IPX800 action service ThingHandler is null!");
+ }
+ }
+
+ @Override
+ @RuleAction(label = "GCE : Reset PLC", description = "Restarts the IPX800")
+ public void reset(
+ @ActionInput(name = "placeholder", label = "Placeholder", required = false, description = "This parameter is not used", type = "java.lang.Integer") @Nullable Integer placeholder) {
+ logger.debug("IPX800 action 'reset' called");
+ Ipx800v3Handler theHandler = this.handler;
+ if (theHandler != null) {
+ theHandler.reset();
+ } else {
+ logger.warn("Method call reset failed because IPX800 action service ThingHandler is null!");
+ }
+ }
+
+ public static void resetCounter(@Nullable ThingActions actions, Integer counter) {
+ invokeMethodOf(actions).resetCounter(counter);
+ }
+
+ public static void reset(@Nullable ThingActions actions, @Nullable Integer placeholder) {
+ invokeMethodOf(actions).reset(placeholder);
+ }
+
+ private static IIpx800Actions invokeMethodOf(@Nullable ThingActions actions) {
+ if (actions == null) {
+ throw new IllegalArgumentException("actions cannot be null");
+ }
+ if (actions.getClass().getName().equals(Ipx800Actions.class.getName())) {
+ if (actions instanceof IIpx800Actions) {
+ return (IIpx800Actions) actions;
+ } else {
+ return (IIpx800Actions) Proxy.newProxyInstance(IIpx800Actions.class.getClassLoader(),
+ new Class[] { IIpx800Actions.class }, (Object proxy, Method method, Object[] args) -> {
+ Method m = actions.getClass().getDeclaredMethod(method.getName(),
+ method.getParameterTypes());
+ return m.invoke(actions, args);
+ });
+ }
+ }
+ throw new IllegalArgumentException("Actions is not an instance of Ipx800Actions");
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/AnalogInputConfiguration.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/AnalogInputConfiguration.java
new file mode 100644
index 0000000000000..aafab96a0872f
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/AnalogInputConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link AnalogInputConfiguration} class holds configuration informations of
+ * an ipx800 Analog Input port.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class AnalogInputConfiguration {
+ public long hysteresis;
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/DigitalInputConfiguration.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/DigitalInputConfiguration.java
new file mode 100644
index 0000000000000..6394d447cd2ee
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/DigitalInputConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.core.Configuration;
+
+/**
+ * The {@link DigitalInputConfiguration} class holds configuration informations of
+ * an ipx800 Digital Input port.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class DigitalInputConfiguration extends Configuration {
+ public long debouncePeriod = 0;
+ public long longPressTime = 0;
+ public long pulsePeriod = 0;
+ public long pulseTimeout = 0;
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/Ipx800Configuration.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/Ipx800Configuration.java
new file mode 100644
index 0000000000000..aae10b3bb3279
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/Ipx800Configuration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Ipx800Configuration} class holds configuration informations of
+ * the ipx800v3 thing.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class Ipx800Configuration {
+ public String hostname = "";
+ public int portNumber = 9870;
+ public int pullInterval = 5000;
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/RelayOutputConfiguration.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/RelayOutputConfiguration.java
new file mode 100644
index 0000000000000..77cc5a3b8f67e
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/config/RelayOutputConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.core.Configuration;
+
+/**
+ * The {@link RelayOutputConfiguration} class holds configuration informations of
+ * an ipx800 relay output.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class RelayOutputConfiguration extends Configuration {
+ public boolean pulse;
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800DeviceConnector.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800DeviceConnector.java
new file mode 100644
index 0000000000000..771ee566f3aa9
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800DeviceConnector.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.handler;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.gce.internal.model.M2MMessageParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Ipx800DeviceConnector} is responsible for connecting,
+ * reading, writing and disconnecting from the Ipx800.
+ *
+ * @author Seebag - Initial Contribution
+ * @author Gaël L'hopital - Ported and adapted for OH2
+ */
+@NonNullByDefault
+public class Ipx800DeviceConnector extends Thread {
+ private final Logger logger = LoggerFactory.getLogger(Ipx800DeviceConnector.class);
+ private static final int DEFAULT_SOCKET_TIMEOUT_MS = 5000;
+ private static final int DEFAULT_RECONNECT_TIMEOUT_MS = 5000;
+ private static final int MAX_KEEPALIVE_FAILURE = 3;
+ private static final String ENDL = "\r\n";
+
+ private final String hostname;
+ public final int portNumber;
+ private @Nullable M2MMessageParser parser;
+
+ private @NonNullByDefault({}) Socket client;
+ private @NonNullByDefault({}) BufferedReader in;
+ private @NonNullByDefault({}) PrintWriter out;
+
+ private int failedKeepalive = 0;
+ private boolean waitingKeepaliveResponse = false;
+
+ public Ipx800DeviceConnector(String hostname, int portNumber, ThingUID uid) {
+ super("OH-binding-" + uid);
+ this.hostname = hostname;
+ this.portNumber = portNumber;
+ setDaemon(true);
+ }
+
+ public synchronized void send(String message) {
+ logger.debug("Sending '{}' to Ipx800", message);
+ out.write(message + ENDL);
+ out.flush();
+ }
+
+ /**
+ * Connect to the ipx800
+ *
+ * @throws IOException
+ */
+ private void connect() throws IOException {
+ disconnect();
+ logger.debug("Connecting {}:{}...", hostname, portNumber);
+ client = new Socket(hostname, portNumber);
+ client.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MS);
+ client.getInputStream().skip(client.getInputStream().available());
+ in = new BufferedReader(new InputStreamReader(client.getInputStream()));
+ out = new PrintWriter(client.getOutputStream(), true);
+ }
+
+ /**
+ * Disconnect the device
+ */
+ private void disconnect() {
+ logger.debug("Disconnecting");
+
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignore) {
+ }
+ this.in = null;
+ }
+ if (out != null) {
+ out.close();
+ this.out = null;
+ }
+ if (client != null) {
+ try {
+ client.close();
+ } catch (IOException ignore) {
+ }
+ this.client = null;
+ }
+ logger.debug("Disconnected");
+ }
+
+ /**
+ * Stop the device thread
+ */
+ public void destroyAndExit() {
+ interrupt();
+ disconnect();
+ }
+
+ /**
+ * 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
+ */
+ private void sendKeepalive() {
+ if (out != null) {
+ if (waitingKeepaliveResponse) {
+ failedKeepalive++;
+ logger.debug("Sending keepalive, attempt {}", failedKeepalive);
+ } else {
+ failedKeepalive = 0;
+ logger.debug("Sending keepalive");
+ }
+ out.println("GetIn01");
+ out.flush();
+ waitingKeepaliveResponse = true;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ waitingKeepaliveResponse = false;
+ failedKeepalive = 0;
+ connect();
+ while (!interrupted()) {
+ if (failedKeepalive > MAX_KEEPALIVE_FAILURE) {
+ throw new IOException("Max keep alive attempts has been reached");
+ }
+ try {
+ String command = in.readLine();
+ waitingKeepaliveResponse = false;
+ if (parser != null) {
+ parser.unsolicitedUpdate(command);
+ }
+ } catch (SocketTimeoutException e) {
+ handleException(e);
+ }
+ }
+ disconnect();
+ } catch (IOException e) {
+ handleException(e);
+ }
+ try {
+ Thread.sleep(DEFAULT_RECONNECT_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ destroyAndExit();
+ }
+ }
+
+ private void handleException(Exception e) {
+ if (!interrupted()) {
+ if (e instanceof SocketTimeoutException) {
+ sendKeepalive();
+ return;
+ } else if (e instanceof IOException) {
+ logger.warn("Communication error : '{}', will retry in {} ms", e, DEFAULT_RECONNECT_TIMEOUT_MS);
+ }
+ if (parser != null) {
+ parser.errorOccurred(e);
+ }
+ }
+ }
+
+ public void setParser(M2MMessageParser parser) {
+ this.parser = parser;
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800EventListener.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800EventListener.java
new file mode 100644
index 0000000000000..efd2292ca3c59
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800EventListener.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This interface defines interface to receive data from IPX800 controller.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public interface Ipx800EventListener {
+
+ /**
+ * Procedure for receive data from IPX800 controller.
+ *
+ * @param port Port (kind and number) receiving update
+ * @param value value updated
+ */
+ void dataReceived(String port, double value);
+
+ /**
+ * Procedure for receiving information fatal error.
+ *
+ * @param e Error occurred.
+ */
+ void errorOccurred(Exception e);
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800v3Handler.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800v3Handler.java
new file mode 100644
index 0000000000000..317a2ec088963
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/handler/Ipx800v3Handler.java
@@ -0,0 +1,392 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.handler;
+
+import static org.openhab.binding.gce.internal.GCEBindingConstants.*;
+
+import java.time.Duration;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.eclipse.smarthome.core.library.CoreItemFactory;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.OpenClosedType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelGroupUID;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
+import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder;
+import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder;
+import org.eclipse.smarthome.core.thing.type.ChannelKind;
+import org.eclipse.smarthome.core.thing.type.ChannelTypeUID;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.UnDefType;
+import org.openhab.binding.gce.internal.action.Ipx800Actions;
+import org.openhab.binding.gce.internal.config.AnalogInputConfiguration;
+import org.openhab.binding.gce.internal.config.DigitalInputConfiguration;
+import org.openhab.binding.gce.internal.config.Ipx800Configuration;
+import org.openhab.binding.gce.internal.config.RelayOutputConfiguration;
+import org.openhab.binding.gce.internal.model.M2MMessageParser;
+import org.openhab.binding.gce.internal.model.PortData;
+import org.openhab.binding.gce.internal.model.PortDefinition;
+import org.openhab.binding.gce.internal.model.StatusFileInterpreter;
+import org.openhab.binding.gce.internal.model.StatusFileInterpreter.StatusEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Ipx800v3Handler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class Ipx800v3Handler extends BaseThingHandler implements Ipx800EventListener {
+ private static final String PROPERTY_SEPARATOR = "-";
+ private static final double ANALOG_SAMPLING = 0.000050354;
+
+ private final Logger logger = LoggerFactory.getLogger(Ipx800v3Handler.class);
+
+ private @NonNullByDefault({}) Ipx800Configuration configuration;
+ private @NonNullByDefault({}) Ipx800DeviceConnector connector;
+ private @Nullable M2MMessageParser parser;
+ private @NonNullByDefault({}) StatusFileInterpreter statusFile;
+ private @Nullable ScheduledFuture> refreshJob;
+
+ private final Map portDatas = new HashMap<>();
+
+ private class LongPressEvaluator implements Runnable {
+ private final ZonedDateTime referenceTime;
+ private final String port;
+ private final String eventChannelId;
+
+ public LongPressEvaluator(Channel channel, String port, PortData portData) {
+ this.referenceTime = portData.getTimestamp();
+ this.port = port;
+ this.eventChannelId = channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT;
+ }
+
+ @Override
+ public void run() {
+ PortData currentData = portDatas.get(port);
+ if (currentData != null && currentData.getValue() == 1 && currentData.getTimestamp() == referenceTime) {
+ triggerChannel(eventChannelId, EVENT_LONG_PRESS);
+ }
+ }
+ }
+
+ public Ipx800v3Handler(Thing thing) {
+ super(thing);
+ logger.debug("Create a IPX800 Handler for thing '{}'", getThing().getUID());
+ }
+
+ @Override
+ public void initialize() {
+
+ configuration = getConfigAs(Ipx800Configuration.class);
+
+ logger.debug("Initializing IPX800 handler for uid '{}'", getThing().getUID());
+
+ statusFile = new StatusFileInterpreter(configuration.hostname, this);
+
+ if (thing.getProperties().isEmpty()) {
+ discoverAttributes();
+ }
+
+ connector = new Ipx800DeviceConnector(configuration.hostname, configuration.portNumber, getThing().getUID());
+ parser = new M2MMessageParser(connector, this);
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ refreshJob = scheduler.scheduleWithFixedDelay(statusFile::read, 3000, configuration.pullInterval,
+ TimeUnit.MILLISECONDS);
+
+ connector.start();
+ }
+
+ @Override
+ public void dispose() {
+ if (refreshJob != null) {
+ refreshJob.cancel(true);
+ refreshJob = null;
+ }
+
+ if (connector != null) {
+ connector.destroyAndExit();
+ }
+ parser = null;
+
+ portDatas.values().stream().forEach(portData -> {
+ if (portData != null) {
+ portData.destroy();
+ }
+ });
+ super.dispose();
+ }
+
+ protected void discoverAttributes() {
+ final Map properties = new HashMap<>();
+
+ properties.put(Thing.PROPERTY_VENDOR, "GCE Electronics");
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, statusFile.getElement(StatusEntry.VERSION));
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, statusFile.getElement(StatusEntry.CONFIG_MAC));
+ updateProperties(properties);
+
+ ThingBuilder thingBuilder = editThing();
+ List channels = new ArrayList<>(getThing().getChannels());
+
+ PortDefinition.asStream().forEach(portDefinition -> {
+ int nbElements = statusFile.getMaxNumberofNodeType(portDefinition);
+ for (int i = 0; i < nbElements; i++) {
+ createChannels(portDefinition, i, channels);
+ }
+ });
+
+ thingBuilder.withChannels(channels);
+ updateThing(thingBuilder.build());
+ }
+
+ private void createChannels(PortDefinition portDefinition, int portIndex, List channels) {
+ String ndx = Integer.toString(portIndex + 1);
+ String advancedChannelTypeName = portDefinition.toString()
+ + (portDefinition.isAdvanced(portIndex) ? "Advanced" : "");
+ ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), portDefinition.toString());
+ ChannelUID mainChannelUID = new ChannelUID(groupUID, ndx);
+ ChannelTypeUID channelType = new ChannelTypeUID(BINDING_ID, advancedChannelTypeName);
+ switch (portDefinition) {
+ case ANALOG:
+ channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER)
+ .withLabel("Analog Input " + ndx).withType(channelType).build());
+ channels.add(ChannelBuilder
+ .create(new ChannelUID(groupUID, ndx + "-voltage"), "Number:ElectricPotential")
+ .withLabel("Voltage " + ndx).withType(new ChannelTypeUID(BINDING_ID, CHANNEL_VOLTAGE)).build());
+ break;
+ case CONTACT:
+ channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.CONTACT).withLabel("Contact " + ndx)
+ .withType(channelType).build());
+ channels.add(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-event"), null)
+ .withLabel("Contact " + ndx + " Event").withKind(ChannelKind.TRIGGER)
+ .withType(new ChannelTypeUID(BINDING_ID, TRIGGER_CONTACT + (portIndex < 8 ? "" : "Advanced")))
+ .build());
+ break;
+ case COUNTER:
+ channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.NUMBER).withLabel("Counter " + ndx)
+ .withType(channelType).build());
+ break;
+ case RELAY:
+ channels.add(ChannelBuilder.create(mainChannelUID, CoreItemFactory.SWITCH).withLabel("Relay " + ndx)
+ .withType(channelType).build());
+ break;
+ }
+ channels.add(ChannelBuilder.create(new ChannelUID(groupUID, ndx + "-duration"), "Number:Time")
+ .withLabel("Previous state duration " + ndx)
+ .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_LAST_STATE_DURATION)).build());
+ }
+
+ @Override
+ public void errorOccurred(Exception e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+
+ private boolean ignoreCondition(double newValue, PortData portData, Configuration configuration,
+ PortDefinition portDefinition, ZonedDateTime now) {
+ if (!portData.isInitializing()) { // Always accept if portData is not initialized
+ double prevValue = portData.getValue();
+ if (newValue == prevValue) { // Always reject if the value did not change
+ return true;
+ }
+ if (portDefinition == PortDefinition.ANALOG) { // For analog values, check histeresis
+ AnalogInputConfiguration config = configuration.as(AnalogInputConfiguration.class);
+ long hysteresis = config.hysteresis / 2;
+ if (newValue <= prevValue + hysteresis && newValue >= prevValue - hysteresis) {
+ return true;
+ }
+ }
+ if (portDefinition == PortDefinition.CONTACT) { // For contact values, check debounce
+ DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
+ if (config.debouncePeriod != 0
+ && now.isBefore(portData.getTimestamp().plus(config.debouncePeriod, ChronoUnit.MILLIS))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void dataReceived(String port, double value) {
+ updateStatus(ThingStatus.ONLINE);
+ Channel channel = thing.getChannel(PortDefinition.asChannelId(port));
+ if (channel != null) {
+ String channelId = channel.getUID().getId();
+ String groupId = channel.getUID().getGroupId();
+ PortData portData = portDatas.get(channelId);
+ if (portData != null && groupId != null) {
+ ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
+ long sinceLastChange = Duration.between(portData.getTimestamp(), now).toMillis();
+ Configuration configuration = channel.getConfiguration();
+ PortDefinition portDefinition = PortDefinition.fromGroupId(groupId);
+ if (ignoreCondition(value, portData, configuration, portDefinition, now)) {
+ logger.debug("Ignore condition met for port '{}' with data '{}'", port, value);
+ return;
+ }
+ logger.debug("About to update port '{}' with data '{}'", port, value);
+ State state = UnDefType.UNDEF;
+ switch (portDefinition) {
+ case COUNTER:
+ state = new DecimalType(value);
+ break;
+ case RELAY:
+ state = value == 1 ? OnOffType.ON : OnOffType.OFF;
+ break;
+ case ANALOG:
+ state = new DecimalType(value);
+ updateState(channelId + PROPERTY_SEPARATOR + CHANNEL_VOLTAGE,
+ new QuantityType<>(value * ANALOG_SAMPLING, SmartHomeUnits.VOLT));
+ break;
+ case CONTACT:
+ DigitalInputConfiguration config = configuration.as(DigitalInputConfiguration.class);
+ portData.cancelPulsing();
+ state = value == 1 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
+ switch ((OpenClosedType) state) {
+ case CLOSED:
+ if (config.longPressTime != 0 && !portData.isInitializing()) {
+ scheduler.schedule(new LongPressEvaluator(channel, port, portData),
+ config.longPressTime, TimeUnit.MILLISECONDS);
+ } else if (config.pulsePeriod != 0) {
+ portData.setPulsing(scheduler.scheduleWithFixedDelay(() -> {
+ triggerPushButtonChannel(channel, EVENT_PULSE);
+ }, config.pulsePeriod, config.pulsePeriod, TimeUnit.MILLISECONDS));
+ if (config.pulseTimeout != 0) {
+ scheduler.schedule(portData::cancelPulsing, config.pulseTimeout,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ break;
+ case OPEN:
+ if (!portData.isInitializing() && config.longPressTime != 0
+ && sinceLastChange < config.longPressTime) {
+ triggerPushButtonChannel(channel, EVENT_SHORT_PRESS);
+ }
+ break;
+ }
+ if (!portData.isInitializing()) {
+ triggerPushButtonChannel(channel, value == 1 ? EVENT_PRESSED : EVENT_RELEASED);
+ }
+ break;
+ }
+
+ updateState(channelId, state);
+ if (!portData.isInitializing()) {
+ updateState(channelId + PROPERTY_SEPARATOR + CHANNEL_LAST_STATE_DURATION,
+ new QuantityType<>(sinceLastChange / 1000, SmartHomeUnits.SECOND));
+ }
+ portData.setData(value, now);
+ } else {
+ logger.debug("Received data '{}' for not configured port '{}'", value, port);
+ }
+ } else {
+ logger.debug("Received data '{}' for not configured channel '{}'", value, port);
+ }
+ }
+
+ protected void triggerPushButtonChannel(Channel channel, String event) {
+ logger.debug("Triggering event '{}' on channel '{}'", event, channel.getUID());
+ triggerChannel(channel.getUID().getId() + PROPERTY_SEPARATOR + TRIGGER_CONTACT, event);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Received channel: {}, command: {}", channelUID, command);
+
+ Channel channel = thing.getChannel(channelUID.getId());
+ String groupId = channelUID.getGroupId();
+
+ if (channel == null || groupId == null) {
+ return;
+ }
+ if (command instanceof OnOffType && isValidPortId(channelUID)
+ && PortDefinition.fromGroupId(groupId) == PortDefinition.RELAY) {
+ RelayOutputConfiguration config = channel.getConfiguration().as(RelayOutputConfiguration.class);
+ String id = channelUID.getIdWithoutGroup();
+ if (parser != null) {
+ parser.setOutput(id, (OnOffType) command == OnOffType.ON ? 1 : 0, config.pulse);
+ }
+ return;
+ }
+ logger.debug("Can not handle command '{}' on channel '{}'", command, channelUID);
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ logger.debug("channelLinked: {}", channelUID);
+ final String channelId = channelUID.getId();
+ if (isValidPortId(channelUID)) {
+ Channel channel = thing.getChannel(channelUID);
+ if (channel != null) {
+ PortData data = new PortData();
+ portDatas.put(channelId, data);
+ }
+ }
+ }
+
+ private boolean isValidPortId(ChannelUID channelUID) {
+ return channelUID.getIdWithoutGroup().chars().allMatch(Character::isDigit);
+ }
+
+ @Override
+ public void channelUnlinked(ChannelUID channelUID) {
+ super.channelUnlinked(channelUID);
+ PortData portData = portDatas.remove(channelUID.getId());
+ if (portData != null) {
+ portData.destroy();
+ }
+ }
+
+ public void resetCounter(int counter) {
+ if (parser != null) {
+ parser.resetCounter(counter);
+ }
+ }
+
+ public void reset() {
+ if (parser != null) {
+ parser.resetPLC();
+ }
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Collections.singletonList(Ipx800Actions.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/M2MMessageParser.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/M2MMessageParser.java
new file mode 100644
index 0000000000000..89a0c378288f3
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/M2MMessageParser.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.model;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gce.internal.handler.Ipx800DeviceConnector;
+import org.openhab.binding.gce.internal.handler.Ipx800EventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class handles message translation to and from the IPX.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class M2MMessageParser {
+ private static final String IO_DESCRIPTOR = "(\\d{32})";
+ private static final Pattern IO_PATTERN = Pattern.compile(IO_DESCRIPTOR);
+ private static final Pattern VALIDATION_PATTERN = Pattern
+ .compile("I=" + IO_DESCRIPTOR + "&O=" + IO_DESCRIPTOR + "&([AC]\\d{1,2}=\\d+&)*[^I]*");
+
+ private final Logger logger = LoggerFactory.getLogger(M2MMessageParser.class);
+ private final Ipx800DeviceConnector connector;
+ private final @Nullable Ipx800EventListener listener;
+
+ private String expectedResponse = "";
+
+ public M2MMessageParser(Ipx800DeviceConnector connector, @Nullable Ipx800EventListener listener) {
+ this.connector = connector;
+ this.listener = listener;
+ connector.setParser(this);
+ }
+
+ /**
+ *
+ * @param data
+ */
+ public void unsolicitedUpdate(String data) {
+ if (IO_PATTERN.matcher(data).matches()) {
+ PortDefinition portDefinition = PortDefinition.fromM2MCommand(expectedResponse);
+ decodeDataLine(portDefinition, data);
+ } else if (VALIDATION_PATTERN.matcher(data).matches()) {
+ for (String status : data.split("&")) {
+ String statusPart[] = status.split("=");
+ int portNumShift = 1;
+ PortDefinition portDefinition = PortDefinition.fromPortName(statusPart[0].substring(0, 1));
+ switch (portDefinition) {
+ case CONTACT:
+ case RELAY: {
+ decodeDataLine(portDefinition, statusPart[1]);
+ break;
+ }
+ case COUNTER:
+ portNumShift = 0; // Align counters on 1 based array
+ case ANALOG: {
+ int portNumber = Integer.parseInt(statusPart[0].substring(1)) + portNumShift;
+ setStatus(portDefinition.getPortName() + portNumber, Double.parseDouble(statusPart[1]));
+ }
+ }
+ }
+ } else if (!expectedResponse.isEmpty()) {
+ setStatus(expectedResponse, Double.parseDouble(data));
+ }
+
+ expectedResponse = "";
+ }
+
+ private void decodeDataLine(PortDefinition portDefinition, String data) {
+ for (int count = 0; count < data.length(); count++) {
+ setStatus(portDefinition.getPortName() + (count + 1), (double) data.charAt(count) - '0');
+ }
+ }
+
+ private void setStatus(String port, double value) {
+ logger.debug("Received {} : {}", port, value);
+ if (listener != null) {
+ listener.dataReceived(port, value);
+ }
+ }
+
+ public void setExpectedResponse(String expectedResponse) {
+ if (expectedResponse.endsWith("s")) { // GetInputs or GetOutputs
+ this.expectedResponse = expectedResponse;
+ } else { // GetAnx or GetCountx
+ PortDefinition portType = PortDefinition.fromM2MCommand(expectedResponse);
+ this.expectedResponse = expectedResponse.replaceAll(portType.getM2mCommand(), portType.getPortName());
+ }
+ }
+
+ /**
+ * 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 = String.format("Set%02d%s%s", 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(String.format("ResetCount%d", targetCounter));
+ }
+
+ public void errorOccurred(Exception e) {
+ logger.warn("Error received from connector : {}", e.getMessage());
+ if (listener != null) {
+ listener.errorOccurred(e);
+ }
+ }
+
+ public void resetPLC() {
+ connector.send("Reset");
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortData.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortData.java
new file mode 100644
index 0000000000000..4b4f923705aaa
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortData.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.model;
+
+import java.time.ZonedDateTime;
+import java.util.concurrent.ScheduledFuture;
+
+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.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class PortData {
+ private double value = -1;
+ private ZonedDateTime timestamp = ZonedDateTime.now();
+ private @Nullable ScheduledFuture> pulsing;
+
+ public void cancelPulsing() {
+ if (pulsing != null) {
+ pulsing.cancel(true);
+ }
+ pulsing = null;
+ }
+
+ public void destroy() {
+ cancelPulsing();
+ }
+
+ public void setData(double value, ZonedDateTime timestamp) {
+ this.value = value;
+ this.timestamp = timestamp;
+ }
+
+ public double getValue() {
+ return value;
+ }
+
+ public ZonedDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ public void setPulsing(ScheduledFuture> pulsing) {
+ this.pulsing = pulsing;
+ }
+
+ public boolean isInitializing() {
+ return value == -1;
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortDefinition.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortDefinition.java
new file mode 100644
index 0000000000000..1d9286dababc7
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/PortDefinition.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.model;
+
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PortDefinition} enum defines and handle port
+ * definition constants
+ *
+ * @author Gaël L'hopital - Initial Contribution
+ */
+@NonNullByDefault
+public enum PortDefinition {
+ COUNTER("count", "C", "GetCount", 8),
+ ANALOG("analog", "A", "GetAn", 4),
+ RELAY("led", "O", "GetOut", 8),
+ CONTACT("btn", "I", "GetIn", 8);
+
+ private final String nodeName; // Name used in the status xml file
+ private final String portName; // Name used by the M2M protocol
+ private final String m2mCommand; // associated M2M command
+ private final int quantity; // base number of ports
+
+ PortDefinition(String nodeName, String portName, String m2mCommand, int quantity) {
+ this.nodeName = nodeName;
+ this.portName = portName;
+ this.m2mCommand = m2mCommand;
+ this.quantity = quantity;
+ }
+
+ public String getNodeName() {
+ return nodeName;
+ }
+
+ public String getPortName() {
+ return portName;
+ }
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+
+ public boolean isAdvanced(int id) {
+ return id >= quantity;
+ }
+
+ public String getM2mCommand() {
+ return m2mCommand;
+ }
+
+ public static Stream asStream() {
+ return Stream.of(PortDefinition.values());
+ }
+
+ public static PortDefinition fromM2MCommand(String m2mCommand) {
+ return asStream().filter(v -> m2mCommand.startsWith(v.m2mCommand)).findFirst().get();
+ }
+
+ public static PortDefinition fromPortName(String portName) {
+ return asStream().filter(v -> portName.startsWith(v.portName)).findFirst().get();
+ }
+
+ public static PortDefinition fromGroupId(String groupId) {
+ return valueOf(groupId.toUpperCase());
+ }
+
+ public static String asChannelId(String portDefinition) {
+ String portKind = portDefinition.substring(0, 1);
+ PortDefinition result = asStream().filter(v -> v.portName.startsWith(portKind)).findFirst().get();
+ return result.toString() + "#" + portDefinition.substring(1);
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/StatusFileInterpreter.java b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/StatusFileInterpreter.java
new file mode 100644
index 0000000000000..07a5aa7032ab1
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/java/org/openhab/binding/gce/internal/model/StatusFileInterpreter.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2010-2020 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.gce.internal.model;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.io.net.http.HttpUtil;
+import org.openhab.binding.gce.internal.handler.Ipx800EventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * This class takes care of interpreting the status.xml file
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class StatusFileInterpreter {
+ private static final String URL_TEMPLATE = "http://%s/globalstatus.xml";
+ private final Logger logger = LoggerFactory.getLogger(StatusFileInterpreter.class);
+ private final String hostname;
+ private @Nullable Document doc;
+ private final Ipx800EventListener listener;
+
+ public static enum StatusEntry {
+ VERSION,
+ CONFIG_MAC;
+ }
+
+ public StatusFileInterpreter(String hostname, Ipx800EventListener listener) {
+ this.hostname = hostname;
+ this.listener = listener;
+ }
+
+ public void read() {
+ try {
+ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ String statusPage = HttpUtil.executeUrl("GET", String.format(URL_TEMPLATE, hostname), 5000);
+ InputStream inputStream = new ByteArrayInputStream(statusPage.getBytes());
+ Document document = builder.parse(inputStream);
+ document.getDocumentElement().normalize();
+ doc = document;
+ pushDatas();
+ inputStream.close();
+ } catch (IOException | SAXException | ParserConfigurationException e) {
+ logger.warn("Unable to read IPX800 status page : {}", e.getMessage());
+ doc = null;
+ }
+ }
+
+ private void pushDatas() {
+ Element root = getRoot();
+ if (root != null) {
+ PortDefinition.asStream().forEach(portDefinition -> {
+ List xmlNodes = getMatchingNodes(root.getChildNodes(), portDefinition.getNodeName());
+ xmlNodes.forEach(xmlNode -> {
+ String sPortNum = xmlNode.getNodeName().replace(portDefinition.getNodeName(), "");
+ int portNum = Integer.parseInt(sPortNum) + 1;
+ double value = Double.parseDouble(xmlNode.getTextContent().replace("dn", "1").replace("up", "0"));
+ listener.dataReceived(String.format("%s%d", portDefinition.getPortName(), portNum), value);
+ });
+ });
+ }
+ }
+
+ public String getElement(StatusEntry entry) {
+ Element root = getRoot();
+ if (root != null) {
+ return root.getElementsByTagName(entry.name().toLowerCase()).item(0).getTextContent();
+ } else {
+ return "";
+ }
+ }
+
+ private List getMatchingNodes(NodeList nodeList, String criteria) {
+ return IntStream.range(0, nodeList.getLength()).boxed().map(nodeList::item)
+ .filter(node -> node.getNodeName().startsWith(criteria))
+ .sorted(Comparator.comparing(o -> o.getNodeName())).collect(Collectors.toList());
+ }
+
+ public int getMaxNumberofNodeType(PortDefinition portDefinition) {
+ Element root = getRoot();
+ if (root != null) {
+ List filteredNodes = getMatchingNodes(root.getChildNodes(), portDefinition.getNodeName());
+ return filteredNodes.size();
+ }
+ return 0;
+ }
+
+ private @Nullable Element getRoot() {
+ if (doc == null) {
+ read();
+ }
+ if (doc != null) {
+ return doc.getDocumentElement();
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..0fcd7050f5980
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ GCE Electronics Binding
+ Provides access to IPX800 PLC build by GCE.
+ Gaël L'hopital
+
+
diff --git a/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/config/gceConfig.xml b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/config/gceConfig.xml
new file mode 100644
index 0000000000000..4f3b11b4afefd
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/config/gceConfig.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ network-address
+ Network Address
+ Network/IP address of the IPX800 without http(s) prefix.
+
+
+ Port Number
+ TCP client connection port.
+ 9870
+ true
+
+
+ Pull Interval
+ Delay for pulling Analog and Counters info (in milliseconds).
+ 5000
+
+
+
+
+
+ Debounce Time
+ 0
+
+
+ Long Press Time
+ Long press time in milliseconds.
+ 0
+
+
+ Pulse Period
+ Pulse period in milliseconds.
+ 0
+
+
+ Pulse Timeout
+ Maximum period for sending pulses in milliseconds.
+ 0
+
+
+
+
+
+ Pulse Output
+ false
+
+
+
+
+
+ Hysteresis
+ 0
+
+
+
+
diff --git a/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/channels.xml b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/channels.xml
new file mode 100644
index 0000000000000..4821a254463fa
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/channels.xml
@@ -0,0 +1,95 @@
+
+
+
+
+ Contact
+ Digital Input
+ Contact
+
+
+
+
+
+ Contact
+ Digital Input
+ Contact
+
+
+
+
+
+ Switch
+ Digital Output
+
+
+
+
+ Switch
+ Digital Output
+
+
+
+
+ Number
+ Counter
+
+
+
+
+ Number
+ Analog Input
+
+
+
+
+
+ Number
+ Analog Input
+
+
+
+
+ Number:Time
+ Previous State Duration
+ Duration of previous state before state change.
+
+
+
+
+ trigger
+ Push Button Trigger Channel
+
+
+ Pressed
+ Released
+ Short press
+ Long press
+ Pulse
+
+
+
+
+
+ trigger
+ Push Button Trigger Channel
+
+
+ Pressed
+ Released
+ Short press
+ Long press
+ Pulse
+
+
+
+
+
+ Number:ElectricPotential
+ Voltage
+ Voltage
+
+
+
+
diff --git a/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/ipx800v3.xml b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/ipx800v3.xml
new file mode 100644
index 0000000000000..21fecb448f505
--- /dev/null
+++ b/bundles/org.openhab.binding.gce/src/main/resources/ESH-INF/thing/ipx800v3.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ IPX800v3
+ The GCE IPX800v3 device
+
+
+
+
+
+
+
+
+
+
+
+
+ Digital Inputs
+
+
+
+ Digital Outputs
+
+
+
+ Counters
+
+
+
+ Analog Inputs
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 18e000ff456d1..7c97447912bc1 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -103,6 +103,7 @@
org.openhab.binding.fsinternetradio
org.openhab.binding.ftpupload
org.openhab.binding.gardena
+ org.openhab.binding.gce
org.openhab.binding.goecharger
org.openhab.binding.globalcache
org.openhab.binding.gpstracker