2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.smartmeter.internal;
15 import java.math.BigDecimal;
16 import java.text.MessageFormat;
17 import java.time.Duration;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.Objects;
24 import java.util.function.Supplier;
26 import javax.measure.Quantity;
27 import javax.measure.Unit;
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.DefaultLocation;
31 import org.eclipse.jdt.annotation.NonNull;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
35 import org.openhab.binding.smartmeter.SmartMeterConfiguration;
36 import org.openhab.binding.smartmeter.internal.conformity.Conformity;
37 import org.openhab.binding.smartmeter.internal.helper.Baudrate;
38 import org.openhab.core.config.core.Configuration;
39 import org.openhab.core.io.transport.serial.SerialPortManager;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.builder.ChannelBuilder;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.thing.type.ChannelType;
51 import org.openhab.core.thing.type.ChannelTypeUID;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.State;
55 import org.openhab.core.types.TypeParser;
56 import org.openhab.core.util.HexUtils;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
60 import io.reactivex.disposables.Disposable;
63 * The {@link SmartMeterHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Matthias Steigenberger - Initial contribution
68 @NonNullByDefault({ DefaultLocation.ARRAY_CONTENTS, DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE,
69 DefaultLocation.TYPE_ARGUMENT })
70 public class SmartMeterHandler extends BaseThingHandler {
72 private static final long DEFAULT_TIMEOUT = 30000;
73 private static final int DEFAULT_REFRESH_PERIOD = 30;
74 private Logger logger = LoggerFactory.getLogger(SmartMeterHandler.class);
75 private MeterDevice<?> smlDevice;
76 private Disposable valueReader;
77 private Conformity conformity;
78 private MeterValueListener valueChangeListener;
79 private SmartMeterChannelTypeProvider channelTypeProvider;
80 private @NonNull Supplier<SerialPortManager> serialPortManagerSupplier;
82 public SmartMeterHandler(Thing thing, SmartMeterChannelTypeProvider channelProvider,
83 Supplier<SerialPortManager> serialPortManagerSupplier) {
85 Objects.requireNonNull(channelProvider, "SmartMeterChannelTypeProvider must not be null");
86 this.channelTypeProvider = channelProvider;
87 this.serialPortManagerSupplier = serialPortManagerSupplier;
91 public void initialize() {
92 logger.debug("Initializing Smartmeter handler.");
95 SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
96 logger.debug("config port = {}", config.port);
98 boolean validConfig = true;
99 String errorMsg = null;
101 if (StringUtils.trimToNull(config.port) == null) {
102 errorMsg = "Parameter 'port' is mandatory and must be configured";
107 byte[] pullSequence = config.initMessage == null ? null
108 : HexUtils.hexToBytes(StringUtils.deleteWhitespace(config.initMessage));
109 int baudrate = config.baudrate == null ? Baudrate.AUTO.getBaudrate()
110 : Baudrate.fromString(config.baudrate).getBaudrate();
111 this.conformity = config.conformity == null ? Conformity.NONE : Conformity.valueOf(config.conformity);
112 this.smlDevice = MeterDeviceFactory.getDevice(serialPortManagerSupplier, config.mode,
113 this.thing.getUID().getAsString(), config.port, pullSequence, baudrate, config.baudrateChangeDelay);
114 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING,
115 "Waiting for messages from device");
117 smlDevice.addValueChangeListener(channelTypeProvider);
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
126 public void dispose() {
129 if (this.valueChangeListener != null) {
130 this.smlDevice.removeValueChangeListener(valueChangeListener);
132 if (this.channelTypeProvider != null) {
133 this.smlDevice.removeValueChangeListener(channelTypeProvider);
137 private void cancelRead() {
138 if (this.valueReader != null) {
139 this.valueReader.dispose();
144 public void handleCommand(ChannelUID channelUID, Command command) {
145 if (command instanceof RefreshType) {
146 updateOBISChannel(channelUID);
148 logger.debug("The SML reader binding is read-only and can not handle command {}", command);
153 * Get new data the device
156 private void updateOBISValue() {
159 valueChangeListener = new MeterValueListener() {
161 public <Q extends @NonNull Quantity<Q>> void valueChanged(MeterValue<Q> value) {
162 ThingBuilder thingBuilder = editThing();
164 String obis = value.getObisCode();
166 String obisChannelString = SmartMeterBindingConstants.getObisChannelId(obis);
167 Channel channel = thing.getChannel(obisChannelString);
168 ChannelTypeUID channelTypeId = channelTypeProvider.getChannelTypeIdForObis(obis);
170 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeId, null);
171 if (channelType != null) {
172 String itemType = channelType.getItemType();
174 State state = getStateForObisValue(value, channel);
175 if (channel == null) {
176 logger.debug("Adding channel: {} with item type: {}", obisChannelString, itemType);
178 // channel has not been created yet
179 ChannelBuilder channelBuilder = ChannelBuilder
180 .create(new ChannelUID(thing.getUID(), obisChannelString), itemType)
181 .withType(channelTypeId);
183 Configuration configuration = new Configuration();
184 configuration.put(SmartMeterBindingConstants.CONFIGURATION_CONVERSION, 1);
185 channelBuilder.withConfiguration(configuration);
186 channelBuilder.withLabel(obis);
187 Map<String, String> channelProps = new HashMap<>();
188 channelProps.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
189 channelBuilder.withProperties(channelProps);
190 channelBuilder.withDescription(
191 MessageFormat.format("Value for OBIS code: {0} with Unit: {1}", obis, value.getUnit()));
192 channel = channelBuilder.build();
193 ChannelUID channelId = channel.getUID();
195 // add all valid channels to the thing builder
196 List<Channel> channels = new ArrayList<>(getThing().getChannels());
197 if (channels.stream().filter((element) -> element.getUID().equals(channelId)).count() == 0) {
198 channels.add(channel);
199 thingBuilder.withChannels(channels);
200 updateThing(thingBuilder.build());
204 if (!channel.getProperties().containsKey(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS)) {
205 addObisPropertyToChannel(obis, channel);
207 updateState(channel.getUID(), state);
209 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
211 logger.warn("No ChannelType found for OBIS {}", obis);
215 private void addObisPropertyToChannel(String obis, Channel channel) {
216 String description = channel.getDescription();
217 String label = channel.getLabel();
218 ChannelBuilder newChannel = ChannelBuilder.create(channel.getUID(), channel.getAcceptedItemType())
219 .withDefaultTags(channel.getDefaultTags()).withConfiguration(channel.getConfiguration())
220 .withDescription(description == null ? "" : description).withKind(channel.getKind())
221 .withLabel(label == null ? "" : label).withType(channel.getChannelTypeUID());
222 Map<String, String> properties = new HashMap<>(channel.getProperties());
223 properties.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
224 newChannel.withProperties(properties);
225 updateThing(editThing().withoutChannel(channel.getUID()).withChannel(newChannel.build()).build());
229 public <Q extends @NonNull Quantity<Q>> void valueRemoved(MeterValue<Q> value) {
230 // channels that are not available are removed
231 String obisChannelId = SmartMeterBindingConstants.getObisChannelId(value.getObisCode());
232 logger.debug("Removing channel: {}", obisChannelId);
233 ThingBuilder thingBuilder = editThing();
234 thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), obisChannelId));
235 updateThing(thingBuilder.build());
239 public void errorOccurred(Throwable e) {
240 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
243 this.smlDevice.addValueChangeListener(valueChangeListener);
245 SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
246 int delay = config.refresh != null ? config.refresh : DEFAULT_REFRESH_PERIOD;
247 valueReader = this.smlDevice.readValues(DEFAULT_TIMEOUT, this.scheduler, Duration.ofSeconds(delay));
250 private void updateOBISChannel(ChannelUID channelId) {
251 if (isLinked(channelId.getId())) {
252 Channel channel = this.thing.getChannel(channelId.getId());
253 if (channel != null) {
254 String obis = channel.getProperties().get(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS);
255 MeterValue<?> value = this.smlDevice.getMeterValue(obis);
257 State state = getStateForObisValue(value, channel);
258 updateState(channel.getUID(), state);
264 @SuppressWarnings("unchecked")
265 private <Q extends Quantity<Q>> State getStateForObisValue(MeterValue<?> value, @Nullable Channel channel) {
266 Unit<?> unit = value.getUnit();
267 String valueString = value.getValue();
269 valueString += " " + value.getUnit();
271 State state = TypeParser.parseState(Arrays.asList(QuantityType.class, StringType.class), valueString);
272 if (channel != null && state instanceof QuantityType) {
273 state = applyConformity(channel, (QuantityType<Q>) state);
274 Number conversionRatio = (Number) channel.getConfiguration()
275 .get(SmartMeterBindingConstants.CONFIGURATION_CONVERSION);
276 if (conversionRatio != null) {
277 state = ((QuantityType<?>) state).divide(BigDecimal.valueOf(conversionRatio.doubleValue()));
283 private <Q extends Quantity<Q>> State applyConformity(Channel channel, QuantityType<Q> currentState) {
285 return this.conformity.apply(channel, currentState, getThing(), this.smlDevice);
286 } catch (Exception e) {
287 logger.warn("Failed to apply negation for channel: {}", channel.getUID(), e);