added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

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

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

View 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

View 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

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

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

View 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
}

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

View 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.");
}
}

View File

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

View 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>&gt; 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();
}

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

View File

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

View File

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

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

View File

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

View File

@@ -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(":|\\*", "_");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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