From d2e67801400a1bf7e7f958efc5900b4fb088ea6a Mon Sep 17 00:00:00 2001 From: Matthias Herrmann Date: Sat, 23 Oct 2021 11:27:13 +0200 Subject: [PATCH] [proteusecometer] Proteus Eco Meter Binding - Initial contribution (#11333) * Proteus Eco Meter Binding Signed-off-by: Matthias Herrmann * Fulfil some conventions and choose better tradeoffs Signed-off-by: Matthias Herrmann * Patch shell script in another PR Signed-off-by: Matthias Herrmann * Move 4 lines into another PR Signed-off-by: Matthias Herrmann * Improvements Signed-off-by: Matthias Herrmann * File based doc Signed-off-by: Matthias Herrmann * Rename identifiers Signed-off-by: Matthias Herrmann * Changed identifier Signed-off-by: Matthias Herrmann * Uniformed unit pattern Signed-off-by: Matthias Herrmann --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 20 +++ .../README.md | 52 ++++++ .../pom.xml | 28 ++++ .../src/main/feature/feature.xml | 9 ++ .../ProteusEcoMeterBindingConstants.java | 41 +++++ .../ProteusEcoMeterConfiguration.java | 25 +++ .../ProteusEcoMeterHandlerFactory.java | 65 ++++++++ .../internal/WrappedException.java | 30 ++++ .../ecometers/ProteusEcoMeterSParser.java | 76 +++++++++ .../ecometers/ProteusEcoMeterSReply.java | 43 +++++ .../ecometers/ProteusEcoMeterSService.java | 78 +++++++++ .../handler/ProteusEcoMeterSHandler.java | 149 ++++++++++++++++++ .../serialport/SerialPortService.java | 28 ++++ .../main/resources/OH-INF/binding/binding.xml | 9 ++ .../OH-INF/i18n/proteusecometer.properties | 24 +++ .../OH-INF/i18n/proteusecometer_de.properties | 24 +++ .../resources/OH-INF/thing/thing-types.xml | 62 ++++++++ bundles/pom.xml | 1 + 20 files changed, 770 insertions(+) create mode 100644 bundles/org.openhab.binding.proteusecometer/NOTICE create mode 100644 bundles/org.openhab.binding.proteusecometer/README.md create mode 100644 bundles/org.openhab.binding.proteusecometer/pom.xml create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterBindingConstants.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterConfiguration.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ProteusEcoMeterHandlerFactory.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/WrappedException.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSParser.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSReply.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/ProteusEcoMeterSService.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/ecometers/handler/ProteusEcoMeterSHandler.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/java/org/openhab/binding/proteusecometer/internal/serialport/SerialPortService.java create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer.properties create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/i18n/proteusecometer_de.properties create mode 100644 bundles/org.openhab.binding.proteusecometer/src/main/resources/OH-INF/thing/thing-types.xml 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.bundles org.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.plugwise org.openhab.binding.plugwiseha org.openhab.binding.powermax + org.openhab.binding.proteusecometer org.openhab.binding.pulseaudio org.openhab.binding.pushbullet org.openhab.binding.pushover