]> git.basschouten.com Git - openhab-addons.git/blob
5fdfb36eb1eca5f9748b77fee2f3356de0ac6ab3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.function.Supplier;
24
25 import javax.measure.Quantity;
26 import javax.measure.Unit;
27
28 import org.apache.commons.lang.StringUtils;
29 import org.eclipse.jdt.annotation.DefaultLocation;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.smartmeter.SmartMeterBindingConstants;
34 import org.openhab.binding.smartmeter.SmartMeterConfiguration;
35 import org.openhab.binding.smartmeter.internal.conformity.Conformity;
36 import org.openhab.binding.smartmeter.internal.helper.Baudrate;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.io.transport.serial.SerialPortManager;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.builder.ChannelBuilder;
48 import org.openhab.core.thing.binding.builder.ThingBuilder;
49 import org.openhab.core.thing.type.ChannelType;
50 import org.openhab.core.thing.type.ChannelTypeUID;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.TypeParser;
55 import org.openhab.core.util.HexUtils;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 import io.reactivex.disposables.Disposable;
60
61 /**
62  * The {@link SmartMeterHandler} is responsible for handling commands, which are
63  * sent to one of the channels.
64  *
65  * @author Matthias Steigenberger - Initial contribution
66  */
67 @NonNullByDefault({ DefaultLocation.ARRAY_CONTENTS, DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE,
68         DefaultLocation.TYPE_ARGUMENT })
69 public class SmartMeterHandler extends BaseThingHandler {
70
71     private static final long DEFAULT_TIMEOUT = 30000;
72     private static final int DEFAULT_REFRESH_PERIOD = 30;
73     private Logger logger = LoggerFactory.getLogger(SmartMeterHandler.class);
74     private MeterDevice<?> smlDevice;
75     private Disposable valueReader;
76     private Conformity conformity;
77     private MeterValueListener valueChangeListener;
78     private SmartMeterChannelTypeProvider channelTypeProvider;
79     private @NonNull Supplier<SerialPortManager> serialPortManagerSupplier;
80
81     public SmartMeterHandler(Thing thing, SmartMeterChannelTypeProvider channelProvider,
82             Supplier<SerialPortManager> serialPortManagerSupplier) {
83         super(thing);
84         Objects.requireNonNull(channelProvider, "SmartMeterChannelTypeProvider must not be null");
85         this.channelTypeProvider = channelProvider;
86         this.serialPortManagerSupplier = serialPortManagerSupplier;
87     }
88
89     @Override
90     public void initialize() {
91         logger.debug("Initializing Smartmeter handler.");
92         cancelRead();
93
94         SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
95         logger.debug("config port = {}", config.port);
96
97         boolean validConfig = true;
98         String errorMsg = null;
99
100         if (StringUtils.trimToNull(config.port) == null) {
101             errorMsg = "Parameter 'port' is mandatory and must be configured";
102             validConfig = false;
103         }
104
105         if (validConfig) {
106             byte[] pullSequence = config.initMessage == null ? null
107                     : HexUtils.hexToBytes(StringUtils.deleteWhitespace(config.initMessage));
108             int baudrate = config.baudrate == null ? Baudrate.AUTO.getBaudrate()
109                     : Baudrate.fromString(config.baudrate).getBaudrate();
110             this.conformity = config.conformity == null ? Conformity.NONE : Conformity.valueOf(config.conformity);
111             this.smlDevice = MeterDeviceFactory.getDevice(serialPortManagerSupplier, config.mode,
112                     this.thing.getUID().getAsString(), config.port, pullSequence, baudrate, config.baudrateChangeDelay);
113             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING,
114                     "Waiting for messages from device");
115
116             smlDevice.addValueChangeListener(channelTypeProvider);
117
118             updateOBISValue();
119         } else {
120             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
121         }
122     }
123
124     @Override
125     public void dispose() {
126         super.dispose();
127         cancelRead();
128         if (this.valueChangeListener != null) {
129             this.smlDevice.removeValueChangeListener(valueChangeListener);
130         }
131         if (this.channelTypeProvider != null) {
132             this.smlDevice.removeValueChangeListener(channelTypeProvider);
133         }
134     }
135
136     private void cancelRead() {
137         if (this.valueReader != null) {
138             this.valueReader.dispose();
139         }
140     }
141
142     @Override
143     public void handleCommand(ChannelUID channelUID, Command command) {
144         if (command instanceof RefreshType) {
145             updateOBISChannel(channelUID);
146         } else {
147             logger.debug("The SML reader binding is read-only and can not handle command {}", command);
148         }
149     }
150
151     /**
152      * Get new data the device
153      *
154      */
155     private void updateOBISValue() {
156         cancelRead();
157
158         valueChangeListener = new MeterValueListener() {
159             @Override
160             public <Q extends @NonNull Quantity<Q>> void valueChanged(MeterValue<Q> value) {
161                 ThingBuilder thingBuilder = editThing();
162
163                 String obis = value.getObisCode();
164
165                 String obisChannelString = SmartMeterBindingConstants.getObisChannelId(obis);
166                 Channel channel = thing.getChannel(obisChannelString);
167                 ChannelTypeUID channelTypeId = channelTypeProvider.getChannelTypeIdForObis(obis);
168
169                 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeId, null);
170                 if (channelType != null) {
171                     String itemType = channelType.getItemType();
172
173                     State state = getStateForObisValue(value, channel);
174                     if (channel == null) {
175                         logger.debug("Adding channel: {} with item type: {}", obisChannelString, itemType);
176
177                         // channel has not been created yet
178                         ChannelBuilder channelBuilder = ChannelBuilder
179                                 .create(new ChannelUID(thing.getUID(), obisChannelString), itemType)
180                                 .withType(channelTypeId);
181
182                         Configuration configuration = new Configuration();
183                         configuration.put(SmartMeterBindingConstants.CONFIGURATION_CONVERSION, 1);
184                         channelBuilder.withConfiguration(configuration);
185                         channelBuilder.withLabel(obis);
186                         Map<String, String> channelProps = new HashMap<>();
187                         channelProps.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
188                         channelBuilder.withProperties(channelProps);
189                         channelBuilder.withDescription(
190                                 MessageFormat.format("Value for OBIS code: {0} with Unit: {1}", obis, value.getUnit()));
191                         channel = channelBuilder.build();
192                         ChannelUID channelId = channel.getUID();
193
194                         // add all valid channels to the thing builder
195                         List<Channel> channels = new ArrayList<>(getThing().getChannels());
196                         if (channels.stream().filter((element) -> element.getUID().equals(channelId)).count() == 0) {
197                             channels.add(channel);
198                             thingBuilder.withChannels(channels);
199                             updateThing(thingBuilder.build());
200                         }
201                     }
202
203                     if (!channel.getProperties().containsKey(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS)) {
204                         addObisPropertyToChannel(obis, channel);
205                     }
206                     if (state != null) {
207                         updateState(channel.getUID(), state);
208                     }
209
210                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
211                 } else {
212                     logger.warn("No ChannelType found for OBIS {}", obis);
213                 }
214             }
215
216             private void addObisPropertyToChannel(String obis, Channel channel) {
217                 String description = channel.getDescription();
218                 String label = channel.getLabel();
219                 ChannelBuilder newChannel = ChannelBuilder.create(channel.getUID(), channel.getAcceptedItemType())
220                         .withDefaultTags(channel.getDefaultTags()).withConfiguration(channel.getConfiguration())
221                         .withDescription(description == null ? "" : description).withKind(channel.getKind())
222                         .withLabel(label == null ? "" : label).withType(channel.getChannelTypeUID());
223                 Map<String, String> properties = new HashMap<>(channel.getProperties());
224                 properties.put(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS, obis);
225                 newChannel.withProperties(properties);
226                 updateThing(editThing().withoutChannel(channel.getUID()).withChannel(newChannel.build()).build());
227             }
228
229             @Override
230             public <Q extends @NonNull Quantity<Q>> void valueRemoved(MeterValue<Q> value) {
231                 // channels that are not available are removed
232                 String obisChannelId = SmartMeterBindingConstants.getObisChannelId(value.getObisCode());
233                 logger.debug("Removing channel: {}", obisChannelId);
234                 ThingBuilder thingBuilder = editThing();
235                 thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), obisChannelId));
236                 updateThing(thingBuilder.build());
237             }
238
239             @Override
240             public void errorOccurred(Throwable e) {
241                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
242             }
243         };
244         this.smlDevice.addValueChangeListener(valueChangeListener);
245
246         SmartMeterConfiguration config = getConfigAs(SmartMeterConfiguration.class);
247         int delay = config.refresh != null ? config.refresh : DEFAULT_REFRESH_PERIOD;
248         valueReader = this.smlDevice.readValues(DEFAULT_TIMEOUT, this.scheduler, Duration.ofSeconds(delay));
249     }
250
251     private void updateOBISChannel(ChannelUID channelId) {
252         if (isLinked(channelId.getId())) {
253             Channel channel = this.thing.getChannel(channelId.getId());
254             if (channel != null) {
255                 String obis = channel.getProperties().get(SmartMeterBindingConstants.CHANNEL_PROPERTY_OBIS);
256                 if (obis != null) {
257                     MeterValue<?> value = this.smlDevice.getMeterValue(obis);
258                     if (value != null) {
259                         State state = getStateForObisValue(value, channel);
260                         if (state != null) {
261                             updateState(channel.getUID(), state);
262                         }
263                     }
264                 }
265             }
266         }
267     }
268
269     @SuppressWarnings("unchecked")
270     private @Nullable <Q extends Quantity<Q>> State getStateForObisValue(MeterValue<?> value,
271             @Nullable Channel channel) {
272         Unit<?> unit = value.getUnit();
273         String valueString = value.getValue();
274         if (unit != null) {
275             valueString += " " + value.getUnit();
276         }
277         State state = TypeParser.parseState(List.of(QuantityType.class, StringType.class), valueString);
278         if (channel != null && state instanceof QuantityType) {
279             state = applyConformity(channel, (QuantityType<Q>) state);
280             Number conversionRatio = (Number) channel.getConfiguration()
281                     .get(SmartMeterBindingConstants.CONFIGURATION_CONVERSION);
282             if (conversionRatio != null) {
283                 state = ((QuantityType<?>) state).divide(BigDecimal.valueOf(conversionRatio.doubleValue()));
284             }
285         }
286         return state;
287     }
288
289     private <Q extends Quantity<Q>> State applyConformity(Channel channel, QuantityType<Q> currentState) {
290         try {
291             return this.conformity.apply(channel, currentState, getThing(), this.smlDevice);
292         } catch (Exception e) {
293             logger.warn("Failed to apply negation for channel: {}", channel.getUID(), e);
294         }
295         return currentState;
296     }
297 }