added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
34
bundles/org.openhab.binding.smartmeter/.classpath
Normal file
34
bundles/org.openhab.binding.smartmeter/.classpath
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="src/3rdparty/java"/>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="module" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.smartmeter/.project
Normal file
23
bundles/org.openhab.binding.smartmeter/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.smartmeter</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
35
bundles/org.openhab.binding.smartmeter/NOTICE
Normal file
35
bundles/org.openhab.binding.smartmeter/NOTICE
Normal file
@@ -0,0 +1,35 @@
|
||||
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
|
||||
|
||||
jsml
|
||||
* License: MPL v2.0 License
|
||||
* Project: https://www.openmuc.org/sml
|
||||
* Source: https://www.openmuc.org/sml
|
||||
|
||||
j62056
|
||||
* License: LGPL v2.1 License
|
||||
* Project: https://www.openmuc.org/iec-62056-21
|
||||
* Source: https://www.openmuc.org/iec-62056-21
|
||||
|
||||
rxjava
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://github.com/ReactiveX/RxJava
|
||||
* Source: https://github.com/ReactiveX/RxJava
|
||||
|
||||
reactive-streams
|
||||
* License: CC0 1.0 License
|
||||
* Project: https://www.reactive-streams.org
|
||||
* Source: https://github.com/reactive-streams/reactive-streams-jvm
|
||||
122
bundles/org.openhab.binding.smartmeter/README.md
Normal file
122
bundles/org.openhab.binding.smartmeter/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# SmartMeter Binding
|
||||
|
||||
This binding retrieves and reads SML messages (PUSH) and supports IEC 62056-21 modes A,B,C (PULL) and D (PUSH).
|
||||
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding supports only one Thing: `meter`
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery is not available, as the binding only reads from serial ports.
|
||||
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The smartmeter thing requires the serial port where the meter device is connected and optionally an refresh interval.
|
||||
|
||||
| Parameter | Name | Description | Required | Default |
|
||||
|-----------|------|-------------|----------|---------|
|
||||
| `port` | The serial port to connect to| URL to use for playing notification sounds, e.g. `/dev/ttyUSB0` | yes | |
|
||||
| `refresh` | The refresh interval in seconds | Defines at which interval the values of the meter device shall be read | no | 20 |
|
||||
| `mode` | The protocol mode to use | Can be `SML` (PUSH mode), `ABC` (PULL) or `D` (PUSH) | no | `SML` |
|
||||
| `baudrateChangeDelay` | Delay of baudrate change in ms | USB to serial converters often require a delay of up to 250ms after the ACK before changing baudrate (only relevant for 'C' mode) | no | 0 |
|
||||
| `baudrate` | (initial) Baudrate | The baudrate of the serial port. If set to `AUTO`, it will be negotiated with the meter. The default is `300` baud for modes A, B, and C and `2400` baud for mode D, and `9600` baud for SML. | no | `AUTO` |
|
||||
|
||||
## Channels
|
||||
|
||||
All available OBIS codes which are read out from the device are created as channels.
|
||||
At every read out the channels are synchronized with the OBIS codes from the device.
|
||||
|
||||
Following conversion from OBIS codes to channel ID is done:
|
||||
`.` is replaced by `-` and `:` or `*` is replaced by `_`.
|
||||
|
||||
e.g.
|
||||
|
||||
| OBIS code | Channel ID |
|
||||
|-------------|------------|
|
||||
|`1-0:1.8.1` | `1-0_1-8-1` |
|
||||
|`1.8.0*00` | `1-8-0_00` |
|
||||
|
||||
|
||||
### Channel Configuration
|
||||
|
||||
**negate:** Energy meters often provide absolute values and provide information about the *energy direction* in a separate bit.
|
||||
With this config you can specify the channel where this bit is located, the bit position and the bits value which shall be set.
|
||||
|
||||
`<negate> ::= <CHANNEL_ID>:<BIT_POSITION>:<BIT_VALUE>[:status]`
|
||||
|
||||
e.g.:
|
||||
|
||||
```
|
||||
"1-0_1-8-0:5:1:status" // negate if status(1-0_1-8-0) and 2^5 = 1
|
||||
"1-0_96-5-5:5:1" // negate if 1-0#96-5-5 and 2^5 = 1
|
||||
```
|
||||
|
||||
## Unit Conversion
|
||||
|
||||
Please use the [Units Of Measurement](https://www.openhab.org/docs/concepts/units-of-measurement.html) concept of openHAB for unit conversion which is fully supported by this binding.
|
||||
Please see the item example on how to use it.
|
||||
*NOTE:* your meter device needs to provide correct unit information to work properly.
|
||||
|
||||
## Full Example
|
||||
|
||||
Things:
|
||||
|
||||
```
|
||||
smartmeter:meter:heating [ port="COM1", refresh=10 ]
|
||||
smartmeter:meter:house [ port="rfc2217://xxx.xxx.xxx.xxx:3002" ]
|
||||
|
||||
smartmeter:meter:BinderPower [port="/dev/ttyUSB0", refresh=5] {
|
||||
Channels:
|
||||
Type 1-0_1-8-0 : 1-0_1-8-0
|
||||
Type 1-0_16-7-0 : 1-0_16-7-0 [
|
||||
negate="1-0_1-8-0:5:1:status"
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Items:
|
||||
|
||||
```
|
||||
Number:Energy HeatingTarif1 "Heating high price tariff [%.2f kWh]" { channel="smartmeter:meter:heating:1-0_1-8-1" }
|
||||
Number:Energy HeatingTarif2 "Heating low price tariff [%.2f kWh]" { channel="smartmeter:meter:heating:1-0_1-8-2" }
|
||||
|
||||
Number:Energy HouseTarif "Tariff [%.2f kWh]" { channel="smartmeter:meter:house:1-0_1-8-0" }
|
||||
|
||||
Number:Power HeatingActualUsage "Heating Current usage [%.2f %unit%]" { channel="smartmeter:meter:heating:1-0_16-7-0" }
|
||||
Number:Power HouseActualUsage "Current usage [%.2f %unit%]" { channel="smartmeter:meter:house:1-0_16-7-0" }
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Octet encoding for OBIS Codes
|
||||
|
||||
- '129-129:199.130.5'
|
||||
- '1-0:0.0.9'
|
||||
|
||||
doesn't work properly.
|
||||
|
||||
Any help/contribution is appreciated!
|
||||
|
||||
## Tested Hardware
|
||||
|
||||
The binding has been successfully tested with below hardware configuration:
|
||||
|
||||
### SML PUSH mode
|
||||
|
||||
- EMH EDL300 meter connected the IR-Reader USB from open hardware project in volkszaehler
|
||||
- EMH eHZ-IW8E2A
|
||||
- ISKRA MT681
|
||||
- EMH eHZ-K
|
||||
|
||||
### IEC 62056-21 Mode C
|
||||
|
||||
- Apator EC3 with IR-Reader from volkszaehler
|
||||
- Landis+Gyr E650 with IR-Reader from volkszaehler
|
||||
|
||||
### IEC 62056-21 Mode D
|
||||
|
||||
- Hager EHZ 361Z5 and EHZ 161L5
|
||||
65
bundles/org.openhab.binding.smartmeter/pom.xml
Normal file
65
bundles/org.openhab.binding.smartmeter/pom.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.smartmeter</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Smartmeter Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava2</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>2.2.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reactivestreams</groupId>
|
||||
<artifactId>reactive-streams</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openmuc</groupId>
|
||||
<artifactId>jsml</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openmuc</groupId>
|
||||
<artifactId>j62056</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<phase>generate-sources</phase>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>src/3rdparty</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
35
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/DataBits.java
vendored
Normal file
35
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/DataBits.java
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
|
||||
/**
|
||||
* The data bits.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public enum DataBits {
|
||||
/**
|
||||
* 5 data bits will be used for each character.
|
||||
*/
|
||||
DATABITS_5(SerialPort.DATABITS_5),
|
||||
/**
|
||||
* 6 data bits will be used for each character.
|
||||
*/
|
||||
DATABITS_6(SerialPort.DATABITS_6),
|
||||
/**
|
||||
* 8 data bits will be used for each character.
|
||||
*/
|
||||
DATABITS_7(SerialPort.DATABITS_7),
|
||||
/**
|
||||
* 8 data bits will be used for each character.
|
||||
*/
|
||||
DATABITS_8(SerialPort.DATABITS_8),;
|
||||
private int odlValue;
|
||||
|
||||
private DataBits(int oldValue) {
|
||||
this.odlValue = oldValue;
|
||||
}
|
||||
|
||||
int getOldValue() {
|
||||
return this.odlValue;
|
||||
}
|
||||
}
|
||||
29
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/FlowControl.java
vendored
Normal file
29
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/FlowControl.java
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
/**
|
||||
* The flow control.
|
||||
*
|
||||
* @see SerialPort#setFlowControl(FlowControl)
|
||||
* @see SerialPortBuilder#setFlowControl(FlowControl)
|
||||
*/
|
||||
public enum FlowControl {
|
||||
/**
|
||||
* No flow control.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Hardware flow control on input and output (RTS/CTS).
|
||||
*
|
||||
* <p>
|
||||
* Sets <b>RFR</b> (ready for receiving) formally known as <b>RTS</b> and the <b>CTS</b> (clear to send) flag.
|
||||
* </p>
|
||||
*/
|
||||
RTS_CTS,
|
||||
|
||||
/**
|
||||
* Software flow control on input and output.
|
||||
*/
|
||||
XON_XOFF
|
||||
|
||||
}
|
||||
343
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/JRxTxPort.java
vendored
Normal file
343
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/JRxTxPort.java
vendored
Normal file
@@ -0,0 +1,343 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
import static org.openhab.core.io.transport.serial.SerialPort.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.openhab.core.io.transport.serial.PortInUseException;
|
||||
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.osgi.util.tracker.ServiceTracker;
|
||||
|
||||
/**
|
||||
* <b>This class is a workaround:</b>
|
||||
* jrxtx library includes <code>gnu.io.*</code> classes and <code>j62056.jar</code> is depending on that. As we are
|
||||
* using nrjavaserial as an implementation of gnu.io, we can't go that way!
|
||||
* -> As a workaround I shaded the {@link JRxTxPort} class which is used by <code>j62056.jar</code> and modified it to
|
||||
* work with any implementation of {@link SerialPort} (formerly it was just working with the implementation
|
||||
* <code>gnu.io.RXTXPort</code>).
|
||||
*
|
||||
* @author MatthiasS
|
||||
*
|
||||
*/
|
||||
class JRxTxPort implements org.openmuc.jrxtx.SerialPort {
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
private org.openhab.core.io.transport.serial.SerialPort rxtxPort;
|
||||
|
||||
private SerialInputStream serialIs;
|
||||
private SerialOutputStream serial0s;
|
||||
|
||||
private String portName;
|
||||
|
||||
private DataBits dataBits;
|
||||
|
||||
private Parity parity;
|
||||
|
||||
private StopBits stopBits;
|
||||
|
||||
private int baudRate;
|
||||
|
||||
private int serialPortTimeout;
|
||||
|
||||
private FlowControl flowControl;
|
||||
|
||||
public static JRxTxPort openSerialPort(String portName, int baudRate, Parity parity, DataBits dataBits,
|
||||
StopBits stopBits, FlowControl flowControl) throws IOException {
|
||||
try {
|
||||
BundleContext bundleContext = FrameworkUtil.getBundle(JRxTxPort.class).getBundleContext();
|
||||
ServiceTracker<SerialPortManager, SerialPortManager> serviceTracker = new ServiceTracker<>(bundleContext,
|
||||
SerialPortManager.class, null);
|
||||
serviceTracker.open();
|
||||
SerialPortManager serialPortManager = serviceTracker.getService();
|
||||
|
||||
SerialPortIdentifier serialPortIdentifier = serialPortManager.getIdentifier(portName);
|
||||
if (serialPortIdentifier == null) {
|
||||
String errMessage = format("Serial port {0} not found or port is busy.", portName);
|
||||
throw new PortNotFoundException(errMessage);
|
||||
}
|
||||
org.openhab.core.io.transport.serial.SerialPort comPort = serialPortIdentifier.open("meterreader", 0);
|
||||
// if (!(comPort instanceof RXTXPort)) {
|
||||
// throw new SerialPortException("Unable to open the serial port. Port is not RXTX.");
|
||||
// }
|
||||
|
||||
try {
|
||||
comPort.setSerialPortParams(baudRate, dataBits.getOldValue(), stopBits.getOldValue(),
|
||||
parity.getOldValue());
|
||||
|
||||
setFlowControl(flowControl, comPort);
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
String message = format("Unable to apply config on serial port.\n{0}", e.getMessage());
|
||||
throw new SerialPortException(message);
|
||||
}
|
||||
|
||||
return new JRxTxPort(comPort, portName, baudRate, parity, dataBits, stopBits, flowControl);
|
||||
// } catch (NoSuchPortException e) {
|
||||
// String errMessage = format("Serial port {0} not found or port is busy.", portName);
|
||||
// throw new PortNotFoundException(errMessage);
|
||||
} catch (PortInUseException e) {
|
||||
String errMessage = format("Serial port {0} is already in use.", portName);
|
||||
throw new PortNotFoundException(errMessage);
|
||||
// } catch (UnsupportedCommOperationException e1) {
|
||||
// throw new IOException(e1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void setFlowControl(FlowControl flowControl,
|
||||
org.openhab.core.io.transport.serial.SerialPort rxtxPort) throws IOException {
|
||||
try {
|
||||
switch (flowControl) {
|
||||
case RTS_CTS:
|
||||
rxtxPort.setFlowControlMode(FLOWCONTROL_RTSCTS_IN | FLOWCONTROL_RTSCTS_OUT);
|
||||
break;
|
||||
case XON_XOFF:
|
||||
rxtxPort.setFlowControlMode(FLOWCONTROL_XONXOFF_IN | FLOWCONTROL_XONXOFF_OUT);
|
||||
|
||||
break;
|
||||
|
||||
case NONE:
|
||||
default:
|
||||
rxtxPort.setFlowControlMode(FLOWCONTROL_NONE);
|
||||
break;
|
||||
}
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
throw new IOException("Failed to set FlowControl mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
private JRxTxPort(org.openhab.core.io.transport.serial.SerialPort comPort, String portName, int baudRate,
|
||||
Parity parity, DataBits dataBits, StopBits stopBits, FlowControl flowControl) throws IOException {
|
||||
this.rxtxPort = comPort;
|
||||
this.portName = portName;
|
||||
this.baudRate = baudRate;
|
||||
this.parity = parity;
|
||||
this.dataBits = dataBits;
|
||||
this.stopBits = stopBits;
|
||||
this.flowControl = flowControl;
|
||||
|
||||
this.closed = false;
|
||||
|
||||
this.serial0s = new SerialOutputStream(this.rxtxPort.getOutputStream());
|
||||
this.serialIs = new SerialInputStream();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
if (isClosed()) {
|
||||
throw new SerialPortException("Serial port is closed");
|
||||
}
|
||||
return this.serialIs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
if (isClosed()) {
|
||||
throw new SerialPortException("Serial port is closed");
|
||||
}
|
||||
|
||||
return this.serial0s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.serial0s.closeStream();
|
||||
this.serialIs.closeStream();
|
||||
this.rxtxPort.close();
|
||||
this.serial0s = null;
|
||||
this.serialIs = null;
|
||||
this.rxtxPort = null;
|
||||
} finally {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
private class SerialInputStream extends InputStream {
|
||||
private static final long SLEEP_TIME = 10L; // sleep appropriate time
|
||||
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
long elapsedTime = 0;
|
||||
|
||||
InputStream serialInputStream = rxtxPort.getInputStream();
|
||||
do {
|
||||
if (serialInputStream.available() > 0) {
|
||||
return serialInputStream.read();
|
||||
}
|
||||
try {
|
||||
Thread.sleep(SLEEP_TIME);
|
||||
elapsedTime += SLEEP_TIME;
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (isClosed()) {
|
||||
throw new SerialPortException("Serial port has been closed.");
|
||||
}
|
||||
} while (getSerialPortTimeout() == 0 || elapsedTime <= getSerialPortTimeout());
|
||||
|
||||
throw new SerialPortTimeoutException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return rxtxPort.getInputStream().available();
|
||||
}
|
||||
|
||||
private void closeStream() throws IOException {
|
||||
rxtxPort.getInputStream().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
JRxTxPort.this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private class SerialOutputStream extends OutputStream {
|
||||
|
||||
private OutputStream serialOutputStream;
|
||||
|
||||
public SerialOutputStream(OutputStream serialOutputStream) {
|
||||
this.serialOutputStream = serialOutputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
checkIfOpen();
|
||||
|
||||
this.serialOutputStream.write(b);
|
||||
}
|
||||
|
||||
private void checkIfOpen() throws SerialPortException {
|
||||
if (isClosed()) {
|
||||
throw new SerialPortException("Port has been closed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
checkIfOpen();
|
||||
this.serialOutputStream.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
checkIfOpen();
|
||||
this.serialOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
checkIfOpen();
|
||||
this.serialOutputStream.flush();
|
||||
}
|
||||
|
||||
private void closeStream() throws IOException {
|
||||
this.serialOutputStream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
JRxTxPort.this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPortName() {
|
||||
return this.portName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBits getDataBits() {
|
||||
return this.dataBits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataBits(DataBits dataBits) throws IOException {
|
||||
this.dataBits = dataBits;
|
||||
updateWrappedPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parity getParity() {
|
||||
return this.parity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParity(Parity parity) throws IOException {
|
||||
this.parity = parity;
|
||||
updateWrappedPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StopBits getStopBits() {
|
||||
return this.stopBits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStopBits(StopBits stopBits) throws IOException {
|
||||
this.stopBits = stopBits;
|
||||
updateWrappedPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaudRate() {
|
||||
return this.baudRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBaudRate(int baudRate) throws IOException {
|
||||
this.baudRate = baudRate;
|
||||
updateWrappedPort();
|
||||
}
|
||||
|
||||
private void updateWrappedPort() throws IOException {
|
||||
try {
|
||||
this.rxtxPort.setSerialPortParams(this.baudRate, this.dataBits.getOldValue(), this.stopBits.getOldValue(),
|
||||
this.parity.getOldValue());
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSerialPortTimeout() {
|
||||
return this.serialPortTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSerialPortTimeout(int serialPortTimeout) throws IOException {
|
||||
this.serialPortTimeout = serialPortTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFlowControl(FlowControl flowControl) throws IOException {
|
||||
setFlowControl(flowControl, this.rxtxPort);
|
||||
this.flowControl = flowControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowControl getFlowControl() {
|
||||
return this.flowControl;
|
||||
}
|
||||
|
||||
}
|
||||
53
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/Parity.java
vendored
Normal file
53
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/Parity.java
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
|
||||
/**
|
||||
* The parity.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public enum Parity {
|
||||
/**
|
||||
* No parity bit will be sent with each data character at all.
|
||||
*/
|
||||
NONE(SerialPort.PARITY_NONE),
|
||||
/**
|
||||
* An odd parity bit will be sent with each data character. I.e. will be set to 1 if the data character contains an
|
||||
* even number of bits set to 1.
|
||||
*/
|
||||
ODD(SerialPort.PARITY_ODD),
|
||||
/**
|
||||
* An even parity bit will be sent with each data character. I.e. will be set to 1 if the data character contains an
|
||||
* odd number of bits set to 1.
|
||||
*/
|
||||
EVEN(SerialPort.PARITY_EVEN),
|
||||
/**
|
||||
* A mark parity bit (i.e. always 1) will be sent with each data character.
|
||||
*/
|
||||
MARK(SerialPort.PARITY_MARK),
|
||||
/**
|
||||
* A space parity bit (i.e. always 0) will be sent with each data character
|
||||
*/
|
||||
SPACE(4),;
|
||||
private static final Parity[] VALUES = values();
|
||||
private int odlValue;
|
||||
|
||||
private Parity(int oldValue) {
|
||||
this.odlValue = oldValue;
|
||||
}
|
||||
|
||||
int getOldValue() {
|
||||
return this.odlValue;
|
||||
}
|
||||
|
||||
static Parity forValue(int parity) {
|
||||
for (Parity p : VALUES) {
|
||||
if (p.odlValue == parity) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// should not occur
|
||||
throw new RuntimeException("Error.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
/**
|
||||
* Signals that the provided serial port name provided via {@link SerialPortBuilder#newBuilder(String)},
|
||||
* {@link SerialPortBuilder#setPortName(String)} doesn't exist on the host system.
|
||||
*/
|
||||
public class PortNotFoundException extends SerialPortException {
|
||||
|
||||
private static final long serialVersionUID = 2766015292714524756L;
|
||||
|
||||
/**
|
||||
* Constructs a new PortNotFoundException with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the detail message.
|
||||
*/
|
||||
public PortNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
172
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/SerialPort.java
vendored
Normal file
172
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/SerialPort.java
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Serial port for communication using UARTs. Can be used for communication protocols such as RS-232 and RS-485.
|
||||
* <p>
|
||||
* A SerialPort is created using {@link SerialPortBuilder}. Once closed it cannot be opened again but has to be
|
||||
* recreated.
|
||||
*/
|
||||
public interface SerialPort extends Closeable {
|
||||
|
||||
/**
|
||||
* Returns the input stream for this serial port.
|
||||
* <p>
|
||||
* Closing the returned InputStream will close the associated serial port.
|
||||
*
|
||||
* @return the InputStream object that can be used to read from the port.
|
||||
* @throws IOException
|
||||
* if an I/O error occurred
|
||||
*/
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the output stream for this serial port.
|
||||
*
|
||||
* @return the OutputStream object that can be used to write to the port.
|
||||
* @throws IOException
|
||||
* if an I/O error occurred.
|
||||
*/
|
||||
OutputStream getOutputStream() throws IOException;
|
||||
|
||||
/**
|
||||
* Closes the serial port.
|
||||
* <p>
|
||||
* Also closes the associated input and output streams.
|
||||
*
|
||||
* @throws IOException
|
||||
* if an I/O error occurred.
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns whether the serial port is currently open and available for communication.
|
||||
*
|
||||
* @return true if the serial port is closed.
|
||||
*/
|
||||
boolean isClosed();
|
||||
|
||||
/**
|
||||
* Get the name of the serial port.
|
||||
*
|
||||
* @return the serial port name.
|
||||
*/
|
||||
String getPortName();
|
||||
|
||||
/**
|
||||
* Get the current data bits config.
|
||||
*
|
||||
* @return the dataBits the data bits.
|
||||
*/
|
||||
DataBits getDataBits();
|
||||
|
||||
/**
|
||||
* Set the data bits.
|
||||
*
|
||||
* @param dataBits
|
||||
* the new dataBits.
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred when setting the new data bits..
|
||||
*/
|
||||
void setDataBits(DataBits dataBits) throws IOException;
|
||||
|
||||
/**
|
||||
* Get the parity.
|
||||
*
|
||||
* @return the new parity.
|
||||
*/
|
||||
Parity getParity();
|
||||
|
||||
/**
|
||||
* Set the new parity.
|
||||
*
|
||||
* @param parity
|
||||
* the new parity.
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred when setting the new parity.
|
||||
*/
|
||||
void setParity(Parity parity) throws IOException;
|
||||
|
||||
/**
|
||||
* Get the current stop bits settings.
|
||||
*
|
||||
* @return the stopBits the stop bits.
|
||||
*/
|
||||
StopBits getStopBits();
|
||||
|
||||
/**
|
||||
* Set the stop bits.
|
||||
*
|
||||
* @param stopBits
|
||||
* the stopBits to set
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred when setting the new stop bits.
|
||||
*/
|
||||
void setStopBits(StopBits stopBits) throws IOException;
|
||||
|
||||
/**
|
||||
* @return the baudRate setting.
|
||||
*
|
||||
* @see #setBaudRate(int)
|
||||
*/
|
||||
int getBaudRate();
|
||||
|
||||
/**
|
||||
* Sets the baud rate of the system.
|
||||
*
|
||||
* @param baudRate
|
||||
* the new baud rate.
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred when setting the new baud rate.
|
||||
*
|
||||
* @see #getBaudRate()
|
||||
*/
|
||||
void setBaudRate(int baudRate) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns setting for serial port timeout. <code>0</code> returns implies that the option is disabled (i.e.,
|
||||
* timeout of infinity).
|
||||
*
|
||||
* @return the serialPortTimeout.
|
||||
*
|
||||
* @see #setSerialPortTimeout(int)
|
||||
*/
|
||||
int getSerialPortTimeout();
|
||||
|
||||
/**
|
||||
* Enable/disable serial port timeout with the specified timeout, in milliseconds. With this option set to a
|
||||
* non-zero timeout, a read() call on the InputStream associated with this serial port will block for only this
|
||||
* amount of time. If the timeout expires, a org.openmuc.jrxtx.SerialPortTimeoutExcepption is raised, though the
|
||||
* serial port is still valid. The option must be enabled prior to entering the blocking operation to have effect.
|
||||
* The timeout must be <code>> 0</code>. A timeout of zero is interpreted as an infinite timeout.
|
||||
*
|
||||
* @param serialPortTimeout
|
||||
* the specified timeout, in milliseconds.
|
||||
* @throws IOException
|
||||
* if there is an error in the underlying protocol.
|
||||
*
|
||||
* @see #getSerialPortTimeout()
|
||||
*/
|
||||
void setSerialPortTimeout(int serialPortTimeout) throws IOException;
|
||||
|
||||
/**
|
||||
* Set the flow control type.
|
||||
*
|
||||
* @param flowControl
|
||||
* the flow control.
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred when setting the new baud rate.
|
||||
*/
|
||||
void setFlowControl(FlowControl flowControl) throws IOException;
|
||||
|
||||
/**
|
||||
* Get the current flow control settings.
|
||||
*
|
||||
* @return the flow control.
|
||||
*/
|
||||
FlowControl getFlowControl();
|
||||
}
|
||||
142
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/SerialPortBuilder.java
vendored
Normal file
142
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/SerialPortBuilder.java
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Builder class for SerialPorts. Provides a convenient way to set the various fields of a SerialPort.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* SerialPort port = newBuilder("/dev/ttyS0")
|
||||
* .setBaudRate(19200)
|
||||
* .setParity(Parity.EVEN)
|
||||
* .build();
|
||||
* InputStream is = port.getInputStream();
|
||||
* ..
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SerialPortBuilder {
|
||||
|
||||
private String portName;
|
||||
private int baudRate;
|
||||
private DataBits dataBits;
|
||||
private Parity parity;
|
||||
private StopBits stopBits;
|
||||
private FlowControl flowControl;
|
||||
|
||||
private SerialPortBuilder(String portName) {
|
||||
this.portName = portName;
|
||||
this.baudRate = 9600;
|
||||
this.dataBits = DataBits.DATABITS_8;
|
||||
this.parity = Parity.EVEN;
|
||||
this.stopBits = StopBits.STOPBITS_1;
|
||||
this.flowControl = FlowControl.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new SerialPortBuilder with the default values.
|
||||
*
|
||||
* @param portName
|
||||
* the serial port name. E.g. on Unix systems: <code>"/dev/ttyUSB0"</code> and on Unix
|
||||
* @return returns the new builder.
|
||||
*/
|
||||
public static SerialPortBuilder newBuilder(String portName) {
|
||||
return new SerialPortBuilder(portName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the serial port name.
|
||||
*
|
||||
* @param portName
|
||||
* the serial port name e.g. <code>"/dev/ttyUSB0"</code>
|
||||
* @return the serial port builder.
|
||||
*/
|
||||
public SerialPortBuilder setPortName(String portName) {
|
||||
this.portName = portName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the baud rate for the serial port. Values such as 9600 or 115200.
|
||||
*
|
||||
* @param baudRate
|
||||
* the baud rate.
|
||||
* @return the serial port builder.
|
||||
*
|
||||
* @see SerialPortBuilder#setBaudRate(int)
|
||||
*/
|
||||
public SerialPortBuilder setBaudRate(int baudRate) {
|
||||
this.baudRate = baudRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of data bits transfered with the serial port.
|
||||
*
|
||||
* @param dataBits
|
||||
* the number of dataBits.
|
||||
* @return the serial port builder.
|
||||
* @see SerialPort#setDataBits(DataBits)
|
||||
*/
|
||||
public SerialPortBuilder setDataBits(DataBits dataBits) {
|
||||
this.dataBits = dataBits;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parity of the serial port.
|
||||
*
|
||||
* @param parity
|
||||
* the parity.
|
||||
* @return the serial port builder.
|
||||
* @see SerialPort#setParity(Parity)
|
||||
*/
|
||||
public SerialPortBuilder setParity(Parity parity) {
|
||||
this.parity = parity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of stop bits after each data bits.
|
||||
*
|
||||
* @param stopBits
|
||||
* the number of stop bits.
|
||||
* @return the serial port builder.
|
||||
*
|
||||
* @see SerialPort#setStopBits(StopBits)
|
||||
*/
|
||||
public SerialPortBuilder setStopBits(StopBits stopBits) {
|
||||
this.stopBits = stopBits;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flow control type.
|
||||
*
|
||||
* @param flowControl
|
||||
* the flow control.
|
||||
*
|
||||
* @return the serial port builder.
|
||||
*
|
||||
* @see SerialPort#setFlowControl(FlowControl)
|
||||
*/
|
||||
public SerialPortBuilder setFlowControl(FlowControl flowControl) {
|
||||
this.flowControl = flowControl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine all of the options that have been set and return a new SerialPort object.
|
||||
*
|
||||
* @return a new serial port object.
|
||||
* @throws IOException
|
||||
* if an I/O exception occurred while opening the serial port.
|
||||
*/
|
||||
public SerialPort build() throws IOException {
|
||||
return JRxTxPort.openSerialPort(portName, baudRate, parity, dataBits, stopBits, flowControl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Signals that a I/O exception with the SerialPort occurred.
|
||||
*
|
||||
* @see SerialPort
|
||||
*/
|
||||
public class SerialPortException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = -4848841747671551647L;
|
||||
|
||||
/**
|
||||
* Constructs a new SerialPortException with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the detail message.
|
||||
*/
|
||||
public SerialPortException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
|
||||
/**
|
||||
* Signals that the read function of the SerialPort input stream has timed out.
|
||||
*/
|
||||
public class SerialPortTimeoutException extends InterruptedIOException {
|
||||
|
||||
private static final long serialVersionUID = -5808479011360793837L;
|
||||
|
||||
public SerialPortTimeoutException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new SerialPortTimeoutException with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the detail message.
|
||||
*/
|
||||
public SerialPortTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
32
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/StopBits.java
vendored
Normal file
32
bundles/org.openhab.binding.smartmeter/src/3rdparty/java/org/openmuc/jrxtx/StopBits.java
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package org.openmuc.jrxtx;
|
||||
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
|
||||
/**
|
||||
* The stop bits.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public enum StopBits {
|
||||
/**
|
||||
* 1 stop bit will be sent at the end of every character.
|
||||
*/
|
||||
STOPBITS_1(SerialPort.STOPBITS_1),
|
||||
/**
|
||||
* 1.5 stop bits will be sent at the end of every character
|
||||
*/
|
||||
STOPBITS_1_5(SerialPort.STOPBITS_1_5),
|
||||
/**
|
||||
* 2 stop bits will be sent at the end of every character
|
||||
*/
|
||||
STOPBITS_2(SerialPort.STOPBITS_2);
|
||||
|
||||
private int odlValue;
|
||||
|
||||
private StopBits(int oldValue) {
|
||||
this.odlValue = oldValue;
|
||||
}
|
||||
|
||||
int getOldValue() {
|
||||
return this.odlValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.smartmeter-${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-smartmeter" description="Smartmeter Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-serial</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.smartmeter/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.smartmeter.internal.ObisCode;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link SmlReaderBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartMeterBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "smartmeter";
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_SMLREADER = new ThingTypeUID(BINDING_ID, "meter");
|
||||
public static final String CONFIGURATION_PORT = "port";
|
||||
public static final String CONFIGURATION_SERIAL_MODE = "mode";
|
||||
public static final String CONFIGURATION_BAUDRATE = "baudrate";
|
||||
public static final String CONFIGURATION_CONFORMITY = "conformity";
|
||||
public static final String CONFIGURATION_INIT_MESSAGE = "initMessage";
|
||||
public static final String CONFIGURATION_CONVERSION = "conversionRatio";
|
||||
public static final String CONFIGURATION_CHANNEL_NEGATE = "negate";
|
||||
public static final String CHANNEL_PROPERTY_OBIS = "obis";
|
||||
public static final String OBIS_PATTERN_CHANNELID = getObisChannelId(ObisCode.OBIS_PATTERN);
|
||||
/** Obis format */
|
||||
public static final String OBIS_FORMAT_MINIMAL = "%d-%d:%d.%d.%d";
|
||||
/** Obis format */
|
||||
public static final String OBIS_FORMAT = OBIS_FORMAT_MINIMAL + "*%d";
|
||||
public static final String CHANNEL_TYPE_METERREADER_OBIS = "channel-type:" + BINDING_ID + ":obis";
|
||||
|
||||
public static String getObisChannelId(String obis) {
|
||||
return obis.replaceAll("\\.", "-").replaceAll(":|\\*", "_");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter;
|
||||
|
||||
/**
|
||||
* The {@link SmartMeterConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*/
|
||||
public class SmartMeterConfiguration {
|
||||
|
||||
public String port;
|
||||
public Integer refresh;
|
||||
public Integer baudrateChangeDelay;
|
||||
public String initMessage;
|
||||
public String baudrate;
|
||||
public String mode;
|
||||
public String conformity;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.connectors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.BackpressureStrategy;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.FlowableEmitter;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Represents a basic implementation of a SML device connector.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
* @author Mathias Gilhuber - Also-By
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ConnectorBase<T> implements IMeterReaderConnector<T> {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
/**
|
||||
* The name of the port where the device is connected as defined in openHAB configuration.
|
||||
*/
|
||||
private String portName;
|
||||
public static final int NUMBER_OF_RETRIES = 3;
|
||||
|
||||
/**
|
||||
* Contructor for basic members.
|
||||
*
|
||||
* This constructor has to be called from derived classes!
|
||||
*
|
||||
*/
|
||||
protected ConnectorBase(String portName) {
|
||||
this.portName = portName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a new IO message.
|
||||
*
|
||||
* @param initMessage
|
||||
* @return The payload
|
||||
* @throws IOException Whenever there was a reading error.
|
||||
*/
|
||||
protected abstract T readNext(byte @Nullable [] initMessage) throws IOException;
|
||||
|
||||
/**
|
||||
* Whether to periodically emit values.
|
||||
*
|
||||
* @return whether periodically emit values or not
|
||||
*/
|
||||
protected boolean applyPeriod() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to apply a retry handling whenever the read out failed.
|
||||
*
|
||||
* @return whether to use the retry handling or not.
|
||||
*/
|
||||
protected boolean applyRetryHandling() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If reading of meter values fail a retry handling shall be implemented here.
|
||||
* The provided publisher publishes the errors.
|
||||
* If a retry shall happen, the returned publisher shall emit an event-
|
||||
*
|
||||
* @param period
|
||||
* @param attempts
|
||||
* @return The publisher which emits events for a retry.
|
||||
*/
|
||||
protected Publisher<?> getRetryPublisher(Duration period, Publisher<Throwable> attempts) {
|
||||
return Flowable.fromPublisher(attempts)
|
||||
.zipWith(Flowable.range(1, NUMBER_OF_RETRIES + 1), (throwable, attempt) -> {
|
||||
if (throwable instanceof TimeoutException || attempt == NUMBER_OF_RETRIES + 1) {
|
||||
throw new RuntimeException(throwable);
|
||||
} else {
|
||||
logger.warn("{}. reading attempt failed: {}. Retrying {}...", attempt, throwable.getMessage(),
|
||||
getPortName());
|
||||
return attempt;
|
||||
}
|
||||
}).flatMap(i -> {
|
||||
retryHook(i);
|
||||
Duration additionalDelay = period;
|
||||
logger.warn("Delaying retry by {}", additionalDelay);
|
||||
return Flowable.timer(additionalDelay.toMillis(), TimeUnit.MILLISECONDS);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a retry shall happen. Clients can do something here.
|
||||
*
|
||||
* @param retryCount The current number of retries
|
||||
*/
|
||||
protected void retryHook(int retryCount) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<T> getMeterValues(byte @Nullable [] initMessage, Duration period, ExecutorService executor) {
|
||||
Flowable<T> itemPublisher = Flowable.<T> create((emitter) -> {
|
||||
emitValues(initMessage, emitter);
|
||||
}, BackpressureStrategy.DROP);
|
||||
|
||||
Flowable<T> result;
|
||||
if (applyPeriod()) {
|
||||
result = Flowable.timer(period.toMillis(), TimeUnit.MILLISECONDS, Schedulers.from(executor))
|
||||
.flatMap(event -> itemPublisher).repeat();
|
||||
} else {
|
||||
result = itemPublisher;
|
||||
}
|
||||
if (applyRetryHandling()) {
|
||||
return result.retryWhen(attempts -> {
|
||||
return Flowable.fromPublisher(getRetryPublisher(period, attempts));
|
||||
});
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitting of values shall happen here. If there is a event based emitting, this can be overriden.
|
||||
*
|
||||
* @param initMessage The message which shall be written before reading the values.
|
||||
* @param emitter The {@link FlowableEmitter} to emit the values to.
|
||||
* @throws IOException thrown if any reading error occurs.
|
||||
*/
|
||||
protected void emitValues(byte @Nullable [] initMessage, FlowableEmitter<@Nullable T> emitter) throws IOException {
|
||||
if (!emitter.isCancelled()) {
|
||||
try {
|
||||
emitter.onNext(readNext(initMessage));
|
||||
emitter.onComplete();
|
||||
} catch (IOException e) {
|
||||
if (!emitter.isCancelled()) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the serial port.
|
||||
*
|
||||
* @return The actual name of the serial port.
|
||||
*/
|
||||
public String getPortName() {
|
||||
return portName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.connectors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
/**
|
||||
* Specifies the generic method to retrieve values from a device
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
* @author Mathias Gilhuber - Also-By
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface IMeterReaderConnector<T> {
|
||||
|
||||
/**
|
||||
* Establishes the connection against the device and reads native encoded SML informations.
|
||||
* Ensures that a connection is opened and notifies any attached listeners
|
||||
*
|
||||
* @param serialParmeter
|
||||
* @param period hint for the connector to emit items in this time intervals.
|
||||
* @return native encoded SML informations from a device.
|
||||
*/
|
||||
Publisher<T> getMeterValues(byte @Nullable [] initMessage, Duration period, ExecutorService executor);
|
||||
|
||||
/**
|
||||
* Opens the connection to the serial port.
|
||||
*
|
||||
* @throws IOException Whenever something goes wrong while opening the connection.
|
||||
*
|
||||
*/
|
||||
void openConnection() throws IOException;
|
||||
|
||||
/**
|
||||
* Closes the connection to the serial port.
|
||||
*
|
||||
*/
|
||||
void closeConnection();
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.connectors.IMeterReaderConnector;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* This represents a meter device.
|
||||
* All read values of the device are cached here and can be obtained. The reading can be started with
|
||||
* {@link #readValues(ScheduledExecutorService, Duration)}
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
* @param <T> The type of Payload which is read from the device.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class MeterDevice<T> {
|
||||
|
||||
private static final int RETRY_DELAY = 2;
|
||||
private final Logger logger = LoggerFactory.getLogger(MeterDevice.class);
|
||||
/**
|
||||
* Controls wether the device info is logged to the OSGi console.
|
||||
*/
|
||||
private boolean printMeterInfo;
|
||||
/**
|
||||
* Map of all values captured from the device during the read request.
|
||||
*/
|
||||
private Map<String, MeterValue<?>> valueCache;
|
||||
private byte @Nullable [] initMessage;
|
||||
/**
|
||||
* The id of the SML device from openHAB configuration.
|
||||
*/
|
||||
private String deviceId;
|
||||
/**
|
||||
* Used to establish the device connection
|
||||
*/
|
||||
IMeterReaderConnector<T> connector;
|
||||
private List<MeterValueListener> valueChangeListeners;
|
||||
|
||||
public MeterDevice(Supplier<SerialPortManager> serialPortManagerSupplier, String deviceId, String serialPort,
|
||||
byte @Nullable [] initMessage, int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.valueCache = new HashMap<>();
|
||||
this.valueChangeListeners = new CopyOnWriteArrayList<>();
|
||||
this.printMeterInfo = true;
|
||||
this.connector = createConnector(serialPortManagerSupplier, serialPort, baudrate, baudrateChangeDelay,
|
||||
protocolMode);
|
||||
RxJavaPlugins.setErrorHandler(error -> {
|
||||
logger.error("Fatal error occured", error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the actual connector that handles the serial port communication and protocol.
|
||||
*
|
||||
* @param serialPortManagerSupplier Supplies the {@link SerialPortManager} which is used to obtain the serial port
|
||||
* implementation
|
||||
* @param serialPort The name of the port to communicate with.
|
||||
* @param baudrate The Baudrate to set for communication.
|
||||
* @param baudrateChangeDelay The delay which is used before changing the baudrate (used only for specific
|
||||
* protocols).
|
||||
* @param protocolMode The {@link ProtocolMode} to use.
|
||||
* @return The connector which handles the serial port communication.
|
||||
*/
|
||||
protected abstract IMeterReaderConnector<T> createConnector(Supplier<SerialPortManager> serialPortManagerSupplier,
|
||||
String serialPort, int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode);
|
||||
|
||||
/**
|
||||
* Gets the configured deviceId.
|
||||
*
|
||||
* @return the id of the SmlDevice from openHAB configuration.
|
||||
*/
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified OBIS value if available.
|
||||
*
|
||||
* @param obis the OBIS code which value should be retrieved.
|
||||
* @return the OBIS value as String if available - otherwise null.
|
||||
*/
|
||||
@Nullable
|
||||
public String getValue(String obisId) {
|
||||
MeterValue<?> smlValue = getMeterValue(obisId);
|
||||
if (smlValue != null) {
|
||||
return smlValue.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified OBIS value if available.
|
||||
*
|
||||
* @param obis the OBIS code which value should be retrieved.
|
||||
* @return the OBIS value if available - otherwise null.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
public <Q extends Quantity<Q>> MeterValue<Q> getMeterValue(String obisId) {
|
||||
if (valueCache.containsKey(obisId)) {
|
||||
return (MeterValue<Q>) valueCache.get(obisId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available OBIS codes.
|
||||
*
|
||||
* @return All cached OBIS codes.
|
||||
*/
|
||||
public Collection<String> getObisCodes() {
|
||||
return new ArrayList<>(this.valueCache.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read values from this device an store them locally against their OBIS code.
|
||||
*
|
||||
* If there is an error in reading, it will be retried {@value #NUMBER_OF_RETRIES} times. The retry will be delayed
|
||||
* by {@code period} seconds.
|
||||
* If its still failing, the connection will be closed and opened again.
|
||||
*
|
||||
* @return The {@link Disposable} which needs to be disposed whenever not used anymore.
|
||||
*
|
||||
*/
|
||||
public Disposable readValues(long timeout, ScheduledExecutorService executorService, Duration period) {
|
||||
return Flowable.fromPublisher(connector.getMeterValues(initMessage, period, executorService))
|
||||
.timeout(timeout + period.toMillis(), TimeUnit.MILLISECONDS, Schedulers.from(executorService))
|
||||
.doOnSubscribe(sub -> {
|
||||
logger.info("Opening connection to {}", getDeviceId());
|
||||
connector.openConnection();
|
||||
}).doOnError(ex -> {
|
||||
if (ex instanceof TimeoutException) {
|
||||
logger.warn("Timeout occured for {}; {}", getDeviceId(), ex.getMessage());
|
||||
} else {
|
||||
logger.warn("Failed to read: {}. Closing connection and trying again in {} seconds...; {}",
|
||||
ex.getMessage(), RETRY_DELAY, getDeviceId(), ex);
|
||||
}
|
||||
connector.closeConnection();
|
||||
notifyReadingError(ex);
|
||||
}).doOnCancel(connector::closeConnection).doOnComplete(connector::closeConnection).share()
|
||||
.retryWhen(
|
||||
publisher -> publisher.delay(RETRY_DELAY, TimeUnit.SECONDS, Schedulers.from(executorService)))
|
||||
.subscribeOn(Schedulers.from(executorService), true).subscribe((value) -> {
|
||||
Map<String, MeterValue<?>> obisCodes = new HashMap<>(valueCache);
|
||||
clearValueCache();
|
||||
populateValueCache(value);
|
||||
printInfo();
|
||||
Collection<String> newObisCodes = getObisCodes();
|
||||
// notify every removed obis code.
|
||||
obisCodes.values().stream().filter((val) -> !newObisCodes.contains(val.getObisCode()))
|
||||
.forEach((val) -> notifyValuesRemoved(val));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all cached values.
|
||||
*
|
||||
* The method will always be called before new values are populated.
|
||||
*/
|
||||
protected void clearValueCache() {
|
||||
valueCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a new value was made available. The value cache needs to be filled here with
|
||||
* {@link #addObisCache(MeterValue)}.
|
||||
*
|
||||
* @param payload The actual payload value.
|
||||
*/
|
||||
protected abstract <Q extends Quantity<Q>> void populateValueCache(T payload);
|
||||
|
||||
/**
|
||||
* Adds a {@link MeterValue} to the current cache.
|
||||
*
|
||||
* @param value The value to add.
|
||||
*/
|
||||
protected <Q extends Quantity<Q>> void addObisCache(MeterValue<Q> value) {
|
||||
logger.debug("Value changed: {}", value);
|
||||
this.valueCache.put(value.getObisCode(), value);
|
||||
this.valueChangeListeners.forEach((listener) -> {
|
||||
try {
|
||||
listener.valueChanged(value);
|
||||
} catch (Exception e) {
|
||||
logger.error("Meter listener failed", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.append("Device: ");
|
||||
stringBuilder.append(getDeviceId());
|
||||
stringBuilder.append(System.lineSeparator());
|
||||
|
||||
for (Entry<String, MeterValue<?>> entry : valueCache.entrySet()) {
|
||||
stringBuilder.append("Obis: " + entry.getKey() + " " + entry.getValue().toString());
|
||||
stringBuilder.append(System.lineSeparator());
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link MeterValueListener} to the list of listeners which gets notified on new values being read.
|
||||
*
|
||||
* @param valueChangeListener The new {@link MeterValueListener}
|
||||
*/
|
||||
public void addValueChangeListener(MeterValueListener valueChangeListener) {
|
||||
this.valueChangeListeners.add(valueChangeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link MeterValueListener} from the list of listeners.
|
||||
*
|
||||
* @param valueChangeListener The listener to remove.
|
||||
*/
|
||||
public void removeValueChangeListener(MeterValueListener valueChangeListener) {
|
||||
this.valueChangeListeners.remove(valueChangeListener);
|
||||
}
|
||||
|
||||
private <Q extends Quantity<Q>> void notifyValuesRemoved(MeterValue<Q> value) {
|
||||
this.valueChangeListeners.forEach((listener) -> listener.valueRemoved(value));
|
||||
}
|
||||
|
||||
private void notifyReadingError(Throwable e) {
|
||||
this.valueChangeListeners.forEach((listener) -> listener.errorOccurred(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the object information with all given SML values to OSGi console.
|
||||
*
|
||||
* It's only called once - except the config was updated.
|
||||
*/
|
||||
protected void printInfo() {
|
||||
if (this.getPrintMeterInfo()) {
|
||||
logger.info("Read out following values: {}", toString());
|
||||
setPrintMeterInfo(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the object information has to be logged to OSGi console.
|
||||
*
|
||||
* @return true if the object information should be logged, otherwise false.
|
||||
*/
|
||||
private Boolean getPrintMeterInfo() {
|
||||
return this.printMeterInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the object information has to be logged to OSGi console.
|
||||
*/
|
||||
private void setPrintMeterInfo(Boolean printMeterInfo) {
|
||||
this.printMeterInfo = printMeterInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.binding.smartmeter.internal.iec62056.Iec62056_21MeterReader;
|
||||
import org.openhab.binding.smartmeter.internal.sml.SmlMeterReader;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
|
||||
/**
|
||||
* Factory to get the correct device reader for a specific {@link ProtocolMode}
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeterDeviceFactory {
|
||||
|
||||
/**
|
||||
* Gets a concrete {@link MeterDevice} for given values.
|
||||
*
|
||||
* @param serialPortManagerSupplier The Supplier of a {@link SerialPortManager}
|
||||
* @param mode The {@link ProtocolMode}.
|
||||
* @param deviceId
|
||||
* @param serialPort The serial port identifier to connect ot.
|
||||
* @param initMessage The message which shall be sent before reading values (or to actually make the meter sent
|
||||
* values).
|
||||
* @param baudrate The baudrate to set before communication.
|
||||
* @param baudrateChangeDelay The change delay before changing the baudrate (used only for specific protocols).
|
||||
* @return The new {@link MeterDevice} or null.
|
||||
*/
|
||||
public static @Nullable MeterDevice<?> getDevice(Supplier<SerialPortManager> serialPortManagerSupplier, String mode,
|
||||
String deviceId, String serialPort, byte @Nullable [] initMessage, int baudrate, int baudrateChangeDelay) {
|
||||
ProtocolMode protocolMode = ProtocolMode.valueOf(mode.toUpperCase());
|
||||
switch (protocolMode) {
|
||||
case D:
|
||||
case ABC:
|
||||
return new Iec62056_21MeterReader(serialPortManagerSupplier, deviceId, serialPort, initMessage,
|
||||
baudrate, baudrateChangeDelay, protocolMode);
|
||||
case SML:
|
||||
return SmlMeterReader.createInstance(serialPortManagerSupplier, deviceId, serialPort, initMessage,
|
||||
baudrate, baudrateChangeDelay);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents one value of the meter device.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeterValue<Q extends Quantity<Q>> {
|
||||
|
||||
private String obis;
|
||||
private String value;
|
||||
@Nullable
|
||||
private Unit<? extends Q> unit;
|
||||
@Nullable
|
||||
private String status;
|
||||
|
||||
public MeterValue(String obis, String value, @Nullable Unit<? extends Q> unit, @Nullable String status) {
|
||||
this.obis = obis;
|
||||
this.unit = unit;
|
||||
this.value = value;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public MeterValue(String obis, String value, @Nullable Unit<Q> unit) {
|
||||
this(obis, value, unit, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values unit.
|
||||
*
|
||||
* @return the string representation of the values unit - otherwise null.
|
||||
*/
|
||||
public @Nullable Unit<? extends Q> getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value
|
||||
*
|
||||
* @return the value as String if available - otherwise null.
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((obis == null) ? 0 : obis.hashCode());
|
||||
result = prime * result + ((status == null) ? 0 : status.hashCode());
|
||||
result = prime * result + ((unit == null) ? 0 : unit.hashCode());
|
||||
result = prime * result + ((value == null) ? 0 : value.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MeterValue<?> other = (MeterValue<?>) obj;
|
||||
if (!obis.equals(other.obis)) {
|
||||
return false;
|
||||
}
|
||||
if (status == null) {
|
||||
if (other.status != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!status.equals(other.status)) {
|
||||
return false;
|
||||
}
|
||||
if (unit == null) {
|
||||
if (other.unit != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!unit.equals(other.unit)) {
|
||||
return false;
|
||||
}
|
||||
if (!value.equals(other.value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MeterValue [obis=" + obis + ", value=" + value + ", unit=" + unit + "]";
|
||||
}
|
||||
|
||||
public String getObisCode() {
|
||||
return this.obis;
|
||||
}
|
||||
|
||||
public @Nullable String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Listener which can be notified whenever new values are read form a meter device.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MeterValueListener {
|
||||
|
||||
/**
|
||||
* Called whenever some (reading-) error occurred.
|
||||
*
|
||||
* @param e The Exception that was thrown.
|
||||
*/
|
||||
public void errorOccurred(Throwable e);
|
||||
|
||||
/**
|
||||
* Called whenever some value was added or changed for a meter device.
|
||||
*
|
||||
* @param value The changed value.
|
||||
*/
|
||||
public <Q extends Quantity<Q>> void valueChanged(MeterValue<Q> value);
|
||||
|
||||
/**
|
||||
* Called whenever some value was removed from the meter device (not available anymore).
|
||||
*
|
||||
* @param value The removed value.
|
||||
*/
|
||||
public <Q extends Quantity<Q>> void valueRemoved(MeterValue<Q> value);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.util.Formatter;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
|
||||
/**
|
||||
* Represents an OBIS code.
|
||||
*
|
||||
* @see For more information see https://de.wikipedia.org/wiki/OBIS-Kennzahlen
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ObisCode {
|
||||
|
||||
public static final String OBIS_PATTERN = "((?<A>[0-9]{1,3})-(?<B>[0-9]{1,3}):)?(?<C>[0-9]{1,3}).(?<D>[0-9]{1,3}).(?<E>[0-9]{1,3})(\\*(?<F>[0-9][0-9]{1,3}))?";
|
||||
|
||||
private static Pattern obisPattern = Pattern.compile(OBIS_PATTERN);
|
||||
@Nullable
|
||||
private Byte a, b, f;
|
||||
private Byte c, d, e;
|
||||
|
||||
private ObisCode(@Nullable Byte a, @Nullable Byte b, Byte c, Byte d, Byte e, @Nullable Byte f) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
this.d = d;
|
||||
this.e = e;
|
||||
this.f = f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link ObisCode} from a String. It must follow the pattern {@value #OBIS_PATTERN}
|
||||
*
|
||||
* @param obis The obis as String.
|
||||
* @return The new Obis code. Can not be null.
|
||||
* @throws IllegalArgumentException If the <code>obis</code> has not the right format.
|
||||
*/
|
||||
public static ObisCode from(String obis) throws IllegalArgumentException {
|
||||
try {
|
||||
Matcher matcher = obisPattern.matcher(obis);
|
||||
if (matcher.find()) {
|
||||
String a = matcher.group("A");
|
||||
String b = matcher.group("B");
|
||||
String c = matcher.group("C");
|
||||
String d = matcher.group("D");
|
||||
String e = matcher.group("E");
|
||||
String f = matcher.group("F");
|
||||
return new ObisCode(a != null && !a.isEmpty() ? (byte) (0xFF & Integer.valueOf(a)) : null,
|
||||
b != null && !b.isEmpty() ? (byte) (0xFF & Integer.valueOf(b)) : null,
|
||||
(byte) (0xFF & Integer.valueOf(c)), (byte) (0xFF & Integer.valueOf(d)),
|
||||
(byte) (0xFF & Integer.valueOf(e)),
|
||||
f != null && !f.isEmpty() ? (byte) (0xFF & Integer.valueOf(f)) : null);
|
||||
}
|
||||
throw new IllegalArgumentException(obis + " is not correctly formated.");
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(obis + " is not correctly formated.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OBIS as a String.
|
||||
*
|
||||
* @return the obis as string.
|
||||
*/
|
||||
public String asDecimalString() {
|
||||
try (Formatter format = new Formatter()) {
|
||||
format.format(SmartMeterBindingConstants.OBIS_FORMAT, a != null ? a & 0xFF : 0, b != null ? b & 0xFF : 0,
|
||||
c & 0xFF, d & 0xFF, e & 0xFF, f != null ? f & 0xFF : 0);
|
||||
return format.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Byte getAGroup() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public @Nullable Byte getBGroup() {
|
||||
return b;
|
||||
}
|
||||
|
||||
public @Nullable Byte getCGroup() {
|
||||
return c;
|
||||
}
|
||||
|
||||
public @Nullable Byte getDGroup() {
|
||||
return d;
|
||||
}
|
||||
|
||||
public @Nullable Byte getEGroup() {
|
||||
return e;
|
||||
}
|
||||
|
||||
public @Nullable Byte getFGroup() {
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return asDecimalString();
|
||||
}
|
||||
|
||||
public boolean matches(@Nullable Byte a, @Nullable Byte b, Byte c, Byte d, Byte e, @Nullable Byte f) {
|
||||
return (this.a == null || a == null || this.a.equals(a)) && (this.b == null || b == null || this.b.equals(b))
|
||||
&& this.c.equals(c) && this.d.equals(d) && this.e.equals(e)
|
||||
&& (this.f == null || f == null || this.f.equals(f));
|
||||
}
|
||||
|
||||
public boolean matches(Byte c, Byte d, Byte e) {
|
||||
return matches(null, null, c, d, e, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.thing.type.StateChannelTypeBuilder;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A {@link ChannelTypeProvider} that listens for changes to the {@link MeterDevice} and updates the
|
||||
* {@link ChannelType}s according to all available OBIS values.
|
||||
* It creates one {@link ChannelType} per available OBIS value.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(service = { ChannelTypeProvider.class, SmartMeterChannelTypeProvider.class })
|
||||
public class SmartMeterChannelTypeProvider implements ChannelTypeProvider, MeterValueListener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartMeterChannelTypeProvider.class);
|
||||
|
||||
private final Map<String, ChannelType> obisChannelMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
|
||||
return obisChannelMap.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
|
||||
return obisChannelMap.values().stream().filter(channelType -> channelType.getUID().equals(channelTypeUID))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(Throwable e) {
|
||||
// Nothing to do if there is an reading error...
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Q extends @NonNull Quantity<Q>> void valueChanged(MeterValue<Q> value) {
|
||||
if (!obisChannelMap.containsKey(value.getObisCode())) {
|
||||
logger.debug("Creating ChannelType for OBIS {}", value.getObisCode());
|
||||
obisChannelMap.put(value.getObisCode(), getChannelType(value.getUnit(), value.getObisCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelType getChannelType(Unit<?> unit, String obis) {
|
||||
String obisChannelId = SmartMeterBindingConstants.getObisChannelId(obis);
|
||||
StateChannelTypeBuilder stateDescriptionBuilder;
|
||||
if (unit != null) {
|
||||
String dimension = UnitUtils.getDimensionName(unit);
|
||||
stateDescriptionBuilder = ChannelTypeBuilder
|
||||
.state(new ChannelTypeUID(SmartMeterBindingConstants.BINDING_ID, obisChannelId), obis,
|
||||
CoreItemFactory.NUMBER + ":" + dimension)
|
||||
.withStateDescription(StateDescriptionFragmentBuilder.create().withReadOnly(true)
|
||||
.withPattern("%.2f %unit%").build().toStateDescription())
|
||||
.withConfigDescriptionURI(URI.create(SmartMeterBindingConstants.CHANNEL_TYPE_METERREADER_OBIS));
|
||||
} else {
|
||||
stateDescriptionBuilder = ChannelTypeBuilder
|
||||
.state(new ChannelTypeUID(SmartMeterBindingConstants.BINDING_ID, obisChannelId), obis,
|
||||
CoreItemFactory.STRING)
|
||||
.withStateDescription(
|
||||
StateDescriptionFragmentBuilder.create().withReadOnly(true).build().toStateDescription());
|
||||
|
||||
}
|
||||
return stateDescriptionBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Q extends @NonNull Quantity<Q>> void valueRemoved(MeterValue<Q> value) {
|
||||
obisChannelMap.remove(value.getObisCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ChannelTypeUID} for the given OBIS code.
|
||||
*
|
||||
* @param obis The obis code.
|
||||
* @return The {@link ChannelTypeUID} or null.
|
||||
*/
|
||||
public ChannelTypeUID getChannelTypeIdForObis(String obis) {
|
||||
return obisChannelMap.get(obis).getUID();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.Conformity;
|
||||
import org.openhab.binding.smartmeter.internal.helper.Baudrate;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.config.core.ConfigOptionProvider;
|
||||
import org.openhab.core.config.core.ParameterOption;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Provides the configuration options for a meter device.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(service = ConfigOptionProvider.class)
|
||||
@NonNullByDefault
|
||||
public class SmartMeterConfigProvider implements ConfigOptionProvider {
|
||||
|
||||
@Override
|
||||
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable Locale locale) {
|
||||
return getParameterOptions(uri, param, null, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
|
||||
@Nullable Locale locale) {
|
||||
if (!SmartMeterBindingConstants.THING_TYPE_SMLREADER.getAsString().equals(uri.getSchemeSpecificPart())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (param) {
|
||||
case SmartMeterBindingConstants.CONFIGURATION_SERIAL_MODE:
|
||||
List<ParameterOption> options = new ArrayList<>();
|
||||
|
||||
for (ProtocolMode mode : ProtocolMode.values()) {
|
||||
options.add(new ParameterOption(mode.name(), mode.toString()));
|
||||
}
|
||||
return options;
|
||||
|
||||
case SmartMeterBindingConstants.CONFIGURATION_BAUDRATE:
|
||||
options = new ArrayList<>();
|
||||
|
||||
for (Baudrate baudrate : Baudrate.values()) {
|
||||
options.add(new ParameterOption(baudrate.getBaudrate() + "", baudrate.toString()));
|
||||
}
|
||||
return options;
|
||||
|
||||
case SmartMeterBindingConstants.CONFIGURATION_CONFORMITY:
|
||||
options = new ArrayList<>();
|
||||
|
||||
for (Conformity conformity : Conformity.values()) {
|
||||
options.add(new ParameterOption(conformity.name(), conformity.toString()));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.MessageFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
import org.openhab.binding.smartmeter.SmartMeterConfiguration;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.Conformity;
|
||||
import org.openhab.binding.smartmeter.internal.helper.Baudrate;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
/**
|
||||
* The {@link SmartMeterHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault({ DefaultLocation.ARRAY_CONTENTS, DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE,
|
||||
DefaultLocation.TYPE_ARGUMENT })
|
||||
public class SmartMeterHandler extends BaseThingHandler {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT = 30000;
|
||||
private static final int DEFAULT_REFRESH_PERIOD = 30;
|
||||
private Logger logger = LoggerFactory.getLogger(SmartMeterHandler.class);
|
||||
private MeterDevice<?> smlDevice;
|
||||
private Disposable valueReader;
|
||||
private Conformity conformity;
|
||||
private MeterValueListener valueChangeListener;
|
||||
private SmartMeterChannelTypeProvider channelTypeProvider;
|
||||
private @NonNull Supplier<SerialPortManager> serialPortManagerSupplier;
|
||||
|
||||
public SmartMeterHandler(Thing thing, SmartMeterChannelTypeProvider channelProvider,
|
||||
Supplier<SerialPortManager> serialPortManagerSupplier) {
|
||||
super(thing);
|
||||
Objects.requireNonNull(channelProvider, "SmartMeterChannelTypeProvider must not be null");
|
||||
this.channelTypeProvider = channelProvider;
|
||||
this.serialPortManagerSupplier = serialPortManagerSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Smartmeter handler.");
|
||||
cancelRead();
|
||||
|
||||
SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
|
||||
logger.debug("config port = {}", config.port);
|
||||
|
||||
boolean validConfig = true;
|
||||
String errorMsg = null;
|
||||
|
||||
if (StringUtils.trimToNull(config.port) == null) {
|
||||
errorMsg = "Parameter 'port' is mandatory and must be configured";
|
||||
validConfig = false;
|
||||
}
|
||||
|
||||
if (validConfig) {
|
||||
byte[] pullSequence = config.initMessage == null ? null
|
||||
: HexUtils.hexToBytes(StringUtils.deleteWhitespace(config.initMessage));
|
||||
int baudrate = config.baudrate == null ? Baudrate.AUTO.getBaudrate()
|
||||
: Baudrate.fromString(config.baudrate).getBaudrate();
|
||||
this.conformity = config.conformity == null ? Conformity.NONE : Conformity.valueOf(config.conformity);
|
||||
this.smlDevice = MeterDeviceFactory.getDevice(serialPortManagerSupplier, config.mode,
|
||||
this.thing.getUID().getAsString(), config.port, pullSequence, baudrate, config.baudrateChangeDelay);
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING,
|
||||
"Waiting for messages from device");
|
||||
|
||||
smlDevice.addValueChangeListener(channelTypeProvider);
|
||||
|
||||
updateOBISValue();
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
cancelRead();
|
||||
if (this.valueChangeListener != null) {
|
||||
this.smlDevice.removeValueChangeListener(valueChangeListener);
|
||||
}
|
||||
if (this.channelTypeProvider != null) {
|
||||
this.smlDevice.removeValueChangeListener(channelTypeProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelRead() {
|
||||
if (this.valueReader != null) {
|
||||
this.valueReader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateOBISChannel(channelUID);
|
||||
} else {
|
||||
logger.debug("The SML reader binding is read-only and can not handle command {}", command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new data the device
|
||||
*
|
||||
*/
|
||||
private void updateOBISValue() {
|
||||
cancelRead();
|
||||
|
||||
valueChangeListener = new MeterValueListener() {
|
||||
@Override
|
||||
public <Q extends @NonNull Quantity<Q>> void valueChanged(MeterValue<Q> value) {
|
||||
ThingBuilder thingBuilder = editThing();
|
||||
|
||||
String obis = value.getObisCode();
|
||||
|
||||
String obisChannelString = SmartMeterBindingConstants.getObisChannelId(obis);
|
||||
Channel channel = thing.getChannel(obisChannelString);
|
||||
ChannelTypeUID channelTypeId = channelTypeProvider.getChannelTypeIdForObis(obis);
|
||||
|
||||
ChannelType channelType = channelTypeProvider.getChannelType(channelTypeId, null);
|
||||
if (channelType != null) {
|
||||
String itemType = channelType.getItemType();
|
||||
|
||||
State state = getStateForObisValue(value, channel);
|
||||
if (channel == null) {
|
||||
logger.debug("Adding channel: {} with item type: {}", obisChannelString, itemType);
|
||||
|
||||
// channel has not been created yet
|
||||
ChannelBuilder channelBuilder = ChannelBuilder
|
||||
.create(new ChannelUID(thing.getUID(), obisChannelString), itemType)
|
||||
.withType(channelTypeId);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put(SmartMeterBindingConstants.CONFIGURATION_CONVERSION, 1);
|
||||
channelBuilder.withConfiguration(configuration);
|
||||
channelBuilder.withLabel(obis);
|
||||
Map<String, String> channelProps = new HashMap<>();
|
||||
channelProps.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
|
||||
channelBuilder.withProperties(channelProps);
|
||||
channelBuilder.withDescription(
|
||||
MessageFormat.format("Value for OBIS code: {0} with Unit: {1}", obis, value.getUnit()));
|
||||
channel = channelBuilder.build();
|
||||
ChannelUID channelId = channel.getUID();
|
||||
|
||||
// add all valid channels to the thing builder
|
||||
List<Channel> channels = new ArrayList<>(getThing().getChannels());
|
||||
if (channels.stream().filter((element) -> element.getUID().equals(channelId)).count() == 0) {
|
||||
channels.add(channel);
|
||||
thingBuilder.withChannels(channels);
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel.getProperties().containsKey(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS)) {
|
||||
addObisPropertyToChannel(obis, channel);
|
||||
}
|
||||
updateState(channel.getUID(), state);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
|
||||
} else {
|
||||
logger.warn("No ChannelType found for OBIS {}", obis);
|
||||
}
|
||||
}
|
||||
|
||||
private void addObisPropertyToChannel(String obis, Channel channel) {
|
||||
String description = channel.getDescription();
|
||||
String label = channel.getLabel();
|
||||
ChannelBuilder newChannel = ChannelBuilder.create(channel.getUID(), channel.getAcceptedItemType())
|
||||
.withDefaultTags(channel.getDefaultTags()).withConfiguration(channel.getConfiguration())
|
||||
.withDescription(description == null ? "" : description).withKind(channel.getKind())
|
||||
.withLabel(label == null ? "" : label).withType(channel.getChannelTypeUID());
|
||||
Map<String, String> properties = new HashMap<>(channel.getProperties());
|
||||
properties.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
|
||||
newChannel.withProperties(properties);
|
||||
updateThing(editThing().withoutChannel(channel.getUID()).withChannel(newChannel.build()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Q extends @NonNull Quantity<Q>> void valueRemoved(MeterValue<Q> value) {
|
||||
// channels that are not available are removed
|
||||
String obisChannelId = SmartMeterBindingConstants.getObisChannelId(value.getObisCode());
|
||||
logger.debug("Removing channel: {}", obisChannelId);
|
||||
ThingBuilder thingBuilder = editThing();
|
||||
thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), obisChannelId));
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(Throwable e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
}
|
||||
};
|
||||
this.smlDevice.addValueChangeListener(valueChangeListener);
|
||||
|
||||
SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
|
||||
int delay = config.refresh != null ? config.refresh : DEFAULT_REFRESH_PERIOD;
|
||||
valueReader = this.smlDevice.readValues(DEFAULT_TIMEOUT, this.scheduler, Duration.ofSeconds(delay));
|
||||
}
|
||||
|
||||
private void updateOBISChannel(ChannelUID channelId) {
|
||||
if (isLinked(channelId.getId())) {
|
||||
Channel channel = this.thing.getChannel(channelId.getId());
|
||||
if (channel != null) {
|
||||
String obis = channel.getProperties().get(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS);
|
||||
MeterValue<?> value = this.smlDevice.getMeterValue(obis);
|
||||
if (value != null) {
|
||||
State state = getStateForObisValue(value, channel);
|
||||
updateState(channel.getUID(), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Q extends Quantity<Q>> State getStateForObisValue(MeterValue<?> value, @Nullable Channel channel) {
|
||||
Unit<?> unit = value.getUnit();
|
||||
String valueString = value.getValue();
|
||||
if (unit != null) {
|
||||
valueString += " " + value.getUnit();
|
||||
}
|
||||
State state = TypeParser.parseState(Arrays.asList(QuantityType.class, StringType.class), valueString);
|
||||
if (channel != null && state instanceof QuantityType) {
|
||||
state = applyConformity(channel, (QuantityType<Q>) state);
|
||||
Number conversionRatio = (Number) channel.getConfiguration()
|
||||
.get(SmartMeterBindingConstants.CONFIGURATION_CONVERSION);
|
||||
if (conversionRatio != null) {
|
||||
state = ((QuantityType<?>) state).divide(BigDecimal.valueOf(conversionRatio.doubleValue()));
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private <Q extends Quantity<Q>> State applyConformity(Channel channel, QuantityType<Q> currentState) {
|
||||
try {
|
||||
return this.conformity.apply(channel, currentState, getThing(), this.smlDevice);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to apply negation for channel: {}", channel.getUID(), e);
|
||||
}
|
||||
return currentState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal;
|
||||
|
||||
import static org.openhab.binding.smartmeter.SmartMeterBindingConstants.THING_TYPE_SMLREADER;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ConfigurationPolicy;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link SmartMeterHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
|
||||
@NonNullByDefault
|
||||
public class SmartMeterHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_SMLREADER);
|
||||
private @NonNullByDefault({}) SmartMeterChannelTypeProvider channelProvider;
|
||||
private @NonNullByDefault({}) Supplier<SerialPortManager> serialPortManagerSupplier = () -> null;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setSmartMeterChannelTypeProvider(SmartMeterChannelTypeProvider provider) {
|
||||
this.channelProvider = provider;
|
||||
}
|
||||
|
||||
protected void unsetSmartMeterChannelTypeProvider(SmartMeterChannelTypeProvider provider) {
|
||||
this.channelProvider = null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setSerialPortManager(SerialPortManager serialPortManager) {
|
||||
serialPortManagerSupplier = () -> serialPortManager;
|
||||
}
|
||||
|
||||
protected void unsetSerialPortManager(SerialPortManager serialPortManager) {
|
||||
this.serialPortManagerSupplier = () -> null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_SMLREADER)) {
|
||||
return new SmartMeterHandler(thing, channelProvider, serialPortManagerSupplier);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.conformity;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
import org.openhab.binding.smartmeter.internal.MeterDevice;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openhab.binding.smartmeter.internal.ObisCode;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.negate.NegateHandler;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Some meters have specific semantics on how to interpret the values which are sent from the meter.
|
||||
* This class handles all such known special cases.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Conformity {
|
||||
|
||||
NONE {
|
||||
@Override
|
||||
public <Q extends Quantity<Q>> State apply(Channel channel, QuantityType<Q> currentState, Thing thing,
|
||||
MeterDevice<?> device) {
|
||||
return retrieveOverwrittenNegate(channel, currentState, thing, device, null);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* See
|
||||
* https://www.vde.com/resource/blob/951000/252eb3cdf1c7f6cdea10847be399da0d/fnn-lastenheft-edl-1-0-2010-01-13-data.pdf
|
||||
*/
|
||||
EDL_FNN {
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.openhab.binding.smartmeter.internal.Conformity#apply(org.openhab.core.thing.Channel,
|
||||
* org.openhab.core.library.types.QuantityType, org.openhab.core.thing.Thing,
|
||||
* org.openhab.binding.smartmeter.internal.MeterDevice)
|
||||
*/
|
||||
@Override
|
||||
public <Q extends Quantity<Q>> QuantityType<?> apply(Channel channel, QuantityType<Q> currentState, Thing thing,
|
||||
MeterDevice<?> device) {
|
||||
return retrieveOverwrittenNegate(channel, currentState, thing, device, () -> {
|
||||
// Negate if this channel has the unit "Watt" and the negate bit is set. Read from all other
|
||||
// channels the state and check if there is a negate bit.
|
||||
String channelObis = channel.getProperties().get(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS);
|
||||
MeterValue<?> value = device.getMeterValue(channelObis);
|
||||
if (value != null && SmartHomeUnits.WATT.isCompatible(value.getUnit())) {
|
||||
for (String obis : device.getObisCodes()) {
|
||||
try {
|
||||
MeterValue<?> otherValue = device.getMeterValue(obis);
|
||||
ObisCode obisCode = ObisCode.from(obis);
|
||||
if (otherValue != null) {
|
||||
if (obisCode.matches((byte) 0x60, (byte) 0x05, (byte) 0x05)) {
|
||||
// we found status status obis 96.5.5
|
||||
if (NegateHandler.isNegateSet(otherValue.getValue(), 5)) {
|
||||
return currentState.negate();
|
||||
}
|
||||
} else if (obisCode.matches((byte) 0x01, (byte) 0x08, (byte) 0x00)) {
|
||||
// check obis 1.8.0 for status if status has negate bit set.
|
||||
String status = otherValue.getStatus();
|
||||
if (status != null && NegateHandler.isNegateSet(status, 5)) {
|
||||
return currentState.negate();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to check negate status for obis {}", obis, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentState;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Conformity.class);
|
||||
|
||||
/**
|
||||
* Applies the overwritten negation setting for the channel.
|
||||
*
|
||||
* @param currentState The current value.
|
||||
* @param thing The {@link Thing}
|
||||
* @param device The {@link MeterDevice}.
|
||||
* @param negateProperty The negate property.
|
||||
* @return The negated value.
|
||||
*/
|
||||
private static <Q extends Quantity<Q>> QuantityType<Q> applyNegation(QuantityType<Q> currentState, Thing thing,
|
||||
MeterDevice<?> device, String negateProperty) {
|
||||
boolean shouldNegateState = NegateHandler.shouldNegateState(negateProperty, channelId -> {
|
||||
Channel negateChannel = thing.getChannel(channelId);
|
||||
if (negateChannel != null) {
|
||||
return device.getMeterValue(
|
||||
negateChannel.getProperties().get(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (shouldNegateState) {
|
||||
return currentState.negate();
|
||||
}
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param channel
|
||||
* @param currentState
|
||||
* @param thing
|
||||
* @param device
|
||||
* @param elseDo If negate property was not overwritten call the given supplier.
|
||||
* @return
|
||||
*/
|
||||
protected <Q extends Quantity<Q>> QuantityType<?> retrieveOverwrittenNegate(Channel channel,
|
||||
QuantityType<Q> currentState, Thing thing, MeterDevice<?> device,
|
||||
@Nullable Supplier<QuantityType<Q>> elseDo) {
|
||||
// Negate setting
|
||||
String negateProperty = (String) channel.getConfiguration()
|
||||
.get(SmartMeterBindingConstants.CONFIGURATION_CHANNEL_NEGATE);
|
||||
if (negateProperty != null && !negateProperty.trim().isEmpty()) {
|
||||
return applyNegation(currentState, thing, device, negateProperty);
|
||||
} else {
|
||||
if (elseDo != null) {
|
||||
return elseDo.get();
|
||||
}
|
||||
return currentState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies any changes according to the conformity and returns the new value.
|
||||
*
|
||||
* @param channel The {@link Channel} for which the conformity should be applied to.
|
||||
* @param currentState The current state of that {@link Channel}
|
||||
* @param thing The {@link Thing} where the channel belongs to.
|
||||
* @param device The {@link MeterDevice} for the Thing.
|
||||
* @return The applied state.
|
||||
*/
|
||||
public abstract <Q extends Quantity<Q>> State apply(Channel channel, QuantityType<Q> currentState, Thing thing,
|
||||
MeterDevice<?> device);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.conformity.negate;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Models the negate bit - namely the OBIS code, whether its in the status bytes and on which position (of the status)
|
||||
* it is encoded.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NegateBitModel {
|
||||
|
||||
private int negatePosition;
|
||||
private boolean negateBit;
|
||||
private String negateObis;
|
||||
private boolean status;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param negatePosition
|
||||
* @param negateBit
|
||||
* @param negateObis
|
||||
* @param status Whether to get the negate bit from status value or from actual value.
|
||||
*/
|
||||
public NegateBitModel(int negatePosition, boolean negateBit, String negateObis, boolean status) {
|
||||
this.negatePosition = negatePosition;
|
||||
this.negateBit = negateBit;
|
||||
this.negateObis = negateObis;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getNegatePosition() {
|
||||
return negatePosition;
|
||||
}
|
||||
|
||||
public boolean isNegateBit() {
|
||||
return negateBit;
|
||||
}
|
||||
|
||||
public String getNegateChannelId() {
|
||||
return negateObis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (negateBit ? 1231 : 1237);
|
||||
result = prime * result + (negateObis.hashCode());
|
||||
result = prime * result + negatePosition;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
NegateBitModel other = (NegateBitModel) obj;
|
||||
if (negateBit != other.negateBit) {
|
||||
return false;
|
||||
}
|
||||
if (!negateObis.equals(other.negateObis)) {
|
||||
return false;
|
||||
}
|
||||
if (negatePosition != other.negatePosition) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.conformity.negate;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
|
||||
/**
|
||||
* Parses the NegateBit property.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NegateBitParser {
|
||||
|
||||
/**
|
||||
* Parsing of negate bit property. This is in the format: {@literal <OBIS>:<POSITION>:<BIT_SET>"}
|
||||
* e.g. "1-0:1-8-0:5:1"
|
||||
*
|
||||
* @param negateProperty
|
||||
* @return The parsed model
|
||||
*/
|
||||
public static NegateBitModel parseNegateProperty(String negateProperty) throws IllegalArgumentException {
|
||||
Pattern obisPattern = Pattern.compile(SmartMeterBindingConstants.OBIS_PATTERN_CHANNELID);
|
||||
try {
|
||||
Matcher matcher = obisPattern.matcher(negateProperty);
|
||||
if (matcher.find()) {
|
||||
String obis = matcher.group();
|
||||
String substring = negateProperty.substring(matcher.end() + 1, negateProperty.length());
|
||||
String[] split = substring.split(":");
|
||||
int negatePosition = Integer.parseInt(split[0]);
|
||||
boolean negateBit = Integer.parseInt(split[1]) == 0 ? false : true;
|
||||
boolean status = split.length > 2 ? split[2].equalsIgnoreCase("status") : false;
|
||||
return new NegateBitModel((byte) negatePosition, negateBit, obis, status);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Negate property cannot be parsed: " + negateProperty, e);
|
||||
}
|
||||
throw new IllegalArgumentException("Negate property cannot be parsed: " + negateProperty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.conformity.negate;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
|
||||
/**
|
||||
* Handles the Negate Bit property for a specific meter value.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NegateHandler {
|
||||
|
||||
/**
|
||||
* Gets whether negation should be applied for the given <code>negateProperty</code> and the {@link MeterValue}
|
||||
* provided by the <code>getObisValueFunction</code>
|
||||
*
|
||||
* @param negateProperty The negate property (in form <OBIS>:<POSITION>:<BIT_SET>)
|
||||
* @param getObisValueFunction The function to get the {@link MeterValue} from an OBIS code.
|
||||
* @return whether to negate or not.
|
||||
*/
|
||||
public static boolean shouldNegateState(String negateProperty,
|
||||
Function<String, @Nullable MeterValue<?>> getObisValueFunction) {
|
||||
NegateBitModel negateModel = NegateBitParser.parseNegateProperty(negateProperty);
|
||||
MeterValue<?> value = getObisValueFunction.apply(negateModel.getNegateChannelId());
|
||||
boolean isStatus = negateModel.isStatus();
|
||||
if (value != null) {
|
||||
String status = value.getStatus();
|
||||
String stringValue;
|
||||
if (isStatus && status != null) {
|
||||
stringValue = status;
|
||||
} else {
|
||||
stringValue = value.getValue();
|
||||
}
|
||||
boolean negateBit = isNegateSet(stringValue, negateModel.getNegatePosition());
|
||||
|
||||
return negateBit == negateModel.isNegateBit();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the bit at position <code>negatePosition</code> is set or not.
|
||||
*
|
||||
* @param value The value which must be a number to check the bit
|
||||
* @param negatePosition The position to check
|
||||
* @return Whether the given bit is set or not
|
||||
*/
|
||||
public static boolean isNegateSet(String value, int negatePosition) {
|
||||
long longValue = Long.parseLong(value);
|
||||
return (longValue & (1L << negatePosition)) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.helper;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Baudrate {
|
||||
AUTO(-1) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return name();
|
||||
}
|
||||
},
|
||||
_300(300),
|
||||
_600(600),
|
||||
_1200(1200),
|
||||
_1800(1800),
|
||||
_2400(2400),
|
||||
_4800(4800),
|
||||
_9600(9600),
|
||||
_19200(19200),
|
||||
_38400(38400);
|
||||
|
||||
private int baudrate;
|
||||
|
||||
private Baudrate(int baudrate) {
|
||||
this.baudrate = baudrate;
|
||||
}
|
||||
|
||||
public int getBaudrate() {
|
||||
return baudrate;
|
||||
}
|
||||
|
||||
public static Baudrate fromBaudrate(int baudrate) {
|
||||
for (Baudrate baud : values()) {
|
||||
if (baud.getBaudrate() == baudrate) {
|
||||
return baud;
|
||||
}
|
||||
}
|
||||
return Baudrate._9600;
|
||||
}
|
||||
|
||||
public static Baudrate fromString(String baudrate) {
|
||||
try {
|
||||
if (baudrate.equalsIgnoreCase(AUTO.name())) {
|
||||
return Baudrate.AUTO;
|
||||
}
|
||||
return valueOf("_" + baudrate.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
return Baudrate.AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getBaudrate() + "bd";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.helper;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ProtocolMode {
|
||||
|
||||
ABC("A,B,C"),
|
||||
D("D"),
|
||||
SML("SML");
|
||||
|
||||
private String label;
|
||||
|
||||
private <T> ProtocolMode(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.helper;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum SerialParameter {
|
||||
|
||||
_8N1(SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE),
|
||||
_7N1(SerialPort.DATABITS_7, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE),
|
||||
_7O1(SerialPort.DATABITS_7, SerialPort.STOPBITS_1, SerialPort.PARITY_ODD),
|
||||
_7E1(SerialPort.DATABITS_7, SerialPort.STOPBITS_1, SerialPort.PARITY_EVEN);
|
||||
|
||||
private int databits;
|
||||
private int stopbits;
|
||||
private int parity;
|
||||
|
||||
private SerialParameter(int databits, int stopbits, int parity) {
|
||||
this.databits = databits;
|
||||
this.stopbits = stopbits;
|
||||
this.parity = parity;
|
||||
}
|
||||
|
||||
public int getDatabits() {
|
||||
return this.databits;
|
||||
}
|
||||
|
||||
public int getStopbits() {
|
||||
return stopbits;
|
||||
}
|
||||
|
||||
public int getParity() {
|
||||
return parity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the enum constant for the serial parameter string.
|
||||
* The parameters must be in format 'StartbitsParityStopbits'
|
||||
* e.g. '7N1', '8N1'
|
||||
*
|
||||
* @param params
|
||||
* @return The found {@link SerialParameter} or {@link SerialParameter#_8N1} if not found
|
||||
*/
|
||||
public static SerialParameter fromString(String params) {
|
||||
try {
|
||||
return valueOf("_" + StringUtils.upperCase(params));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return SerialParameter._8N1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.iec62056;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.connectors.IMeterReaderConnector;
|
||||
import org.openhab.binding.smartmeter.internal.MeterDevice;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openmuc.j62056.DataMessage;
|
||||
import org.openmuc.j62056.DataSet;
|
||||
|
||||
/**
|
||||
* Reads meter values from an IEC 62056-21 compatible device with mode A,B,C or D.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Iec62056_21MeterReader extends MeterDevice<DataMessage> {
|
||||
|
||||
public Iec62056_21MeterReader(Supplier<SerialPortManager> serialPortManagerSupplier, String deviceId,
|
||||
String serialPort, byte @Nullable [] initMessage, int baudrate, int baudrateChangeDelay,
|
||||
ProtocolMode protocolMode) {
|
||||
super(serialPortManagerSupplier, deviceId, serialPort, initMessage, baudrate, baudrateChangeDelay,
|
||||
protocolMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IMeterReaderConnector<DataMessage> createConnector(Supplier<SerialPortManager> serialPortManagerSupplier,
|
||||
String serialPort, int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode) {
|
||||
return new Iec62056_21SerialConnector(serialPortManagerSupplier, serialPort, baudrate, baudrateChangeDelay,
|
||||
protocolMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <Q extends @NonNull Quantity<Q>> void populateValueCache(DataMessage smlFile) {
|
||||
for (DataSet dataSet : smlFile.getDataSets()) {
|
||||
String address = dataSet.getAddress();
|
||||
if (address != null && !address.isEmpty()) {
|
||||
addObisCache(new MeterValue<Q>(address, dataSet.getValue(),
|
||||
Iec62056_21UnitConversion.getUnit(dataSet.getUnit())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.iec62056;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.connectors.ConnectorBase;
|
||||
import org.openhab.binding.smartmeter.internal.helper.Baudrate;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openmuc.j62056.DataMessage;
|
||||
import org.openmuc.j62056.Iec21Port;
|
||||
import org.openmuc.j62056.Iec21Port.Builder;
|
||||
import org.openmuc.j62056.ModeDListener;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.FlowableEmitter;
|
||||
|
||||
/**
|
||||
* This connector reads meter values with IEC62056-21 protocol.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Iec62056_21SerialConnector extends ConnectorBase<DataMessage> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(Iec62056_21SerialConnector.class);
|
||||
private int baudrate;
|
||||
private int baudrateChangeDelay;
|
||||
private ProtocolMode protocolMode;
|
||||
@Nullable
|
||||
private Iec21Port iec21Port;
|
||||
|
||||
public Iec62056_21SerialConnector(Supplier<SerialPortManager> serialPortManagerSupplier, String portName,
|
||||
int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode) {
|
||||
super(portName);
|
||||
this.baudrate = baudrate;
|
||||
this.baudrateChangeDelay = baudrateChangeDelay;
|
||||
this.protocolMode = protocolMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean applyPeriod() {
|
||||
return protocolMode != ProtocolMode.D;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean applyRetryHandling() {
|
||||
return protocolMode != ProtocolMode.D;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Publisher<?> getRetryPublisher(Duration period, Publisher<Throwable> attempts) {
|
||||
if (protocolMode == ProtocolMode.D) {
|
||||
return Flowable.empty();
|
||||
} else {
|
||||
return super.getRetryPublisher(period, attempts);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DataMessage readNext(byte @Nullable [] initMessage) throws IOException {
|
||||
if (iec21Port != null) {
|
||||
DataMessage dataMessage = iec21Port.read();
|
||||
logger.debug("Datamessage read: {}", dataMessage);
|
||||
return dataMessage;
|
||||
}
|
||||
throw new IOException("SerialPort was not yet created!");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void emitValues(byte @Nullable [] initMessage, FlowableEmitter<@Nullable DataMessage> emitter)
|
||||
throws IOException {
|
||||
switch (protocolMode) {
|
||||
case ABC:
|
||||
super.emitValues(initMessage, emitter);
|
||||
break;
|
||||
case D:
|
||||
if (iec21Port != null) {
|
||||
iec21Port.listen(new ModeDListener() {
|
||||
|
||||
@Override
|
||||
public void newDataMessage(@Nullable DataMessage dataMessage) {
|
||||
logger.debug("Datamessage read: {}", dataMessage);
|
||||
emitter.onNext(dataMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionWhileListening(@Nullable Exception e) {
|
||||
logger.warn("Exception while listening for mode D data message", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case SML:
|
||||
throw new IOException("SML mode not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection() throws IOException {
|
||||
Builder iec21Builder = new Iec21Port.Builder(getPortName());
|
||||
if (Baudrate.fromBaudrate(this.baudrate) != Baudrate.AUTO) {
|
||||
iec21Builder.setInitialBaudrate(this.baudrate);
|
||||
}
|
||||
iec21Builder.setBaudRateChangeDelay(baudrateChangeDelay);
|
||||
iec21Builder.enableVerboseMode(true);
|
||||
iec21Port = iec21Builder.buildAndOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeConnection() {
|
||||
if (iec21Port != null) {
|
||||
iec21Port.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.iec62056;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Converts a unit from IEC62056-21 protocol to a {@link Unit}
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Iec62056_21UnitConversion {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Iec62056_21UnitConversion.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static @Nullable <Q extends Quantity<Q>> Unit<Q> getUnit(String unit) {
|
||||
if (!unit.isEmpty()) {
|
||||
try {
|
||||
return (Unit<Q>) UnitUtils.parseUnit(" " + unit);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse unit {}: {}", unit, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.sml;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openmuc.jsml.structures.EMessageBody;
|
||||
import org.openmuc.jsml.structures.SmlFile;
|
||||
import org.openmuc.jsml.structures.SmlMessage;
|
||||
import org.openmuc.jsml.structures.responses.SmlAttentionRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlGetListRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlGetProcParameterRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlGetProfileListRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlGetProfilePackRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlPublicCloseRes;
|
||||
import org.openmuc.jsml.structures.responses.SmlPublicOpenRes;
|
||||
|
||||
/**
|
||||
* Class to parse a SML_FILE
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmlFileDebugOutput {
|
||||
|
||||
private SmlFileDebugOutput() {
|
||||
// private constructor to hide the implicit public one, since static methods should be accessed in static way so
|
||||
// there is no need of public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the whole SML_File
|
||||
*
|
||||
* @param smlFile
|
||||
* the SML file
|
||||
*/
|
||||
public static void printFile(SmlFile smlFile, Consumer<String> consumer) {
|
||||
List<SmlMessage> smlMessages = smlFile.getMessages();
|
||||
|
||||
for (SmlMessage smlMessage : smlMessages) {
|
||||
EMessageBody messageBody = smlMessage.getMessageBody().getTag();
|
||||
|
||||
switch (messageBody) {
|
||||
case OPEN_REQUEST:
|
||||
parseOpenRequest(smlMessage, consumer);
|
||||
break;
|
||||
case OPEN_RESPONSE:
|
||||
parseOpenResponse(smlMessage, consumer);
|
||||
break;
|
||||
case CLOSE_REQUEST:
|
||||
parseCloseRequest(smlMessage, consumer);
|
||||
break;
|
||||
case CLOSE_RESPONSE:
|
||||
parseCloseResponse(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROFILE_PACK_REQUEST:
|
||||
parseGetProfilePackRequest(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROFILE_PACK_RESPONSE:
|
||||
parseGetProfilePackResponse(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROFILE_LIST_REQUEST:
|
||||
parseGetProfileListRequest(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROFILE_LIST_RESPONSE:
|
||||
parseGetProfileListResponse(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROC_PARAMETER_REQUEST:
|
||||
parseGetProcParameterRequest(smlMessage, consumer);
|
||||
break;
|
||||
case GET_PROC_PARAMETER_RESPONSE:
|
||||
parseGetProcParameterResponse(smlMessage, consumer);
|
||||
break;
|
||||
case SET_PROC_PARAMETER_REQUEST:
|
||||
parseSetProcParameterRequest(smlMessage, consumer);
|
||||
break;
|
||||
case GET_LIST_REQUEST:
|
||||
parseGetListRequest(smlMessage, consumer);
|
||||
break;
|
||||
case GET_LIST_RESPONSE:
|
||||
parseGetListResponse(smlMessage, consumer);
|
||||
break;
|
||||
case ATTENTION_RESPONSE:
|
||||
parseAttentionResponse(smlMessage, consumer);
|
||||
break;
|
||||
default:
|
||||
consumer.accept("type not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Responses =================================
|
||||
|
||||
private static void parseGetListResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetListResponse");
|
||||
SmlGetListRes sml_listRes = (SmlGetListRes) smlMessage.getMessageBody().getChoice();
|
||||
|
||||
// consumer.accept(sml_listRes.toString());
|
||||
|
||||
consumer.accept(sml_listRes.toStringIndent(" "));
|
||||
}
|
||||
|
||||
private static void parseAttentionResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got AttentionResponse");
|
||||
SmlAttentionRes sml_attentionRes = (SmlAttentionRes) smlMessage.getMessageBody().getChoice();
|
||||
consumer.accept(sml_attentionRes.toString());
|
||||
}
|
||||
|
||||
private static void parseGetProcParameterResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProcParameterResponse");
|
||||
SmlGetProcParameterRes sml_getProcParameterRes = (SmlGetProcParameterRes) smlMessage.getMessageBody()
|
||||
.getChoice();
|
||||
consumer.accept(sml_getProcParameterRes.toString());
|
||||
}
|
||||
|
||||
private static void parseGetProfileListResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProfileListResponse");
|
||||
SmlGetProfileListRes sml_getProfileListRes = (SmlGetProfileListRes) smlMessage.getMessageBody().getChoice();
|
||||
consumer.accept(sml_getProfileListRes.toString());
|
||||
}
|
||||
|
||||
private static void parseOpenResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got OpenResponse");
|
||||
SmlPublicOpenRes sml_PublicOpenRes = (SmlPublicOpenRes) smlMessage.getMessageBody().getChoice();
|
||||
consumer.accept(sml_PublicOpenRes.toString());
|
||||
}
|
||||
|
||||
private static void parseCloseResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got CloseResponse");
|
||||
SmlPublicCloseRes sml_PublicCloseRes = (SmlPublicCloseRes) smlMessage.getMessageBody().getChoice();
|
||||
consumer.accept(sml_PublicCloseRes.toString());
|
||||
}
|
||||
|
||||
private static void parseGetProfilePackResponse(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProfilePackResponse");
|
||||
SmlGetProfilePackRes sml_getProfilePackRes = (SmlGetProfilePackRes) smlMessage.getMessageBody().getChoice();
|
||||
consumer.accept(sml_getProfilePackRes.toString());
|
||||
}
|
||||
|
||||
// ========================= Requests =================================
|
||||
|
||||
private static void parseCloseRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got CloseRequest");
|
||||
}
|
||||
|
||||
private static void parseGetProfileListRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProfileListRequest");
|
||||
}
|
||||
|
||||
private static void parseGetProfilePackRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProfilePackRequest");
|
||||
}
|
||||
|
||||
private static void parseOpenRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got OpenRequest");
|
||||
}
|
||||
|
||||
private static void parseGetProcParameterRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetProcParameterRequest");
|
||||
}
|
||||
|
||||
private static void parseSetProcParameterRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got SetProcParameterRequest");
|
||||
}
|
||||
|
||||
private static void parseGetListRequest(SmlMessage smlMessage, Consumer<String> consumer) {
|
||||
consumer.accept("Got GetListRequest");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.sml;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.connectors.IMeterReaderConnector;
|
||||
import org.openhab.binding.smartmeter.internal.MeterDevice;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openmuc.jsml.structures.ASNObject;
|
||||
import org.openmuc.jsml.structures.EMessageBody;
|
||||
import org.openmuc.jsml.structures.SmlFile;
|
||||
import org.openmuc.jsml.structures.SmlList;
|
||||
import org.openmuc.jsml.structures.SmlListEntry;
|
||||
import org.openmuc.jsml.structures.SmlMessage;
|
||||
import org.openmuc.jsml.structures.SmlStatus;
|
||||
import org.openmuc.jsml.structures.responses.SmlGetListRes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Represents a SML capable device.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
* @author Mathias Gilhuber - Also-By
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class SmlMeterReader extends MeterDevice<SmlFile> {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(SmlMeterReader.class);
|
||||
|
||||
/**
|
||||
* Static factory method to create a SmlDevice object with a serial connector member.
|
||||
*
|
||||
* @param serialPortManagerSupplier
|
||||
*
|
||||
* @param deviceId the id of the device as defined in openHAB configuration.
|
||||
* @param pullRequestRequired identicates if SML values have to be actively requested.
|
||||
* @param serialPort the port where the device is connected as defined in openHAB configuration.
|
||||
* @param serialParameter
|
||||
* @param initMessage
|
||||
*/
|
||||
public static SmlMeterReader createInstance(Supplier<SerialPortManager> serialPortManagerSupplier, String deviceId,
|
||||
String serialPort, byte @Nullable [] initMessage, int baudrate, int baudrateChangeDelay) {
|
||||
SmlMeterReader device = new SmlMeterReader(serialPortManagerSupplier, deviceId, serialPort, initMessage,
|
||||
baudrate, baudrateChangeDelay, ProtocolMode.SML);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to create a SmlDevice object with a serial connector member.
|
||||
*
|
||||
* @param deviceId the id of the device as defined in openHAB configuration.
|
||||
* @param serialPort the port where the device is connected as defined in openHAB configuration.
|
||||
* @param serialParameter
|
||||
* @param initMessage
|
||||
* @param baudrate
|
||||
*/
|
||||
private SmlMeterReader(Supplier<SerialPortManager> serialPortManagerSupplier, String deviceId, String serialPort,
|
||||
byte @Nullable [] initMessage, int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode) {
|
||||
super(serialPortManagerSupplier, deviceId, serialPort, initMessage, baudrate, baudrateChangeDelay,
|
||||
protocolMode);
|
||||
|
||||
logger.debug("Created SmlDevice instance {} with serial connector on port {}", deviceId, serialPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes native SML informations from the device and stores them locally until the next read request.
|
||||
*
|
||||
* @param smlFile the native SML informations from the device
|
||||
*/
|
||||
@Override
|
||||
protected void populateValueCache(SmlFile smlFile) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Read out following SML file: {}", System.lineSeparator());
|
||||
SmlFileDebugOutput.printFile(smlFile, (msg) -> logger.trace(msg));
|
||||
}
|
||||
List<SmlMessage> smlMessages = smlFile.getMessages();
|
||||
|
||||
if (smlMessages != null) {
|
||||
int messageCount = smlMessages.size();
|
||||
|
||||
if (messageCount <= 0) {
|
||||
logger.warn("{}: no valid SML messages list retrieved.", this.toString());
|
||||
}
|
||||
|
||||
for (int i = 0; i < messageCount; i++) {
|
||||
SmlMessage smlMessage = smlMessages.get(i);
|
||||
|
||||
int tag = smlMessage.getMessageBody().getTag().id();
|
||||
|
||||
if (tag != EMessageBody.GET_LIST_RESPONSE.id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SmlGetListRes listResponse = (SmlGetListRes) smlMessage.getMessageBody().getChoice();
|
||||
SmlList smlValueList = listResponse.getValList();
|
||||
SmlListEntry[] smlListEntries = smlValueList.getValListEntry();
|
||||
|
||||
for (SmlListEntry entry : smlListEntries) {
|
||||
SmlValueExtractor valueExtractor = new SmlValueExtractor(entry);
|
||||
String obis = valueExtractor.getObisCode();
|
||||
|
||||
MeterValue<?> smlValue = getMeterValue(obis);
|
||||
|
||||
if (smlValue == null) {
|
||||
smlValue = valueExtractor.getSmlValue();
|
||||
}
|
||||
|
||||
SmlStatus status = entry.getStatus();
|
||||
if (status != null) {
|
||||
String statusValue = readStatus(status, obis);
|
||||
if (statusValue != null) {
|
||||
smlValue.setStatus(statusValue);
|
||||
}
|
||||
}
|
||||
|
||||
addObisCache(smlValue);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("{}: no valid SML messages list retrieved.", this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String readStatus(SmlStatus status, String obis) {
|
||||
ASNObject choice = status.getChoice();
|
||||
if (choice != null) {
|
||||
String statusValue = choice.toString();
|
||||
return statusValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IMeterReaderConnector<SmlFile> createConnector(Supplier<SerialPortManager> serialPortManagerSupplier,
|
||||
String serialPort, int baudrate, int baudrateChangeDelay, ProtocolMode protocolMode) {
|
||||
return new SmlSerialConnector(serialPortManagerSupplier, serialPort, baudrate, baudrateChangeDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void printInfo() {
|
||||
super.printInfo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.sml;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Stack;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.smartmeter.connectors.ConnectorBase;
|
||||
import org.openhab.binding.smartmeter.internal.helper.Baudrate;
|
||||
import org.openhab.binding.smartmeter.internal.helper.SerialParameter;
|
||||
import org.openhab.core.io.transport.serial.PortInUseException;
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.openmuc.jsml.structures.SmlFile;
|
||||
import org.openmuc.jsml.transport.Transport;
|
||||
|
||||
/**
|
||||
* Represents a serial SML device connector.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
* @author Mathias Gilhuber - Also-By
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class SmlSerialConnector extends ConnectorBase<SmlFile> {
|
||||
|
||||
private static final Transport TRANSPORT = new Transport();
|
||||
|
||||
private Supplier<SerialPortManager> serialManagerSupplier;
|
||||
@NonNullByDefault({})
|
||||
private SerialPort serialPort;
|
||||
@Nullable
|
||||
private DataInputStream is;
|
||||
@Nullable
|
||||
private DataOutputStream os;
|
||||
private int baudrate;
|
||||
|
||||
/**
|
||||
* Constructor to create a serial connector instance.
|
||||
*
|
||||
* @param portName the port where the device is connected as defined in openHAB configuration.
|
||||
*/
|
||||
public SmlSerialConnector(Supplier<SerialPortManager> serialPortManagerSupplier, String portName) {
|
||||
super(portName);
|
||||
this.serialManagerSupplier = serialPortManagerSupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to create a serial connector instance with a specific serial parameter.
|
||||
*
|
||||
* @param portName the port where the device is connected as defined in openHAB configuration.
|
||||
* @param baudrate
|
||||
* @throws IOException
|
||||
*/
|
||||
public SmlSerialConnector(Supplier<SerialPortManager> serialPortManagerSupplier, String portName, int baudrate,
|
||||
int baudrateChangeDelay) {
|
||||
this(serialPortManagerSupplier, portName);
|
||||
this.baudrate = baudrate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmlFile readNext(byte @Nullable [] initMessage) throws IOException {
|
||||
if (initMessage != null) {
|
||||
logger.debug("Writing init message: {}", HexUtils.bytesToHex(initMessage, " "));
|
||||
if (os != null) {
|
||||
os.write(initMessage);
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// read out the whole buffer. We are only interested in the most recent SML file.
|
||||
Stack<SmlFile> smlFiles = new Stack<>();
|
||||
do {
|
||||
logger.trace("Reading {}. SML message", smlFiles.size() + 1);
|
||||
smlFiles.push(TRANSPORT.getSMLFile(is));
|
||||
} while (is != null && is.available() > 0);
|
||||
if (smlFiles.isEmpty()) {
|
||||
throw new IOException(getPortName() + " : There is no SML file in buffer. Try to increase Refresh rate.");
|
||||
}
|
||||
logger.debug("{} : Read {} SML files from Buffer", this.getPortName(), smlFiles.size());
|
||||
return smlFiles.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection() throws IOException {
|
||||
closeConnection();
|
||||
SerialPortIdentifier id = serialManagerSupplier.get().getIdentifier(getPortName());
|
||||
if (id != null) {
|
||||
try {
|
||||
serialPort = id.open("meterreaderbinding", 0);
|
||||
} catch (PortInUseException e) {
|
||||
throw new IOException(MessageFormat
|
||||
.format("Error at SerialConnector.openConnection: unable to open port {0}.", getPortName()), e);
|
||||
}
|
||||
SerialParameter serialParameter = SerialParameter._8N1;
|
||||
int baudrateToUse = this.baudrate == Baudrate.AUTO.getBaudrate() ? Baudrate._9600.getBaudrate()
|
||||
: this.baudrate;
|
||||
try {
|
||||
serialPort.setSerialPortParams(baudrateToUse, serialParameter.getDatabits(),
|
||||
serialParameter.getStopbits(), serialParameter.getParity());
|
||||
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
|
||||
try {
|
||||
serialPort.enableReceiveTimeout(100);
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
// doesn't matter (rfc2217 is not supporting this)
|
||||
}
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
throw new IOException(MessageFormat.format(
|
||||
"Error at SerialConnector.openConnection: unable to set serial port parameters for port {0}.",
|
||||
getPortName()), e);
|
||||
}
|
||||
// serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
|
||||
serialPort.notifyOnDataAvailable(true);
|
||||
is = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
|
||||
os = new DataOutputStream(new BufferedOutputStream(serialPort.getOutputStream()));
|
||||
} else {
|
||||
throw new IllegalStateException(MessageFormat.format("No provider for port {0} found", getPortName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @{inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void closeConnection() {
|
||||
try {
|
||||
if (is != null) {
|
||||
is.close();
|
||||
is = null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to close serial input stream", e);
|
||||
}
|
||||
try {
|
||||
if (os != null) {
|
||||
os.close();
|
||||
os = null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to close serial output stream", e);
|
||||
}
|
||||
if (serialPort != null) {
|
||||
serialPort.close();
|
||||
serialPort = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean applyPeriod() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.sml;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openmuc.jsml.EUnit;
|
||||
|
||||
/**
|
||||
* Converts a {@link EUnit} to an {@link Unit}.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmlUnitConversion {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static @Nullable <Q extends Quantity<Q>> Unit<Q> getUnit(EUnit unit) {
|
||||
Unit<?> javaUnit = null;
|
||||
switch (unit) {
|
||||
case AMPERE:
|
||||
javaUnit = SmartHomeUnits.AMPERE;
|
||||
break;
|
||||
case AMPERE_HOUR:
|
||||
javaUnit = SmartHomeUnits.AMPERE.divide(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case AMPERE_PER_METRE:
|
||||
javaUnit = SmartHomeUnits.AMPERE.multiply(SIUnits.METRE);
|
||||
break;
|
||||
case AMPERE_SQUARED_HOURS:
|
||||
javaUnit = SmartHomeUnits.AMPERE.pow(2).multiply(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case BAR:
|
||||
javaUnit = SIUnits.PASCAL.multiply(100000);
|
||||
break;
|
||||
case COULOMB:
|
||||
javaUnit = SmartHomeUnits.COULOMB;
|
||||
break;
|
||||
|
||||
case CUBIC_METRE:
|
||||
case CUBIC_METRE_CORRECTED:
|
||||
javaUnit = SIUnits.CUBIC_METRE;
|
||||
break;
|
||||
case CUBIC_METRE_PER_DAY:
|
||||
case CUBIC_METRE_PER_DAY_CORRECTED:
|
||||
javaUnit = SIUnits.CUBIC_METRE.divide(SmartHomeUnits.DAY);
|
||||
break;
|
||||
case CUBIC_METRE_PER_HOUR:
|
||||
case CUBIC_METRE_PER_HOUR_CORRECTED:
|
||||
javaUnit = SIUnits.CUBIC_METRE.divide(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
|
||||
case DAY:
|
||||
javaUnit = SmartHomeUnits.DAY;
|
||||
break;
|
||||
case DEGREE:
|
||||
javaUnit = SmartHomeUnits.DEGREE_ANGLE;
|
||||
break;
|
||||
case DEGREE_CELSIUS:
|
||||
javaUnit = SIUnits.CELSIUS;
|
||||
break;
|
||||
|
||||
case FARAD:
|
||||
javaUnit = SmartHomeUnits.FARAD;
|
||||
break;
|
||||
case HENRY:
|
||||
javaUnit = SmartHomeUnits.HENRY;
|
||||
break;
|
||||
case HERTZ:
|
||||
javaUnit = SmartHomeUnits.HERTZ;
|
||||
break;
|
||||
case HOUR:
|
||||
javaUnit = SmartHomeUnits.HOUR;
|
||||
break;
|
||||
case JOULE:
|
||||
javaUnit = SmartHomeUnits.JOULE;
|
||||
break;
|
||||
case JOULE_PER_HOUR:
|
||||
javaUnit = SmartHomeUnits.JOULE.divide(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case KELVIN:
|
||||
javaUnit = SmartHomeUnits.KELVIN;
|
||||
break;
|
||||
case KILOGRAM:
|
||||
javaUnit = SIUnits.KILOGRAM;
|
||||
case KILOGRAM_PER_SECOND:
|
||||
javaUnit = SIUnits.KILOGRAM.divide(SmartHomeUnits.SECOND);
|
||||
break;
|
||||
case LITRE:
|
||||
javaUnit = SmartHomeUnits.LITRE;
|
||||
break;
|
||||
case MASS_DENSITY:
|
||||
break;
|
||||
case METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case METRE:
|
||||
javaUnit = SIUnits.METRE;
|
||||
break;
|
||||
case METRE_PER_SECOND:
|
||||
javaUnit = SmartHomeUnits.METRE_PER_SECOND;
|
||||
break;
|
||||
case MOLE_PERCENT:
|
||||
javaUnit = SmartHomeUnits.MOLE;
|
||||
break;
|
||||
case MONTH:
|
||||
javaUnit = SmartHomeUnits.YEAR.divide(12);
|
||||
break;
|
||||
case NEWTON:
|
||||
javaUnit = SmartHomeUnits.NEWTON;
|
||||
case NEWTONMETER:
|
||||
javaUnit = SmartHomeUnits.NEWTON.multiply(SIUnits.METRE);
|
||||
break;
|
||||
case OHM:
|
||||
javaUnit = SmartHomeUnits.OHM;
|
||||
break;
|
||||
case OHM_METRE:
|
||||
javaUnit = SmartHomeUnits.OHM.multiply(SIUnits.METRE);
|
||||
break;
|
||||
case PASCAL:
|
||||
javaUnit = SIUnits.PASCAL;
|
||||
break;
|
||||
case PASCAL_SECOND:
|
||||
javaUnit = SIUnits.PASCAL.multiply(SmartHomeUnits.SECOND);
|
||||
break;
|
||||
case PERCENTAGE:
|
||||
javaUnit = SmartHomeUnits.PERCENT;
|
||||
break;
|
||||
case SECOND:
|
||||
javaUnit = SmartHomeUnits.SECOND;
|
||||
break;
|
||||
case TESLA:
|
||||
javaUnit = SmartHomeUnits.TESLA;
|
||||
break;
|
||||
case VAR:
|
||||
javaUnit = SmartHomeUnits.WATT.alternate("Var");
|
||||
break;
|
||||
case VAR_HOUR:
|
||||
javaUnit = SmartHomeUnits.WATT.alternate("Var").multiply(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case VOLT:
|
||||
javaUnit = SmartHomeUnits.VOLT;
|
||||
break;
|
||||
case VOLT_AMPERE:
|
||||
javaUnit = SmartHomeUnits.VOLT.multiply(SmartHomeUnits.AMPERE);
|
||||
break;
|
||||
case VOLT_AMPERE_HOUR:
|
||||
javaUnit = SmartHomeUnits.VOLT.multiply(SmartHomeUnits.AMPERE).multiply(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case VOLT_PER_METRE:
|
||||
javaUnit = SmartHomeUnits.WATT.divide(SIUnits.METRE);
|
||||
break;
|
||||
case VOLT_SQUARED_HOURS:
|
||||
javaUnit = SmartHomeUnits.VOLT.pow(2).multiply(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case WATT:
|
||||
javaUnit = SmartHomeUnits.WATT;
|
||||
break;
|
||||
case WATT_HOUR:
|
||||
javaUnit = SmartHomeUnits.WATT.multiply(SmartHomeUnits.HOUR);
|
||||
break;
|
||||
case WEBER:
|
||||
javaUnit = SmartHomeUnits.WEBER;
|
||||
break;
|
||||
case WEEK:
|
||||
javaUnit = SmartHomeUnits.WEEK;
|
||||
break;
|
||||
case YEAR:
|
||||
javaUnit = SmartHomeUnits.YEAR;
|
||||
break;
|
||||
|
||||
// not clearly defined yet:
|
||||
case VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE:
|
||||
break;
|
||||
case ENERGY_PER_VOLUME:
|
||||
break;
|
||||
case CALORIFIC_VALUE:
|
||||
break;
|
||||
|
||||
// no unit possible:
|
||||
case MIN:
|
||||
case OTHER_UNIT:
|
||||
case RESERVED:
|
||||
case COUNT:
|
||||
case CURRENCY:
|
||||
case EMPTY:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (Unit<Q>) javaUnit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter.internal.sml;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openmuc.jsml.EObis;
|
||||
import org.openmuc.jsml.EUnit;
|
||||
import org.openmuc.jsml.structures.ASNObject;
|
||||
import org.openmuc.jsml.structures.SmlListEntry;
|
||||
|
||||
/**
|
||||
* Proxy class to encapsulate a openMUC SML_ListEntry-Object to read informations.
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
* @author Mathias Gilhuber - Also-By
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class SmlValueExtractor {
|
||||
|
||||
/**
|
||||
* Stores the original value object from jSML
|
||||
*/
|
||||
SmlListEntry smlListEntry;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param obis
|
||||
*/
|
||||
public SmlValueExtractor(SmlListEntry listEntry) {
|
||||
smlListEntry = listEntry;
|
||||
}
|
||||
|
||||
public <Q extends Quantity<Q>> MeterValue<Q> getSmlValue() {
|
||||
return new MeterValue<Q>(getObisCode(), getValue(), SmlUnitConversion.getUnit(getUnit()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values unit.
|
||||
*
|
||||
* @return the values unit if available - Integer.MIN_VALUE.
|
||||
*/
|
||||
public EUnit getUnit() {
|
||||
return EUnit.from(smlListEntry.getUnit().getVal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values unit.
|
||||
*
|
||||
* @return the string representation of the values unit - otherwise null.
|
||||
*/
|
||||
public String getUnitName() {
|
||||
EUnit unitEnum = getUnit();
|
||||
return unitEnum.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human readable name of the OBIS code.
|
||||
*
|
||||
* @return The name of the obis code or {@link EObis#UNKNOWN} if not known
|
||||
*/
|
||||
public String getObisName() {
|
||||
String obisName = null;
|
||||
EObis smlUnit = Arrays.asList(EObis.values()).stream()
|
||||
.filter((a) -> a.obisCode().equals(smlListEntry.getObjName())).findAny().orElseGet(() -> EObis.UNKNOWN);
|
||||
obisName = smlUnit.name();
|
||||
|
||||
return obisName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Value: '" + this.getValue() + "' Unit: '" + this.getUnitName() + "' Scaler:'" + this.getScaler() + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value
|
||||
*
|
||||
* @return the value as String if available - otherwise null.
|
||||
*/
|
||||
public String getValue() {
|
||||
org.openmuc.jsml.structures.SmlValue smlValue = smlListEntry.getValue();
|
||||
ASNObject choice = smlValue.getChoice();
|
||||
String value = choice.toString();
|
||||
try {
|
||||
value = scaleValue(Double.parseDouble(value)) + "";
|
||||
} catch (Exception e) {
|
||||
// value is no numeric value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scaler which has to be applied to the value.
|
||||
*
|
||||
* @return scaler which has to be applied to the value.
|
||||
*/
|
||||
double getScaler() {
|
||||
int scaler = 0;
|
||||
|
||||
if (smlListEntry.getScaler().isSelected()) {
|
||||
byte scalerByte = smlListEntry.getScaler().getVal();
|
||||
|
||||
scaler = Integer.parseInt(String.format("%02x", scalerByte), 16);
|
||||
|
||||
if (scaler > 127) {
|
||||
scaler -= 256;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.pow(10, scaler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the value if necessary
|
||||
*
|
||||
* @return a string representation of the scaled value.
|
||||
*/
|
||||
Double scaleValue(Double originalValue) {
|
||||
return originalValue * getScaler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Byte to Integer conversion.
|
||||
*
|
||||
* @param byte to convert to Integer.
|
||||
*/
|
||||
private static int byteToInt(byte b) {
|
||||
return Integer.parseInt(String.format("%02x", b), 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts hex encoded OBIS to formatted string.
|
||||
*
|
||||
* @return the hex encoded OBIS code as readable string.
|
||||
*/
|
||||
protected static String getObisAsString(byte[] octetBytes) {
|
||||
String formattedObis = String.format(SmartMeterBindingConstants.OBIS_FORMAT_MINIMAL, byteToInt(octetBytes[0]),
|
||||
byteToInt(octetBytes[1]), byteToInt(octetBytes[2]), byteToInt(octetBytes[3]), byteToInt(octetBytes[4]));
|
||||
|
||||
return formattedObis;
|
||||
}
|
||||
|
||||
public String getObisCode() {
|
||||
return getObisAsString(smlListEntry.getObjName().getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="smartmeter" 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>Smartmeter Binding</name>
|
||||
<description>
|
||||
The Smartmeter binding is able to read SML messages (PUSH) and supports IEC 62056-21 modes A,B,C (PULL)
|
||||
and D (PUSH).
|
||||
</description>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="channel-type:smartmeter:obis">
|
||||
|
||||
<parameter name="negate" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Negate Property</label>
|
||||
<description>e.g. 1-0_1-8-0:5:1:status //negate if status(1-0_1-8-0) and 2^5 = 1</description>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="smartmeter"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
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="meter">
|
||||
<label>Smart Meter</label>
|
||||
<description>The meter device to read the SML or IEC 62056-21 messages from</description>
|
||||
<config-description>
|
||||
<parameter name="port" type="text">
|
||||
<label>Serial Port</label>
|
||||
<description>The device serial port (e.g. /dev/tty0 or COM1)</description>
|
||||
<required>true</required>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<context>serial-port</context>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer">
|
||||
<advanced>true</advanced>
|
||||
<label>Refresh Rate</label>
|
||||
<description>Refresh rate in seconds</description>
|
||||
<default>10</default>
|
||||
<unitLabel>s</unitLabel>
|
||||
</parameter>
|
||||
<parameter name="baudrateChangeDelay" type="integer">
|
||||
<advanced>true</advanced>
|
||||
<label>Delay of Baudrate Change</label>
|
||||
<unitLabel>ms</unitLabel>
|
||||
<default>0</default>
|
||||
<description>USB to serial converters often require a delay of up to 250ms after the ACK before changing baudrate</description>
|
||||
</parameter>
|
||||
<parameter name="baudrate" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Baudrate</label>
|
||||
<default>AUTO</default>
|
||||
<description>The baudrate of the serial port. If set to 'AUTO', it is dependent on the selected mode. The default is
|
||||
300 baud for modes A, B, and C and 2400 baud for mode D, and 9600 baud for SML.</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
</parameter>
|
||||
<parameter name="mode" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>The Protocol Mode to Use</label>
|
||||
<default>SML</default>
|
||||
<description>Can be SML (PUSH mode), Mode A,B,C (PULL)or D (PUSH)</description>
|
||||
</parameter>
|
||||
<parameter name="conformity" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Conform to Specific Standard Semantics</label>
|
||||
<default>NONE</default>
|
||||
<description>Reserved to conform to special semantics specified in specific standards. EDL_FNN: Currently applies
|
||||
the energy direction to WATT channels (which are absolute values) (see fnn lastenheft edl)</description>
|
||||
<limitToOptions>true</limitToOptions>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.openhab.binding.smartmeter.connectors.ConnectorBase;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class MockMeterReaderConnector extends ConnectorBase<Object> {
|
||||
|
||||
private boolean applyRetry;
|
||||
private Supplier<Object> readNextSupplier;
|
||||
|
||||
protected MockMeterReaderConnector(String portName, boolean applyRetry, Supplier<Object> readNextSupplier) {
|
||||
super(portName);
|
||||
this.applyRetry = applyRetry;
|
||||
this.readNextSupplier = readNextSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeConnection() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object readNext(byte[] initMessage) throws IOException {
|
||||
try {
|
||||
return readNextSupplier.get();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean applyRetryHandling() {
|
||||
return this.applyRetry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean applyPeriod() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void retryHook(int retryCount) {
|
||||
super.retryHook(retryCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.binding.smartmeter.connectors.ConnectorBase;
|
||||
import org.openhab.binding.smartmeter.connectors.IMeterReaderConnector;
|
||||
import org.openhab.binding.smartmeter.internal.MeterDevice;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValueListener;
|
||||
import org.openhab.binding.smartmeter.internal.helper.ProtocolMode;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class TestMeterReading {
|
||||
|
||||
@Test
|
||||
public void testContinousReading() throws Exception {
|
||||
final Duration period = Duration.ofSeconds(1);
|
||||
final int executionCount = 5;
|
||||
MockMeterReaderConnector connector = getMockedConnector(false, () -> new Object());
|
||||
MeterDevice<Object> meter = getMeterDevice(connector);
|
||||
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
|
||||
meter.addValueChangeListener(changeListener);
|
||||
Disposable disposable = meter.readValues(5000, Executors.newScheduledThreadPool(1), period);
|
||||
try {
|
||||
verify(changeListener, after(executionCount * period.toMillis() + period.toMillis() / 2).never())
|
||||
.errorOccurred(any());
|
||||
verify(changeListener, times(executionCount)).valueChanged(any());
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryHandling() {
|
||||
final Duration period = Duration.ofSeconds(1);
|
||||
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
|
||||
throw new IllegalArgumentException();
|
||||
}));
|
||||
MeterDevice<Object> meter = getMeterDevice(connector);
|
||||
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
|
||||
meter.addValueChangeListener(changeListener);
|
||||
Disposable disposable = meter.readValues(5000, Executors.newScheduledThreadPool(1), period);
|
||||
try {
|
||||
verify(changeListener, after(
|
||||
period.toMillis() + 2 * period.toMillis() * ConnectorBase.NUMBER_OF_RETRIES + period.toMillis() / 2)
|
||||
.times(1)).errorOccurred(any());
|
||||
verify(connector, times(ConnectorBase.NUMBER_OF_RETRIES)).retryHook(ArgumentMatchers.anyInt());
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeoutHandling() {
|
||||
final Duration period = Duration.ofSeconds(2);
|
||||
final int timeout = 5000;
|
||||
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
|
||||
try {
|
||||
Thread.sleep(timeout + 2000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
return new Object();
|
||||
}));
|
||||
MeterDevice<Object> meter = getMeterDevice(connector);
|
||||
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
|
||||
meter.addValueChangeListener(changeListener);
|
||||
Disposable disposable = meter.readValues(5000, Executors.newScheduledThreadPool(2), period);
|
||||
try {
|
||||
verify(changeListener, after(timeout + 3000).times(1)).errorOccurred(any(TimeoutException.class));
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotReportToFallbackException() {
|
||||
final Duration period = Duration.ofSeconds(2);
|
||||
final int timeout = 5000;
|
||||
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
|
||||
try {
|
||||
Thread.sleep(timeout + 2000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
throw new RuntimeException(new IOException("fucked up"));
|
||||
}));
|
||||
MeterDevice<Object> meter = getMeterDevice(connector);
|
||||
Consumer<Throwable> errorHandler = mock(Consumer.class);
|
||||
RxJavaPlugins.setErrorHandler(errorHandler);
|
||||
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
|
||||
meter.addValueChangeListener(changeListener);
|
||||
Disposable disposable = meter.readValues(5000, Executors.newScheduledThreadPool(2), period);
|
||||
try {
|
||||
verify(changeListener, after(timeout + 3000).times(1)).errorOccurred(any(TimeoutException.class));
|
||||
verifyNoMoreInteractions(errorHandler);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
MockMeterReaderConnector getMockedConnector(boolean applyRetry, Supplier<Object> readNextSupplier) {
|
||||
return new MockMeterReaderConnector("Test port", applyRetry, readNextSupplier);
|
||||
}
|
||||
|
||||
MeterDevice<Object> getMeterDevice(ConnectorBase<Object> connector) {
|
||||
return new MeterDevice<Object>(() -> mock(SerialPortManager.class), "id", "port", null, 9600, 0,
|
||||
ProtocolMode.SML) {
|
||||
|
||||
@Override
|
||||
protected @NonNull IMeterReaderConnector<Object> createConnector(
|
||||
@NonNull Supplier<@NonNull SerialPortManager> serialPortManagerSupplier, @NonNull String serialPort,
|
||||
int baudrate, int baudrateChangeDelay, @NonNull ProtocolMode protocolMode) {
|
||||
return connector;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <Q extends @NonNull Quantity<Q>> void populateValueCache(Object smlFile) {
|
||||
addObisCache(new MeterValue("123", "333", null));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.smartmeter;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.smartmeter.internal.MeterValue;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.negate.NegateBitModel;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.negate.NegateBitParser;
|
||||
import org.openhab.binding.smartmeter.internal.conformity.negate.NegateHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Matthias Steigenberger - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class TestNegateBit {
|
||||
|
||||
@Test
|
||||
public void testNegateBitParsing() {
|
||||
String negateProperty = "1-0_1-8-0:5:1";
|
||||
NegateBitModel parseNegateProperty = NegateBitParser.parseNegateProperty(negateProperty);
|
||||
Assert.assertEquals("1-0_1-8-0", parseNegateProperty.getNegateChannelId());
|
||||
Assert.assertEquals(5, parseNegateProperty.getNegatePosition());
|
||||
Assert.assertEquals(true, parseNegateProperty.isNegateBit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegateHandlingTrue() {
|
||||
String negateProperty = "1-0_1-8-0:5:1";
|
||||
|
||||
boolean negateState = NegateHandler.shouldNegateState(negateProperty, obis -> {
|
||||
return new MeterValue<>(obis, "65954", null);
|
||||
});
|
||||
|
||||
Assert.assertTrue(negateState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegateHandlingFalse() {
|
||||
String negateProperty = "1-0_1-8-0:5:1";
|
||||
|
||||
boolean negateState = NegateHandler.shouldNegateState(negateProperty, obis -> {
|
||||
return new MeterValue<>(obis, "0", null, "65922");
|
||||
});
|
||||
|
||||
Assert.assertFalse(negateState);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user