[proteusecometer] Proteus Eco Meter Binding - Initial contribution (#11333)

* 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>
This commit is contained in:
Matthias Herrmann
2021-10-23 11:27:13 +02:00
committed by GitHub
parent 8337f8b92d
commit d2e6780140
20 changed files with 770 additions and 0 deletions

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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 = "";
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>