]> git.basschouten.com Git - openhab-addons.git/blob
9289660a7cec81a702536f424e6eb1dcd5dd738c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.homematic.internal.handler;
14
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16 import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
17
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Objects;
27 import java.util.concurrent.Future;
28
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.homematic.internal.common.HomematicConfig;
31 import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
32 import org.openhab.binding.homematic.internal.converter.ConverterException;
33 import org.openhab.binding.homematic.internal.converter.ConverterFactory;
34 import org.openhab.binding.homematic.internal.converter.ConverterTypeException;
35 import org.openhab.binding.homematic.internal.converter.TypeConverter;
36 import org.openhab.binding.homematic.internal.misc.HomematicClientException;
37 import org.openhab.binding.homematic.internal.misc.HomematicConstants;
38 import org.openhab.binding.homematic.internal.model.HmChannel;
39 import org.openhab.binding.homematic.internal.model.HmDatapoint;
40 import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
41 import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
42 import org.openhab.binding.homematic.internal.model.HmDevice;
43 import org.openhab.binding.homematic.internal.model.HmParamsetType;
44 import org.openhab.binding.homematic.internal.type.HomematicChannelTypeProvider;
45 import org.openhab.binding.homematic.internal.type.HomematicTypeGeneratorImpl;
46 import org.openhab.binding.homematic.internal.type.MetadataUtils;
47 import org.openhab.binding.homematic.internal.type.UidUtils;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.config.core.validation.ConfigValidationException;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.StopMoveType;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.Channel;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.thing.binding.ThingHandler;
60 import org.openhab.core.thing.binding.builder.ChannelBuilder;
61 import org.openhab.core.thing.type.ChannelType;
62 import org.openhab.core.thing.type.ChannelTypeUID;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.RefreshType;
65 import org.openhab.core.types.State;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * The {@link HomematicThingHandler} is responsible for handling commands, which are sent to one of the channels.
71  *
72  * @author Gerhard Riegler - Initial contribution
73  */
74 public class HomematicThingHandler extends BaseThingHandler {
75     private final Logger logger = LoggerFactory.getLogger(HomematicThingHandler.class);
76     private final HomematicChannelTypeProvider channelTypeProvider;
77     private Future<?> initFuture;
78     private final Object initLock = new Object();
79     private volatile boolean deviceDeletionPending = false;
80
81     public HomematicThingHandler(Thing thing, HomematicChannelTypeProvider channelTypeProvider) {
82         super(thing);
83         this.channelTypeProvider = channelTypeProvider;
84     }
85
86     @Override
87     public void initialize() {
88         if (initFuture != null) {
89             return;
90         }
91
92         initFuture = scheduler.submit(() -> {
93             initFuture = null;
94             try {
95                 synchronized (initLock) {
96                     doInitializeInBackground();
97                 }
98             } catch (HomematicClientException ex) {
99                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
100             } catch (IOException ex) {
101                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
102             } catch (GatewayNotAvailableException ex) {
103                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ex.getMessage());
104             } catch (Exception ex) {
105                 logger.error("{}", ex.getMessage(), ex);
106                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, ex.getMessage());
107             }
108         });
109     }
110
111     private void doInitializeInBackground() throws GatewayNotAvailableException, HomematicClientException, IOException {
112         HomematicGateway gateway = getHomematicGateway();
113         HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
114         HmChannel channelZero = device.getChannel(0);
115         loadHomematicChannelValues(channelZero);
116         updateStatus(device);
117         logger.debug("Initializing thing '{}' from gateway '{}'", getThing().getUID(), gateway.getId());
118
119         // update properties
120         Map<String, String> properties = editProperties();
121         setProperty(properties, channelZero, PROPERTY_BATTERY_TYPE, VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE);
122         setProperty(properties, channelZero, Thing.PROPERTY_FIRMWARE_VERSION, VIRTUAL_DATAPOINT_NAME_FIRMWARE);
123         setProperty(properties, channelZero, Thing.PROPERTY_SERIAL_NUMBER, device.getAddress());
124         setProperty(properties, channelZero, PROPERTY_AES_KEY, DATAPOINT_NAME_AES_KEY);
125         updateProperties(properties);
126
127         // update data point list for reconfigurable channels
128         for (HmChannel channel : device.getChannels()) {
129             if (channel.isReconfigurable()) {
130                 loadHomematicChannelValues(channel);
131                 if (channel.checkForChannelFunctionChange()) {
132                     gateway.updateChannelValueDatapoints(channel);
133                 }
134             }
135         }
136
137         // update configurations
138         Configuration config = editConfiguration();
139         for (HmChannel channel : device.getChannels()) {
140             loadHomematicChannelValues(channel);
141             for (HmDatapoint dp : channel.getDatapoints()) {
142                 if (dp.getParamsetType() == HmParamsetType.MASTER) {
143                     config.put(MetadataUtils.getParameterName(dp), getValueForConfiguration(dp));
144                 }
145             }
146         }
147         updateConfiguration(config);
148
149         boolean channelsChanged = false;
150
151         // update thing channel list for reconfigurable channels (relies on the new value of the
152         // CHANNEL_FUNCTION datapoint fetched during configuration update)
153         List<Channel> thingChannels = new ArrayList<>(getThing().getChannels());
154
155         if (thingChannels.isEmpty()) {
156             for (HmChannel channel : device.getChannels()) {
157                 for (HmDatapoint dp : channel.getDatapoints()) {
158                     if (HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)
159                             || dp.getParamsetType() != HmParamsetType.VALUES) {
160                         continue;
161                     }
162                     ChannelUID channelUID = UidUtils.generateChannelUID(dp, getThing().getUID());
163                     if (containsChannel(thingChannels, channelUID)) {
164                         // Channel is already present
165                         continue;
166                     }
167
168                     ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dp);
169                     ChannelType channelType = channelTypeProvider.getInternalChannelType(channelTypeUID);
170                     if (channelType == null) {
171                         channelType = HomematicTypeGeneratorImpl.createChannelType(dp, channelTypeUID);
172                         channelTypeProvider.addChannelType(channelType);
173                     }
174
175                     Channel thingChannel = ChannelBuilder.create(channelUID, MetadataUtils.getItemType(dp))
176                             .withLabel(MetadataUtils.getLabel(dp))
177                             .withDescription(MetadataUtils.getDatapointDescription(dp)).withType(channelType.getUID())
178                             .build();
179                     thingChannels.add(thingChannel);
180
181                     logger.debug(
182                             "Updated value datapoints for channel {} of device '{}' (function {}), now has {} datapoints",
183                             channel, channel.getDevice().getAddress(), channel.getCurrentFunction(),
184                             channel.getDatapoints().size());
185                 }
186             }
187             channelsChanged = true;
188         }
189
190         if (updateDynamicChannelList(device, thingChannels)) {
191             channelsChanged = true;
192         }
193
194         if (channelsChanged) {
195             updateThing(editThing().withChannels(thingChannels).build());
196         }
197
198         thingChannels.forEach(channel -> {
199             if (isLinked(channel.getUID())) {
200                 channelLinked(channel.getUID());
201             }
202         });
203     }
204
205     /**
206      * Update the given thing channel list to reflect the device's current datapoint set
207      *
208      * @return true if the list was modified, false if it was not modified
209      */
210     private boolean updateDynamicChannelList(HmDevice device, List<Channel> thingChannels) {
211         boolean changed = false;
212         for (HmChannel channel : device.getChannels()) {
213             if (!channel.isReconfigurable()) {
214                 continue;
215             }
216             final String expectedFunction = channel
217                     .getDatapoint(HmParamsetType.MASTER, HomematicConstants.DATAPOINT_NAME_CHANNEL_FUNCTION)
218                     .getOptionValue();
219             final String propertyName = String.format(PROPERTY_DYNAMIC_FUNCTION_FORMAT, channel.getNumber());
220
221             // remove thing channels that were configured for a different function
222             Iterator<Channel> channelIter = thingChannels.iterator();
223             while (channelIter.hasNext()) {
224                 Map<String, String> properties = channelIter.next().getProperties();
225                 String function = properties.get(propertyName);
226                 if (function != null && !function.equals(expectedFunction)) {
227                     channelIter.remove();
228                     changed = true;
229                 }
230             }
231             for (HmDatapoint dp : channel.getDatapoints()) {
232                 if (HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)
233                         || dp.getParamsetType() != HmParamsetType.VALUES) {
234                     continue;
235                 }
236                 ChannelUID channelUID = UidUtils.generateChannelUID(dp, getThing().getUID());
237                 if (containsChannel(thingChannels, channelUID)) {
238                     // Channel is already present -> channel configuration likely hasn't changed
239                     continue;
240                 }
241
242                 Map<String, String> channelProps = new HashMap<>();
243                 channelProps.put(propertyName, expectedFunction);
244
245                 ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dp);
246                 ChannelType channelType = channelTypeProvider.getInternalChannelType(channelTypeUID);
247                 if (channelType == null) {
248                     channelType = HomematicTypeGeneratorImpl.createChannelType(dp, channelTypeUID);
249                     channelTypeProvider.addChannelType(channelType);
250                 }
251
252                 Channel thingChannel = ChannelBuilder.create(channelUID, MetadataUtils.getItemType(dp))
253                         .withProperties(channelProps).withLabel(MetadataUtils.getLabel(dp))
254                         .withDescription(MetadataUtils.getDatapointDescription(dp)).withType(channelType.getUID())
255                         .build();
256                 thingChannels.add(thingChannel);
257                 changed = true;
258             }
259         }
260
261         return changed;
262     }
263
264     /**
265      * Checks whether the given list includes a channel with the given UID
266      */
267     private static boolean containsChannel(List<Channel> channels, ChannelUID channelUID) {
268         for (Channel channel : channels) {
269             ChannelUID uid = channel.getUID();
270             if (Objects.equals(channelUID.getGroupId(), uid.getGroupId())
271                     && Objects.equals(channelUID.getId(), uid.getId())) {
272                 return true;
273             }
274         }
275         return false;
276     }
277
278     /**
279      * Sets a thing property with a datapoint value.
280      */
281     private void setProperty(Map<String, String> properties, HmChannel channelZero, String propertyName,
282             String datapointName) {
283         HmDatapoint dp = channelZero
284                 .getDatapoint(new HmDatapointInfo(HmParamsetType.VALUES, channelZero, datapointName));
285         if (dp != null) {
286             properties.put(propertyName, Objects.toString(dp.getValue(), ""));
287         }
288     }
289
290     @Override
291     public void channelLinked(ChannelUID channelUID) {
292         handleRefresh(channelUID);
293     }
294
295     /**
296      * Updates the state of the given channel.
297      */
298     protected void handleRefresh(ChannelUID channelUID) {
299         try {
300             if (thing.getStatus() == ThingStatus.ONLINE) {
301                 logger.debug("Updating channel '{}' from thing id '{}'", channelUID, getThing().getUID().getId());
302                 updateChannelState(channelUID);
303             }
304         } catch (Exception ex) {
305             logger.warn("{}", ex.getMessage());
306         }
307     }
308
309     @Override
310     public void handleCommand(ChannelUID channelUID, Command command) {
311         logger.debug("Received command '{}' for channel '{}'", command, channelUID);
312         HmDatapoint dp = null;
313         try {
314             HomematicGateway gateway = getHomematicGateway();
315             HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
316             if (RefreshType.REFRESH == command) {
317                 logger.debug("Refreshing {}", dpInfo);
318                 dpInfo = new HmDatapointInfo(dpInfo.getAddress(), HmParamsetType.VALUES, 0,
319                         VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY);
320                 dp = gateway.getDatapoint(dpInfo);
321                 sendDatapoint(dp, new HmDatapointConfig(), Boolean.TRUE);
322             } else {
323                 Channel channel = getThing().getChannel(channelUID.getId());
324                 if (channel == null) {
325                     logger.warn("Channel '{}' not found in thing '{}' on gateway '{}'", channelUID, getThing().getUID(),
326                             gateway.getId());
327                 } else {
328                     if (StopMoveType.STOP == command && DATAPOINT_NAME_LEVEL.equals(dpInfo.getName())) {
329                         // special case with stop type (rollershutter)
330                         dpInfo.setName(DATAPOINT_NAME_STOP);
331                         HmDatapoint stopDp = gateway.getDatapoint(dpInfo);
332                         ChannelUID stopChannelUID = UidUtils.generateChannelUID(stopDp, getThing().getUID());
333                         handleCommand(stopChannelUID, OnOffType.ON);
334                     } else {
335                         dp = gateway.getDatapoint(dpInfo);
336                         TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
337                         Object newValue = converter.convertToBinding(command, dp);
338                         HmDatapointConfig config = getChannelConfig(channel, dp);
339                         sendDatapoint(dp, config, newValue);
340                     }
341                 }
342             }
343         } catch (HomematicClientException | GatewayNotAvailableException ex) {
344             logger.warn("{}", ex.getMessage());
345         } catch (IOException ex) {
346             if (dp != null && dp.getChannel().getDevice().isOffline()) {
347                 logger.warn("Device '{}' is OFFLINE, can't send command '{}' for channel '{}'",
348                         dp.getChannel().getDevice().getAddress(), command, channelUID);
349                 logger.trace("{}", ex.getMessage(), ex);
350             } else {
351                 logger.error("{}", ex.getMessage(), ex);
352             }
353         } catch (ConverterTypeException ex) {
354             logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
355         } catch (Exception ex) {
356             logger.error("{}", ex.getMessage(), ex);
357         }
358     }
359
360     private void sendDatapoint(HmDatapoint dp, HmDatapointConfig config, Object newValue)
361             throws IOException, HomematicClientException, GatewayNotAvailableException {
362         String rxMode = getRxModeForDatapointTransmission(dp.getName(), dp.getValue(), newValue);
363         getHomematicGateway().sendDatapoint(dp, config, newValue, rxMode);
364     }
365
366     /**
367      * Returns the rx mode that shall be used for transmitting a new value of a datapoint to the device. The
368      * HomematicThingHandler always uses the default rx mode; custom thing handlers can override this method to
369      * adjust the rx mode.
370      *
371      * @param datapointName The datapoint that will be updated on the device
372      * @param currentValue The current value of the datapoint
373      * @param newValue The value that will be sent to the device
374      * @return The rxMode ({@link org.openhab.binding.homematic.internal.HomematicBindingConstants#RX_BURST_MODE
375      *         "BURST"} for burst mode,
376      *         {@link org.openhab.binding.homematic.internal.HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for
377      *         wakeup mode, or null for the default mode)
378      */
379     protected String getRxModeForDatapointTransmission(String datapointName, Object currentValue, Object newValue) {
380         return null;
381     }
382
383     /**
384      * Evaluates the channel and datapoint for this channelUID and updates the state of the channel.
385      */
386     private void updateChannelState(ChannelUID channelUID)
387             throws GatewayNotAvailableException, HomematicClientException, IOException, ConverterException {
388         HomematicGateway gateway = getHomematicGateway();
389         HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
390         HmDatapoint dp = gateway.getDatapoint(dpInfo);
391         Channel channel = getThing().getChannel(channelUID.getId());
392         updateChannelState(dp, channel);
393     }
394
395     /**
396      * Sets the configuration or evaluates the channel for this datapoint and updates the state of the channel.
397      */
398     protected void updateDatapointState(HmDatapoint dp) {
399         try {
400             updateStatus(dp.getChannel().getDevice());
401
402             if (dp.getParamsetType() == HmParamsetType.MASTER) {
403                 // update configuration
404                 Configuration config = editConfiguration();
405                 config.put(MetadataUtils.getParameterName(dp), getValueForConfiguration(dp));
406                 updateConfiguration(config);
407             } else if (!HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)) {
408                 // update channel
409                 ChannelUID channelUID = UidUtils.generateChannelUID(dp, thing.getUID());
410                 Channel channel = thing.getChannel(channelUID.getId());
411                 if (channel != null) {
412                     updateChannelState(dp, channel);
413                 } else {
414                     logger.warn("Channel not found for datapoint '{}'", new HmDatapointInfo(dp));
415                 }
416             }
417         } catch (GatewayNotAvailableException ex) {
418             // ignore
419         } catch (Exception ex) {
420             logger.error("{}", ex.getMessage(), ex);
421         }
422     }
423
424     private @Nullable Object getValueForConfiguration(HmDatapoint dp) {
425         if (dp.getValue() == null) {
426             return null;
427         }
428         if (dp.isEnumType()) {
429             return dp.getOptionValue();
430         }
431         if (dp.isNumberType()) {
432             // For number datapoints that are only used depending on the value of other datapoints,
433             // the CCU may return invalid (out of range) values if the datapoint currently is not in use.
434             // Make sure to not invalidate the whole configuration by returning the datapoint's default
435             // value in that case.
436             final boolean minValid, maxValid;
437             if (dp.isFloatType()) {
438                 Double numValue = dp.getDoubleValue();
439                 minValid = dp.getMinValue() == null || numValue >= dp.getMinValue().doubleValue();
440                 maxValid = dp.getMaxValue() == null || numValue <= dp.getMaxValue().doubleValue();
441             } else {
442                 Integer numValue = dp.getIntegerValue();
443                 minValid = dp.getMinValue() == null || numValue >= dp.getMinValue().intValue();
444                 maxValid = dp.getMaxValue() == null || numValue <= dp.getMaxValue().intValue();
445             }
446             if (minValid && maxValid) {
447                 return dp.getValue();
448             }
449             logger.warn("Value for datapoint {} is outside of valid range, using default value for config.", dp);
450             return dp.getDefaultValue();
451         }
452         return dp.getValue();
453     }
454
455     /**
456      * Converts the value of the datapoint to a State, updates the channel and also sets the thing status if necessary.
457      */
458     private void updateChannelState(final HmDatapoint dp, Channel channel)
459             throws IOException, GatewayNotAvailableException, ConverterException {
460         if (dp.isTrigger()) {
461             final Object value = dp.getValue();
462             if (value != null && !value.equals(dp.getPreviousValue())) {
463                 triggerChannel(channel.getUID(), value.toString());
464             }
465         } else if (isLinked(channel)) {
466             loadHomematicChannelValues(dp.getChannel());
467
468             TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
469             State state = converter.convertFromBinding(dp);
470             if (state != null) {
471                 updateState(channel.getUID(), state);
472             } else {
473                 logger.debug("Failed to get converted state from datapoint '{}'", dp.getName());
474             }
475         }
476     }
477
478     /**
479      * Loads all values for the given Homematic channel if it is not initialized.
480      */
481     private void loadHomematicChannelValues(HmChannel hmChannel) throws GatewayNotAvailableException, IOException {
482         if (!hmChannel.isInitialized()) {
483             synchronized (this) {
484                 if (!hmChannel.isInitialized()) {
485                     try {
486                         getHomematicGateway().loadChannelValues(hmChannel);
487                     } catch (IOException ex) {
488                         if (hmChannel.getDevice().isOffline()) {
489                             logger.warn("Device '{}' is OFFLINE, can't update channel '{}'",
490                                     hmChannel.getDevice().getAddress(), hmChannel.getNumber());
491                         } else {
492                             throw ex;
493                         }
494                     }
495                 }
496             }
497         }
498     }
499
500     /**
501      * Updates the thing status based on device status.
502      */
503     private void updateStatus(HmDevice device) throws GatewayNotAvailableException, IOException {
504         loadHomematicChannelValues(device.getChannel(0));
505
506         ThingStatus oldStatus = thing.getStatus();
507         if (oldStatus == ThingStatus.UNINITIALIZED) {
508             return;
509         }
510         ThingStatus newStatus = ThingStatus.ONLINE;
511         ThingStatusDetail newDetail = ThingStatusDetail.NONE;
512
513         if ((getBridge() != null) && (getBridge().getStatus() == ThingStatus.OFFLINE)) {
514             newStatus = ThingStatus.OFFLINE;
515             newDetail = ThingStatusDetail.BRIDGE_OFFLINE;
516         } else if (device.isFirmwareUpdating()) {
517             newStatus = ThingStatus.OFFLINE;
518             newDetail = ThingStatusDetail.FIRMWARE_UPDATING;
519         } else if (device.isUnreach()) {
520             newStatus = ThingStatus.OFFLINE;
521             newDetail = ThingStatusDetail.COMMUNICATION_ERROR;
522         } else if (device.isConfigPending() || device.isUpdatePending()) {
523             newDetail = ThingStatusDetail.CONFIGURATION_PENDING;
524         }
525
526         if (thing.getStatus() != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
527             updateStatus(newStatus, newDetail);
528         }
529         if (oldStatus == ThingStatus.OFFLINE && newStatus == ThingStatus.ONLINE) {
530             initialize();
531         }
532     }
533
534     /**
535      * Returns true, if the channel is linked at least to one item.
536      */
537     private boolean isLinked(Channel channel) {
538         return channel != null && super.isLinked(channel.getUID().getId());
539     }
540
541     /**
542      * Returns the channel config for the given datapoint.
543      */
544     protected HmDatapointConfig getChannelConfig(HmDatapoint dp) {
545         ChannelUID channelUid = UidUtils.generateChannelUID(dp, getThing().getUID());
546         Channel channel = getThing().getChannel(channelUid.getId());
547         return channel != null ? getChannelConfig(channel, dp) : new HmDatapointConfig();
548     }
549
550     /**
551      * Returns the config for a channel.
552      */
553     private HmDatapointConfig getChannelConfig(Channel channel, HmDatapoint dp) {
554         return channel.getConfiguration().as(HmDatapointConfig.class);
555     }
556
557     /**
558      * Returns the Homematic gateway if the bridge is available.
559      */
560     private HomematicGateway getHomematicGateway() throws GatewayNotAvailableException {
561         final Bridge bridge = getBridge();
562         if (bridge != null) {
563             HomematicBridgeHandler bridgeHandler = (HomematicBridgeHandler) bridge.getHandler();
564             if (bridgeHandler != null && bridgeHandler.getGateway() != null) {
565                 return bridgeHandler.getGateway();
566             }
567         }
568
569         throw new GatewayNotAvailableException("HomematicGateway not yet available!");
570     }
571
572     @Override
573     public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
574             throws ConfigValidationException {
575         super.handleConfigurationUpdate(configurationParameters);
576
577         try {
578             HomematicGateway gateway = getHomematicGateway();
579             HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
580
581             for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
582                 String key = configurationParameter.getKey();
583                 Object newValue = configurationParameter.getValue();
584
585                 if (key.startsWith("HMP_")) {
586                     key = key.substring(4);
587                     int sepPos = key.indexOf("_");
588                     Integer channelNumber = Integer.valueOf(key.substring(0, sepPos));
589                     String dpName = key.substring(sepPos + 1);
590
591                     HmDatapointInfo dpInfo = new HmDatapointInfo(device.getAddress(), HmParamsetType.MASTER,
592                             channelNumber, dpName);
593                     HmDatapoint dp = device.getChannel(channelNumber).getDatapoint(dpInfo);
594
595                     if (dp != null) {
596                         try {
597                             if (newValue != null) {
598                                 if (newValue instanceof BigDecimal decimal) {
599                                     if (dp.isIntegerType()) {
600                                         newValue = decimal.intValue();
601                                     } else if (dp.isFloatType()) {
602                                         newValue = decimal.doubleValue();
603                                     }
604                                 } else if (newValue instanceof String string && dp.isEnumType()) {
605                                     newValue = dp.getOptionIndex(string);
606                                 }
607                                 if (!Objects.equals(dp.getValue(), newValue)) {
608                                     sendDatapoint(dp, new HmDatapointConfig(), newValue);
609                                 }
610                             }
611                         } catch (IOException ex) {
612                             logger.error("Error setting thing property {}: {}", dpInfo, ex.getMessage());
613                         }
614                     } else {
615                         logger.error("Can't find datapoint for thing property {}", dpInfo);
616                     }
617                 }
618             }
619             gateway.triggerDeviceValuesReload(device);
620         } catch (HomematicClientException | GatewayNotAvailableException ex) {
621             logger.error("Error setting thing properties: {}", ex.getMessage(), ex);
622         }
623     }
624
625     @SuppressWarnings("null")
626     @Override
627     public synchronized void handleRemoval() {
628         final Bridge bridge;
629         final ThingHandler handler;
630
631         if ((bridge = getBridge()) == null || (handler = bridge.getHandler()) == null) {
632             super.handleRemoval();
633             return;
634         }
635
636         final HomematicConfig config = bridge.getConfiguration().as(HomematicConfig.class);
637         final boolean factoryResetOnDeletion = config.isFactoryResetOnDeletion();
638         final boolean unpairOnDeletion = factoryResetOnDeletion || config.isUnpairOnDeletion();
639
640         if (unpairOnDeletion) {
641             deviceDeletionPending = true;
642             ((HomematicBridgeHandler) handler).deleteFromGateway(UidUtils.getHomematicAddress(thing),
643                     factoryResetOnDeletion, false, true);
644         } else {
645             super.handleRemoval();
646         }
647     }
648
649     /**
650      * Called by the bridgeHandler when this device has been removed from the gateway.
651      */
652     public synchronized void deviceRemoved() {
653         deviceDeletionPending = false;
654         if (getThing().getStatus() == ThingStatus.REMOVING) {
655             // thing removal was initiated
656             updateStatus(ThingStatus.REMOVED);
657         } else {
658             // device removal was initiated on homematic side, thing is not removed
659             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
660         }
661     }
662
663     /**
664      * Called by the bridgeHandler when the device for this thing has been added to the gateway.
665      * This is used to reconnect a device that was previously unpaired.
666      *
667      * @param device The device that has been added to the gateway
668      */
669     public void deviceLoaded(HmDevice device) {
670         try {
671             updateStatus(device);
672         } catch (GatewayNotAvailableException ex) {
673             // ignore
674         } catch (IOException ex) {
675             logger.warn("Could not reinitialize the device '{}': {}", device.getAddress(), ex.getMessage(), ex);
676         }
677     }
678
679     /**
680      * Returns whether the device deletion is pending.
681      *
682      * @return true, if the deletion of this device on its gateway has been triggered but has not yet completed
683      */
684     public synchronized boolean isDeletionPending() {
685         return deviceDeletionPending;
686     }
687 }