added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user