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