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