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