added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.gce</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,192 @@
# GCE Binding
This binding aims to handle various GCE Electronics equipments.
IPX800 is a 8 relay webserver from gce-electronics with a lot of possibilities:
* 8 Digital Input
* 8 Relay (250V / 10A / channel)
* 4 Analog Input
* 8 Counters
* Ability to cascade up to 3 extensions for a total of 32 inputs / 32 relay
Each IPX800 connected to openHAB must be configured with the setting 'Send data on status changed' on the website in M2M > TCP client.
To make it simple, IPX800 is a simple device that drives output and retrieves input.
On input we generally connect push buttons (for instance house switchs), on ouputs we can connect light bulbs for instance.
Features of the binding:
* Multi ipx support
* Direct TCP connection
* Auto reconnect
* Simple clic/Long press
* Pulse mode support
## Binding Configuration
There is no configuration at binding level.
## Thing Configuration
The IPX800v3 (ID : 'ipx800v3') accepts the following configuration parameters :
| Property | Default | Required | Description |
|---------------------|---------|----------|-----------------------------|
| hostname | | Yes | IP address or hostname. |
| portNumber | 9870 | No | TCP client connection port. |
| pullInterval* | 5000 | No | Refresh interval (in ms) |
The binding will query periodically the 'globalstatus.xml' page of the IPX to get fresh informations.
This is especially usefull for Analog inputs and Counter as modification of these values on PLC side does not trigger any M2M message.
The thing provides four groups of channels.
### Digital Inputs
This represents the inputs of the PLC. Each can be open or closed.
They are usually commuted by physical devices like pushbuttons, magnets...
#### Digital Input Channels (contacts)
Each input will have these associated channels:
| Group | Channel Name | Item Type | R/W | Description |
|----------|------------------------|-------------|-----|-----------------------------------------------------------------------------|
| contact | `portnumber` | Contact | R | Status of the actual port (OPEN, CLOSED) |
| contact | `portnumber`-duration | Number:Time | R | Updated when the port status changes to the duration of the previous state. |
Associated events:
| Channel Type ID | Options | Description | Conf Dependency |
|--------------------|-------------------|--------------------------------------------------|-----------------|
| `portnumber`-event | | Triggered on or after a port status change | |
| | PRESSED | Triggered when state changes from OPEN to CLOSED | |
| | RELEASED | Triggered when state changes from CLOSED to OPEN | |
| | LONG_PRESS | Triggered when RELEASED after a long period | longPressTime |
| | SHORT_PRESS | Triggered when RELEASED before a long period | longPressTime |
| | PULSE | Triggered during CLOSED state | pulsePeriod |
#### Configuration
| Property | Default | Unit | Description |
|-----------------|---------|------|---------------------------------------------------------------------------------|
| debouncePeriod | 0(*) | ms | Debounce time (ignores flappling within this time). No debounce is done if '0'. |
| longPressTime | 0(*) | ms | Delay before triggering long press event. Ignored if '0'. |
| pulsePeriod | 0(*) | ms | Period of pulse event triggering while the entry is closed. Ignored if '0'. |
| pulseTimeout | 0(*) | ms | Period of time after pulsing will be stopped. None if '0'. |
* Values below 100ms should be avoided as the JVM could skip them and proceed in the same time slice.
### Digital Outputs Channels (relays)
Each output will have these associated channels:
| Group | Channel Name | Item Type | R/W | Description |
|----------|------------------------|-------------|-----|-----------------------------------------------------------------------------|
| relay | `portnumber` | Switch | R/W | Status of the actual port (ON, OFF) |
| relay | `portnumber`-duration | Number:Time | R | Updated when the port status changes to the duration of the previous state. |
#### Configuration
| Property | Default | Description |
|-----------------|---------|--------------------------------------------------------------------------|
| pulse | false | If set, the output will be in pulse mode, releasing it after the contact |
### Counters Channels
Each counter will have these associated channels:
| Group | Channel Name | Item Type | R/W | Description |
|----------|--------------------------|-------------|-----|--------------------------------------------------------------------------------|
| counter | `counternumber` | Number | R | Actual value of the counter |
| counter | `counternumber`-duration | Number:Time | R | Updated when the counter status changes to the duration of the previous state. |
#### Configuration
This channel has no configuration setting.
### Analog Inputs Channels
Each analog port will have these associated channels:
| Group | Channel Name | Item Type | R/W | Description |
|--------|-----------------------|--------------------------|-----|-----------------------------------------------------------------------------|
| analog | `portnumber` | Number | R | Value of the port. |
| analog | `portnumber`-duration | Number:Time | R | Updated when the port status changes to the duration of the previous state. |
| analog | `portnumber`-voltage | Number:ElectricPotential | R | Electrical equivalency of the analogic value |
#### Configuration
| Property | Default | Description |
|------------|---------|-------------------------------------------------------------------------------------|
| hysteresis | 0 | If set, the channel will ignore status if change (+ or -) is less than hysteresis/2 |
## Rule Actions
Multiple actions are supported by this binding. In classic rules these are accessible as shown in the example below:
Getting ipxActions variable in scripts
```
val ipxActions = getActions("gce","gce:ipx800v3:43cc8d07")
if(null === ipxActions) {
logInfo("actions", "ipxActions not found, check thing ID")
return
} else {
// do something with sunActions
}
```
### resetCounter(counterId)
Resets the value of the given counter to 0.
* `counterId` (Integer) - id of the counter.
### reset(placeholder)
Restarts the PLC.
* `placeholder` (Integer) - This parameter is not used (can be null).
## Example
### Things
ipx800.things
```java
Thing gce:ipx800v3:ipx "IPX800" @ "diningroom" [hostname="192.168.0.144", portNumber=9870] {
Channels:
Type contact : contact#1 [ // Aimant Détection porte de garage ouverte
debouncePeriod=2500,
pulsePeriod=1000,
pulseTimeout=60000
]
Type contact : contact#2 [ // Aimant Détection porte de garage fermée
debouncePeriod=2500
]
Type relay : relay#8 [ // Actionneur porte de garage
pulse=true
]
}
```
ipx800.items
```java
Group gIPXInputs "Inputs" <input>
Contact input1 "Porte garage ouverte [%s]" <contact> (gIPXInputs) {channel="gce:ipx800v3:ipx:contact#1"}
Contact input2 "Porte garage fermée [%s]" <contact> (gIPXInputs) {channel="gce:ipx800v3:ipx:contact#2"}
Group gIPXOutputs "Outputs" <output>
Switch output3 "Chaudière" <furnace> (gIPXOutputs) {channel="gce:ipx800v3:ipx:relay#3"}
Switch output4 "Lumière Porche" <light> (gIPXOutputs) {channel="gce:ipx800v3:ipx:relay#4"}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.gce</artifactId>
<name>openHAB Add-ons :: Bundles :: GCE Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.gce-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-gce" description="GCE Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.gce/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,43 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link GCEBindingConstants} class defines common constants, which are used
* across the whole binding.
*
* @author Gaël L'hopital - Initial Contribution
*/
@NonNullByDefault
public class GCEBindingConstants {
public static final String BINDING_ID = "gce";
// Bridge Type UID
public static final ThingTypeUID IPXV3_THING_TYPE = new ThingTypeUID(BINDING_ID, "ipx800v3");
public static final String CHANNEL_LAST_STATE_DURATION = "duration";
public static final String CHANNEL_VOLTAGE = "voltage";
public static final String TRIGGER_CONTACT = "contact-trigger";
public static final String EVENT_PRESSED = "PRESSED";
public static final String EVENT_RELEASED = "RELEASED";
public static final String EVENT_SHORT_PRESS = "SHORT_PRESS";
public static final String EVENT_LONG_PRESS = "LONG_PRESS";
public static final String EVENT_PULSE = "PULSE";
// Adressable thing
}

View File

@@ -0,0 +1,51 @@
/**
* 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;
import static org.openhab.binding.gce.internal.GCEBindingConstants.IPXV3_THING_TYPE;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.gce.internal.handler.Ipx800v3Handler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link GCEHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.gce")
public class GCEHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> 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;
}
}

View File

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

View File

@@ -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.openhab.binding.gce.internal.handler.Ipx800v3Handler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {Ipx800Actions } defines rule actions for the GCE binding.
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof Ipx800Actions</i> 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 <i>actions</i> 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");
}
}

View File

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

View File

@@ -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.openhab.core.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;
}

View File

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

View File

@@ -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.openhab.core.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;
}

View File

@@ -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.openhab.binding.gce.internal.model.M2MMessageParser;
import org.openhab.core.thing.ThingUID;
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;
}
}

View File

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

View File

@@ -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.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.openhab.core.config.core.Configuration;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
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<String, @Nullable PortData> 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<String, String> 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<Channel> 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<Channel> 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<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(Ipx800Actions.class);
}
}

View File

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

View File

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

View File

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

View File

@@ -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.openhab.binding.gce.internal.handler.Ipx800EventListener;
import org.openhab.core.io.net.http.HttpUtil;
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<Node> 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<Node> 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<Node> 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;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="gce" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>GCE Electronics Binding</name>
<description>Provides access to IPX800 PLC build by GCE.</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:gce:ipx800v3Config">
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Network Address</label>
<description>Network/IP address of the IPX800 without http(s) prefix.</description>
</parameter>
<parameter name="portNumber" type="integer" required="false">
<label>Port Number</label>
<description>TCP client connection port.</description>
<default>9870</default>
<advanced>true</advanced>
</parameter>
<parameter name="pullInterval" type="integer" required="false" min="500" max="60000" unit="ms">
<label>Pull Interval</label>
<description>Delay for pulling Analog and Counters info (in milliseconds).</description>
<default>5000</default>
</parameter>
</config-description>
<config-description uri="channel-type:gce:contactConfig">
<parameter name="debouncePeriod" type="integer" min="0" max="3000" unit="ms">
<label>Debounce Time</label>
<default>0</default>
</parameter>
<parameter name="longPressTime" type="integer" step="1000" min="0" max="5000" unit="ms">
<label>Long Press Time</label>
<description>Long press time in milliseconds.</description>
<default>0</default>
</parameter>
<parameter name="pulsePeriod" type="integer" step="500" min="0" max="50000" unit="ms">
<label>Pulse Period</label>
<description>Pulse period in milliseconds.</description>
<default>0</default>
</parameter>
<parameter name="pulseTimeout" type="integer" step="500" min="0" max="50000" unit="ms">
<label>Pulse Timeout</label>
<description>Maximum period for sending pulses in milliseconds.</description>
<default>0</default>
</parameter>
</config-description>
<config-description uri="channel-type:gce:relayConfig">
<parameter name="pulse" type="boolean">
<label>Pulse Output</label>
<default>false</default>
</parameter>
</config-description>
<config-description uri="channel-type:gce:analogConfig">
<parameter name="hysteresis" type="integer" min="0">
<label>Hysteresis</label>
<default>0</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="gce" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="contact">
<item-type>Contact</item-type>
<label>Digital Input</label>
<category>Contact</category>
<state pattern="%s" readOnly="true"/>
<config-description-ref uri="channel-type:gce:contactConfig"/>
</channel-type>
<channel-type id="contactAdvanced" advanced="true">
<item-type>Contact</item-type>
<label>Digital Input</label>
<category>Contact</category>
<state pattern="%s" readOnly="true"/>
<config-description-ref uri="channel-type:gce:contactConfig"/>
</channel-type>
<channel-type id="relay">
<item-type>Switch</item-type>
<label>Digital Output</label>
<config-description-ref uri="channel-type:gce:relayConfig"/>
</channel-type>
<channel-type id="relayAdvanced" advanced="true">
<item-type>Switch</item-type>
<label>Digital Output</label>
<config-description-ref uri="channel-type:gce:relayConfig"/>
</channel-type>
<channel-type id="counter">
<item-type>Number</item-type>
<label>Counter</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="analog">
<item-type>Number</item-type>
<label>Analog Input</label>
<state readOnly="true" pattern="%d %unit%"/>
<config-description-ref uri="channel-type:gce:analogConfig"/>
</channel-type>
<channel-type id="analogAdvanced" advanced="true">
<item-type>Number</item-type>
<label>Analog Input</label>
<config-description-ref uri="channel-type:gce:analogConfig"/>
</channel-type>
<channel-type id="duration" advanced="true">
<item-type>Number:Time</item-type>
<label>Previous State Duration</label>
<description>Duration of previous state before state change.</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="contact-trigger">
<kind>trigger</kind>
<label>Push Button Trigger Channel</label>
<event>
<options>
<option value="PRESSED">Pressed</option>
<option value="RELEASED">Released</option>
<option value="SHORT_PRESS">Short press</option>
<option value="LONG_PRESS">Long press</option>
<option value="PULSE">Pulse</option>
</options>
</event>
</channel-type>
<channel-type id="contact-triggerAdvanced" advanced="true">
<kind>trigger</kind>
<label>Push Button Trigger Channel</label>
<event>
<options>
<option value="PRESSED">Pressed</option>
<option value="RELEASED">Released</option>
<option value="SHORT_PRESS">Short press</option>
<option value="LONG_PRESS">Long press</option>
<option value="PULSE">Pulse</option>
</options>
</event>
</channel-type>
<channel-type id="voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Voltage</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="gce" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="ipx800v3">
<label>IPX800v3</label>
<description>The GCE IPX800v3 device</description>
<channel-groups>
<channel-group id="contact" typeId="contacts"/>
<channel-group id="relay" typeId="relays"/>
<channel-group id="counter" typeId="counters"/>
<channel-group id="analog" typeId="analogs"/>
</channel-groups>
<config-description-ref uri="thing-type:gce:ipx800v3Config"/>
</thing-type>
<channel-group-type id="contacts">
<label>Digital Inputs</label>
</channel-group-type>
<channel-group-type id="relays">
<label>Digital Outputs</label>
</channel-group-type>
<channel-group-type id="counters">
<label>Counters</label>
</channel-group-type>
<channel-group-type id="analogs">
<label>Analog Inputs</label>
</channel-group-type>
</thing:thing-descriptions>