]> git.basschouten.com Git - openhab-addons.git/blob
0a9a577c9e8b1f32e2651bb9a9d6f80ed2fcfef8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 HueStateDescriptionProvider 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(), e.getMessage(), e);
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                 String message = e.getMessage();
168                 return message != null && //
169                         !message.contains("SocketTimeout") && //
170                         !message.contains("ConnectException") && //
171                         !message.contains("SocketException") && //
172                         !message.contains("NoRouteToHostException");
173             }
174             return true;
175         }
176
177         protected abstract void doConnectedRun() throws IOException, ApiException;
178     }
179
180     private final Runnable sensorPollingRunnable = new PollingRunnable() {
181         @Override
182         protected void doConnectedRun() throws IOException, ApiException {
183             Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
184
185             final HueDeviceDiscoveryService discovery = discoveryService;
186
187             for (final FullSensor sensor : hueBridge.getSensors()) {
188                 String sensorId = sensor.getId();
189
190                 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
191                 if (sensorStatusListener == null) {
192                     logger.trace("Hue sensor '{}' added.", sensorId);
193
194                     if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
195                         discovery.addSensorDiscovery(sensor);
196                     }
197
198                     lastSensorStates.put(sensorId, sensor);
199                 } else {
200                     if (sensorStatusListener.onSensorStateChanged(sensor)) {
201                         lastSensorStates.put(sensorId, sensor);
202                     }
203                 }
204                 lastSensorStateCopy.remove(sensorId);
205             }
206
207             // Check for removed sensors
208             lastSensorStateCopy.forEach((sensorId, sensor) -> {
209                 logger.trace("Hue sensor '{}' removed.", sensorId);
210                 lastSensorStates.remove(sensorId);
211
212                 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
213                 if (sensorStatusListener != null) {
214                     sensorStatusListener.onSensorRemoved();
215                 }
216
217                 if (discovery != null && sensor != null) {
218                     discovery.removeSensorDiscovery(sensor);
219                 }
220             });
221         }
222     };
223
224     private final Runnable lightPollingRunnable = new PollingRunnable() {
225         @Override
226         protected void doConnectedRun() throws IOException, ApiException {
227             updateLights();
228             updateGroups();
229         }
230
231         private void updateLights() throws IOException, ApiException {
232             Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
233
234             List<FullLight> lights;
235             if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
236                 lights = hueBridge.getFullLights();
237             } else {
238                 lights = hueBridge.getFullConfig().getLights();
239             }
240
241             final HueDeviceDiscoveryService discovery = discoveryService;
242
243             for (final FullLight fullLight : lights) {
244                 final String lightId = fullLight.getId();
245
246                 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
247                 if (lightStatusListener == null) {
248                     logger.trace("Hue light '{}' added.", lightId);
249
250                     if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
251                         discovery.addLightDiscovery(fullLight);
252                     }
253
254                     lastLightStates.put(lightId, fullLight);
255                 } else {
256                     if (lightStatusListener.onLightStateChanged(fullLight)) {
257                         lastLightStates.put(lightId, fullLight);
258                     }
259                 }
260                 lastLightStateCopy.remove(lightId);
261             }
262
263             // Check for removed lights
264             lastLightStateCopy.forEach((lightId, light) -> {
265                 logger.trace("Hue light '{}' removed.", lightId);
266                 lastLightStates.remove(lightId);
267
268                 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
269                 if (lightStatusListener != null) {
270                     lightStatusListener.onLightRemoved();
271                 }
272
273                 if (discovery != null && light != null) {
274                     discovery.removeLightDiscovery(light);
275                 }
276             });
277         }
278
279         private void updateGroups() throws IOException, ApiException {
280             Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
281
282             List<FullGroup> groups = hueBridge.getGroups();
283
284             final HueDeviceDiscoveryService discovery = discoveryService;
285
286             for (final FullGroup fullGroup : groups) {
287                 State groupState = new State();
288                 boolean on = false;
289                 int sumBri = 0;
290                 int nbBri = 0;
291                 State colorRef = null;
292                 HSBType firstColorHsb = null;
293                 for (String lightId : fullGroup.getLightIds()) {
294                     FullLight light = lastLightStates.get(lightId);
295                     if (light != null) {
296                         final State lightState = light.getState();
297                         logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
298                                 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
299                                 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
300                                 lightState.getColorMode(), lightState.getXY());
301                         if (lightState.isOn()) {
302                             on = true;
303                             sumBri += lightState.getBrightness();
304                             nbBri++;
305                             if (lightState.getColorMode() != null) {
306                                 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
307                                 if (firstColorHsb == null) {
308                                     // first color light
309                                     firstColorHsb = lightHsb;
310                                     colorRef = lightState;
311                                 } else if (!lightHsb.equals(firstColorHsb)) {
312                                     colorRef = null;
313                                 }
314                             }
315                         }
316                     }
317                 }
318                 groupState.setOn(on);
319                 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
320                 if (colorRef != null) {
321                     groupState.setColormode(colorRef.getColorMode());
322                     groupState.setHue(colorRef.getHue());
323                     groupState.setSaturation(colorRef.getSaturation());
324                     groupState.setColorTemperature(colorRef.getColorTemperature());
325                     groupState.setXY(colorRef.getXY());
326                 }
327                 fullGroup.setState(groupState);
328                 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
329                         fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
330                         groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
331                         groupState.getXY());
332
333                 String groupId = fullGroup.getId();
334
335                 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
336                 if (groupStatusListener == null) {
337                     logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
338                             fullGroup.getLightIds().size());
339
340                     if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
341                         discovery.addGroupDiscovery(fullGroup);
342                     }
343
344                     lastGroupStates.put(groupId, fullGroup);
345                 } else {
346                     if (groupStatusListener.onGroupStateChanged(fullGroup)) {
347                         lastGroupStates.put(groupId, fullGroup);
348                     }
349                 }
350                 lastGroupStateCopy.remove(groupId);
351             }
352
353             // Check for removed groups
354             lastGroupStateCopy.forEach((groupId, group) -> {
355                 logger.trace("Hue group '{}' removed.", groupId);
356                 lastGroupStates.remove(groupId);
357
358                 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
359                 if (groupStatusListener != null) {
360                     groupStatusListener.onGroupRemoved();
361                 }
362
363                 if (discovery != null && group != null) {
364                     discovery.removeGroupDiscovery(group);
365                 }
366             });
367         }
368     };
369
370     private final Runnable scenePollingRunnable = new PollingRunnable() {
371         @Override
372         protected void doConnectedRun() throws IOException, ApiException {
373             List<Scene> scenes = hueBridge.getScenes();
374             logger.trace("Scenes detected: {}", scenes);
375
376             setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
377             notifyGroupSceneUpdate(scenes);
378         }
379
380         private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
381             Map<String, String> groupNames = groups.entrySet().stream()
382                     .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
383             List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
384                     .collect(Collectors.toList());
385             stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
386                     stateOptions);
387             consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
388                     + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
389         }
390     };
391
392     private boolean lastBridgeConnectionState = false;
393
394     private boolean propertiesInitializedSuccessfully = false;
395
396     private @Nullable Future<?> initJob;
397     private @Nullable ScheduledFuture<?> lightPollingJob;
398     private @Nullable ScheduledFuture<?> sensorPollingJob;
399     private @Nullable ScheduledFuture<?> scenePollingJob;
400
401     private @NonNullByDefault({}) HueBridge hueBridge = null;
402     private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
403
404     private List<String> consoleScenesList = new ArrayList<>();
405
406     public HueBridgeHandler(Bridge bridge, HueStateDescriptionProvider stateDescriptionOptionProvider) {
407         super(bridge);
408         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
409     }
410
411     @Override
412     public Collection<Class<? extends ThingHandlerService>> getServices() {
413         return Collections.singleton(HueDeviceDiscoveryService.class);
414     }
415
416     @Override
417     public void handleCommand(ChannelUID channelUID, Command command) {
418         if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
419             recallScene(command.toString());
420         }
421     }
422
423     @Override
424     public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
425             long fadeTime) {
426         if (hueBridge != null) {
427             listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
428             hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
429                 try {
430                     hueBridge.handleErrors(result);
431                     listener.setPollBypass(fadeTime);
432                 } catch (Exception e) {
433                     listener.unsetPollBypass();
434                     handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
435                 }
436             }).exceptionally(e -> {
437                 listener.unsetPollBypass();
438                 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
439                 return null;
440             });
441         } else {
442             logger.debug("No bridge connected or selected. Cannot set light state.");
443         }
444     }
445
446     @Override
447     public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
448         if (hueBridge != null) {
449             hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
450                 try {
451                     hueBridge.handleErrors(result);
452                 } catch (Exception e) {
453                     handleSensorUpdateException(sensor, e);
454                 }
455             }).exceptionally(e -> {
456                 handleSensorUpdateException(sensor, e);
457                 return null;
458             });
459         } else {
460             logger.debug("No bridge connected or selected. Cannot set sensor state.");
461         }
462     }
463
464     @Override
465     public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
466         if (hueBridge != null) {
467             hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
468                 try {
469                     hueBridge.handleErrors(result);
470                 } catch (Exception e) {
471                     handleSensorUpdateException(sensor, e);
472                 }
473             }).exceptionally(e -> {
474                 handleSensorUpdateException(sensor, e);
475                 return null;
476             });
477         } else {
478             logger.debug("No bridge connected or selected. Cannot set sensor config.");
479         }
480     }
481
482     @Override
483     public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
484         if (hueBridge != null) {
485             setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
486             hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
487                 try {
488                     hueBridge.handleErrors(result);
489                     setGroupPollBypass(group, fadeTime);
490                 } catch (Exception e) {
491                     unsetGroupPollBypass(group);
492                     handleGroupUpdateException(group, e);
493                 }
494             }).exceptionally(e -> {
495                 unsetGroupPollBypass(group);
496                 handleGroupUpdateException(group, e);
497                 return null;
498             });
499         } else {
500             logger.debug("No bridge connected or selected. Cannot set group state.");
501         }
502     }
503
504     private void setGroupPollBypass(FullGroup group, long bypassTime) {
505         group.getLightIds().forEach((lightId) -> {
506             final LightStatusListener listener = lightStatusListeners.get(lightId);
507             if (listener != null) {
508                 listener.setPollBypass(bypassTime);
509             }
510         });
511     }
512
513     private void unsetGroupPollBypass(FullGroup group) {
514         group.getLightIds().forEach((lightId) -> {
515             final LightStatusListener listener = lightStatusListeners.get(lightId);
516             if (listener != null) {
517                 listener.unsetPollBypass();
518             }
519         });
520     }
521
522     private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
523             long fadeTime, Throwable e) {
524         if (e instanceof DeviceOffException) {
525             if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
526                 // If there is only a change of the color temperature, we do not want the light
527                 // to be turned on (i.e. change its brightness).
528                 return;
529             } else {
530                 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
531                 updateLightState(listener, light, stateUpdate, fadeTime);
532             }
533         } else if (e instanceof EntityNotAvailableException) {
534             logger.debug("Error while accessing light: {}", e.getMessage(), e);
535             final HueDeviceDiscoveryService discovery = discoveryService;
536             if (discovery != null) {
537                 discovery.removeLightDiscovery(light);
538             }
539             listener.onLightGone();
540         } else {
541             handleThingUpdateException("light", e);
542         }
543     }
544
545     private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
546         if (e instanceof EntityNotAvailableException) {
547             logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
548             final HueDeviceDiscoveryService discovery = discoveryService;
549             if (discovery != null) {
550                 discovery.removeSensorDiscovery(sensor);
551             }
552             final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
553             if (listener != null) {
554                 listener.onSensorGone();
555             }
556         } else {
557             handleThingUpdateException("sensor", e);
558         }
559     }
560
561     private void handleGroupUpdateException(FullGroup group, Throwable e) {
562         if (e instanceof EntityNotAvailableException) {
563             logger.debug("Error while accessing group: {}", e.getMessage(), e);
564             final HueDeviceDiscoveryService discovery = discoveryService;
565             if (discovery != null) {
566                 discovery.removeGroupDiscovery(group);
567             }
568             final GroupStatusListener listener = groupStatusListeners.get(group.getId());
569             if (listener != null) {
570                 listener.onGroupGone();
571             }
572         } else {
573             handleThingUpdateException("group", e);
574         }
575     }
576
577     private void handleThingUpdateException(String thingType, Throwable e) {
578         if (e instanceof IOException) {
579             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
580         } else if (e instanceof ApiException) {
581             // This should not happen - if it does, it is most likely some bug that should be reported.
582             logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
583         } else if (e instanceof IllegalStateException) {
584             logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
585         }
586     }
587
588     private void startLightPolling() {
589         ScheduledFuture<?> job = lightPollingJob;
590         if (job == null || job.isCancelled()) {
591             long lightPollingInterval;
592             int configPollingInterval = hueBridgeConfig.getPollingInterval();
593             if (configPollingInterval < 1) {
594                 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
595                 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
596                         lightPollingInterval);
597             } else {
598                 lightPollingInterval = configPollingInterval;
599             }
600             // Delay the first execution to give a chance to have all light and group things registered
601             lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
602                     TimeUnit.SECONDS);
603         }
604     }
605
606     private void stopLightPolling() {
607         ScheduledFuture<?> job = lightPollingJob;
608         if (job != null) {
609             job.cancel(true);
610         }
611         lightPollingJob = null;
612     }
613
614     private void startSensorPolling() {
615         ScheduledFuture<?> job = sensorPollingJob;
616         if (job == null || job.isCancelled()) {
617             int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
618             if (configSensorPollingInterval > 0) {
619                 long sensorPollingInterval;
620                 if (configSensorPollingInterval < 50) {
621                     sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
622                     logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
623                             sensorPollingInterval);
624                 } else {
625                     sensorPollingInterval = configSensorPollingInterval;
626                 }
627                 // Delay the first execution to give a chance to have all sensor things registered
628                 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
629                         TimeUnit.MILLISECONDS);
630             }
631         }
632     }
633
634     private void stopSensorPolling() {
635         ScheduledFuture<?> job = sensorPollingJob;
636         if (job != null) {
637             job.cancel(true);
638         }
639         sensorPollingJob = null;
640     }
641
642     private void startScenePolling() {
643         ScheduledFuture<?> job = scenePollingJob;
644         if (job == null || job.isCancelled()) {
645             // Delay the first execution to give a chance to have all group things registered
646             scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
647                     TimeUnit.SECONDS);
648         }
649     }
650
651     private void stopScenePolling() {
652         ScheduledFuture<?> job = scenePollingJob;
653         if (job != null) {
654             job.cancel(true);
655         }
656         scenePollingJob = null;
657     }
658
659     @Override
660     public void dispose() {
661         logger.debug("Handler disposed.");
662         Future<?> job = initJob;
663         if (job != null) {
664             job.cancel(true);
665         }
666         stopLightPolling();
667         stopSensorPolling();
668         stopScenePolling();
669         if (hueBridge != null) {
670             hueBridge = null;
671         }
672     }
673
674     @Override
675     public void initialize() {
676         logger.debug("Initializing hue bridge handler.");
677         hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
678
679         String ip = hueBridgeConfig.getIpAddress();
680         if (ip == null || ip.isEmpty()) {
681             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
682                     "@text/offline.conf-error-no-ip-address");
683         } else {
684             if (hueBridge == null) {
685                 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
686                 hueBridge.setTimeout(5000);
687
688                 updateStatus(ThingStatus.UNKNOWN);
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 }