]> git.basschouten.com Git - openhab-addons.git/blob
b341d053522779c902f4f9456acb029f03358240
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.hue.internal.handler;
14
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
17
18 import java.io.IOException;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.Callable;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.stream.Collectors;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.hue.internal.ApiVersionUtils;
37 import org.openhab.binding.hue.internal.Config;
38 import org.openhab.binding.hue.internal.ConfigUpdate;
39 import org.openhab.binding.hue.internal.FullConfig;
40 import org.openhab.binding.hue.internal.FullGroup;
41 import org.openhab.binding.hue.internal.FullLight;
42 import org.openhab.binding.hue.internal.FullSensor;
43 import org.openhab.binding.hue.internal.HueBridge;
44 import org.openhab.binding.hue.internal.HueConfigStatusMessage;
45 import org.openhab.binding.hue.internal.Scene;
46 import org.openhab.binding.hue.internal.State;
47 import org.openhab.binding.hue.internal.StateUpdate;
48 import org.openhab.binding.hue.internal.config.HueBridgeConfig;
49 import org.openhab.binding.hue.internal.discovery.HueDeviceDiscoveryService;
50 import org.openhab.binding.hue.internal.exceptions.ApiException;
51 import org.openhab.binding.hue.internal.exceptions.DeviceOffException;
52 import org.openhab.binding.hue.internal.exceptions.EntityNotAvailableException;
53 import org.openhab.binding.hue.internal.exceptions.LinkButtonException;
54 import org.openhab.binding.hue.internal.exceptions.UnauthorizedException;
55 import org.openhab.core.config.core.Configuration;
56 import org.openhab.core.config.core.status.ConfigStatusMessage;
57 import org.openhab.core.library.types.HSBType;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.ThingTypeUID;
65 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.StateOption;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 /**
73  * {@link HueBridgeHandler} is the handler for a hue bridge and connects it to
74  * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
75  *
76  * @author Dennis Nobel - Initial contribution
77  * @author Oliver Libutzki - Adjustments
78  * @author Kai Kreuzer - improved state handling
79  * @author Andre Fuechsel - implemented getFullLights(), startSearch()
80  * @author Thomas Höfer - added thing properties
81  * @author Stefan Bußweiler - Added new thing status handling
82  * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
83  * @author Denis Dudnik - switched to internally integrated source of Jue library
84  * @author Samuel Leisering - Added support for sensor API
85  * @author Christoph Weitkamp - Added support for sensor API
86  * @author Laurent Garnier - Added support for groups
87  */
88 @NonNullByDefault
89 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
90
91     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
92
93     private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
94
95     private static final String DEVICE_TYPE = "EclipseSmartHome";
96
97     private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
98
99     private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
100     private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
101
102     private final Map<String, FullLight> lastLightStates = new ConcurrentHashMap<>();
103     private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
104     private final Map<String, FullGroup> lastGroupStates = new ConcurrentHashMap<>();
105
106     private @Nullable HueDeviceDiscoveryService discoveryService;
107     private final Map<String, LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
108     private final Map<String, SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
109     private final Map<String, GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
110
111     final ReentrantLock pollingLock = new ReentrantLock();
112
113     abstract class PollingRunnable implements Runnable {
114         @Override
115         public void run() {
116             try {
117                 pollingLock.lock();
118                 if (!lastBridgeConnectionState) {
119                     // if user is not set in configuration try to create a new user on Hue bridge
120                     if (hueBridgeConfig.getUserName() == null) {
121                         hueBridge.getFullConfig();
122                     }
123                     lastBridgeConnectionState = tryResumeBridgeConnection();
124                 }
125                 if (lastBridgeConnectionState) {
126                     doConnectedRun();
127                     if (thing.getStatus() != ThingStatus.ONLINE) {
128                         updateStatus(ThingStatus.ONLINE);
129                     }
130                 }
131             } catch (UnauthorizedException | IllegalStateException e) {
132                 if (isReachable(hueBridge.getIPAddress())) {
133                     lastBridgeConnectionState = false;
134                     if (onNotAuthenticated()) {
135                         updateStatus(ThingStatus.ONLINE);
136                     }
137                 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
138                     lastBridgeConnectionState = false;
139                     onConnectionLost();
140                 }
141             } catch (ApiException | IOException e) {
142                 if (hueBridge != null && lastBridgeConnectionState) {
143                     logger.debug("Connection to Hue Bridge {} lost.", hueBridge.getIPAddress());
144                     lastBridgeConnectionState = false;
145                     onConnectionLost();
146                 }
147             } catch (RuntimeException e) {
148                 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
149                 lastBridgeConnectionState = false;
150                 onConnectionLost();
151             } finally {
152                 pollingLock.unlock();
153             }
154         }
155
156         private boolean isReachable(String ipAddress) {
157             try {
158                 // note that InetAddress.isReachable is unreliable, see
159                 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
160                 // That's why we do an HTTP access instead
161
162                 // If there is no connection, this line will fail
163                 hueBridge.authenticate("invalid");
164             } catch (IOException e) {
165                 return false;
166             } catch (ApiException e) {
167                 if (e.getMessage().contains("SocketTimeout") || e.getMessage().contains("ConnectException")
168                         || e.getMessage().contains("SocketException")
169                         || e.getMessage().contains("NoRouteToHostException")) {
170                     return false;
171                 } else {
172                     // this seems to be only an authentication issue
173                     return true;
174                 }
175             }
176             return true;
177         }
178
179         protected abstract void doConnectedRun() throws IOException, ApiException;
180     }
181
182     private final Runnable sensorPollingRunnable = new PollingRunnable() {
183         @Override
184         protected void doConnectedRun() throws IOException, ApiException {
185             Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
186
187             final HueDeviceDiscoveryService discovery = discoveryService;
188
189             for (final FullSensor sensor : hueBridge.getSensors()) {
190                 String sensorId = sensor.getId();
191
192                 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
193                 if (sensorStatusListener == null) {
194                     logger.trace("Hue sensor '{}' added.", sensorId);
195
196                     if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
197                         discovery.addSensorDiscovery(sensor);
198                     }
199
200                     lastSensorStates.put(sensorId, sensor);
201                 } else {
202                     if (sensorStatusListener.onSensorStateChanged(sensor)) {
203                         lastSensorStates.put(sensorId, sensor);
204                     }
205                 }
206                 lastSensorStateCopy.remove(sensorId);
207             }
208
209             // Check for removed sensors
210             lastSensorStateCopy.forEach((sensorId, sensor) -> {
211                 logger.trace("Hue sensor '{}' removed.", sensorId);
212                 lastSensorStates.remove(sensorId);
213
214                 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
215                 if (sensorStatusListener != null) {
216                     sensorStatusListener.onSensorRemoved();
217                 }
218
219                 if (discovery != null && sensor != null) {
220                     discovery.removeSensorDiscovery(sensor);
221                 }
222             });
223         }
224     };
225
226     private final Runnable lightPollingRunnable = new PollingRunnable() {
227         @Override
228         protected void doConnectedRun() throws IOException, ApiException {
229             updateLights();
230             updateGroups();
231         }
232
233         private void updateLights() throws IOException, ApiException {
234             Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
235
236             List<FullLight> lights;
237             if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
238                 lights = hueBridge.getFullLights();
239             } else {
240                 lights = hueBridge.getFullConfig().getLights();
241             }
242
243             final HueDeviceDiscoveryService discovery = discoveryService;
244
245             for (final FullLight fullLight : lights) {
246                 final String lightId = fullLight.getId();
247
248                 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
249                 if (lightStatusListener == null) {
250                     logger.trace("Hue light '{}' added.", lightId);
251
252                     if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
253                         discovery.addLightDiscovery(fullLight);
254                     }
255
256                     lastLightStates.put(lightId, fullLight);
257                 } else {
258                     if (lightStatusListener.onLightStateChanged(fullLight)) {
259                         lastLightStates.put(lightId, fullLight);
260                     }
261                 }
262                 lastLightStateCopy.remove(lightId);
263             }
264
265             // Check for removed lights
266             lastLightStateCopy.forEach((lightId, light) -> {
267                 logger.trace("Hue light '{}' removed.", lightId);
268                 lastLightStates.remove(lightId);
269
270                 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
271                 if (lightStatusListener != null) {
272                     lightStatusListener.onLightRemoved();
273                 }
274
275                 if (discovery != null && light != null) {
276                     discovery.removeLightDiscovery(light);
277                 }
278             });
279         }
280
281         private void updateGroups() throws IOException, ApiException {
282             Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
283
284             List<FullGroup> groups = hueBridge.getGroups();
285
286             final HueDeviceDiscoveryService discovery = discoveryService;
287
288             for (final FullGroup fullGroup : groups) {
289                 State groupState = new State();
290                 boolean on = false;
291                 int sumBri = 0;
292                 int nbBri = 0;
293                 State colorRef = null;
294                 HSBType firstColorHsb = null;
295                 for (String lightId : fullGroup.getLightIds()) {
296                     FullLight light = lastLightStates.get(lightId);
297                     if (light != null) {
298                         final State lightState = light.getState();
299                         logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
300                                 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
301                                 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
302                                 lightState.getColorMode(), lightState.getXY());
303                         if (lightState.isOn()) {
304                             on = true;
305                             sumBri += lightState.getBrightness();
306                             nbBri++;
307                             if (lightState.getColorMode() != null) {
308                                 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
309                                 if (firstColorHsb == null) {
310                                     // first color light
311                                     firstColorHsb = lightHsb;
312                                     colorRef = lightState;
313                                 } else if (!lightHsb.equals(firstColorHsb)) {
314                                     colorRef = null;
315                                 }
316                             }
317                         }
318                     }
319                 }
320                 groupState.setOn(on);
321                 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
322                 if (colorRef != null) {
323                     groupState.setColormode(colorRef.getColorMode());
324                     groupState.setHue(colorRef.getHue());
325                     groupState.setSaturation(colorRef.getSaturation());
326                     groupState.setColorTemperature(colorRef.getColorTemperature());
327                     groupState.setXY(colorRef.getXY());
328                 }
329                 fullGroup.setState(groupState);
330                 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
331                         fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
332                         groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
333                         groupState.getXY());
334
335                 String groupId = fullGroup.getId();
336
337                 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
338                 if (groupStatusListener == null) {
339                     logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
340                             fullGroup.getLightIds().size());
341
342                     if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
343                         discovery.addGroupDiscovery(fullGroup);
344                     }
345
346                     lastGroupStates.put(groupId, fullGroup);
347                 } else {
348                     if (groupStatusListener.onGroupStateChanged(fullGroup)) {
349                         lastGroupStates.put(groupId, fullGroup);
350                     }
351                 }
352                 lastGroupStateCopy.remove(groupId);
353             }
354
355             // Check for removed groups
356             lastGroupStateCopy.forEach((groupId, group) -> {
357                 logger.trace("Hue group '{}' removed.", groupId);
358                 lastGroupStates.remove(groupId);
359
360                 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
361                 if (groupStatusListener != null) {
362                     groupStatusListener.onGroupRemoved();
363                 }
364
365                 if (discovery != null && group != null) {
366                     discovery.removeGroupDiscovery(group);
367                 }
368             });
369         }
370     };
371
372     private final Runnable scenePollingRunnable = new PollingRunnable() {
373         @Override
374         protected void doConnectedRun() throws IOException, ApiException {
375             List<Scene> scenes = hueBridge.getScenes();
376             logger.trace("Scenes detected: {}", scenes);
377
378             setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
379             notifyGroupSceneUpdate(scenes);
380         }
381
382         private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
383             Map<String, String> groupNames = groups.entrySet().stream()
384                     .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
385             List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
386                     .collect(Collectors.toList());
387             stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
388                     stateOptions);
389             consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
390                     + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
391         }
392     };
393
394     private boolean lastBridgeConnectionState = false;
395
396     private boolean propertiesInitializedSuccessfully = false;
397
398     private @Nullable Future<?> initJob;
399     private @Nullable ScheduledFuture<?> lightPollingJob;
400     private @Nullable ScheduledFuture<?> sensorPollingJob;
401     private @Nullable ScheduledFuture<?> scenePollingJob;
402
403     private @NonNullByDefault({}) HueBridge hueBridge = null;
404     private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
405
406     private List<String> consoleScenesList = new ArrayList<>();
407
408     public HueBridgeHandler(Bridge bridge, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
409         super(bridge);
410         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
411     }
412
413     @Override
414     public Collection<Class<? extends ThingHandlerService>> getServices() {
415         return Collections.singleton(HueDeviceDiscoveryService.class);
416     }
417
418     @Override
419     public void handleCommand(ChannelUID channelUID, Command command) {
420         if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
421             recallScene(command.toString());
422         }
423     }
424
425     @Override
426     public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
427             long fadeTime) {
428         if (hueBridge != null) {
429             listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
430             hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
431                 try {
432                     hueBridge.handleErrors(result);
433                     listener.setPollBypass(fadeTime);
434                 } catch (Exception e) {
435                     listener.unsetPollBypass();
436                     handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
437                 }
438             }).exceptionally(e -> {
439                 listener.unsetPollBypass();
440                 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
441                 return null;
442             });
443         } else {
444             logger.debug("No bridge connected or selected. Cannot set light state.");
445         }
446     }
447
448     @Override
449     public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
450         if (hueBridge != null) {
451             hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
452                 try {
453                     hueBridge.handleErrors(result);
454                 } catch (Exception e) {
455                     handleSensorUpdateException(sensor, e);
456                 }
457             }).exceptionally(e -> {
458                 handleSensorUpdateException(sensor, e);
459                 return null;
460             });
461         } else {
462             logger.debug("No bridge connected or selected. Cannot set sensor state.");
463         }
464     }
465
466     @Override
467     public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
468         if (hueBridge != null) {
469             hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
470                 try {
471                     hueBridge.handleErrors(result);
472                 } catch (Exception e) {
473                     handleSensorUpdateException(sensor, e);
474                 }
475             }).exceptionally(e -> {
476                 handleSensorUpdateException(sensor, e);
477                 return null;
478             });
479         } else {
480             logger.debug("No bridge connected or selected. Cannot set sensor config.");
481         }
482     }
483
484     @Override
485     public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
486         if (hueBridge != null) {
487             setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
488             hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
489                 try {
490                     hueBridge.handleErrors(result);
491                     setGroupPollBypass(group, fadeTime);
492                 } catch (Exception e) {
493                     unsetGroupPollBypass(group);
494                     handleGroupUpdateException(group, e);
495                 }
496             }).exceptionally(e -> {
497                 unsetGroupPollBypass(group);
498                 handleGroupUpdateException(group, e);
499                 return null;
500             });
501         } else {
502             logger.debug("No bridge connected or selected. Cannot set group state.");
503         }
504     }
505
506     private void setGroupPollBypass(FullGroup group, long bypassTime) {
507         group.getLightIds().forEach((lightId) -> {
508             final LightStatusListener listener = lightStatusListeners.get(lightId);
509             if (listener != null) {
510                 listener.setPollBypass(bypassTime);
511             }
512         });
513     }
514
515     private void unsetGroupPollBypass(FullGroup group) {
516         group.getLightIds().forEach((lightId) -> {
517             final LightStatusListener listener = lightStatusListeners.get(lightId);
518             if (listener != null) {
519                 listener.unsetPollBypass();
520             }
521         });
522     }
523
524     private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
525             long fadeTime, Throwable e) {
526         if (e instanceof DeviceOffException) {
527             if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
528                 // If there is only a change of the color temperature, we do not want the light
529                 // to be turned on (i.e. change its brightness).
530                 return;
531             } else {
532                 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
533                 updateLightState(listener, light, stateUpdate, fadeTime);
534             }
535         } else if (e instanceof EntityNotAvailableException) {
536             logger.debug("Error while accessing light: {}", e.getMessage(), e);
537             final HueDeviceDiscoveryService discovery = discoveryService;
538             if (discovery != null) {
539                 discovery.removeLightDiscovery(light);
540             }
541             listener.onLightGone();
542         } else {
543             handleThingUpdateException("light", e);
544         }
545     }
546
547     private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
548         if (e instanceof EntityNotAvailableException) {
549             logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
550             final HueDeviceDiscoveryService discovery = discoveryService;
551             if (discovery != null) {
552                 discovery.removeSensorDiscovery(sensor);
553             }
554             final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
555             if (listener != null) {
556                 listener.onSensorGone();
557             }
558         } else {
559             handleThingUpdateException("sensor", e);
560         }
561     }
562
563     private void handleGroupUpdateException(FullGroup group, Throwable e) {
564         if (e instanceof EntityNotAvailableException) {
565             logger.debug("Error while accessing group: {}", e.getMessage(), e);
566             final HueDeviceDiscoveryService discovery = discoveryService;
567             if (discovery != null) {
568                 discovery.removeGroupDiscovery(group);
569             }
570             final GroupStatusListener listener = groupStatusListeners.get(group.getId());
571             if (listener != null) {
572                 listener.onGroupGone();
573             }
574         } else {
575             handleThingUpdateException("group", e);
576         }
577     }
578
579     private void handleThingUpdateException(String thingType, Throwable e) {
580         if (e instanceof IOException) {
581             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
582         } else if (e instanceof ApiException) {
583             // This should not happen - if it does, it is most likely some bug that should be reported.
584             logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
585         } else if (e instanceof IllegalStateException) {
586             logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
587         }
588     }
589
590     private void startLightPolling() {
591         ScheduledFuture<?> job = lightPollingJob;
592         if (job == null || job.isCancelled()) {
593             long lightPollingInterval;
594             int configPollingInterval = hueBridgeConfig.getPollingInterval();
595             if (configPollingInterval < 1) {
596                 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
597                 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
598                         lightPollingInterval);
599             } else {
600                 lightPollingInterval = configPollingInterval;
601             }
602             // Delay the first execution to give a chance to have all light and group things registered
603             lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
604                     TimeUnit.SECONDS);
605         }
606     }
607
608     private void stopLightPolling() {
609         ScheduledFuture<?> job = lightPollingJob;
610         if (job != null) {
611             job.cancel(true);
612         }
613         lightPollingJob = null;
614     }
615
616     private void startSensorPolling() {
617         ScheduledFuture<?> job = sensorPollingJob;
618         if (job == null || job.isCancelled()) {
619             int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
620             if (configSensorPollingInterval > 0) {
621                 long sensorPollingInterval;
622                 if (configSensorPollingInterval < 50) {
623                     sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
624                     logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
625                             sensorPollingInterval);
626                 } else {
627                     sensorPollingInterval = configSensorPollingInterval;
628                 }
629                 // Delay the first execution to give a chance to have all sensor things registered
630                 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
631                         TimeUnit.MILLISECONDS);
632             }
633         }
634     }
635
636     private void stopSensorPolling() {
637         ScheduledFuture<?> job = sensorPollingJob;
638         if (job != null) {
639             job.cancel(true);
640         }
641         sensorPollingJob = null;
642     }
643
644     private void startScenePolling() {
645         ScheduledFuture<?> job = scenePollingJob;
646         if (job == null || job.isCancelled()) {
647             // Delay the first execution to give a chance to have all group things registered
648             scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
649                     TimeUnit.SECONDS);
650         }
651     }
652
653     private void stopScenePolling() {
654         ScheduledFuture<?> job = scenePollingJob;
655         if (job != null) {
656             job.cancel(true);
657         }
658         scenePollingJob = null;
659     }
660
661     @Override
662     public void dispose() {
663         logger.debug("Handler disposed.");
664         Future<?> job = initJob;
665         if (job != null) {
666             job.cancel(true);
667         }
668         stopLightPolling();
669         stopSensorPolling();
670         stopScenePolling();
671         if (hueBridge != null) {
672             hueBridge = null;
673         }
674     }
675
676     @Override
677     public void initialize() {
678         logger.debug("Initializing hue bridge handler.");
679         hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
680
681         String ip = hueBridgeConfig.getIpAddress();
682         if (ip == null || ip.isEmpty()) {
683             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
684                     "@text/offline.conf-error-no-ip-address");
685         } else {
686             if (hueBridge == null) {
687                 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
688                 hueBridge.setTimeout(5000);
689
690                 // Try a first connection that will fail, then try to authenticate,
691                 // and finally change the bridge status to ONLINE
692                 initJob = scheduler.submit(new PollingRunnable() {
693                     @Override
694                     protected void doConnectedRun() throws IOException, ApiException {
695                     }
696                 });
697             }
698             onUpdate();
699         }
700     }
701
702     public @Nullable String getUserName() {
703         return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
704     }
705
706     private synchronized void onUpdate() {
707         if (hueBridge != null) {
708             startLightPolling();
709             startSensorPolling();
710             startScenePolling();
711         }
712     }
713
714     /**
715      * This method is called whenever the connection to the {@link HueBridge} is lost.
716      */
717     public void onConnectionLost() {
718         logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
719         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
720     }
721
722     /**
723      * This method is called whenever the connection to the {@link HueBridge} is resumed.
724      *
725      * @throws ApiException if the physical device does not support this API call
726      * @throws IOException if the physical device could not be reached
727      */
728     private void onConnectionResumed() throws IOException, ApiException {
729         logger.debug("Bridge connection resumed.");
730
731         if (!propertiesInitializedSuccessfully) {
732             FullConfig fullConfig = hueBridge.getFullConfig();
733             Config config = fullConfig.getConfig();
734             if (config != null) {
735                 Map<String, String> properties = editProperties();
736                 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
737                 serialNumber = serialNumber.toLowerCase();
738                 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
739                 properties.put(PROPERTY_MODEL_ID, config.getModelId());
740                 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
741                 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
742                 updateProperties(properties);
743                 propertiesInitializedSuccessfully = true;
744             }
745         }
746     }
747
748     /**
749      * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
750      *
751      * @return True if USER_NAME was not null.
752      * @throws ApiException if the physical device does not support this API call
753      * @throws IOException if the physical device could not be reached
754      */
755     private boolean tryResumeBridgeConnection() throws IOException, ApiException {
756         logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
757         if (hueBridgeConfig.getUserName() == null) {
758             logger.warn(
759                     "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
760             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
761                     "@text/offline.conf-error-no-username");
762             return false;
763         } else {
764             onConnectionResumed();
765             return true;
766         }
767     }
768
769     /**
770      * This method is called whenever the connection to the {@link HueBridge} is available,
771      * but requests are not allowed due to a missing or invalid authentication.
772      * <p>
773      * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
774      * be requested from the bridge.
775      *
776      * @param bridge the hue bridge the connection is not authorized
777      * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
778      */
779     public boolean onNotAuthenticated() {
780         if (hueBridge == null) {
781             return false;
782         }
783         String userName = hueBridgeConfig.getUserName();
784         if (userName == null) {
785             createUser();
786         } else {
787             try {
788                 hueBridge.authenticate(userName);
789                 return true;
790             } catch (Exception e) {
791                 handleAuthenticationFailure(e, userName);
792             }
793         }
794         return false;
795     }
796
797     private void createUser() {
798         try {
799             String newUser = createUserOnPhysicalBridge();
800             updateBridgeThingConfiguration(newUser);
801         } catch (LinkButtonException ex) {
802             handleLinkButtonNotPressed(ex);
803         } catch (Exception ex) {
804             handleExceptionWhileCreatingUser(ex);
805         }
806     }
807
808     private String createUserOnPhysicalBridge() throws IOException, ApiException {
809         logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
810                 hueBridgeConfig.getIpAddress());
811         String userName = hueBridge.link(DEVICE_TYPE);
812         logger.info("User has been successfully added to Hue bridge.");
813         return userName;
814     }
815
816     private void updateBridgeThingConfiguration(String userName) {
817         Configuration config = editConfiguration();
818         config.put(USER_NAME, userName);
819         try {
820             updateConfiguration(config);
821             logger.debug("Updated configuration parameter '{}'", USER_NAME);
822             hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
823         } catch (IllegalStateException e) {
824             logger.trace("Configuration update failed.", e);
825             logger.warn("Unable to update configuration of Hue bridge.");
826             logger.warn("Please configure the user name manually.");
827         }
828     }
829
830     private void handleAuthenticationFailure(Exception ex, String userName) {
831         logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
832         logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
833         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
834                 "@text/offline.conf-error-invalid-username");
835     }
836
837     private void handleLinkButtonNotPressed(LinkButtonException ex) {
838         logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
839         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
840                 "@text/offline.conf-error-press-pairing-button");
841     }
842
843     private void handleExceptionWhileCreatingUser(Exception ex) {
844         logger.warn("Failed creating new user on Hue bridge", ex);
845         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
846                 "@text/offline.conf-error-creation-username");
847     }
848
849     @Override
850     public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
851         if (discoveryService == null) {
852             discoveryService = listener;
853             getFullLights().forEach(listener::addLightDiscovery);
854             getFullSensors().forEach(listener::addSensorDiscovery);
855             getFullGroups().forEach(listener::addGroupDiscovery);
856             return true;
857         }
858
859         return false;
860     }
861
862     @Override
863     public boolean unregisterDiscoveryListener() {
864         if (discoveryService != null) {
865             discoveryService = null;
866             return true;
867         }
868
869         return false;
870     }
871
872     @Override
873     public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
874         final String lightId = lightStatusListener.getLightId();
875         if (!lightStatusListeners.containsKey(lightId)) {
876             lightStatusListeners.put(lightId, lightStatusListener);
877             final FullLight lastLightState = lastLightStates.get(lightId);
878             if (lastLightState != null) {
879                 lightStatusListener.onLightAdded(lastLightState);
880             }
881
882             return true;
883         }
884         return false;
885     }
886
887     @Override
888     public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
889         return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
890     }
891
892     @Override
893     public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
894         final String sensorId = sensorStatusListener.getSensorId();
895         if (!sensorStatusListeners.containsKey(sensorId)) {
896             sensorStatusListeners.put(sensorId, sensorStatusListener);
897             final FullSensor lastSensorState = lastSensorStates.get(sensorId);
898             if (lastSensorState != null) {
899                 sensorStatusListener.onSensorAdded(lastSensorState);
900             }
901             return true;
902         }
903
904         return false;
905     }
906
907     @Override
908     public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
909         return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
910     }
911
912     @Override
913     public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
914         final String groupId = groupStatusListener.getGroupId();
915         if (!groupStatusListeners.containsKey(groupId)) {
916             groupStatusListeners.put(groupId, groupStatusListener);
917             final FullGroup lastGroupState = lastGroupStates.get(groupId);
918             if (lastGroupState != null) {
919                 groupStatusListener.onGroupAdded(lastGroupState);
920             }
921             return true;
922         }
923
924         return false;
925     }
926
927     @Override
928     public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
929         return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
930     }
931
932     /**
933      * Recall scene to all lights that belong to the scene.
934      *
935      * @param id the ID of the scene to activate
936      */
937     @Override
938     public void recallScene(String id) {
939         if (hueBridge != null) {
940             hueBridge.recallScene(id).thenAccept(result -> {
941                 try {
942                     hueBridge.handleErrors(result);
943                 } catch (Exception e) {
944                     logger.debug("Error while recalling scene: {}", e.getMessage());
945                 }
946             }).exceptionally(e -> {
947                 logger.debug("Error while recalling scene: {}", e.getMessage());
948                 return null;
949             });
950         } else {
951             logger.debug("No bridge connected or selected. Cannot activate scene.");
952         }
953     }
954
955     @Override
956     public @Nullable FullLight getLightById(String lightId) {
957         return lastLightStates.get(lightId);
958     }
959
960     @Override
961     public @Nullable FullSensor getSensorById(String sensorId) {
962         return lastSensorStates.get(sensorId);
963     }
964
965     @Override
966     public @Nullable FullGroup getGroupById(String groupId) {
967         return lastGroupStates.get(groupId);
968     }
969
970     public List<FullLight> getFullLights() {
971         List<FullLight> ret = withReAuthentication("search for new lights", () -> {
972             return hueBridge.getFullLights();
973         });
974         return ret != null ? ret : List.of();
975     }
976
977     public List<FullSensor> getFullSensors() {
978         List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
979             return hueBridge.getSensors();
980         });
981         return ret != null ? ret : List.of();
982     }
983
984     public List<FullGroup> getFullGroups() {
985         List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
986             return hueBridge.getGroups();
987         });
988         return ret != null ? ret : List.of();
989     }
990
991     public void startSearch() {
992         withReAuthentication("start search mode", () -> {
993             hueBridge.startSearch();
994             return null;
995         });
996     }
997
998     public void startSearch(List<String> serialNumbers) {
999         withReAuthentication("start search mode", () -> {
1000             hueBridge.startSearch(serialNumbers);
1001             return null;
1002         });
1003     }
1004
1005     private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1006         if (hueBridge != null) {
1007             try {
1008                 try {
1009                     return runnable.call();
1010                 } catch (UnauthorizedException | IllegalStateException e) {
1011                     lastBridgeConnectionState = false;
1012                     if (onNotAuthenticated()) {
1013                         return runnable.call();
1014                     }
1015                 }
1016             } catch (Exception e) {
1017                 logger.debug("Bridge cannot {}.", taskDescription, e);
1018             }
1019         }
1020         return null;
1021     }
1022
1023     private void notifyGroupSceneUpdate(List<Scene> scenes) {
1024         groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1025     }
1026
1027     public List<String> listScenesForConsole() {
1028         return consoleScenesList;
1029     }
1030
1031     @Override
1032     public Collection<ConfigStatusMessage> getConfigStatus() {
1033         // The bridge IP address to be used for checks
1034         // Check whether an IP address is provided
1035         String ip = hueBridgeConfig.getIpAddress();
1036         if (ip == null || ip.isEmpty()) {
1037             return List.of(ConfigStatusMessage.Builder.error(HOST)
1038                     .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());
1039         } else {
1040             return List.of();
1041         }
1042     }
1043 }