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