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.oceanic</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,132 @@
# Oceanic Binding
This binding integrates the Oceanic water softener and management system (www.oceanic.be, but also distributed by Syr in Germany (www.syr.de)).
The binding supports the Limex IQ and Limex Pro water softeners and require the optional CAN-Serial gateway has to be installed
## Supported Things
- **serial** - A water softener connected to the openHAB host by means of a serial port
- **network** - A water softener that can be reached through a TCP proxy. See Known Issues below as when to use this kind of configuration
## Thing Configuration
The **serial** Thing configuration requires the name of the serial port that is used to connect openHAB with the Oceanic unit, and the interval period in seconds to poll the Oceanic unit
The **network** Thing configuration requires the hostname or ip address of the proxy, the TCP port number to connect to, and the interval period in seconds to poll the Oceanic unit
## Channels
All things support the following channels (non-exhaustive):
| Channel Type ID | Item Type | Description |
|--------------------------------------|-----------|-----------------------------------------------------------------|
| alarm | String | Current alarm description, if any |
| alert | String | Current alert description, if any, to notify a shortage of salt |
| totalflow | Number | Current flow in l/min |
| maxflow | Number | Maximum flow recorded, in l/min |
| reserve | Number | Water reserve in l before regeneration has to start |
| cycle | String | Indicates the stage of the regeneration cycle |
| endofcycle | String | Indicates the time to the end of the current cycle |
| endofgeneration | String | Indicates the time to the end of the current generation |
| inlethardness | Number | Water hardness at the inlet |
| outlethardness | Number | Water hardness at the outlet |
| salt | String | Volume of salt remaining, in kg |
| consumption(today)(currentweek)(...) | String | Water consumption, in l, for that period |
| regeneratenow | Switch | Start an immediate regeneration |
| regeneratelater | Switch | Start a delayed regeneration |
| lastgeneration | DateTime | Date and Time of the last regeneration cycle |
| pressure | Number | Water pressure, in bar |
| minpressure | Number | Minimum water pressure recorded, in bar |
| maxpressure | Number | Maximum water pressure recorded, in bar |
| normalregenerations | Number | Number of regenerations completed |
| serviceregenerations | Number | Number of service regenerations completed |
| incompleteregenerations | Number | Number of incomplete regenerations |
| allregenerations | Number | Number of all regenerations |
## Full Example
.things
```
Thing oceanic:serial:s1 [ port="/dev/tty.usbserial-FTWGX64N", interval=60]
Thing oceanic:network:s2 [ ipAddress="192.168.0.6", portNumber=9000, interval=60]
```
.items
```
Number oceanicVolume "volume [%d]" (oceanic) {channel="oceanic:serial:s1:totalflow"}
String oceanicAlarm "alarm: [%s]" (oceanic) {channel="oceanic:serial:s1:alarm"}
String oceanicAlert "alert: [%s]" (oceanic) {channel="oceanic:serial:s1:alert"}
Number oceanicReserve (oceanic) {channel="oceanic:serial:s1:reserve"}
String oceanicCycle (oceanic) {channel="oceanic:serial:s1:cycle"}
String oceanicEOC (oceanic) {channel="oceanic:serial:s1:endofcycle"}
String oceanicEOG (oceanic) {channel="oceanic:serial:s1:endofgeneration"}
String oceanicHU (oceanic) {channel="oceanic:serial:s1:hardnessunit"}
Number oceanicInletHardness (oceanic) {channel="oceanic:serial:s1:inlethardness"}
Number oceanicOutletHardness (oceanic) {channel="oceanic:serial:s1:outlethardness"}
String oceanicCylState (oceanic) {channel="oceanic:serial:s1:cylinderstate"}
Number oceanicSalt (oceanic) {channel="oceanic:serial:s1:salt"}
Number oceanicConsToday "volume today is [%d]" (oceanic) {channel="oceanic:serial:s1:consumptiontoday"}
Number oceanicConsYday "volume yesterday was [%d]"(oceanic) {channel="oceanic:serial:s1:consumptionyesterday"}
Number oceanicPressure (oceanic) {channel="oceanic:serial:s1:pressure"}
DateTime oceanicLastGeneration (oceanic) {channel="oceanic:serial:s1:lastgeneration"}
Number oceanicAllGen (oceanic) {channel="oceanic:serial:s1:allregenerations"}
Number oceanicMaxFlow (oceanic) {channel="oceanic:serial:s1:maxflow"}
Number oceanicConsThisWk "volume this week is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptioncurrentweek"}
Number oceanicConsThisMnth "volume this month is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptioncurrentmonth"}
Number oceanicConsLastMnth "volume last month is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptionlastmonth"}
Number oceanicConsComplete "volume all time is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptioncomplete"}
Number oceanicConsUntreated "volume untreated is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptionuntreated"}
Number oceanicConsLastWk "volume last week is [%d]"(oceanic) {channel="oceanic:serial:s1:consumptionlastweek"}
```
## Known issues
The Oceanic binding makes use of the nrjavaserial library, and unfortunately java and serial ports never have been a great marriage.
Although some work is being done to improve things (<https://github.com/eclipse/smarthome/issues/4465>), the best thing is to avoid serial ports as much as possible, as some issues (<https://github.com/NeuronRobotics/nrjavaserial/issues/96>) are not resolved.
For example, On Ubuntu 17.10 nrjavaserial seems to return only HEX 00 characters through the InputStream of the SerialPort.
Within the Oceanic binding two routes are provided:
1. Connect to the Oceanic softener over a serial port that is outside the scope of the Java Virtual Machine, setup a TCP "proxy" on the host that is connected to the softener, and make openHAB connect to that proxy over a plain TCP connection. This can be achieved with ```socat```:
```
/usr/bin/socat -v TCP-LISTEN:9000 /dev/ttyUSB0,raw,echo=0
```
In the above example, the name of the host running socat, and the TCP port number 9000, will be part of the **network** Thing configuration
2. Connect to the Oceanic softener over a serial port on the openHAB host and use ```socat``` to pipe the data from that serial port to a pseudo tty, which has to be manipulated in a CommPortIdentifier.PORT_RAW manner.
```
/usr/bin/socat -v /dev/ttyUSB0,raw,echo=0 pty,link=/dev/ttyS1,raw,echo=0
```
Both workarounds can be implemented using a systemd system manager script, for example:
```
[Install]
WantedBy=multi-user.target
[Service]
#Type=forking
ExecStart=/usr/bin/socat -v /dev/ttyUSB0,raw,echo=0 pty,link=/dev/ttyS1,raw,echo=0
#PIDFile=/var/run/socat.pid
User=root
Restart=always
RestartSec=10
```
However, in order to fix permissions at the OS level, one has to issue following commands in order to make /dev/ttyS1 accessible by the 'openhab' system user (that is used to start up the openHAB runtime), and to make the tty both readable and writable.
```
sudo useradd -G dialout openhab
sudo chgrp dialout /dev/ttyS1
sudo chmod 777 /dev/ttyS1
```
Alternatively, these commands can be executed through a script that is attached to the systemd system manager script.

View File

@@ -0,0 +1,21 @@
<?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/xsd/maven-4.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.oceanic</artifactId>
<name>openHAB Add-ons :: Bundles :: Oceanic Binding</name>
<properties>
<bnd.importpackage>gnu.io;version="[3.12,6)"</bnd.importpackage>
</properties>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.oceanic-${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-oceanic" description="Oceanic Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.oceanic/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,23 @@
/**
* 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.oceanic.internal;
/**
* @author Karel Goderis - Initial contribution
*/
public class NetworkOceanicBindingConfiguration {
public String ipAddress;
public Integer portNumber;
public Integer interval;
}

View File

@@ -0,0 +1,305 @@
/**
* 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.oceanic.internal;
import java.io.InvalidClassException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Type;
/**
* The {@link OceanicBinding} class defines common constants, which are used
* across the whole binding.
*
* @author Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class OceanicBindingConstants {
public static final String BINDING_ID = "oceanic";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SERIAL = new ThingTypeUID(BINDING_ID, "serial");
public static final ThingTypeUID THING_TYPE_NETWORK = new ThingTypeUID(BINDING_ID, "network");
// List of all Channel ids
public enum OceanicChannelSelector {
getSRN("serial", StringType.class, ValueSelectorType.GET, true),
getMAC("mac", StringType.class, ValueSelectorType.GET, true),
getDNA("name", StringType.class, ValueSelectorType.GET, true),
getSCR("type", StringType.class, ValueSelectorType.GET, true) {
@Override
public String convertValue(String value) {
int index = Integer.valueOf(value);
String convertedValue = value;
switch (index) {
case 0:
convertedValue = "Single";
break;
case 1:
convertedValue = "Double Alternative";
break;
case 2:
convertedValue = "Triple Alternative";
break;
case 3:
convertedValue = "Double Parallel";
break;
case 4:
convertedValue = "Triple Parallel";
break;
case 5:
convertedValue = "Single Filter";
break;
case 6:
convertedValue = "Double Filter";
break;
case 7:
convertedValue = "Triple Filter";
break;
default:
break;
}
return convertedValue;
}
},
getALM("alarm", StringType.class, ValueSelectorType.GET, false) {
@Override
public String convertValue(String value) {
int index = Integer.valueOf(value);
String convertedValue = value;
switch (index) {
case 0:
convertedValue = "No Alarm";
break;
case 1:
convertedValue = "Lack of salt during regeneration";
break;
case 2:
convertedValue = "Water pressure too low";
break;
case 3:
convertedValue = "Water pressure too high";
break;
case 4:
convertedValue = "Pressure sensor failure";
break;
case 5:
convertedValue = "Camshaft failure";
break;
default:
break;
}
return convertedValue;
}
},
getNOT("alert", StringType.class, ValueSelectorType.GET, false) {
@Override
public String convertValue(String value) {
int index = Integer.valueOf(value);
String convertedValue = value;
switch (index) {
case 0:
convertedValue = "No Alert";
break;
case 1:
convertedValue = "Imminent lack of salt";
break;
default:
break;
}
return convertedValue;
}
},
getFLO("totalflow", DecimalType.class, ValueSelectorType.GET, false),
getRES("reserve", DecimalType.class, ValueSelectorType.GET, false),
getCYN("cycle", StringType.class, ValueSelectorType.GET, false),
getCYT("endofcycle", StringType.class, ValueSelectorType.GET, false),
getRTI("endofregeneration", StringType.class, ValueSelectorType.GET, false),
getWHU("hardnessunit", StringType.class, ValueSelectorType.GET, false) {
@Override
public String convertValue(String value) {
int index = Integer.valueOf(value);
String convertedValue = value;
switch (index) {
case 0:
convertedValue = "dH";
break;
case 1:
convertedValue = "fH";
break;
case 2:
convertedValue = "e";
break;
case 3:
convertedValue = "mg CaCO3/l";
break;
case 4:
convertedValue = "ppm";
break;
case 5:
convertedValue = "mmol/l";
break;
case 6:
convertedValue = "mval/l";
break;
default:
break;
}
return convertedValue;
}
},
getIWH("inlethardness", DecimalType.class, ValueSelectorType.GET, false),
getOWH("outlethardness", DecimalType.class, ValueSelectorType.GET, false),
getRG1("cylinderstate", StringType.class, ValueSelectorType.GET, false) {
@Override
public String convertValue(String value) {
int index = Integer.valueOf(value);
String convertedValue = value;
switch (index) {
case 0:
convertedValue = "No regeneration";
break;
case 1:
convertedValue = "Paused";
break;
case 2:
convertedValue = "Regeneration";
break;
default:
break;
}
return convertedValue;
}
},
setSV1("salt", DecimalType.class, ValueSelectorType.SET, false),
getSV1("salt", DecimalType.class, ValueSelectorType.GET, false),
setSIR("regeneratenow", OnOffType.class, ValueSelectorType.SET, false),
setSDR("regeneratelater", OnOffType.class, ValueSelectorType.SET, false),
setSMR("multiregenerate", OnOffType.class, ValueSelectorType.SET, false),
getMOF("consumptionmonday", DecimalType.class, ValueSelectorType.GET, false),
getTUF("consumptiontuesday", DecimalType.class, ValueSelectorType.GET, false),
getWEF("consumptionwednesday", DecimalType.class, ValueSelectorType.GET, false),
getTHF("consumptionthursday", DecimalType.class, ValueSelectorType.GET, false),
getFRF("consumptionfriday", DecimalType.class, ValueSelectorType.GET, false),
getSAF("consumptionsaturday", DecimalType.class, ValueSelectorType.GET, false),
getSUF("consumptionsunday", DecimalType.class, ValueSelectorType.GET, false),
getTOF("consumptiontoday", DecimalType.class, ValueSelectorType.GET, false),
getYEF("consumptionyesterday", DecimalType.class, ValueSelectorType.GET, false),
getCWF("consumptioncurrentweek", DecimalType.class, ValueSelectorType.GET, false),
getLWF("consumptionlastweek", DecimalType.class, ValueSelectorType.GET, false),
getCMF("consumptioncurrentmonth", DecimalType.class, ValueSelectorType.GET, false),
getLMF("consumptionlastmonth", DecimalType.class, ValueSelectorType.GET, false),
getCOF("consumptioncomplete", DecimalType.class, ValueSelectorType.GET, false),
getUWF("consumptionuntreated", DecimalType.class, ValueSelectorType.GET, false),
getTFO("consumptionpeaklevel", DecimalType.class, ValueSelectorType.GET, false),
getPRS("pressure", DecimalType.class, ValueSelectorType.GET, false),
getMXP("maxpressure", DecimalType.class, ValueSelectorType.GET, false),
getMNP("minpressure", DecimalType.class, ValueSelectorType.GET, false),
getMXF("maxflow", DecimalType.class, ValueSelectorType.GET, false),
getLAR("lastgeneration", DateTimeType.class, ValueSelectorType.GET, false) {
@Override
public String convertValue(String value) {
final SimpleDateFormat inDateFormatter = new SimpleDateFormat("dd.MM.yy HH:mm:ss");
final SimpleDateFormat outDateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
try {
Date date = inDateFormatter.parse(value);
return outDateFormatter.format(date);
} catch (ParseException fpe) {
throw new IllegalArgumentException(value + " is not in a valid format.", fpe);
}
}
},
getNOR("normalregenerations", DecimalType.class, ValueSelectorType.GET, false),
getSRE("serviceregenerations", DecimalType.class, ValueSelectorType.GET, false),
getINR("incompleteregenerations", DecimalType.class, ValueSelectorType.GET, false),
getTOR("allregenerations", DecimalType.class, ValueSelectorType.GET, false);
private final String text;
private Class<? extends Type> typeClass;
private ValueSelectorType typeValue;
private boolean isProperty;
private OceanicChannelSelector(final String text, Class<? extends Type> typeClass, ValueSelectorType typeValue,
boolean isProperty) {
this.text = text;
this.typeClass = typeClass;
this.typeValue = typeValue;
this.isProperty = isProperty;
}
@Override
public String toString() {
return text;
}
public Class<? extends Type> getTypeClass() {
return typeClass;
}
public ValueSelectorType getTypeValue() {
return typeValue;
}
public boolean isProperty() {
return isProperty;
}
/**
* Procedure to convert selector string to value selector class.
*
* @param valueSelectorText selector string e.g. RawData, Command, Temperature
* @return corresponding selector value.
* @throws InvalidClassException Not valid class for value selector.
*/
public static OceanicChannelSelector getValueSelector(String valueSelectorText,
ValueSelectorType valueSelectorType) throws IllegalArgumentException {
for (OceanicChannelSelector c : OceanicChannelSelector.values()) {
if (c.text.equals(valueSelectorText) && c.typeValue == valueSelectorType) {
return c;
}
}
throw new IllegalArgumentException("Not valid value selector");
}
public static ValueSelectorType getValueSelectorType(String valueSelectorText) throws IllegalArgumentException {
for (OceanicChannelSelector c : OceanicChannelSelector.values()) {
if (c.text.equals(valueSelectorText)) {
return c.typeValue;
}
}
throw new IllegalArgumentException("Not valid value selector");
}
public String convertValue(String value) {
return value;
}
public enum ValueSelectorType {
GET,
SET
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.oceanic.internal;
import static org.openhab.binding.oceanic.internal.OceanicBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.binding.oceanic.internal.handler.NetworkOceanicThingHandler;
import org.openhab.binding.oceanic.internal.handler.SerialOceanicThingHandler;
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 OceanicHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Karel Goderis - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.oceanic")
public class OceanicHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_SERIAL, THING_TYPE_NETWORK).collect(Collectors.toSet()));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_SERIAL)) {
return new SerialOceanicThingHandler(thing);
}
if (thingTypeUID.equals(THING_TYPE_NETWORK)) {
return new NetworkOceanicThingHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.oceanic.internal;
/**
* @author Karel Goderis - Initial contribution
*/
public class SerialOceanicBindingConfiguration {
public String port;
public Integer interval;
}

View File

@@ -0,0 +1,94 @@
/**
* 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.oceanic.internal;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Throttler} is helper class that regulates the frequency at which messages/packets are sent to
* the serial port.
*
* @author Karel Goderis - Initial Contribution
*/
public class Throttler {
private static Logger logger = LoggerFactory.getLogger(Throttler.class);
public static final long INTERVAL = 1000;
private static ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, Long> timestamps = new ConcurrentHashMap<>();
public static void lock(String key) {
if (!locks.containsKey(key)) {
locks.put(key, new ReentrantLock());
}
locks.get(key).lock();
if (timestamps.get(key) != null) {
long lastStamp = timestamps.get(key);
long timeToWait = Math.max(INTERVAL - (System.currentTimeMillis() - lastStamp), 0);
if (timeToWait > 0) {
try {
Thread.sleep(timeToWait);
} catch (InterruptedException e) {
logger.error("An exception occurred while putting the thread to sleep : '{}'", e.getMessage());
}
}
}
}
public static void unlock(String key) {
if (locks.containsKey(key)) {
timestamps.put(key, System.currentTimeMillis());
locks.get(key).unlock();
}
}
public static void lock() {
for (ReentrantLock aLock : locks.values()) {
aLock.lock();
}
long lastStamp = 0;
for (Long aStamp : timestamps.values()) {
if (aStamp > lastStamp) {
lastStamp = aStamp;
}
}
long timeToWait = Math.max(INTERVAL - (System.currentTimeMillis() - lastStamp), 0);
if (timeToWait > 0) {
try {
Thread.sleep(timeToWait);
} catch (InterruptedException e) {
logger.error("An exception occurred while putting the thread to sleep : '{}'", e.getMessage());
}
}
}
public static void unlock() {
for (String key : locks.keySet()) {
if (locks.get(key).isHeldByCurrentThread()) {
timestamps.put(key, System.currentTimeMillis());
locks.get(key).unlock();
}
}
}
}

View File

@@ -0,0 +1,217 @@
/**
* 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.oceanic.internal.handler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.oceanic.internal.NetworkOceanicBindingConfiguration;
import org.openhab.binding.oceanic.internal.Throttler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NetworkOceanicThingHandler} implements {@link OceanicThingHandler} for a Oceanic water softener that is
* reached using a socat TCP proxy
*
* @author Karel Goderis - Initial contribution
*/
public class NetworkOceanicThingHandler extends OceanicThingHandler {
private static final int REQUEST_TIMEOUT = 3000;
private static final int RECONNECT_INTERVAL = 15;
private final Logger logger = LoggerFactory.getLogger(NetworkOceanicThingHandler.class);
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
protected ScheduledFuture<?> reconnectJob;
public NetworkOceanicThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
try {
socket = new Socket(config.ipAddress, config.portNumber);
socket.setSoTimeout(REQUEST_TIMEOUT);
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
updateStatus(ThingStatus.ONLINE);
} catch (UnknownHostException e) {
logger.error("An exception occurred while resolving host {}:{} : '{}'", config.ipAddress, config.portNumber,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not resolve host " + config.ipAddress + ": " + e.getMessage());
} catch (IOException e) {
logger.debug("An exception occurred while connecting to host {}:{} : '{}'", config.ipAddress,
config.portNumber, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not connect to host " + config.ipAddress + ": " + e.getMessage());
reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
logger.error("An exception occurred while disconnecting to host {}:{} : '{}'", config.ipAddress,
config.portNumber, e.getMessage());
} finally {
socket = null;
outputStream = null;
inputStream = null;
}
}
super.dispose();
}
@Override
protected String requestResponse(String commandAsString) {
synchronized (this) {
if (getThing().getStatus() == ThingStatus.ONLINE) {
NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
Throttler.lock(config.ipAddress);
String request = commandAsString + "\r";
byte[] dataBuffer = new byte[bufferSize];
byte[] tmpData = new byte[bufferSize];
String line;
int len = -1;
int index = 0;
boolean sequenceFound = false;
final byte lineFeed = (byte) '\n';
final byte carriageReturn = (byte) '\r';
final byte nullChar = (byte) '\0';
final byte eChar = (byte) 'E';
final byte rChar = (byte) 'R';
try {
logger.debug("Sending request '{}'", request);
outputStream.write(request.getBytes());
outputStream.flush();
while (true) {
if ((len = inputStream.read(tmpData)) > -1) {
if (logger.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) {
sb.append(String.format("%02X ", tmpData[i]));
}
logger.trace("Read {} bytes : {}", len, sb.toString());
}
for (int i = 0; i < len; i++) {
if (logger.isTraceEnabled()) {
logger.trace("Byte {} equals '{}' (hex '{}')", i,
new String(new byte[] { tmpData[i] }), String.format("%02X", tmpData[i]));
}
if (tmpData[i] == nullChar && !sequenceFound) {
sequenceFound = true;
logger.trace("Start of sequence found");
}
if (sequenceFound && tmpData[i] != lineFeed && tmpData[i] != carriageReturn
&& tmpData[i] != nullChar) {
dataBuffer[index++] = tmpData[i];
if (logger.isTraceEnabled()) {
logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
new String(new byte[] { dataBuffer[index - 1] }),
String.format("%02X", dataBuffer[index - 1]));
}
}
if (sequenceFound && i >= 2) {
if (tmpData[i - 2] == eChar && tmpData[i - 1] == rChar && tmpData[i] == rChar) {
// Received ERR from the device.
return null;
}
}
if (sequenceFound && i > 0
&& (tmpData[i - 1] != carriageReturn && tmpData[i] == nullChar)) {
index = 0;
// Ignore trash received
if (logger.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < i; j++) {
sb.append(String.format("%02X ", tmpData[j]));
}
logger.trace("Ingoring {} bytes : {}", i, sb);
}
}
if (sequenceFound && (tmpData[i] == carriageReturn)) {
if (index > 0) {
line = new String(Arrays.copyOf(dataBuffer, index));
logger.debug("Received response '{}'", line);
line = StringUtils.chomp(line);
line = line.replace(",", ".");
line = line.trim();
index = 0;
return line;
}
}
if (index == bufferSize) {
index = 0;
}
}
}
}
} catch (IOException e) {
logger.debug("An exception occurred while quering host {}:{} : '{}'", config.ipAddress,
config.portNumber, e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
} finally {
Throttler.unlock(config.ipAddress);
}
}
return null;
}
}
private Runnable reconnectRunnable = () -> {
dispose();
initialize();
};
}

View File

@@ -0,0 +1,169 @@
/**
* 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.oceanic.internal.handler;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.binding.oceanic.internal.OceanicBindingConstants.OceanicChannelSelector;
import org.openhab.core.thing.Channel;
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.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.TypeParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OceanicThingHandler} is the abstract class responsible for handling commands, which are
* sent to one of the channels
*
* @author Karel Goderis - Initial contribution
*/
public abstract class OceanicThingHandler extends BaseThingHandler {
public static final String INTERVAL = "interval";
public static final String BUFFER_SIZE = "buffer";
private final Logger logger = LoggerFactory.getLogger(OceanicThingHandler.class);
protected int bufferSize;
protected ScheduledFuture<?> pollingJob;
protected static String lastLineReceived = "";
public OceanicThingHandler(@NonNull Thing thing) {
super(thing);
}
private Runnable resetRunnable = () -> {
dispose();
initialize();
};
private Runnable pollingRunnable = () -> {
try {
if (getThing().getStatus() == ThingStatus.ONLINE) {
for (Channel aChannel : getThing().getChannels()) {
for (OceanicChannelSelector selector : OceanicChannelSelector.values()) {
ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.toString());
if (aChannel.getUID().equals(theChannelUID)
&& selector.getTypeValue() == OceanicChannelSelector.ValueSelectorType.GET) {
String response = requestResponse(selector.name());
if (response != null && response != "") {
if (selector.isProperty()) {
logger.debug("Updating the property '{}' with value '{}'", selector.toString(),
selector.convertValue(response));
Map<String, String> properties = editProperties();
properties.put(selector.toString(), selector.convertValue(response));
updateProperties(properties);
} else {
State value = createStateForType(selector, response);
updateState(theChannelUID, value);
}
} else {
logger.warn("Received an empty answer for '{}'", selector.name());
}
}
}
}
}
} catch (Exception e) {
logger.error("An exception occurred while polling the Oceanic Water Softener: '{}'", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduler.schedule(resetRunnable, 0, TimeUnit.SECONDS);
}
};
@Override
public void initialize() {
if (getConfig().get(BUFFER_SIZE) == null) {
bufferSize = 1024;
} else {
bufferSize = ((BigDecimal) getConfig().get(BUFFER_SIZE)).intValue();
}
if (pollingJob == null || pollingJob.isCancelled()) {
pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 1,
((BigDecimal) getConfig().get(INTERVAL)).intValue(), TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
pollingJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (getThing().getStatus() == ThingStatus.ONLINE) {
if (!(command instanceof RefreshType)) {
String commandAsString = command.toString();
String channelID = channelUID.getId();
for (Channel aChannel : getThing().getChannels()) {
if (aChannel.getUID().equals(channelUID)) {
try {
OceanicChannelSelector selector = OceanicChannelSelector.getValueSelector(channelID,
OceanicChannelSelector.ValueSelectorType.SET);
switch (selector) {
case setSV1:
commandAsString = selector.name() + commandAsString;
break;
default:
commandAsString = selector.name();
break;
}
String response = requestResponse(commandAsString);
if (response.equals("ERR")) {
logger.error("An error occurred while setting '{}' to {}", selector.toString(),
commandAsString);
}
} catch (IllegalArgumentException e) {
logger.warn(
"An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
channelID, command.toString());
}
break;
}
}
}
}
}
@SuppressWarnings("unchecked")
private State createStateForType(OceanicChannelSelector selector, String value) {
Class<? extends Type> typeClass = selector.getTypeClass();
List<Class<? extends State>> stateTypeList = new ArrayList<>();
stateTypeList.add((Class<? extends State>) typeClass);
State state = TypeParser.parseState(stateTypeList, selector.convertValue(value));
return state;
}
protected abstract String requestResponse(String commandAsString);
}

View File

@@ -0,0 +1,322 @@
/**
* 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.oceanic.internal.handler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Enumeration;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.oceanic.internal.SerialOceanicBindingConfiguration;
import org.openhab.binding.oceanic.internal.Throttler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.RXTXCommDriver;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
/**
* The {@link SerialOceanicThingHandler} implements {@link OceanicThingHandler} for a Oceanic water softener that is
* directly connected to a serial port of the openHAB host
*
* @author Karel Goderis - Initial contribution
*/
public class SerialOceanicThingHandler extends OceanicThingHandler {
private static final long REQUEST_TIMEOUT = 10000;
private static final int BAUD = 19200;
private final Logger logger = LoggerFactory.getLogger(SerialOceanicThingHandler.class);
private SerialPort serialPort;
private CommPortIdentifier portId;
private InputStream inputStream;
private OutputStream outputStream;
private SerialPortReader readerThread;
public SerialOceanicThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
SerialOceanicBindingConfiguration config = getConfigAs(SerialOceanicBindingConfiguration.class);
if (serialPort == null && config.port != null) {
if (portId == null) {
try {
RXTXCommDriver rxtxCommDriver = new RXTXCommDriver();
rxtxCommDriver.initialize();
CommPortIdentifier.addPortName(config.port, CommPortIdentifier.PORT_RAW, rxtxCommDriver);
portId = CommPortIdentifier.getPortIdentifier(config.port);
} catch (NoSuchPortException e) {
logger.error("An exception occurred while setting up serial port '{}' : '{}'", config.port,
e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not setup serial port " + serialPort + ": " + e.getMessage());
return;
}
}
if (portId != null) {
try {
serialPort = portId.open(this.getThing().getUID().getBindingId(), 2000);
} catch (PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not open serial port " + serialPort + ": " + e.getMessage());
return;
}
try {
inputStream = serialPort.getInputStream();
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not open serial port " + serialPort + ": " + e.getMessage());
return;
}
serialPort.notifyOnDataAvailable(true);
try {
serialPort.setSerialPortParams(BAUD, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
} catch (UnsupportedCommOperationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not configure serial port " + serialPort + ": " + e.getMessage());
return;
}
try {
outputStream = serialPort.getOutputStream();
updateStatus(ThingStatus.ONLINE);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Could not communicate with the serial port " + serialPort + ": " + e.getMessage());
return;
}
readerThread = new SerialPortReader(inputStream);
readerThread.start();
} else {
StringBuilder sb = new StringBuilder();
@SuppressWarnings("rawtypes")
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
sb.append(id.getName() + "\n");
}
}
logger.error("Serial port '{}' could not be found. Available ports are:\n {}", config.port, sb);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
}
}
}
@Override
public void dispose() {
if (readerThread != null) {
try {
readerThread.interrupt();
readerThread.join();
} catch (InterruptedException e) {
logger.error("An exception occurred while interrupting the serial port reader thread : {}",
e.getMessage(), e);
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the input stream: {}", e.getMessage());
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the output stream: {}", e.getMessage());
}
}
if (serialPort != null) {
serialPort.close();
}
readerThread = null;
inputStream = null;
outputStream = null;
serialPort = null;
super.dispose();
}
@Override
protected String requestResponse(String commandAsString) {
synchronized (this) {
SerialOceanicBindingConfiguration config = getConfigAs(SerialOceanicBindingConfiguration.class);
Throttler.lock(config.port);
lastLineReceived = "";
String request = commandAsString + "\r";
String response = null;
try {
if (logger.isTraceEnabled()) {
logger.trace("Requesting : {} ('{}')", request, request.getBytes());
}
outputStream.write(request.getBytes());
outputStream.flush();
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error writing '" + request + "' to serial port " + config.port + " : " + e.getMessage());
}
long timeStamp = System.currentTimeMillis();
while (lastLineReceived.equals("")) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
logger.error("An exception occurred while putting the thread to sleep: {}", e.getMessage());
}
if (System.currentTimeMillis() - timeStamp > REQUEST_TIMEOUT) {
logger.warn("A timeout occurred while requesting data from the water softener");
readerThread.reset();
break;
}
}
response = lastLineReceived;
Throttler.unlock(config.port);
return response;
}
}
public class SerialPortReader extends Thread {
private boolean interrupted = false;
private InputStream inputStream;
int index = 0;
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss,SSS");
public SerialPortReader(InputStream in) {
this.inputStream = in;
this.setName("SerialPortReader-" + getThing().getUID());
}
public void reset() {
logger.trace("Resetting the SerialPortReader");
index = 0;
}
@Override
public void interrupt() {
logger.trace("Interrupting the SerialPortReader");
interrupted = true;
super.interrupt();
}
@Override
public void run() {
logger.trace("Starting the serial port reader");
byte[] dataBuffer = new byte[bufferSize];
byte[] tmpData = new byte[bufferSize];
String line;
final byte lineFeed = (byte) '\n';
final byte carriageReturn = (byte) '\r';
final byte nullChar = (byte) '\0';
long sleep = 10;
int len = -1;
try {
while (!interrupted) {
logger.trace("Reading the inputStream");
if ((len = inputStream.read(tmpData)) > -1) {
if (logger.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) {
sb.append(String.format("%02X ", tmpData[i]));
}
logger.trace("Read {} bytes : {}", len, sb.toString());
}
for (int i = 0; i < len; i++) {
if (logger.isTraceEnabled()) {
logger.trace("Byte {} equals '{}' (hex '{}')", i, new String(new byte[] { tmpData[i] }),
String.format("%02X", tmpData[i]));
}
if (tmpData[i] != lineFeed && tmpData[i] != carriageReturn && tmpData[i] != nullChar) {
dataBuffer[index++] = tmpData[i];
if (logger.isTraceEnabled()) {
logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
new String(new byte[] { dataBuffer[index - 1] }),
String.format("%02X", dataBuffer[index - 1]));
}
}
if (i > 0 && (tmpData[i] == lineFeed || tmpData[i] == carriageReturn
|| tmpData[i] == nullChar)) {
if (index > 0) {
if (logger.isTraceEnabled()) {
logger.trace("The resulting line is '{}'",
new String(Arrays.copyOf(dataBuffer, index)));
}
line = StringUtils.chomp(new String(Arrays.copyOf(dataBuffer, index)));
line = line.replace(",", ".");
line = line.trim();
index = 0;
lastLineReceived = line;
break;
}
}
if (index == bufferSize) {
index = 0;
}
}
}
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
// Move on silently
}
}
} catch (Exception e) {
logger.error("An exception occurred while reading serial port : {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="oceanic" 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>openHAB Oceanic Binding</name>
<description>This is the binding for Oceanic Water Softener.</description>
<author>Karel Goderis</author>
</binding:binding>

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="oceanic"
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">
<!-- Oceanic Channel Types -->
<channel-type id="alarm">
<item-type>String</item-type>
<label>Alarm</label>
<description>Current alarm description, if any</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="alert">
<item-type>String</item-type>
<label>Alert</label>
<description>Current alert description, if any, to notify a shortage of salt</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="flow" advanced="true">
<item-type>Decimal</item-type>
<label>Flow</label>
<description>Flow in l/min</description>
<state pattern="%.1f l/min" readOnly="true"></state>
</channel-type>
<channel-type id="reserve" advanced="true">
<item-type>Decimal</item-type>
<label>Water Reserve</label>
<description>Water reserve in l before regeneration has to start</description>
<state pattern="%d l" readOnly="true"></state>
</channel-type>
<channel-type id="cycle">
<item-type>String</item-type>
<label>Cycle</label>
<description>Indicates the stage of the regeneration cycle</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="time" advanced="true">
<item-type>String</item-type>
<label>Date/Time</label>
<description>Date/Time stamp</description>
<state pattern="%1$td.%1$tm.%1$tY %1$tT" readOnly="true"></state>
</channel-type>
<channel-type id="unit" advanced="true">
<item-type>String</item-type>
<label>Unit</label>
<description>Hardness unit used to express hardness</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="hardness" advanced="true">
<item-type>Number</item-type>
<label>Water Hardness</label>
<description>Water hardness expressed using the chosen hardness unit</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="cylinderstate">
<item-type>String</item-type>
<label>Cylinder State</label>
<description>Indicates the state of the regeneration cylinder(s)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="salt">
<item-type>Number</item-type>
<label>Salt</label>
<description>Volume of salt remaining, in kg</description>
<state pattern="%d kg" readOnly="true"></state>
</channel-type>
<channel-type id="regeneratenow">
<item-type>Switch</item-type>
<label>Regenerate Now</label>
<description>Start immediate regeneration</description>
</channel-type>
<channel-type id="regeneratelater">
<item-type>Switch</item-type>
<label>Regenerate Later</label>
<description>Start a delayed regeneration</description>
</channel-type>
<channel-type id="multiregenerate">
<item-type>Switch</item-type>
<label>Start Multi-regeneration</label>
<description>Start a multi-regeneration</description>
</channel-type>
<channel-type id="consumption" advanced="true">
<item-type>Number</item-type>
<label>Water Consumption</label>
<description>Water consumption, in l</description>
<state pattern="%d l" readOnly="true"></state>
</channel-type>
<channel-type id="pressure">
<item-type>Number</item-type>
<label>Water Pressure</label>
<description>Water pressure, in bar</description>
<state pattern="%.1f bar" readOnly="true"></state>
</channel-type>
<channel-type id="number" advanced="true">
<item-type>Number</item-type>
<label>Regenerations</label>
<description>Number of regenerations</description>
<state readOnly="true"></state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="oceanic"
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="network">
<label>Oceanic Water Softener</label>
<description>Oceanic Water Softener connected through a network proxy</description>
<channels>
<channel id="alarm" typeId="alarm"/>
<channel id="alert" typeId="alert"/>
<channel id="totalflow" typeId="flow"/>
<channel id="reserve" typeId="reserve"/>
<channel id="cycle" typeId="cycle"/>
<channel id="endofcycle" typeId="time"/>
<channel id="endofregeneration" typeId="time"/>
<channel id="hardnessunit" typeId="unit"/>
<channel id="inlethardness" typeId="hardness"/>
<channel id="outlethardness" typeId="hardness"/>
<channel id="cylinderstate" typeId="cylinderstate"/>
<channel id="salt" typeId="salt"/>
<channel id="regeneratenow" typeId="regeneratenow"/>
<channel id="regeneratelater" typeId="regeneratelater"/>
<channel id="multiregenerate" typeId="multiregenerate"/>
<channel id="consumptionmonday" typeId="consumption"/>
<channel id="consumptiontuesday" typeId="consumption"/>
<channel id="consumptionwednesday" typeId="consumption"/>
<channel id="consumptionthursday" typeId="consumption"/>
<channel id="consumptionfriday" typeId="consumption"/>
<channel id="consumptionsaturday" typeId="consumption"/>
<channel id="consumptionsunday" typeId="consumption"/>
<channel id="consumptiontoday" typeId="consumption"/>
<channel id="consumptionyesterday" typeId="consumption"/>
<channel id="consumptioncurrentweek" typeId="consumption"/>
<channel id="consumptionlastweek" typeId="consumption"/>
<channel id="consumptioncurrentmonth" typeId="consumption"/>
<channel id="consumptionlastmonth" typeId="consumption"/>
<channel id="consumptioncomplete" typeId="consumption"/>
<channel id="consumptionuntreated" typeId="consumption"/>
<channel id="consumptionpeaklevel" typeId="consumption"/>
<channel id="pressure" typeId="pressure"/>
<channel id="maxpressure" typeId="pressure"/>
<channel id="minpressure" typeId="pressure"/>
<channel id="maxflow" typeId="flow"/>
<channel id="lastgeneration" typeId="time"/>
<channel id="normalregenerations" typeId="number"/>
<channel id="serviceregenerations" typeId="number"/>
<channel id="incompleteregenerations" typeId="number"/>
<channel id="allregenerations" typeId="number"/>
</channels>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>Network Address</label>
<description>Network address of the network proxy</description>
</parameter>
<parameter name="portNumber" type="integer" required="true">
<description>Port number of the network proxy</description>
<required>false</required>
<label>Port</label>
</parameter>
<parameter name="interval" type="decimal" required="true">
<label>Polling Interval</label>
<description>Interval in seconds to poll the Oceanic Water Softener for status updates</description>
<default>60</default>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="oceanic"
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="serial">
<label>Oceanic Water Softener</label>
<description>Oceanic Water Softener connected through a serial port</description>
<channels>
<channel id="alarm" typeId="alarm"/>
<channel id="alert" typeId="alert"/>
<channel id="totalflow" typeId="flow"/>
<channel id="reserve" typeId="reserve"/>
<channel id="cycle" typeId="cycle"/>
<channel id="endofcycle" typeId="time"/>
<channel id="endofregeneration" typeId="time"/>
<channel id="hardnessunit" typeId="unit"/>
<channel id="inlethardness" typeId="hardness"/>
<channel id="outlethardness" typeId="hardness"/>
<channel id="cylinderstate" typeId="cylinderstate"/>
<channel id="salt" typeId="salt"/>
<channel id="regeneratenow" typeId="regeneratenow"/>
<channel id="regeneratelater" typeId="regeneratelater"/>
<channel id="multiregenerate" typeId="multiregenerate"/>
<channel id="consumptionmonday" typeId="consumption"/>
<channel id="consumptiontuesday" typeId="consumption"/>
<channel id="consumptionwednesday" typeId="consumption"/>
<channel id="consumptionthursday" typeId="consumption"/>
<channel id="consumptionfriday" typeId="consumption"/>
<channel id="consumptionsaturday" typeId="consumption"/>
<channel id="consumptionsunday" typeId="consumption"/>
<channel id="consumptiontoday" typeId="consumption"/>
<channel id="consumptionyesterday" typeId="consumption"/>
<channel id="consumptioncurrentweek" typeId="consumption"/>
<channel id="consumptionlastweek" typeId="consumption"/>
<channel id="consumptioncurrentmonth" typeId="consumption"/>
<channel id="consumptionlastmonth" typeId="consumption"/>
<channel id="consumptioncomplete" typeId="consumption"/>
<channel id="consumptionuntreated" typeId="consumption"/>
<channel id="consumptionpeaklevel" typeId="consumption"/>
<channel id="pressure" typeId="pressure"/>
<channel id="maxpressure" typeId="pressure"/>
<channel id="minpressure" typeId="pressure"/>
<channel id="maxflow" typeId="flow"/>
<channel id="lastgeneration" typeId="time"/>
<channel id="normalregenerations" typeId="number"/>
<channel id="serviceregenerations" typeId="number"/>
<channel id="incompleteregenerations" typeId="number"/>
<channel id="allregenerations" typeId="number"/>
</channels>
<config-description>
<parameter name="port" type="text" required="true">
<label>Serial Port</label>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
<description>Serial Port the Oceanic Water Softener is connected to</description>
</parameter>
<parameter name="interval" type="decimal" required="true">
<label>Polling Interval</label>
<description>Interval in seconds to poll the Oceanic Water Softener for status updates</description>
<default>60</default>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>