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