]> git.basschouten.com Git - openhab-addons.git/blob
6f4e88cefd6dcddf72d685eb2b0a8c4b57eff826
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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_FAN_BREEZE_STATE:
209                 logger.trace("Fan enable/disable breeze command");
210                 api.executeDeviceAction(deviceId,
211                         command == OnOffType.ON ? BondDeviceAction.BREEZE_ON : BondDeviceAction.BREEZE_OFF, null);
212                 break;
213
214             case CHANNEL_FAN_BREEZE_MEAN:
215                 // TODO(SRGDamia1): write array command fxn
216                 logger.trace("Support for fan breeze settings not yet available");
217                 break;
218
219             case CHANNEL_FAN_BREEZE_VAR:
220                 // TODO(SRGDamia1): write array command fxn
221                 logger.trace("Support for fan breeze settings not yet available");
222                 break;
223
224             case CHANNEL_FAN_DIRECTION:
225                 logger.trace("Fan direction command {}", command.toString());
226                 if (command instanceof StringType) {
227                     api.executeDeviceAction(deviceId, BondDeviceAction.SET_DIRECTION,
228                             command.toString().equals("winter") ? -1 : 1);
229                 }
230                 break;
231
232             case CHANNEL_LIGHT_POWER:
233                 logger.trace("Fan light state command");
234                 api.executeDeviceAction(deviceId,
235                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
236                         null);
237                 break;
238
239             case CHANNEL_LIGHT_BRIGHTNESS:
240                 if (command instanceof PercentType) {
241                     PercentType pctCommand = (PercentType) command;
242                     value = pctCommand.intValue();
243                     if (value == 0) {
244                         action = BondDeviceAction.TURN_LIGHT_OFF;
245                         value = null;
246                     } else {
247                         action = BondDeviceAction.SET_BRIGHTNESS;
248                     }
249                     logger.trace("Fan light brightness command with value of {}", value);
250                     api.executeDeviceAction(deviceId, action, value);
251                 } else if (command instanceof IncreaseDecreaseType) {
252                     logger.trace("Fan light brightness increase/decrease command {}", command);
253                     api.executeDeviceAction(deviceId,
254                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
255                                     ? BondDeviceAction.INCREASE_BRIGHTNESS
256                                     : BondDeviceAction.DECREASE_BRIGHTNESS),
257                             null);
258                 } else if (command instanceof OnOffType) {
259                     logger.trace("Fan light brightness command {}", command);
260                     api.executeDeviceAction(deviceId,
261                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
262                             null);
263                 } else {
264                     logger.info("Unsupported command on fan light brightness channel");
265                 }
266                 break;
267
268             case CHANNEL_UP_LIGHT_ENABLE:
269                 api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_UP_LIGHT_ON
270                         : BondDeviceAction.TURN_UP_LIGHT_OFF, null);
271                 break;
272
273             case CHANNEL_UP_LIGHT_POWER:
274                 // To turn on the up light, we first have to enable it and then turn on the lights
275                 enableUpLight();
276                 api.executeDeviceAction(deviceId,
277                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
278                         null);
279                 break;
280
281             case CHANNEL_UP_LIGHT_BRIGHTNESS:
282                 enableUpLight();
283                 if (command instanceof PercentType) {
284                     PercentType pctCommand = (PercentType) command;
285                     value = pctCommand.intValue();
286                     if (value == 0) {
287                         action = BondDeviceAction.TURN_LIGHT_OFF;
288                         value = null;
289                     } else {
290                         action = BondDeviceAction.SET_UP_LIGHT_BRIGHTNESS;
291                     }
292                     logger.trace("Fan up light brightness command with value of {}", value);
293                     api.executeDeviceAction(deviceId, action, value);
294                 } else if (command instanceof IncreaseDecreaseType) {
295                     logger.trace("Fan uplight brightness increase/decrease command {}", command);
296                     api.executeDeviceAction(deviceId,
297                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
298                                     ? BondDeviceAction.INCREASE_UP_LIGHT_BRIGHTNESS
299                                     : BondDeviceAction.DECREASE_UP_LIGHT_BRIGHTNESS),
300                             null);
301                 } else if (command instanceof OnOffType) {
302                     logger.trace("Fan up light brightness command {}", command);
303                     api.executeDeviceAction(deviceId,
304                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
305                             null);
306                 } else {
307                     logger.info("Unsupported command on fan up light brightness channel");
308                 }
309                 break;
310
311             case CHANNEL_DOWN_LIGHT_ENABLE:
312                 api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_DOWN_LIGHT_ON
313                         : BondDeviceAction.TURN_DOWN_LIGHT_OFF, null);
314                 break;
315
316             case CHANNEL_DOWN_LIGHT_POWER:
317                 // To turn on the down light, we first have to enable it and then turn on the lights
318                 api.executeDeviceAction(deviceId, BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
319                 api.executeDeviceAction(deviceId,
320                         command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
321                         null);
322                 break;
323
324             case CHANNEL_DOWN_LIGHT_BRIGHTNESS:
325                 enableDownLight();
326                 if (command instanceof PercentType) {
327                     PercentType pctCommand = (PercentType) command;
328                     value = pctCommand.intValue();
329                     if (value == 0) {
330                         action = BondDeviceAction.TURN_LIGHT_OFF;
331                         value = null;
332                     } else {
333                         action = BondDeviceAction.SET_DOWN_LIGHT_BRIGHTNESS;
334                     }
335                     logger.trace("Fan down light brightness command with value of {}", value);
336                     api.executeDeviceAction(deviceId, action, value);
337                 } else if (command instanceof IncreaseDecreaseType) {
338                     logger.trace("Fan down light brightness increase/decrease command");
339                     api.executeDeviceAction(deviceId,
340                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
341                                     ? BondDeviceAction.INCREASE_DOWN_LIGHT_BRIGHTNESS
342                                     : BondDeviceAction.DECREASE_DOWN_LIGHT_BRIGHTNESS),
343                             null);
344                 } else if (command instanceof OnOffType) {
345                     logger.trace("Fan down light brightness command {}", command);
346                     api.executeDeviceAction(deviceId,
347                             command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
348                             null);
349                 } else {
350                     logger.debug("Unsupported command on fan down light brightness channel");
351                 }
352                 break;
353
354             case CHANNEL_FLAME:
355                 if (command instanceof PercentType) {
356                     PercentType pctCommand = (PercentType) command;
357                     value = pctCommand.intValue();
358                     if (value == 0) {
359                         action = BondDeviceAction.TURN_OFF;
360                         value = null;
361                     } else {
362                         action = BondDeviceAction.SET_FLAME;
363                     }
364                     logger.trace("Fireplace flame command with value of {}", value);
365                     api.executeDeviceAction(deviceId, action, value);
366                 } else if (command instanceof IncreaseDecreaseType) {
367                     logger.trace("Fireplace flame increase/decrease command");
368                     api.executeDeviceAction(deviceId,
369                             ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
370                                     ? BondDeviceAction.INCREASE_FLAME
371                                     : BondDeviceAction.DECREASE_FLAME),
372                             null);
373                 } else if (command instanceof OnOffType) {
374                     api.executeDeviceAction(deviceId,
375                             command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF, null);
376                 } else {
377                     logger.info("Unsupported command on flame channel");
378                 }
379                 break;
380
381             case CHANNEL_ROLLERSHUTTER:
382                 logger.trace("Rollershutter command {}", command);
383                 if (command.equals(PercentType.ZERO)) {
384                     command = UpDownType.UP;
385                 } else if (command.equals(PercentType.HUNDRED)) {
386                     command = UpDownType.DOWN;
387                 }
388                 if (command == UpDownType.UP) {
389                     action = BondDeviceAction.OPEN;
390                 } else if (command == UpDownType.DOWN) {
391                     action = BondDeviceAction.CLOSE;
392                 } else if (command == StopMoveType.STOP) {
393                     action = BondDeviceAction.HOLD;
394                 }
395                 if (action != null) {
396                     api.executeDeviceAction(deviceId, action, null);
397                 }
398                 break;
399
400             default:
401                 logger.info("Command {} on unknown channel {}, {}", command.toFullString(), channelUID.getId(),
402                         channelUID.toString());
403                 return;
404         }
405     }
406
407     private void enableUpLight() {
408         Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
409                 BondDeviceAction.TURN_UP_LIGHT_ON, null);
410     }
411
412     private void enableDownLight() {
413         Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
414                 BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
415     }
416
417     @Override
418     public void initialize() {
419         config = getConfigAs(BondDeviceConfiguration.class);
420         logger.trace("Starting initialization for Bond device with device id {}.", config.deviceId);
421         fullyInitialized = false;
422         disposed = false;
423
424         // set the thing status to UNKNOWN temporarily
425         updateStatus(ThingStatus.UNKNOWN);
426
427         scheduler.execute(this::initializeThing);
428     }
429
430     @Override
431     public synchronized void dispose() {
432         logger.debug("Disposing thing handler for {}.", this.getThing().getUID());
433         // Mark handler as disposed as soon as possible to halt updates
434         disposed = true;
435         fullyInitialized = false;
436
437         final ScheduledFuture<?> pollingJob = this.pollingJob;
438         if (pollingJob != null && !pollingJob.isCancelled()) {
439             pollingJob.cancel(true);
440         }
441         this.pollingJob = null;
442     }
443
444     private void initializeThing() {
445         String deviceId = config.deviceId;
446
447         if (deviceId == null) {
448             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
449                     "@text/offline.conf-error.no-device-id");
450             return;
451         }
452
453         if (!getBridgeAndAPI()) {
454             return;
455         }
456         BondHttpApi api = this.api;
457         if (api == null) {
458             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.no-api");
459             return;
460         }
461
462         try {
463             logger.trace("Getting device information for {} ({})", config.deviceId, this.getThing().getLabel());
464             deviceInfo = api.getDevice(deviceId);
465             logger.trace("Getting device properties for {} ({})", config.deviceId, this.getThing().getLabel());
466             deviceProperties = api.getDeviceProperties(deviceId);
467         } catch (BondException e) {
468             if (!e.wasBridgeSetOffline()) {
469                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
470             }
471             return;
472         }
473
474         final BondDevice devInfo = this.deviceInfo;
475         final BondDeviceProperties devProperties = this.deviceProperties;
476         BondDeviceType devType;
477         String devHash;
478         if (devInfo == null || devProperties == null || (devType = devInfo.type) == null
479                 || (devHash = devInfo.hash) == null) {
480             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
481                     "@text/offline.conf-error.no-device-properties");
482             return;
483         }
484
485         // Anytime the configuration has changed or the binding has been updated,
486         // recreate the thing to make sure all possible channels are available
487         // NOTE: This will cause the thing to be disposed and re-initialized
488         if (wasThingUpdatedExternally(devInfo)) {
489             recreateAllChannels(devType, devHash);
490             return;
491         }
492
493         updateDevicePropertiesFromBond(devInfo, devProperties);
494
495         deleteExtraChannels(devInfo.actions);
496
497         startPollingJob();
498
499         // Now we're online!
500         updateStatus(ThingStatus.ONLINE);
501         fullyInitialized = true;
502         logger.debug("Finished initializing device!");
503     }
504
505     private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
506         if (value == null) {
507             return;
508         }
509         thingProperties.put(key, value);
510     }
511
512     private void updateDevicePropertiesFromBond(BondDevice devInfo, BondDeviceProperties devProperties) {
513         // Update all the thing properties based on the result
514         Map<String, String> thingProperties = new HashMap<String, String>();
515         updateProperty(thingProperties, CONFIG_DEVICE_ID, config.deviceId);
516         logger.trace("Updating device name to {}", devInfo.name);
517         updateProperty(thingProperties, PROPERTIES_DEVICE_NAME, devInfo.name);
518         logger.trace("Updating other device properties for {} ({})", config.deviceId, this.getThing().getLabel());
519         updateProperty(thingProperties, PROPERTIES_TEMPLATE_NAME, devInfo.template);
520         thingProperties.put(PROPERTIES_MAX_SPEED, String.valueOf(devProperties.maxSpeed));
521         thingProperties.put(PROPERTIES_TRUST_STATE, String.valueOf(devProperties.trustState));
522         thingProperties.put(PROPERTIES_ADDRESS, String.valueOf(devProperties.addr));
523         thingProperties.put(PROPERTIES_RF_FREQUENCY, String.valueOf(devProperties.freq));
524         logger.trace("Saving properties for {} ({})", config.deviceId, this.getThing().getLabel());
525         updateProperties(thingProperties);
526     }
527
528     private synchronized void recreateAllChannels(BondDeviceType currentType, String currentHash) {
529         if (hasConfigurationError()) {
530             logger.trace("Don't recreate channels, I've been disposed!");
531             return;
532         }
533
534         logger.debug("Recreating all possible channels for a {} for {} ({})",
535                 currentType.getThingTypeUID().getAsString(), config.deviceId, this.getThing().getLabel());
536
537         // Create a new configuration
538         final Map<String, Object> map = new HashMap<>();
539         map.put(CONFIG_DEVICE_ID, Objects.requireNonNull(config.deviceId));
540         map.put(CONFIG_LATEST_HASH, currentHash);
541         Configuration newConfiguration = new Configuration(map);
542
543         // Change the thing type back to itself to force all channels to be re-created from XML
544         changeThingType(currentType.getThingTypeUID(), newConfiguration);
545     }
546
547     private synchronized void deleteExtraChannels(List<BondDeviceAction> currentActions) {
548         logger.trace("Deleting channels based on the available actions");
549         // Get the thing to edit
550         ThingBuilder thingBuilder = editThing();
551
552         // Now, look at the whole list of possible channels
553         List<Channel> possibleChannels = this.getThing().getChannels();
554         Set<String> availableChannelIds = new HashSet<>();
555
556         for (BondDeviceAction action : currentActions) {
557             String actionType = action.getChannelTypeId();
558             if (actionType != null) {
559                 availableChannelIds.add(actionType);
560                 logger.trace(" Action: {}, Relevant Channel Type Id: {}", action.getActionId(), actionType);
561             }
562         }
563         // Remove power channels if we have a dimmer channel for them;
564         // the dimmer channel already covers the power case
565         if (availableChannelIds.contains(CHANNEL_FAN_SPEED)) {
566             availableChannelIds.remove(CHANNEL_POWER);
567             availableChannelIds.remove(CHANNEL_FAN_POWER);
568         }
569         if (availableChannelIds.contains(CHANNEL_LIGHT_BRIGHTNESS)) {
570             availableChannelIds.remove(CHANNEL_LIGHT_POWER);
571         }
572         if (availableChannelIds.contains(CHANNEL_UP_LIGHT_BRIGHTNESS)) {
573             availableChannelIds.remove(CHANNEL_UP_LIGHT_POWER);
574         }
575         if (availableChannelIds.contains(CHANNEL_DOWN_LIGHT_BRIGHTNESS)) {
576             availableChannelIds.remove(CHANNEL_DOWN_LIGHT_POWER);
577         }
578         if (availableChannelIds.contains(CHANNEL_FLAME)) {
579             availableChannelIds.remove(CHANNEL_POWER);
580         }
581
582         for (Channel channel : possibleChannels) {
583             if (availableChannelIds.contains(channel.getUID().getId())) {
584                 logger.trace("      ++++ Keeping: {}", channel.getUID().getId());
585             } else {
586                 thingBuilder.withoutChannel(channel.getUID());
587                 logger.trace("      ---- Dropping: {}", channel.getUID().getId());
588             }
589         }
590
591         // Add all the channels
592         logger.trace("Saving the thing with extra channels removed");
593         updateThing(thingBuilder.build());
594     }
595
596     public String getDeviceId() {
597         String deviceId = config.deviceId;
598         return deviceId == null ? "" : deviceId;
599     }
600
601     public synchronized void updateChannelsFromState(@Nullable BondDeviceState updateState) {
602         if (hasConfigurationError()) {
603             return;
604         }
605
606         if (updateState == null) {
607             logger.debug("No state information provided to update channels with");
608             return;
609         }
610
611         logger.debug("Updating channels from state for {} ({})", config.deviceId, this.getThing().getLabel());
612
613         updateStatus(ThingStatus.ONLINE);
614
615         updateState(CHANNEL_POWER, updateState.power == 0 ? OnOffType.OFF : OnOffType.ON);
616         boolean fanOn;
617         final BondDevice devInfo = this.deviceInfo;
618         if (devInfo != null && devInfo.actions.contains(BondDeviceAction.TURN_FP_FAN_OFF)) {
619             fanOn = updateState.fpfanPower != 0;
620             updateState(CHANNEL_FAN_POWER, fanOn ? OnOffType.OFF : OnOffType.ON);
621             updateState(CHANNEL_FAN_SPEED, new PercentType(updateState.fpfanSpeed));
622         } else {
623             fanOn = updateState.power != 0;
624             int value = 1;
625             BondDeviceProperties devProperties = this.deviceProperties;
626             if (devProperties != null) {
627                 double maxSpeed = devProperties.maxSpeed;
628                 value = (int) (((double) updateState.speed / maxSpeed) * 100);
629                 logger.trace("Raw fan speed: {}, Percent: {}", updateState.speed, value);
630             } else if (updateState.speed != 0 && this.getThing().getThingTypeUID().equals(THING_TYPE_BOND_FAN)) {
631                 logger.info("Unable to convert fan speed to a percent for {}!", this.getThing().getLabel());
632             }
633             updateState(CHANNEL_FAN_SPEED, formPercentType(fanOn, value));
634         }
635         updateState(CHANNEL_FAN_BREEZE_STATE, updateState.breeze[0] == 0 ? OnOffType.OFF : OnOffType.ON);
636         updateState(CHANNEL_FAN_BREEZE_MEAN, new PercentType(updateState.breeze[1]));
637         updateState(CHANNEL_FAN_BREEZE_VAR, new PercentType(updateState.breeze[2]));
638         updateState(CHANNEL_FAN_DIRECTION,
639                 updateState.direction == 1 ? new StringType("summer") : new StringType("winter"));
640         updateState(CHANNEL_FAN_TIMER, new DecimalType(updateState.timer));
641
642         updateState(CHANNEL_LIGHT_POWER, updateState.light == 0 ? OnOffType.OFF : OnOffType.ON);
643         updateState(CHANNEL_LIGHT_BRIGHTNESS, formPercentType(updateState.light != 0, updateState.brightness));
644
645         updateState(CHANNEL_UP_LIGHT_ENABLE, updateState.upLight == 0 ? OnOffType.OFF : OnOffType.ON);
646         updateState(CHANNEL_UP_LIGHT_POWER,
647                 (updateState.upLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
648         updateState(CHANNEL_UP_LIGHT_BRIGHTNESS,
649                 formPercentType((updateState.upLight == 1 && updateState.light == 1), updateState.upLightBrightness));
650
651         updateState(CHANNEL_DOWN_LIGHT_ENABLE, updateState.downLight == 0 ? OnOffType.OFF : OnOffType.ON);
652         updateState(CHANNEL_DOWN_LIGHT_POWER,
653                 (updateState.downLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
654         updateState(CHANNEL_DOWN_LIGHT_BRIGHTNESS, formPercentType(
655                 (updateState.downLight == 1 && updateState.light == 1), updateState.downLightBrightness));
656
657         updateState(CHANNEL_FLAME, formPercentType(updateState.power != 0, updateState.flame));
658
659         updateState(CHANNEL_ROLLERSHUTTER, formPercentType(updateState.open != 0, 100));
660     }
661
662     private PercentType formPercentType(boolean isOn, int value) {
663         if (!isOn) {
664             return PercentType.ZERO;
665         } else {
666             return new PercentType(value);
667         }
668     }
669
670     private boolean hasConfigurationError() {
671         final ThingStatusInfo statusInfo = getThing().getStatusInfo();
672         return statusInfo.getStatus() == ThingStatus.OFFLINE
673                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR || disposed
674                 || config.deviceId == null;
675     }
676
677     private synchronized boolean wasThingUpdatedExternally(BondDevice devInfo) {
678         // Check if the Bond hash tree has changed
679         final String lastDeviceConfigurationHash = config.lastDeviceConfigurationHash;
680         boolean updatedHashTree = !devInfo.hash.equals(lastDeviceConfigurationHash);
681         if (updatedHashTree) {
682             logger.debug("Hash tree of device has been updated by Bond.");
683             logger.debug("Current state is {}, prior state was {}.", devInfo.hash, lastDeviceConfigurationHash);
684         }
685         return updatedHashTree;
686     }
687
688     private boolean getBridgeAndAPI() {
689         Bridge myBridge = this.getBridge();
690         if (myBridge == null) {
691             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
692                     "@text/offline.conf-error.no-bridge");
693
694             return false;
695         } else {
696             BondBridgeHandler myBridgeHandler = (BondBridgeHandler) myBridge.getHandler();
697             if (myBridgeHandler != null) {
698                 this.api = myBridgeHandler.getBridgeAPI();
699                 return true;
700             } else {
701                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
702                         "@text/offline.conf-error.no-bridge");
703                 return false;
704             }
705         }
706     }
707
708     // Start polling for state
709     private synchronized void startPollingJob() {
710         final ScheduledFuture<?> pollingJob = this.pollingJob;
711         if (pollingJob == null || pollingJob.isCancelled()) {
712             Runnable pollingCommand = () -> {
713                 BondHttpApi api = this.api;
714                 if (api == null) {
715                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
716                             "@text/offline.comm-error.no-api");
717                     return;
718                 }
719
720                 String deviceId = Objects.requireNonNull(config.deviceId);
721                 logger.trace("Polling for current state for {} ({})", deviceId, this.getThing().getLabel());
722                 try {
723                     deviceState = api.getDeviceState(deviceId);
724                     updateChannelsFromState(deviceState);
725                 } catch (BondException e) {
726                     if (!e.wasBridgeSetOffline()) {
727                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
728                     }
729                 }
730             };
731             this.pollingJob = scheduler.scheduleWithFixedDelay(pollingCommand, 60, 300, TimeUnit.SECONDS);
732         }
733     }
734
735     @Override
736     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
737         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
738                 && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
739             if (!fullyInitialized) {
740                 scheduler.execute(this::initializeThing);
741             } else {
742                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
743                 // restart the polling job when the bridge goes back online
744                 startPollingJob();
745             }
746         } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
747             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
748             // stop the polling job when the bridge goes offline
749             ScheduledFuture<?> pollingJob = this.pollingJob;
750             if (pollingJob != null) {
751                 pollingJob.cancel(true);
752                 this.pollingJob = null;
753             }
754         }
755     }
756 }