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