]> git.basschouten.com Git - openhab-addons.git/commitdiff
[proteusecometer] Proteus Eco Meter Binding - Initial contribution (#11333)
authorMatthias Herrmann <matthias.mh.herrmann@gmail.com>
Sat, 23 Oct 2021 09:27:13 +0000 (11:27 +0200)
committerGitHub <noreply@github.com>
Sat, 23 Oct 2021 09:27:13 +0000 (11:27 +0200)
* Proteus Eco Meter Binding

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Fulfil some conventions and choose better tradeoffs

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Patch shell script in another PR

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Move 4 lines into another PR

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Improvements

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* File based doc

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Rename identifiers

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Changed identifier

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
* Uniformed unit pattern

Signed-off-by: Matthias Herrmann <matthias.mh.herrmann@gmail.com>
20 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.proteusecometer/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/README.md [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/WrappedException.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSParser.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSReply.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSService.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/handler/ProteusEcoMeterSHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/serialport/SerialPortService.java [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer.properties [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index eac0069afb549b80dd2a5843ad75a313d062e92d..a74a451fe98079805ca7058669cb719e5c44c717 100644 (file)
 /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
index 89b1ccaa0a9c2f1b0b4e93644ac2f27b3a5988a7..67288274817e9859a673dc0c91d81d3e4a266611 100644 (file)
       <artifactId>org.openhab.binding.powermax</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.proteusecometer</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.pulseaudio</artifactId>
diff --git a/bundles/org.openhab.binding.proteusecometer/NOTICE b/bundles/org.openhab.binding.proteusecometer/NOTICE
new file mode 100644 (file)
index 0000000..f4d0fdd
--- /dev/null
@@ -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 (file)
index 0000000..97ed575
--- /dev/null
@@ -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 (file)
index 0000000..73dfaf9
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  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.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.proteusecometer</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: ProteusEcoMeter Binding</name>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>com.fazecast</groupId>
+      <artifactId>jSerialComm</artifactId>
+      <version>2.7.0</version>
+      <scope>compile</scope>
+    </dependency>
+
+  </dependencies>
+
+</project>
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 (file)
index 0000000..f9a8f7d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.proteusecometer-${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-proteusecometer" description="ProteusEcoMeter Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.proteusecometer/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..2049160
--- /dev/null
@@ -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 (file)
index 0000000..ff0e163
--- /dev/null
@@ -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 (file)
index 0000000..1638db5
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..34c8ea1
--- /dev/null
@@ -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 (file)
index 0000000..f3c7177
--- /dev/null
@@ -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<ProteusEcoMeterSReply> 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 (file)
index 0000000..868a806
--- /dev/null
@@ -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 (file)
index 0000000..a24909f
--- /dev/null
@@ -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<ProteusEcoMeterSReply> read(final String portId, final SerialPortService serialPort)
+            throws IOException {
+        logger.trace("communicate");
+
+        final InputStream inputStream = serialPort.getInputStream(portId, 115200, 8, 1, 0);
+        final Supplier<Optional<ProteusEcoMeterSReply>> 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<ProteusEcoMeterSReply> 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 (file)
index 0000000..36c1cee
--- /dev/null
@@ -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<ProteusEcoMeterSReply> 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 (file)
index 0000000..a238110
--- /dev/null
@@ -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 (file)
index 0000000..405445d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="proteusecometer" 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>Proteus EcoMeter</name>
+       <description>Puts your EcoMeter data into openHAB</description>
+
+</binding:binding>
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 (file)
index 0000000..0abc473
--- /dev/null
@@ -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 (file)
index 0000000..096bb83
--- /dev/null
@@ -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 (file)
index 0000000..72cf68f
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="proteusecometer"
+       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="EcoMeterS">
+               <label>Proteus EcoMeter S</label>
+               <description>Sensor for measuring water level of a cistern. Connected via USB</description>
+
+               <channels>
+                       <channel id="temperature" typeId="Temperature"/>
+                       <channel id="sensorLevel" typeId="SensorLevel"/>
+                       <channel id="usableLevel" typeId="UsableLevel"/>
+                       <channel id="usableLevelInPercent" typeId="UsableLevelInPercent"/>
+                       <channel id="totalCapacity" typeId="TotalCapacity"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="usbPort" type="text" required="true">
+                               <context>serial-port</context>
+                               <label>USB Port</label>
+                               <description>USB port the device is connected to i.e. /dev/ttyUSB0</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="Temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>Temperature measured by the sensor</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="SensorLevel">
+               <item-type>Number:Length</item-type>
+               <label>Sensor Level</label>
+               <description>The distance between the sensor and the water surface</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="UsableLevel">
+               <item-type>Number:Volume</item-type>
+               <label>Usable Level in litre</label>
+               <description>The usable level in litre</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="UsableLevelInPercent">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Usable Level in percent</label>
+               <description>The usable level in percent</description>
+               <state readOnly="true" pattern="%.2f %unit%"/>
+       </channel-type>
+
+       <channel-type id="TotalCapacity">
+               <item-type>Number:Volume</item-type>
+               <label>Total Capacity</label>
+               <description>The total capacity of your cistern/tank</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+</thing:thing-descriptions>
index 3debe4b5ac9e80f5094482d101c032dc481d2e4b..345fc9daecd35afc5b28e1c6adfdc4991756a331 100644 (file)
     <module>org.openhab.binding.plugwise</module>
     <module>org.openhab.binding.plugwiseha</module>
     <module>org.openhab.binding.powermax</module>
+    <module>org.openhab.binding.proteusecometer</module>
     <module>org.openhab.binding.pulseaudio</module>
     <module>org.openhab.binding.pushbullet</module>
     <module>org.openhab.binding.pushover</module>