]> git.basschouten.com Git - openhab-addons.git/blob
5925fbb4d5f185d247590c1ff8f2535f89779878
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.smartmeter.internal;
14
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;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.function.Supplier;
25
26 import javax.measure.Quantity;
27 import javax.measure.Unit;
28
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;
59
60 import io.reactivex.disposables.Disposable;
61
62 /**
63  * The {@link SmartMeterHandler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * @author Matthias Steigenberger - Initial contribution
67  */
68 @NonNullByDefault({ DefaultLocation.ARRAY_CONTENTS, DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE,
69         DefaultLocation.TYPE_ARGUMENT })
70 public class SmartMeterHandler extends BaseThingHandler {
71
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;
81
82     public SmartMeterHandler(Thing thing, SmartMeterChannelTypeProvider channelProvider,
83             Supplier<SerialPortManager> serialPortManagerSupplier) {
84         super(thing);
85         Objects.requireNonNull(channelProvider, "SmartMeterChannelTypeProvider must not be null");
86         this.channelTypeProvider = channelProvider;
87         this.serialPortManagerSupplier = serialPortManagerSupplier;
88     }
89
90     @Override
91     public void initialize() {
92         logger.debug("Initializing Smartmeter handler.");
93         cancelRead();
94
95         SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
96         logger.debug("config port = {}", config.port);
97
98         boolean validConfig = true;
99         String errorMsg = null;
100
101         if (StringUtils.trimToNull(config.port) == null) {
102             errorMsg = "Parameter 'port' is mandatory and must be configured";
103             validConfig = false;
104         }
105
106         if (validConfig) {
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");
116
117             smlDevice.addValueChangeListener(channelTypeProvider);
118
119             updateOBISValue();
120         } else {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
122         }
123     }
124
125     @Override
126     public void dispose() {
127         super.dispose();
128         cancelRead();
129         if (this.valueChangeListener != null) {
130             this.smlDevice.removeValueChangeListener(valueChangeListener);
131         }
132         if (this.channelTypeProvider != null) {
133             this.smlDevice.removeValueChangeListener(channelTypeProvider);
134         }
135     }
136
137     private void cancelRead() {
138         if (this.valueReader != null) {
139             this.valueReader.dispose();
140         }
141     }
142
143     @Override
144     public void handleCommand(ChannelUID channelUID, Command command) {
145         if (command instanceof RefreshType) {
146             updateOBISChannel(channelUID);
147         } else {
148             logger.debug("The SML reader binding is read-only and can not handle command {}", command);
149         }
150     }
151
152     /**
153      * Get new data the device
154      *
155      */
156     private void updateOBISValue() {
157         cancelRead();
158
159         valueChangeListener = new MeterValueListener() {
160             @Override
161             public <Q extends @NonNull Quantity<Q>> void valueChanged(MeterValue<Q> value) {
162                 ThingBuilder thingBuilder = editThing();
163
164                 String obis = value.getObisCode();
165
166                 String obisChannelString = SmartMeterBindingConstants.getObisChannelId(obis);
167                 Channel channel = thing.getChannel(obisChannelString);
168                 ChannelTypeUID channelTypeId = channelTypeProvider.getChannelTypeIdForObis(obis);
169
170                 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeId, null);
171                 if (channelType != null) {
172                     String itemType = channelType.getItemType();
173
174                     State state = getStateForObisValue(value, channel);
175                     if (channel == null) {
176                         logger.debug("Adding channel: {} with item type: {}", obisChannelString, itemType);
177
178                         // channel has not been created yet
179                         ChannelBuilder channelBuilder = ChannelBuilder
180                                 .create(new ChannelUID(thing.getUID(), obisChannelString), itemType)
181                                 .withType(channelTypeId);
182
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();
194
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());
201                         }
202                     }
203
204                     if (!channel.getProperties().containsKey(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS)) {
205                         addObisPropertyToChannel(obis, channel);
206                     }
207                     updateState(channel.getUID(), state);
208
209                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
210                 } else {
211                     logger.warn("No ChannelType found for OBIS {}", obis);
212                 }
213             }
214
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());
226             }
227
228             @Override
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());
236             }
237
238             @Override
239             public void errorOccurred(Throwable e) {
240                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
241             }
242         };
243         this.smlDevice.addValueChangeListener(valueChangeListener);
244
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));
248     }
249
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);
256                 if (value != null) {
257                     State state = getStateForObisValue(value, channel);
258                     updateState(channel.getUID(), state);
259                 }
260             }
261         }
262     }
263
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();
268         if (unit != null) {
269             valueString += " " + value.getUnit();
270         }
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()));
278             }
279         }
280         return state;
281     }
282
283     private <Q extends Quantity<Q>> State applyConformity(Channel channel, QuantityType<Q> currentState) {
284         try {
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);
288         }
289         return currentState;
290     }
291 }