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