]> git.basschouten.com Git - openhab-addons.git/blob
c4baa76feb5a0d353f2275e75b8586cd8774af5d
[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                 // Try a first connection that will fail, then try to authenticate,
689                 // and finally change the bridge status to ONLINE
690                 initJob = scheduler.submit(new PollingRunnable() {
691                     @Override
692                     protected void doConnectedRun() throws IOException, ApiException {
693                     }
694                 });
695             }
696             onUpdate();
697         }
698     }
699
700     public @Nullable String getUserName() {
701         return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
702     }
703
704     private synchronized void onUpdate() {
705         if (hueBridge != null) {
706             startLightPolling();
707             startSensorPolling();
708             startScenePolling();
709         }
710     }
711
712     /**
713      * This method is called whenever the connection to the {@link HueBridge} is lost.
714      */
715     public void onConnectionLost() {
716         logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
717         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
718     }
719
720     /**
721      * This method is called whenever the connection to the {@link HueBridge} is resumed.
722      *
723      * @throws ApiException if the physical device does not support this API call
724      * @throws IOException if the physical device could not be reached
725      */
726     private void onConnectionResumed() throws IOException, ApiException {
727         logger.debug("Bridge connection resumed.");
728
729         if (!propertiesInitializedSuccessfully) {
730             FullConfig fullConfig = hueBridge.getFullConfig();
731             Config config = fullConfig.getConfig();
732             if (config != null) {
733                 Map<String, String> properties = editProperties();
734                 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
735                 serialNumber = serialNumber.toLowerCase();
736                 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
737                 properties.put(PROPERTY_MODEL_ID, config.getModelId());
738                 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
739                 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
740                 updateProperties(properties);
741                 propertiesInitializedSuccessfully = true;
742             }
743         }
744     }
745
746     /**
747      * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
748      *
749      * @return True if USER_NAME was not null.
750      * @throws ApiException if the physical device does not support this API call
751      * @throws IOException if the physical device could not be reached
752      */
753     private boolean tryResumeBridgeConnection() throws IOException, ApiException {
754         logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
755         if (hueBridgeConfig.getUserName() == null) {
756             logger.warn(
757                     "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
758             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
759                     "@text/offline.conf-error-no-username");
760             return false;
761         } else {
762             onConnectionResumed();
763             return true;
764         }
765     }
766
767     /**
768      * This method is called whenever the connection to the {@link HueBridge} is available,
769      * but requests are not allowed due to a missing or invalid authentication.
770      * <p>
771      * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
772      * be requested from the bridge.
773      *
774      * @param bridge the hue bridge the connection is not authorized
775      * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
776      */
777     public boolean onNotAuthenticated() {
778         if (hueBridge == null) {
779             return false;
780         }
781         String userName = hueBridgeConfig.getUserName();
782         if (userName == null) {
783             createUser();
784         } else {
785             try {
786                 hueBridge.authenticate(userName);
787                 return true;
788             } catch (Exception e) {
789                 handleAuthenticationFailure(e, userName);
790             }
791         }
792         return false;
793     }
794
795     private void createUser() {
796         try {
797             String newUser = createUserOnPhysicalBridge();
798             updateBridgeThingConfiguration(newUser);
799         } catch (LinkButtonException ex) {
800             handleLinkButtonNotPressed(ex);
801         } catch (Exception ex) {
802             handleExceptionWhileCreatingUser(ex);
803         }
804     }
805
806     private String createUserOnPhysicalBridge() throws IOException, ApiException {
807         logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
808                 hueBridgeConfig.getIpAddress());
809         String userName = hueBridge.link(DEVICE_TYPE);
810         logger.info("User has been successfully added to Hue bridge.");
811         return userName;
812     }
813
814     private void updateBridgeThingConfiguration(String userName) {
815         Configuration config = editConfiguration();
816         config.put(USER_NAME, userName);
817         try {
818             updateConfiguration(config);
819             logger.debug("Updated configuration parameter '{}'", USER_NAME);
820             hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
821         } catch (IllegalStateException e) {
822             logger.trace("Configuration update failed.", e);
823             logger.warn("Unable to update configuration of Hue bridge.");
824             logger.warn("Please configure the user name manually.");
825         }
826     }
827
828     private void handleAuthenticationFailure(Exception ex, String userName) {
829         logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
830         logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
831         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
832                 "@text/offline.conf-error-invalid-username");
833     }
834
835     private void handleLinkButtonNotPressed(LinkButtonException ex) {
836         logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
837         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
838                 "@text/offline.conf-error-press-pairing-button");
839     }
840
841     private void handleExceptionWhileCreatingUser(Exception ex) {
842         logger.warn("Failed creating new user on Hue bridge", ex);
843         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
844                 "@text/offline.conf-error-creation-username");
845     }
846
847     @Override
848     public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
849         if (discoveryService == null) {
850             discoveryService = listener;
851             getFullLights().forEach(listener::addLightDiscovery);
852             getFullSensors().forEach(listener::addSensorDiscovery);
853             getFullGroups().forEach(listener::addGroupDiscovery);
854             return true;
855         }
856
857         return false;
858     }
859
860     @Override
861     public boolean unregisterDiscoveryListener() {
862         if (discoveryService != null) {
863             discoveryService = null;
864             return true;
865         }
866
867         return false;
868     }
869
870     @Override
871     public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
872         final String lightId = lightStatusListener.getLightId();
873         if (!lightStatusListeners.containsKey(lightId)) {
874             lightStatusListeners.put(lightId, lightStatusListener);
875             final FullLight lastLightState = lastLightStates.get(lightId);
876             if (lastLightState != null) {
877                 lightStatusListener.onLightAdded(lastLightState);
878             }
879
880             return true;
881         }
882         return false;
883     }
884
885     @Override
886     public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
887         return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
888     }
889
890     @Override
891     public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
892         final String sensorId = sensorStatusListener.getSensorId();
893         if (!sensorStatusListeners.containsKey(sensorId)) {
894             sensorStatusListeners.put(sensorId, sensorStatusListener);
895             final FullSensor lastSensorState = lastSensorStates.get(sensorId);
896             if (lastSensorState != null) {
897                 sensorStatusListener.onSensorAdded(lastSensorState);
898             }
899             return true;
900         }
901
902         return false;
903     }
904
905     @Override
906     public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
907         return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
908     }
909
910     @Override
911     public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
912         final String groupId = groupStatusListener.getGroupId();
913         if (!groupStatusListeners.containsKey(groupId)) {
914             groupStatusListeners.put(groupId, groupStatusListener);
915             final FullGroup lastGroupState = lastGroupStates.get(groupId);
916             if (lastGroupState != null) {
917                 groupStatusListener.onGroupAdded(lastGroupState);
918             }
919             return true;
920         }
921
922         return false;
923     }
924
925     @Override
926     public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
927         return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
928     }
929
930     /**
931      * Recall scene to all lights that belong to the scene.
932      *
933      * @param id the ID of the scene to activate
934      */
935     @Override
936     public void recallScene(String id) {
937         if (hueBridge != null) {
938             hueBridge.recallScene(id).thenAccept(result -> {
939                 try {
940                     hueBridge.handleErrors(result);
941                 } catch (Exception e) {
942                     logger.debug("Error while recalling scene: {}", e.getMessage());
943                 }
944             }).exceptionally(e -> {
945                 logger.debug("Error while recalling scene: {}", e.getMessage());
946                 return null;
947             });
948         } else {
949             logger.debug("No bridge connected or selected. Cannot activate scene.");
950         }
951     }
952
953     @Override
954     public @Nullable FullLight getLightById(String lightId) {
955         return lastLightStates.get(lightId);
956     }
957
958     @Override
959     public @Nullable FullSensor getSensorById(String sensorId) {
960         return lastSensorStates.get(sensorId);
961     }
962
963     @Override
964     public @Nullable FullGroup getGroupById(String groupId) {
965         return lastGroupStates.get(groupId);
966     }
967
968     public List<FullLight> getFullLights() {
969         List<FullLight> ret = withReAuthentication("search for new lights", () -> {
970             return hueBridge.getFullLights();
971         });
972         return ret != null ? ret : List.of();
973     }
974
975     public List<FullSensor> getFullSensors() {
976         List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
977             return hueBridge.getSensors();
978         });
979         return ret != null ? ret : List.of();
980     }
981
982     public List<FullGroup> getFullGroups() {
983         List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
984             return hueBridge.getGroups();
985         });
986         return ret != null ? ret : List.of();
987     }
988
989     public void startSearch() {
990         withReAuthentication("start search mode", () -> {
991             hueBridge.startSearch();
992             return null;
993         });
994     }
995
996     public void startSearch(List<String> serialNumbers) {
997         withReAuthentication("start search mode", () -> {
998             hueBridge.startSearch(serialNumbers);
999             return null;
1000         });
1001     }
1002
1003     private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1004         if (hueBridge != null) {
1005             try {
1006                 try {
1007                     return runnable.call();
1008                 } catch (UnauthorizedException | IllegalStateException e) {
1009                     lastBridgeConnectionState = false;
1010                     if (onNotAuthenticated()) {
1011                         return runnable.call();
1012                     }
1013                 }
1014             } catch (Exception e) {
1015                 logger.debug("Bridge cannot {}.", taskDescription, e);
1016             }
1017         }
1018         return null;
1019     }
1020
1021     private void notifyGroupSceneUpdate(List<Scene> scenes) {
1022         groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1023     }
1024
1025     public List<String> listScenesForConsole() {
1026         return consoleScenesList;
1027     }
1028
1029     @Override
1030     public Collection<ConfigStatusMessage> getConfigStatus() {
1031         // The bridge IP address to be used for checks
1032         // Check whether an IP address is provided
1033         String ip = hueBridgeConfig.getIpAddress();
1034         if (ip == null || ip.isEmpty()) {
1035             return List.of(ConfigStatusMessage.Builder.error(HOST)
1036                     .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());
1037         } else {
1038             return List.of();
1039         }
1040     }
1041 }