]> git.basschouten.com Git - openhab-addons.git/blob
89c7571b81e1b4ffd276e48abb91d453b5010052
[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.bondhome.internal.handler;
14
15 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
16
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Set;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bondhome.internal.BondException;
29 import org.openhab.binding.bondhome.internal.api.BondDevice;
30 import org.openhab.binding.bondhome.internal.api.BondDeviceAction;
31 import org.openhab.binding.bondhome.internal.api.BondDeviceProperties;
32 import org.openhab.binding.bondhome.internal.api.BondDeviceState;
33 import org.openhab.binding.bondhome.internal.api.BondDeviceType;
34 import org.openhab.binding.bondhome.internal.api.BondHttpApi;
35 import org.openhab.binding.bondhome.internal.config.BondDeviceConfiguration;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.IncreaseDecreaseType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StopMoveType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.library.types.UpDownType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.thing.binding.BaseThingHandler;
52 import org.openhab.core.thing.binding.builder.ThingBuilder;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link BondDeviceHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Sara Geleskie Damiano - Initial contribution
63  * @author Cody Cutrer - Significant rework on channels to more closely match openHAB's model.
64  */
65 @NonNullByDefault
66 public class BondDeviceHandler extends BaseThingHandler {
67     private final Logger logger = LoggerFactory.getLogger(BondDeviceHandler.class);
68
69     private @NonNullByDefault({}) BondDeviceConfiguration config;
70     private @Nullable BondHttpApi api;
71
72     private @Nullable BondDevice deviceInfo;
73     private @Nullable BondDeviceProperties deviceProperties;
74     private @Nullable BondDeviceState deviceState;
75
76     private @Nullable ScheduledFuture<?> pollingJob;
77
78     private volatile boolean disposed;
79     private volatile boolean fullyInitialized;
80
81     public BondDeviceHandler(Thing thing) {
82         super(thing);
83         disposed = true;
84         fullyInitialized = false;
85     }
86
87     @Override
88     public void handleCommand(ChannelUID channelUID, Command command) {
89         if (hasConfigurationError() || !fullyInitialized) {
90             logger.trace(
91                     "Bond device handler for {} received command {} on channel {} but is not yet prepared to handle it.",
92                     config.deviceId, command, channelUID);
93             return;
94         }
95         String deviceId = Objects.requireNonNull(config.deviceId);
96
97         logger.trace("Bond device handler for {} received command {} on channel {}", config.deviceId, command,
98                 channelUID);
99         final BondHttpApi api = this.api;
100         if (api == null) {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.no-api");
102             // Re-attempt initialization
103             scheduler.schedule(() -> {
104                 logger.trace("Re-attempting initialization");
105                 initialize();
106             }, 30, TimeUnit.SECONDS);
107             return;
108         }
109
110         if (command instanceof RefreshType) {
111             logger.trace("Executing refresh command");
112             try {
113                 deviceState = api.getDeviceState(deviceId);
114                 updateChannelsFromState(deviceState);
115             } catch (BondException e) {
116                 if (!e.wasBridgeSetOffline()) {
117                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
118                 }
119             }
120             return;
121         }
122
123         BondDeviceAction action = null;
124         @Nullable
125         Integer value = null;
126         final BondDevice devInfo = Objects.requireNonNull(this.deviceInfo);
127         switch (channelUID.getId()) {
128             case CHANNEL_POWER:
129                 logger.trace("Power state command");
130                 api.executeDeviceAction(deviceId,
131                         command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF, null);
132                 break;
133
134             case CHANNEL_COMMAND:
135                 logger.trace("{} command", command.toString());
136                 try {
137                     action = BondDeviceAction.valueOf(command.toString());
138                 } catch (IllegalArgumentException e) {
139                     logger.warn("Received unknown command {}.", command);
140                     break;
141                 }
142
143                 if (devInfo.actions.contains(action)) {
144                     api.executeDeviceAction(deviceId, action, null);
145                 } else {
146                     logger.warn("Device {} does not support command {}.", config.deviceId, command);
147                 }
148                 break;
149
150             case CHANNEL_FAN_POWER:
151                 logger.trace("Fan power state command");
152                 api.executeDeviceAction(deviceId,
153                         command == OnOffType.ON ? BondDeviceAction.TURN_FP_FAN_ON : BondDeviceAction.TURN_FP_FAN_OFF,
154                         null);
155                 break;
156
157             case CHANNEL_FAN_SPEED:
158                 logger.trace("Fan speed command");
159                 if (command instanceof PercentType) {
160                     if (devInfo.actions.contains(BondDeviceAction.SET_FP_FAN)) {
161                         value = ((PercentType) command).intValue();
162                         if (value == 0) {
163                             action = BondDeviceAction.TURN_FP_FAN_OFF;
164                             value = null;
165                         } else {
166                             action = BondDeviceAction.SET_FP_FAN;
167                         }
168                     } else {
169                         BondDeviceProperties devProperties = this.deviceProperties;
170                         if (devProperties != null) {
171                             int maxSpeed = devProperties.maxSpeed;
172                             value = (int) Math.ceil(((PercentType) command).intValue() * maxSpeed / 100);
173                         } else {
174                             value = 1;
175                         }
176                         if (value == 0) {
177                             action = BondDeviceAction.TURN_OFF;
178                             value = null;
179                         } else {
180                             action = BondDeviceAction.SET_SPEED;
181                         }
182                     }
183                     logger.trace("Fan speed command with speed set as {}", value);
184                     api.executeDeviceAction(deviceId, action, value);
185                 } else if (command instanceof IncreaseDecreaseType) {
186                     logger.trace("Fan increase/decrease speed command");
187                     api.executeDeviceAction(deviceId,
188                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
189                                     ? BondDeviceAction.INCREASE_SPEED
190                                     : BondDeviceAction.DECREASE_SPEED),
191                             null);
192                 } else if (command instanceof OnOffType) {
193                     logger.trace("Fan speed command {}", command);
194                     if (devInfo.actions.contains(BondDeviceAction.TURN_FP_FAN_ON)) {
195                         action = command == OnOffType.ON ? BondDeviceAction.TURN_FP_FAN_ON
196                                 : BondDeviceAction.TURN_FP_FAN_OFF;
197                     } else if (devInfo.actions.contains(BondDeviceAction.TURN_ON)) {
198                         action = command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF;
199                     }
200                     if (action != null) {
201                         api.executeDeviceAction(deviceId, action, null);
202                     }
203                 } else {
204                     logger.info("Unsupported command on fan speed channel");
205                 }
206                 break;
207
208             case CHANNEL_RAW_FAN_SPEED:
209                 if (command instanceof DecimalType) {
210                     value = ((DecimalType) command).intValue();
211                     BondDeviceProperties devProperties = this.deviceProperties;
212                     if (devProperties != null) {
213                         if (value < 1) {
214                             // Interpret any 0 or less value as a request to turn off
215                             action = BondDeviceAction.TURN_OFF;
216                             value = null;
217                         } else {
218                             action = BondDeviceAction.SET_SPEED;
219                             value = Math.min(value, devProperties.maxSpeed);
220                         }
221                         logger.trace("Fan raw speed command with speed set as {}, action as {}", value, action);
222                         api.executeDeviceAction(deviceId, action, value);
223                     }
224                 } else {
225                     logger.info("Unsupported command on raw fan speed channel");
226                 }
227                 break;
228
229             case CHANNEL_FAN_BREEZE_STATE:
230                 logger.trace("Fan enable/disable breeze command");
231                 api.executeDeviceAction(deviceId,
232                         command == OnOffType.ON ? BondDeviceAction.BREEZE_ON : BondDeviceAction.BREEZE_OFF, null);
233                 break;
234
235             case CHANNEL_FAN_BREEZE_MEAN:
236                 // TODO(SRGDamia1): write array command fxn
237                 logger.trace("Support for fan breeze settings not yet available");
238                 break;
239
240             case CHANNEL_FAN_BREEZE_VAR:
241                 // TODO(SRGDamia1): write array command fxn
242                 logger.trace("Support for fan breeze settings not yet available");
243                 break;
244
245             case CHANNEL_FAN_DIRECTION:
246                 logger.trace("Fan direction command {}", command.toString());
247                 if (command instanceof StringType) {
248                     api.executeDeviceAction(deviceId, BondDeviceAction.SET_DIRECTION,
249                             command.toString().equals("winter") ? -1 : 1);
250                 }
251                 break;
252
253             case CHANNEL_LIGHT_POWER:
254                 logger.trace("Fan light state command");
255                 api.executeDeviceAction(deviceId,
256                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
257                         null);
258                 break;
259
260             case CHANNEL_LIGHT_BRIGHTNESS:
261                 if (command instanceof PercentType) {
262                     PercentType pctCommand = (PercentType) command;
263                     value = pctCommand.intValue();
264                     if (value == 0) {
265                         action = BondDeviceAction.TURN_LIGHT_OFF;
266                         value = null;
267                     } else {
268                         action = BondDeviceAction.SET_BRIGHTNESS;
269                     }
270                     logger.trace("Fan light brightness command with value of {}", value);
271                     api.executeDeviceAction(deviceId, action, value);
272                 } else if (command instanceof IncreaseDecreaseType) {
273                     logger.trace("Fan light brightness increase/decrease command {}", command);
274                     api.executeDeviceAction(deviceId,
275                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
276                                     ? BondDeviceAction.INCREASE_BRIGHTNESS
277                                     : BondDeviceAction.DECREASE_BRIGHTNESS),
278                             null);
279                 } else if (command instanceof OnOffType) {
280                     logger.trace("Fan light brightness command {}", command);
281                     api.executeDeviceAction(deviceId,
282                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
283                             null);
284                 } else {
285                     logger.info("Unsupported command on fan light brightness channel");
286                 }
287                 break;
288
289             case CHANNEL_UP_LIGHT_ENABLE:
290                 api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_UP_LIGHT_ON
291                         : BondDeviceAction.TURN_UP_LIGHT_OFF, null);
292                 break;
293
294             case CHANNEL_UP_LIGHT_POWER:
295                 // To turn on the up light, we first have to enable it and then turn on the lights
296                 enableUpLight();
297                 api.executeDeviceAction(deviceId,
298                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
299                         null);
300                 break;
301
302             case CHANNEL_UP_LIGHT_BRIGHTNESS:
303                 enableUpLight();
304                 if (command instanceof PercentType) {
305                     PercentType pctCommand = (PercentType) command;
306                     value = pctCommand.intValue();
307                     if (value == 0) {
308                         action = BondDeviceAction.TURN_LIGHT_OFF;
309                         value = null;
310                     } else {
311                         action = BondDeviceAction.SET_UP_LIGHT_BRIGHTNESS;
312                     }
313                     logger.trace("Fan up light brightness command with value of {}", value);
314                     api.executeDeviceAction(deviceId, action, value);
315                 } else if (command instanceof IncreaseDecreaseType) {
316                     logger.trace("Fan uplight brightness increase/decrease command {}", command);
317                     api.executeDeviceAction(deviceId,
318                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
319                                     ? BondDeviceAction.INCREASE_UP_LIGHT_BRIGHTNESS
320                                     : BondDeviceAction.DECREASE_UP_LIGHT_BRIGHTNESS),
321                             null);
322                 } else if (command instanceof OnOffType) {
323                     logger.trace("Fan up light brightness command {}", command);
324                     api.executeDeviceAction(deviceId,
325                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
326                             null);
327                 } else {
328                     logger.info("Unsupported command on fan up light brightness channel");
329                 }
330                 break;
331
332             case CHANNEL_DOWN_LIGHT_ENABLE:
333                 api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_DOWN_LIGHT_ON
334                         : BondDeviceAction.TURN_DOWN_LIGHT_OFF, null);
335                 break;
336
337             case CHANNEL_DOWN_LIGHT_POWER:
338                 // To turn on the down light, we first have to enable it and then turn on the lights
339                 api.executeDeviceAction(deviceId, BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
340                 api.executeDeviceAction(deviceId,
341                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
342                         null);
343                 break;
344
345             case CHANNEL_DOWN_LIGHT_BRIGHTNESS:
346                 enableDownLight();
347                 if (command instanceof PercentType) {
348                     PercentType pctCommand = (PercentType) command;
349                     value = pctCommand.intValue();
350                     if (value == 0) {
351                         action = BondDeviceAction.TURN_LIGHT_OFF;
352                         value = null;
353                     } else {
354                         action = BondDeviceAction.SET_DOWN_LIGHT_BRIGHTNESS;
355                     }
356                     logger.trace("Fan down light brightness command with value of {}", value);
357                     api.executeDeviceAction(deviceId, action, value);
358                 } else if (command instanceof IncreaseDecreaseType) {
359                     logger.trace("Fan down light brightness increase/decrease command");
360                     api.executeDeviceAction(deviceId,
361                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
362                                     ? BondDeviceAction.INCREASE_DOWN_LIGHT_BRIGHTNESS
363                                     : BondDeviceAction.DECREASE_DOWN_LIGHT_BRIGHTNESS),
364                             null);
365                 } else if (command instanceof OnOffType) {
366                     logger.trace("Fan down light brightness command {}", command);
367                     api.executeDeviceAction(deviceId,
368                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
369                             null);
370                 } else {
371                     logger.debug("Unsupported command on fan down light brightness channel");
372                 }
373                 break;
374
375             case CHANNEL_FLAME:
376                 if (command instanceof PercentType) {
377                     PercentType pctCommand = (PercentType) command;
378                     value = pctCommand.intValue();
379                     if (value == 0) {
380                         action = BondDeviceAction.TURN_OFF;
381                         value = null;
382                     } else {
383                         action = BondDeviceAction.SET_FLAME;
384                     }
385                     logger.trace("Fireplace flame command with value of {}", value);
386                     api.executeDeviceAction(deviceId, action, value);
387                 } else if (command instanceof IncreaseDecreaseType) {
388                     logger.trace("Fireplace flame increase/decrease command");
389                     api.executeDeviceAction(deviceId,
390                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
391                                     ? BondDeviceAction.INCREASE_FLAME
392                                     : BondDeviceAction.DECREASE_FLAME),
393                             null);
394                 } else if (command instanceof OnOffType) {
395                     api.executeDeviceAction(deviceId,
396                             command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF, null);
397                 } else {
398                     logger.info("Unsupported command on flame channel");
399                 }
400                 break;
401
402             case CHANNEL_ROLLERSHUTTER:
403                 logger.trace("Rollershutter command {}", command);
404                 if (command.equals(PercentType.ZERO)) {
405                     command = UpDownType.UP;
406                 } else if (command.equals(PercentType.HUNDRED)) {
407                     command = UpDownType.DOWN;
408                 }
409                 if (command == UpDownType.UP) {
410                     action = BondDeviceAction.OPEN;
411                 } else if (command == UpDownType.DOWN) {
412                     action = BondDeviceAction.CLOSE;
413                 } else if (command == StopMoveType.STOP) {
414                     action = BondDeviceAction.HOLD;
415                 }
416                 if (action != null) {
417                     api.executeDeviceAction(deviceId, action, null);
418                 }
419                 break;
420
421             default:
422                 logger.info("Command {} on unknown channel {}, {}", command.toFullString(), channelUID.getId(),
423                         channelUID.toString());
424                 return;
425         }
426     }
427
428     private void enableUpLight() {
429         Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
430                 BondDeviceAction.TURN_UP_LIGHT_ON, null);
431     }
432
433     private void enableDownLight() {
434         Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
435                 BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
436     }
437
438     @Override
439     public void initialize() {
440         config = getConfigAs(BondDeviceConfiguration.class);
441         logger.trace("Starting initialization for Bond device with device id {}.", config.deviceId);
442         fullyInitialized = false;
443         disposed = false;
444
445         // set the thing status to UNKNOWN temporarily
446         updateStatus(ThingStatus.UNKNOWN);
447
448         scheduler.execute(this::initializeThing);
449     }
450
451     @Override
452     public synchronized void dispose() {
453         logger.debug("Disposing thing handler for {}.", this.getThing().getUID());
454         // Mark handler as disposed as soon as possible to halt updates
455         disposed = true;
456         fullyInitialized = false;
457
458         final ScheduledFuture<?> pollingJob = this.pollingJob;
459         if (pollingJob != null && !pollingJob.isCancelled()) {
460             pollingJob.cancel(true);
461         }
462         this.pollingJob = null;
463     }
464
465     private void initializeThing() {
466         String deviceId = config.deviceId;
467
468         if (deviceId == null) {
469             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
470                     "@text/offline.conf-error.no-device-id");
471             return;
472         }
473
474         if (!getBridgeAndAPI()) {
475             return;
476         }
477         BondHttpApi api = this.api;
478         if (api == null) {
479             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.no-api");
480             return;
481         }
482
483         try {
484             logger.trace("Getting device information for {} ({})", config.deviceId, this.getThing().getLabel());
485             deviceInfo = api.getDevice(deviceId);
486             logger.trace("Getting device properties for {} ({})", config.deviceId, this.getThing().getLabel());
487             deviceProperties = api.getDeviceProperties(deviceId);
488         } catch (BondException e) {
489             if (!e.wasBridgeSetOffline()) {
490                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
491             }
492             return;
493         }
494
495         final BondDevice devInfo = this.deviceInfo;
496         final BondDeviceProperties devProperties = this.deviceProperties;
497         BondDeviceType devType;
498         String devHash;
499         if (devInfo == null || devProperties == null || (devType = devInfo.type) == null
500                 || (devHash = devInfo.hash) == null) {
501             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
502                     "@text/offline.conf-error.no-device-properties");
503             return;
504         }
505
506         // Anytime the configuration has changed or the binding has been updated,
507         // recreate the thing to make sure all possible channels are available
508         // NOTE: This will cause the thing to be disposed and re-initialized
509         if (wasThingUpdatedExternally(devInfo)) {
510             recreateAllChannels(devType, devHash);
511             return;
512         }
513
514         updateDevicePropertiesFromBond(devInfo, devProperties);
515
516         deleteExtraChannels(devInfo.actions);
517
518         startPollingJob();
519
520         // Now we're online!
521         updateStatus(ThingStatus.ONLINE);
522         fullyInitialized = true;
523         logger.debug("Finished initializing device!");
524     }
525
526     private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
527         if (value == null) {
528             return;
529         }
530         thingProperties.put(key, value);
531     }
532
533     private void updateDevicePropertiesFromBond(BondDevice devInfo, BondDeviceProperties devProperties) {
534         // Update all the thing properties based on the result
535         Map<String, String> thingProperties = new HashMap<String, String>();
536         updateProperty(thingProperties, CONFIG_DEVICE_ID, config.deviceId);
537         logger.trace("Updating device name to {}", devInfo.name);
538         updateProperty(thingProperties, PROPERTIES_DEVICE_NAME, devInfo.name);
539         logger.trace("Updating other device properties for {} ({})", config.deviceId, this.getThing().getLabel());
540         updateProperty(thingProperties, PROPERTIES_TEMPLATE_NAME, devInfo.template);
541         thingProperties.put(PROPERTIES_MAX_SPEED, String.valueOf(devProperties.maxSpeed));
542         thingProperties.put(PROPERTIES_TRUST_STATE, String.valueOf(devProperties.trustState));
543         thingProperties.put(PROPERTIES_ADDRESS, String.valueOf(devProperties.addr));
544         thingProperties.put(PROPERTIES_RF_FREQUENCY, String.valueOf(devProperties.freq));
545         logger.trace("Saving properties for {} ({})", config.deviceId, this.getThing().getLabel());
546         updateProperties(thingProperties);
547     }
548
549     private synchronized void recreateAllChannels(BondDeviceType currentType, String currentHash) {
550         if (hasConfigurationError()) {
551             logger.trace("Don't recreate channels, I've been disposed!");
552             return;
553         }
554
555         logger.debug("Recreating all possible channels for a {} for {} ({})",
556                 currentType.getThingTypeUID().getAsString(), config.deviceId, this.getThing().getLabel());
557
558         // Create a new configuration
559         final Map<String, Object> map = new HashMap<>();
560         map.put(CONFIG_DEVICE_ID, Objects.requireNonNull(config.deviceId));
561         map.put(CONFIG_LATEST_HASH, currentHash);
562         Configuration newConfiguration = new Configuration(map);
563
564         // Change the thing type back to itself to force all channels to be re-created from XML
565         changeThingType(currentType.getThingTypeUID(), newConfiguration);
566     }
567
568     private synchronized void deleteExtraChannels(List<BondDeviceAction> currentActions) {
569         logger.trace("Deleting channels based on the available actions");
570         // Get the thing to edit
571         ThingBuilder thingBuilder = editThing();
572         final BondDevice devInfo = this.deviceInfo;
573
574         // Now, look at the whole list of possible channels
575         List<Channel> possibleChannels = this.getThing().getChannels();
576         Set<String> availableChannelIds = new HashSet<>();
577
578         for (BondDeviceAction action : currentActions) {
579             String actionType = action.getChannelTypeId();
580             if (actionType != null) {
581                 availableChannelIds.add(actionType);
582                 logger.trace(" Action: {}, Relevant Channel Type Id: {}", action.getActionId(), actionType);
583             }
584         }
585         // Remove power channels if we have a dimmer channel for them;
586         // the dimmer channel already covers the power case.
587         // Add the raw channel for advanced users if we're a ceiling fan.
588         if (availableChannelIds.contains(CHANNEL_FAN_SPEED)) {
589             availableChannelIds.remove(CHANNEL_POWER);
590             availableChannelIds.remove(CHANNEL_FAN_POWER);
591             if (devInfo != null && devInfo.type == BondDeviceType.CEILING_FAN) {
592                 availableChannelIds.add(CHANNEL_RAW_FAN_SPEED);
593             }
594         }
595         if (availableChannelIds.contains(CHANNEL_LIGHT_BRIGHTNESS)) {
596             availableChannelIds.remove(CHANNEL_LIGHT_POWER);
597         }
598         if (availableChannelIds.contains(CHANNEL_UP_LIGHT_BRIGHTNESS)) {
599             availableChannelIds.remove(CHANNEL_UP_LIGHT_POWER);
600         }
601         if (availableChannelIds.contains(CHANNEL_DOWN_LIGHT_BRIGHTNESS)) {
602             availableChannelIds.remove(CHANNEL_DOWN_LIGHT_POWER);
603         }
604         if (availableChannelIds.contains(CHANNEL_FLAME)) {
605             availableChannelIds.remove(CHANNEL_POWER);
606         }
607
608         for (Channel channel : possibleChannels) {
609             if (availableChannelIds.contains(channel.getUID().getId())) {
610                 logger.trace("      ++++ Keeping: {}", channel.getUID().getId());
611             } else {
612                 thingBuilder.withoutChannel(channel.getUID());
613                 logger.trace("      ---- Dropping: {}", channel.getUID().getId());
614             }
615         }
616
617         // Add all the channels
618         logger.trace("Saving the thing with extra channels removed");
619         updateThing(thingBuilder.build());
620     }
621
622     public String getDeviceId() {
623         String deviceId = config.deviceId;
624         return deviceId == null ? "" : deviceId;
625     }
626
627     public synchronized void updateChannelsFromState(@Nullable BondDeviceState updateState) {
628         if (hasConfigurationError()) {
629             return;
630         }
631
632         if (updateState == null) {
633             logger.debug("No state information provided to update channels with");
634             return;
635         }
636
637         logger.debug("Updating channels from state for {} ({})", config.deviceId, this.getThing().getLabel());
638
639         updateStatus(ThingStatus.ONLINE);
640
641         updateState(CHANNEL_POWER, updateState.power == 0 ? OnOffType.OFF : OnOffType.ON);
642         boolean fanOn;
643         final BondDevice devInfo = this.deviceInfo;
644         if (devInfo != null && devInfo.actions.contains(BondDeviceAction.TURN_FP_FAN_OFF)) {
645             fanOn = updateState.fpfanPower != 0;
646             updateState(CHANNEL_FAN_POWER, fanOn ? OnOffType.OFF : OnOffType.ON);
647             updateState(CHANNEL_FAN_SPEED, new PercentType(updateState.fpfanSpeed));
648         } else {
649             fanOn = updateState.power != 0;
650             int value = 1;
651             BondDeviceProperties devProperties = this.deviceProperties;
652             if (devProperties != null) {
653                 double maxSpeed = devProperties.maxSpeed;
654                 value = (int) (((double) updateState.speed / maxSpeed) * 100);
655                 logger.trace("Raw fan speed: {}, Percent: {}", updateState.speed, value);
656             } else if (updateState.speed != 0 && this.getThing().getThingTypeUID().equals(THING_TYPE_BOND_FAN)) {
657                 logger.info("Unable to convert fan speed to a percent for {}!", this.getThing().getLabel());
658             }
659             updateState(CHANNEL_FAN_SPEED, formPercentType(fanOn, value));
660             updateState(CHANNEL_RAW_FAN_SPEED, fanOn ? new DecimalType(updateState.speed) : DecimalType.ZERO);
661         }
662         updateState(CHANNEL_FAN_BREEZE_STATE, updateState.breeze[0] == 0 ? OnOffType.OFF : OnOffType.ON);
663         updateState(CHANNEL_FAN_BREEZE_MEAN, new PercentType(updateState.breeze[1]));
664         updateState(CHANNEL_FAN_BREEZE_VAR, new PercentType(updateState.breeze[2]));
665         updateState(CHANNEL_FAN_DIRECTION,
666                 updateState.direction == 1 ? new StringType("summer") : new StringType("winter"));
667         updateState(CHANNEL_FAN_TIMER, new DecimalType(updateState.timer));
668
669         updateState(CHANNEL_LIGHT_POWER, updateState.light == 0 ? OnOffType.OFF : OnOffType.ON);
670         updateState(CHANNEL_LIGHT_BRIGHTNESS, formPercentType(updateState.light != 0, updateState.brightness));
671
672         updateState(CHANNEL_UP_LIGHT_ENABLE, updateState.upLight == 0 ? OnOffType.OFF : OnOffType.ON);
673         updateState(CHANNEL_UP_LIGHT_POWER,
674                 (updateState.upLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
675         updateState(CHANNEL_UP_LIGHT_BRIGHTNESS,
676                 formPercentType((updateState.upLight == 1 && updateState.light == 1), updateState.upLightBrightness));
677
678         updateState(CHANNEL_DOWN_LIGHT_ENABLE, updateState.downLight == 0 ? OnOffType.OFF : OnOffType.ON);
679         updateState(CHANNEL_DOWN_LIGHT_POWER,
680                 (updateState.downLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
681         updateState(CHANNEL_DOWN_LIGHT_BRIGHTNESS, formPercentType(
682                 (updateState.downLight == 1 && updateState.light == 1), updateState.downLightBrightness));
683
684         updateState(CHANNEL_FLAME, formPercentType(updateState.power != 0, updateState.flame));
685
686         updateState(CHANNEL_ROLLERSHUTTER, formPercentType(updateState.open != 0, 100));
687     }
688
689     private PercentType formPercentType(boolean isOn, int value) {
690         if (!isOn) {
691             return PercentType.ZERO;
692         } else {
693             return new PercentType(value);
694         }
695     }
696
697     private boolean hasConfigurationError() {
698         final ThingStatusInfo statusInfo = getThing().getStatusInfo();
699         return statusInfo.getStatus() == ThingStatus.OFFLINE
700                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR || disposed
701                 || config.deviceId == null;
702     }
703
704     private synchronized boolean wasThingUpdatedExternally(BondDevice devInfo) {
705         // Check if the Bond hash tree has changed
706         final String lastDeviceConfigurationHash = config.lastDeviceConfigurationHash;
707         boolean updatedHashTree = !devInfo.hash.equals(lastDeviceConfigurationHash);
708         if (updatedHashTree) {
709             logger.debug("Hash tree of device has been updated by Bond.");
710             logger.debug("Current state is {}, prior state was {}.", devInfo.hash, lastDeviceConfigurationHash);
711         }
712         return updatedHashTree;
713     }
714
715     private boolean getBridgeAndAPI() {
716         Bridge myBridge = this.getBridge();
717         if (myBridge == null) {
718             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
719                     "@text/offline.conf-error.no-bridge");
720
721             return false;
722         } else {
723             BondBridgeHandler myBridgeHandler = (BondBridgeHandler) myBridge.getHandler();
724             if (myBridgeHandler != null) {
725                 this.api = myBridgeHandler.getBridgeAPI();
726                 return true;
727             } else {
728                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
729                         "@text/offline.conf-error.no-bridge");
730                 return false;
731             }
732         }
733     }
734
735     // Start polling for state
736     private synchronized void startPollingJob() {
737         final ScheduledFuture<?> pollingJob = this.pollingJob;
738         if (pollingJob == null || pollingJob.isCancelled()) {
739             Runnable pollingCommand = () -> {
740                 BondHttpApi api = this.api;
741                 if (api == null) {
742                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
743                             "@text/offline.comm-error.no-api");
744                     return;
745                 }
746
747                 String deviceId = Objects.requireNonNull(config.deviceId);
748                 logger.trace("Polling for current state for {} ({})", deviceId, this.getThing().getLabel());
749                 try {
750                     deviceState = api.getDeviceState(deviceId);
751                     updateChannelsFromState(deviceState);
752                 } catch (BondException e) {
753                     if (!e.wasBridgeSetOffline()) {
754                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
755                     }
756                 }
757             };
758             this.pollingJob = scheduler.scheduleWithFixedDelay(pollingCommand, 60, 300, TimeUnit.SECONDS);
759         }
760     }
761
762     @Override
763     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
764         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
765                 && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
766             if (!fullyInitialized) {
767                 scheduler.execute(this::initializeThing);
768             } else {
769                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
770                 // restart the polling job when the bridge goes back online
771                 startPollingJob();
772             }
773         } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
774             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
775             // stop the polling job when the bridge goes offline
776             ScheduledFuture<?> pollingJob = this.pollingJob;
777             if (pollingJob != null) {
778                 pollingJob.cancel(true);
779                 this.pollingJob = null;
780             }
781         }
782     }
783 }