diff --git a/CODEOWNERS b/CODEOWNERS
index eac0069afb5..a74a451fe98 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -240,6 +240,7 @@
/bundles/org.openhab.binding.plugwise/ @wborn
/bundles/org.openhab.binding.plugwiseha/ @lsiepel
/bundles/org.openhab.binding.powermax/ @lolodomo
+/bundles/org.openhab.binding.proteusecometer/ @2chilled
/bundles/org.openhab.binding.pulseaudio/ @peuter
/bundles/org.openhab.binding.pushbullet/ @hakan42
/bundles/org.openhab.binding.pushover/ @cweitkamp
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 89b1ccaa0a9..67288274817 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1181,6 +1181,11 @@
org.openhab.binding.powermax${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.proteusecometer
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.pulseaudio
diff --git a/bundles/org.openhab.binding.proteusecometer/NOTICE b/bundles/org.openhab.binding.proteusecometer/NOTICE
new file mode 100644
index 00000000000..f4d0fdd9867
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/NOTICE
@@ -0,0 +1,20 @@
+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
+
+== Third-party Content
+
+jSerialComm
+* License: Apache 2.0 License
+* Project: https://github.com/Fazecast/jSerialComm
+* Source: https://github.com/Fazecast/jSerialComm
diff --git a/bundles/org.openhab.binding.proteusecometer/README.md b/bundles/org.openhab.binding.proteusecometer/README.md
new file mode 100644
index 00000000000..97ed57594a0
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/README.md
@@ -0,0 +1,52 @@
+# ProteusEcoMeter Binding
+
+This is the binding for the Proteus EcoMeter S, which is able to report the level of a cistern or tank.
+
+Note that this binding currently supports no write channels.
+This means you have to configure your sensor by considering the manual of the product (using wireless display).
+After doing that the binding comes into play and helps you to get your measured values into openHAB.
+Please be patient while waiting for the first received data.
+The sensor reports at an interval of approx. 1h, except when the water level changes relatively fast.
+
+## Supported Things
+
+Proteus EcoMeter S.
+The binding has been tested with this EcoMeter sensor only.
+
+## Discovery
+
+No auto discovery implemented yet.
+
+## Thing Configuration
+
+Plug the wireless display into an USB port.
+Note [openHAB Serial Port documentation](https://www.openhab.org/docs/administration/serial.html) for general serial port configuration.
+After that you can add the device as thing and configure the usbPort your OS generated for the display.
+
+```
+UID: proteusecometer:EcoMeterS:e90705eaa4
+label: Proteus EcoMeter S
+thingTypeUID: proteusecometer:EcoMeterS
+configuration:
+ usbPort: /dev/ttyUSB0
+```
+
+## Channels
+
+| channel | type | description |
+|-----------------------|----------------------|------------------------------------------------------|
+| temperature | Number:Temperature | Temperature measured by the sensor |
+| sensorLevel | Number:Length | Distance between sensor and water surface |
+| usableLevel | Number:Volume | How much liquid is usable |
+| usableLevelInPercent | Number:Dimensionless | How much liquid is usable relative to total capacity |
+| totalCapacity | Number:Volume | Total capacity of measured cistern/tank |
+
+## Full Example
+
+Thing proteusecometer:EcoMeterS:e90705eaa4 "Proteus EcoMeter S" [ usbPort="/dev/ttyUSB0" ]
+
+Number:Temperature Temperature "Measured temperature [%.1f °C]" { channel="proteusecometer:EcoMeterS:e90705eaa4:temperature" }
+Number:Length SensorLevelCm "Sensor Level" { channel="proteusecometer:EcoMeterS:e90705eaa4:sensorLevel" }
+Number:Volume UsableLevel "Usable Level" { channel="proteusecometer:EcoMeterS:e90705eaa4:usableLevel" }
+Number:Dimensionless UsableLevelinpercent "Usable Level" { channel="proteusecometer:EcoMeterS:e90705eaa4:usableLevelInPercent" }
+Number:Volume TotalCapacityinliter "Total Capacity" { channel="proteusecometer:EcoMeterS:e90705eaa4:totalCapacity" }
diff --git a/bundles/org.openhab.binding.proteusecometer/pom.xml b/bundles/org.openhab.binding.proteusecometer/pom.xml
new file mode 100644
index 00000000000..73dfaf9b9ca
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/pom.xml
@@ -0,0 +1,28 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.2.0-SNAPSHOT
+
+
+ org.openhab.binding.proteusecometer
+
+ openHAB Add-ons :: Bundles :: ProteusEcoMeter Binding
+
+
+
+
+ com.fazecast
+ jSerialComm
+ 2.7.0
+ compile
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/feature/feature.xml b/bundles/org.openhab.binding.proteusecometer/src/main/feature/feature.xml
new file mode 100644
index 00000000000..f9a8f7d8eec
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.proteusecometer/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterBindingConstants.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterBindingConstants.java
new file mode 100644
index 00000000000..2049160fc51
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterBindingConstants.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ProteusEcoMeterBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Matthias Herrmann - Initial contribution
+ */
+@NonNullByDefault
+public class ProteusEcoMeterBindingConstants {
+
+ private static final String BINDING_ID = "proteusecometer";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ECO_METER_S = new ThingTypeUID(BINDING_ID, "EcoMeterS");
+
+ public static final String TEMPERATURE = "temperature";
+
+ public static final String SENSOR_LEVEL = "sensorLevel";
+
+ public static final String USABLE_LEVEL = "usableLevel";
+
+ public static final String USABLE_LEVEL_IN_PERCENT = "usableLevelInPercent";
+
+ public static final String TOTAL_CAPACITY = "totalCapacity";
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterConfiguration.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterConfiguration.java
new file mode 100644
index 00000000000..ff0e1637354
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterConfiguration.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ProteusEcoMeterConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Matthias Herrmann - Initial contribution
+ */
+@NonNullByDefault
+public class ProteusEcoMeterConfiguration {
+ public String usbPort = "";
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterHandlerFactory.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterHandlerFactory.java
new file mode 100644
index 00000000000..1638db5cbe3
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterHandlerFactory.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal;
+
+import static org.openhab.binding.proteusecometer.internal.ProteusEcoMeterBindingConstants.THING_TYPE_ECO_METER_S;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.proteusecometer.internal.ecometers.handler.ProteusEcoMeterSHandler;
+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.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ProteusEcoMeterHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Matthias Herrmann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.proteusecometer", service = ThingHandlerFactory.class)
+public class ProteusEcoMeterHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ECO_METER_S);
+ private final Logger logger = LoggerFactory.getLogger(ProteusEcoMeterHandlerFactory.class);
+
+ @Activate
+ public ProteusEcoMeterHandlerFactory() {
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_ECO_METER_S.equals(thingTypeUID)) {
+ logger.trace("Creating ProteusEcoMeterSHandler");
+ return new ProteusEcoMeterSHandler(thing);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/WrappedException.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/WrappedException.java
new file mode 100644
index 00000000000..34c8ea1f11b
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/WrappedException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Allows you to transform an {@link Exception} to {@link RuntimeException} to circumvent checked exception
+ * issues.
+ *
+ * @author Matthias Herrmann - Initial contribution
+ */
+@NonNullByDefault
+public class WrappedException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public WrappedException(final Exception wrapped) {
+ super(wrapped);
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSParser.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSParser.java
new file mode 100644
index 00000000000..f3c7177df80
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSParser.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal.ecometers;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parse the bytes from the device
+ *
+ * @author Matthias Herrmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+class ProteusEcoMeterSParser {
+
+ private final Logger logger = LoggerFactory.getLogger(ProteusEcoMeterSParser.class);
+
+ /**
+ * @param bytes Raw bytes send from the device
+ * @return A structured version of the bytes, if possible
+ */
+ public Optional parseFromBytes(final byte[] bytes) {
+ return Optional.ofNullable(bytes).flatMap(b -> {
+ final String hexString = HexUtils.bytesToHex(b);
+ logger.trace("Received hex string: {}", hexString);
+
+ if (hexString.length() < 4) {
+ return Optional.empty();
+ } else {
+ final String marker = hexString.substring(0, 4);
+ if (!"5349".equals(marker)) {
+ logger.trace("Marker is not {} but {}", "5349", marker);
+ return Optional.empty();
+ } else if (hexString.length() < 40) {
+ logger.trace("hexString is of length {}, expected >= 40", hexString.length());
+ return Optional.empty();
+ } else {
+ try {
+ return Optional
+ .of(new ProteusEcoMeterSReply(parseInt(hexString.substring(26, 28), "tempInFahrenheit"),
+ parseInt(hexString.substring(28, 32), "sensorLevelInCm"),
+ parseInt(hexString.substring(32, 36), "usableLevelInLiter"),
+ parseInt(hexString.substring(36, 40), "totalCapacityInLiter")));
+ } catch (final NumberFormatException e) {
+ logger.debug("Error while parsing numbers", e);
+ return Optional.empty();
+ }
+ }
+ }
+ });
+ }
+
+ private Integer parseInt(final String toParse, final String fieldName) throws NumberFormatException {
+ try {
+ return Integer.parseInt(toParse, 16);
+ } catch (final NumberFormatException e) {
+ logger.trace("Unable to parse field {}", fieldName, e);
+ throw e;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSReply.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSReply.java
new file mode 100644
index 00000000000..868a8064318
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSReply.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal.ecometers;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The reply of Proteus EcoMeter S
+ *
+ * @author Matthias Herrmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ProteusEcoMeterSReply {
+ public final double tempInFahrenheit;
+ public final int sensorLevelInCm;
+ public final int usableLevelInLiter;
+ public final int totalCapacityInLiter;
+
+ public ProteusEcoMeterSReply(final double tempInFahrenheit, final int sensorLevelInCm, final int usableLevelInLiter,
+ final int totalCapacityInLiter) {
+ this.tempInFahrenheit = tempInFahrenheit;
+ this.sensorLevelInCm = sensorLevelInCm;
+ this.usableLevelInLiter = usableLevelInLiter;
+ this.totalCapacityInLiter = totalCapacityInLiter;
+ }
+
+ @Override
+ public String toString() {
+ return "ProteusEcoMeterSReply [sensorLevelInCm=" + sensorLevelInCm + ", tempInFahrenheit=" + tempInFahrenheit
+ + ", totalCapacityInLiter=" + totalCapacityInLiter + ", usableLevelInLiter=" + usableLevelInLiter + "]";
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSService.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSService.java
new file mode 100644
index 00000000000..a24909fbf5d
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSService.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal.ecometers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.proteusecometer.internal.WrappedException;
+import org.openhab.binding.proteusecometer.internal.serialport.SerialPortService;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Read from Proteus EcoMeter S
+ *
+ * @author Matthias Herrmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ProteusEcoMeterSService {
+
+ private final Logger logger = LoggerFactory.getLogger(ProteusEcoMeterSService.class);
+
+ /**
+ * Initialize the communication with the device, i.e. open the serial port etc.
+ *
+ * @return {@code true} if we can communicate with the device
+ * @throws IOException
+ */
+ public Stream read(final String portId, final SerialPortService serialPort)
+ throws IOException {
+ logger.trace("communicate");
+
+ final InputStream inputStream = serialPort.getInputStream(portId, 115200, 8, 1, 0);
+ final Supplier> supplier = () -> {
+ logger.trace("Input stream opened for the port");
+
+ try {
+ final byte[] deviceBytes = new byte[22];
+ inputStream.read(deviceBytes, 0, 22);
+ final String hexString = HexUtils.bytesToHex(deviceBytes);
+ logger.trace("Received hex string: {}", hexString);
+ final ProteusEcoMeterSParser parser = new ProteusEcoMeterSParser();
+ final Optional dataOpt = parser.parseFromBytes(deviceBytes);
+
+ if (dataOpt.isEmpty()) {
+ logger.warn("Received bytes I don't understand: {}", hexString);
+ }
+ return dataOpt;
+ } catch (final IOException e) {
+ throw new WrappedException(e);
+ } finally {
+ try {
+ inputStream.close();
+ } catch (final IOException e) {
+ }
+ }
+ };
+
+ return Stream.generate(supplier).takeWhile(reply -> !Thread.interrupted()).filter(Optional::isPresent)
+ .map(Optional::get);
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/handler/ProteusEcoMeterSHandler.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/handler/ProteusEcoMeterSHandler.java
new file mode 100644
index 00000000000..36c1cee74c3
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/handler/ProteusEcoMeterSHandler.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal.ecometers.handler;
+
+import static org.openhab.binding.proteusecometer.internal.ProteusEcoMeterBindingConstants.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.proteusecometer.internal.ProteusEcoMeterConfiguration;
+import org.openhab.binding.proteusecometer.internal.WrappedException;
+import org.openhab.binding.proteusecometer.internal.ecometers.ProteusEcoMeterSReply;
+import org.openhab.binding.proteusecometer.internal.ecometers.ProteusEcoMeterSService;
+import org.openhab.binding.proteusecometer.internal.serialport.SerialPortService;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.MetricPrefix;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fazecast.jSerialComm.SerialPort;
+
+/**
+ * The {@link ProteusEcoMeterSHandler} updates thing channels when receiving data
+ *
+ * @author Matthias Herrmann - Initial contribution
+ */
+@NonNullByDefault
+public class ProteusEcoMeterSHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(ProteusEcoMeterSHandler.class);
+ private @Nullable SerialPort serialPort;
+ private ProteusEcoMeterConfiguration config = new ProteusEcoMeterConfiguration();
+ private @Nullable ScheduledFuture> job;
+ private SerialPortService serialPortService = new SerialPortService() {
+ @NonNullByDefault
+ public InputStream getInputStream(String portId, int baudRate, int numDataBits, int numStopBits, int parity) {
+ try {
+ ProteusEcoMeterSHandler.this.serialPort = SerialPort.getCommPort(portId);
+ final SerialPort localSerialPort = ProteusEcoMeterSHandler.this.serialPort;
+ if (localSerialPort == null) {
+ throw new IOException("SerialPort.getCommPort(" + portId + ") returned null");
+ }
+ localSerialPort.closePort();
+
+ localSerialPort.setBaudRate(baudRate);
+ localSerialPort.setNumDataBits(numDataBits);
+ localSerialPort.setNumStopBits(numStopBits);
+ localSerialPort.setParity(parity);
+ localSerialPort.openPort();
+ localSerialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0);
+ final InputStream inputStream = localSerialPort.getInputStream();
+ if (inputStream == null) {
+ throw new IOException("serialPort.getInputStream() returned null");
+ }
+ return inputStream;
+ } catch (final Exception e) {
+ closeSerialPort();
+ throw new WrappedException(e);
+ }
+ }
+ };
+
+ public ProteusEcoMeterSHandler(final Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(ProteusEcoMeterConfiguration.class);
+ updateStatus(ThingStatus.UNKNOWN);
+ job = scheduler.schedule(() -> handleDeviceReplies(), 0, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // at the moment there are no commands supported. The Eco Meter S would support configuration
+ // commands, but this is not implemented yet
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ closeSerialPort();
+ final ScheduledFuture> localJob = job;
+ if (localJob != null) {
+ localJob.cancel(true);
+ job = null;
+ }
+ }
+
+ private void handleDeviceReplies() {
+ final Duration retryInitDelay = Duration.ofSeconds(10);
+ try {
+ final ProteusEcoMeterSService ecoMeterSService = new ProteusEcoMeterSService();
+ final Stream replyStream = ecoMeterSService.read(config.usbPort, serialPortService);
+ updateStatus(ThingStatus.ONLINE);
+
+ replyStream.forEach(reply -> {
+ updateState(SENSOR_LEVEL, new QuantityType<>(reply.sensorLevelInCm, MetricPrefix.CENTI(SIUnits.METRE)));
+ updateState(USABLE_LEVEL, new QuantityType<>(reply.usableLevelInLiter, Units.LITRE));
+ updateState(USABLE_LEVEL_IN_PERCENT, new QuantityType<>(
+ 100d / reply.totalCapacityInLiter * reply.usableLevelInLiter, Units.PERCENT));
+ updateState(TEMPERATURE, new QuantityType<>(reply.tempInFahrenheit, ImperialUnits.FAHRENHEIT));
+ updateState(TOTAL_CAPACITY, new QuantityType<>(reply.totalCapacityInLiter, Units.LITRE));
+ });
+ logger.debug("The reply stream ended unexpectedly. Retrying in {}", retryInitDelay);
+ } catch (final Exception e) {
+ logger.debug("Error communicating with eco meter s. Retrying in {}", retryInitDelay, e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "Error reading from Port: " + e.getMessage());
+ } finally {
+ closeSerialPort();
+ job = scheduler.schedule(this::handleDeviceReplies, retryInitDelay.getSeconds(), TimeUnit.SECONDS);
+ }
+ }
+
+ private void closeSerialPort() {
+ if (serialPort != null) {
+ final boolean closed = serialPort.closePort();
+ logger.debug("serialPort.closePort() returned {}", closed);
+ serialPort = null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/serialport/SerialPortService.java b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/serialport/SerialPortService.java
new file mode 100644
index 00000000000..a23811097ad
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/serialport/SerialPortService.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 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.proteusecometer.internal.serialport;
+
+import java.io.InputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Abstract over serial port implementations
+ *
+ * @author Matthias Herrmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+public interface SerialPortService {
+ public InputStream getInputStream(String portId, int baudRate, int numDataBits, int numStopBits, int parity);
+}
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..405445d3db0
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Proteus EcoMeter
+ Puts your EcoMeter data into openHAB
+
+
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer.properties b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer.properties
new file mode 100644
index 00000000000..0abc473d29b
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer.properties
@@ -0,0 +1,24 @@
+# binding
+binding.proteusecometer.name = Proteus EcoMeter
+binding.proteusecometer.description = Puts your EcoMeter data into openHAB
+# thing types
+thing-type.proteusecometer.EcoMeterS.label = Proteus EcoMeter S
+thing-type.proteusecometer.EcoMeterS.description = Sensor for measuring water level of a cistern. Connected via USB
+
+thing-type.config.proteusecometer.EcoMeterS.usbPort.label = USB Port
+thing-type.config.proteusecometer.EcoMeterS.usbPort.description = USB port the device is connected to i.e. /dev/ttyUSB0
+# channel types
+channel-type.proteusecometer.Temperature.label = Temperature
+channel-type.proteusecometer.Temperature.description = Temperature measured by the sensor
+
+channel-type.proteusecometer.SensorLevel.label = Sensor Level
+channel-type.proteusecometer.SensorLevel.description = The distance between the sensor and the water surface
+
+channel-type.proteusecometer.UsableLevel.label = Usable Level in litre
+channel-type.proteusecometer.UsableLevel.description = The usable level in litre
+
+channel-type.proteusecometer.UsableLevelInPercent.label = Usable Level in percent
+channel-type.proteusecometer.UsableLevelInPercent.description = The usable level in percent
+
+channel-type.proteusecometer.TotalCapacity.label = Total Capacity
+channel-type.proteusecometer.TotalCapacity.description = The total capacity of your cistern/tank
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer_de.properties b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer_de.properties
new file mode 100644
index 00000000000..096bb83b076
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer_de.properties
@@ -0,0 +1,24 @@
+# binding
+binding.proteusecometer.name = Proteus EcoMeter
+binding.proteusecometer.description = EcoMeter Sensordaten in openHAB
+# thing types
+thing-type.proteusecometer.EcoMeterS.label = Proteus EcoMeter S
+thing-type.proteusecometer.EcoMeterS.description = Füllstandsanzeige für Zisterne, Wassertanks, Erdtanks
+
+thing-type.config.proteusecometer.EcoMeterS.usbPort.label = USB Port
+thing-type.config.proteusecometer.EcoMeterS.usbPort.description = USB Port des Geräts, z.B. /dev/ttyUSB0
+# channel types
+channel-type.proteusecometer.Temperature.label = Temperatur
+channel-type.proteusecometer.Temperature.description = Umgebungstemperatur des Sensors
+
+channel-type.proteusecometer.SensorLevel.label = Sensorhöhe
+channel-type.proteusecometer.SensorLevel.description = Sensorhöhe über Flüssigkeitsoberfläche
+
+channel-type.proteusecometer.UsableLevel.label = Füllmenge in Liter
+channel-type.proteusecometer.UsableLevel.description = Füllmenge in Liter
+
+channel-type.proteusecometer.UsableLevelInPercent.label = Füllmenge in Prozent
+channel-type.proteusecometer.UsableLevelInPercent.description = Füllmenge in Prozent
+
+channel-type.proteusecometer.TotalCapacity.label = Gesamtkapazität
+channel-type.proteusecometer.TotalCapacity.description = Gesamtkapazität des Messobjekts
diff --git a/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..72cf68f730c
--- /dev/null
+++ b/bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+ Sensor for measuring water level of a cistern. Connected via USB
+
+
+
+
+
+
+
+
+
+
+
+ serial-port
+
+ USB port the device is connected to i.e. /dev/ttyUSB0
+
+
+
+
+
+ Number:Temperature
+
+ Temperature measured by the sensor
+
+
+
+
+ Number:Length
+
+ The distance between the sensor and the water surface
+
+
+
+
+ Number:Volume
+
+ The usable level in litre
+
+
+
+
+ Number:Dimensionless
+
+ The usable level in percent
+
+
+
+
+ Number:Volume
+
+ The total capacity of your cistern/tank
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 3debe4b5ac9..345fc9daecd 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -272,6 +272,7 @@
org.openhab.binding.plugwiseorg.openhab.binding.plugwisehaorg.openhab.binding.powermax
+ org.openhab.binding.proteusecometerorg.openhab.binding.pulseaudioorg.openhab.binding.pushbulletorg.openhab.binding.pushover