2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hue.internal.handler;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
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;
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;
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;
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.
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
96 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
98 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
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);
103 private static final String DEVICE_TYPE = "openHAB";
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;
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<>();
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<>();
121 final ReentrantLock pollingLock = new ReentrantLock();
123 abstract class PollingRunnable implements Runnable {
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();
133 lastBridgeConnectionState = tryResumeBridgeConnection();
135 if (lastBridgeConnectionState) {
137 if (thing.getStatus() != ThingStatus.ONLINE) {
138 updateStatus(ThingStatus.ONLINE);
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);
149 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
150 lastBridgeConnectionState = false;
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;
159 } catch (RuntimeException e) {
160 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
161 lastBridgeConnectionState = false;
164 pollingLock.unlock();
168 private boolean isReachable(String ipAddress) {
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
174 // If there is no connection, this line will fail
175 hueBridge.authenticate("invalid");
176 } catch (ConfigurationException | IOException e) {
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");
189 protected abstract void doConnectedRun() throws IOException, ApiException;
192 private final Runnable sensorPollingRunnable = new PollingRunnable() {
194 protected void doConnectedRun() throws IOException, ApiException {
195 Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
197 final HueDeviceDiscoveryService discovery = discoveryService;
199 for (final FullSensor sensor : hueBridge.getSensors()) {
200 String sensorId = sensor.getId();
202 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
203 if (sensorStatusListener == null) {
204 logger.trace("Hue sensor '{}' added.", sensorId);
206 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
207 discovery.addSensorDiscovery(sensor);
210 lastSensorStates.put(sensorId, sensor);
212 if (sensorStatusListener.onSensorStateChanged(sensor)) {
213 lastSensorStates.put(sensorId, sensor);
216 lastSensorStateCopy.remove(sensorId);
219 // Check for removed sensors
220 lastSensorStateCopy.forEach((sensorId, sensor) -> {
221 logger.trace("Hue sensor '{}' removed.", sensorId);
222 lastSensorStates.remove(sensorId);
224 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
225 if (sensorStatusListener != null) {
226 sensorStatusListener.onSensorRemoved();
229 if (discovery != null && sensor != null) {
230 discovery.removeSensorDiscovery(sensor);
236 private final Runnable lightPollingRunnable = new PollingRunnable() {
238 protected void doConnectedRun() throws IOException, ApiException {
243 private void updateLights() throws IOException, ApiException {
244 Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
246 List<FullLight> lights;
247 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
248 lights = hueBridge.getFullLights();
250 lights = hueBridge.getFullConfig().getLights();
253 final HueDeviceDiscoveryService discovery = discoveryService;
255 for (final FullLight fullLight : lights) {
256 final String lightId = fullLight.getId();
258 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
259 if (lightStatusListener == null) {
260 logger.trace("Hue light '{}' added.", lightId);
262 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
263 discovery.addLightDiscovery(fullLight);
266 lastLightStates.put(lightId, fullLight);
268 if (lightStatusListener.onLightStateChanged(fullLight)) {
269 lastLightStates.put(lightId, fullLight);
272 lastLightStateCopy.remove(lightId);
275 // Check for removed lights
276 lastLightStateCopy.forEach((lightId, light) -> {
277 logger.trace("Hue light '{}' removed.", lightId);
278 lastLightStates.remove(lightId);
280 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
281 if (lightStatusListener != null) {
282 lightStatusListener.onLightRemoved();
285 if (discovery != null && light != null) {
286 discovery.removeLightDiscovery(light);
291 private void updateGroups() throws IOException, ApiException {
292 Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
294 List<FullGroup> groups = hueBridge.getGroups();
296 final HueDeviceDiscoveryService discovery = discoveryService;
298 for (final FullGroup fullGroup : groups) {
299 State groupState = new State();
303 State colorRef = null;
304 HSBType firstColorHsb = null;
305 for (String lightId : fullGroup.getLightIds()) {
306 FullLight light = lastLightStates.get(lightId);
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()) {
315 sumBri += lightState.getBrightness();
317 if (lightState.getColorMode() != null) {
318 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
319 if (firstColorHsb == null) {
321 firstColorHsb = lightHsb;
322 colorRef = lightState;
323 } else if (!lightHsb.equals(firstColorHsb)) {
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());
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(),
345 String groupId = fullGroup.getId();
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());
352 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
353 discovery.addGroupDiscovery(fullGroup);
356 lastGroupStates.put(groupId, fullGroup);
358 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
359 lastGroupStates.put(groupId, fullGroup);
362 lastGroupStateCopy.remove(groupId);
365 // Check for removed groups
366 lastGroupStateCopy.forEach((groupId, group) -> {
367 logger.trace("Hue group '{}' removed.", groupId);
368 lastGroupStates.remove(groupId);
370 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
371 if (groupStatusListener != null) {
372 groupStatusListener.onGroupRemoved();
375 if (discovery != null && group != null) {
376 discovery.removeGroupDiscovery(group);
382 private final Runnable scenePollingRunnable = new PollingRunnable() {
384 protected void doConnectedRun() throws IOException, ApiException {
385 List<Scene> scenes = hueBridge.getScenes();
386 logger.trace("Scenes detected: {}", scenes);
388 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
389 notifyGroupSceneUpdate(scenes);
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),
399 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
400 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
404 private boolean lastBridgeConnectionState = false;
406 private boolean propertiesInitializedSuccessfully = false;
408 private @Nullable Future<?> initJob;
409 private @Nullable ScheduledFuture<?> lightPollingJob;
410 private @Nullable ScheduledFuture<?> sensorPollingJob;
411 private @Nullable ScheduledFuture<?> scenePollingJob;
413 private @NonNullByDefault({}) HueBridge hueBridge = null;
414 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
416 private List<String> consoleScenesList = new ArrayList<>();
418 public HueBridgeHandler(Bridge bridge, HttpClient httpClient,
419 HueStateDescriptionProvider stateDescriptionOptionProvider, TranslationProvider i18nProvider,
420 LocaleProvider localeProvider) {
422 this.httpClient = httpClient;
423 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
424 this.i18nProvider = i18nProvider;
425 this.localeProvider = localeProvider;
429 public Collection<Class<? extends ThingHandlerService>> getServices() {
430 return Set.of(HueDeviceDiscoveryService.class);
434 public void handleCommand(ChannelUID channelUID, Command command) {
435 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
436 recallScene(command.toString());
441 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
443 if (hueBridge != null) {
444 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
445 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
447 hueBridge.handleErrors(result);
448 listener.setPollBypass(fadeTime);
449 } catch (Exception e) {
450 listener.unsetPollBypass();
451 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
453 }).exceptionally(e -> {
454 listener.unsetPollBypass();
455 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
459 logger.debug("No bridge connected or selected. Cannot set light state.");
464 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
465 if (hueBridge != null) {
466 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
468 hueBridge.handleErrors(result);
469 } catch (Exception e) {
470 handleSensorUpdateException(sensor, e);
472 }).exceptionally(e -> {
473 handleSensorUpdateException(sensor, e);
477 logger.debug("No bridge connected or selected. Cannot set sensor state.");
482 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
483 if (hueBridge != null) {
484 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
486 hueBridge.handleErrors(result);
487 } catch (Exception e) {
488 handleSensorUpdateException(sensor, e);
490 }).exceptionally(e -> {
491 handleSensorUpdateException(sensor, e);
495 logger.debug("No bridge connected or selected. Cannot set sensor config.");
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 -> {
505 hueBridge.handleErrors(result);
506 setGroupPollBypass(group, fadeTime);
507 } catch (Exception e) {
508 unsetGroupPollBypass(group);
509 handleGroupUpdateException(group, e);
511 }).exceptionally(e -> {
512 unsetGroupPollBypass(group);
513 handleGroupUpdateException(group, e);
517 logger.debug("No bridge connected or selected. Cannot set group state.");
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);
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();
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).
547 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
548 updateLightState(listener, light, stateUpdate, fadeTime);
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);
556 listener.onLightGone();
558 handleThingUpdateException("light", e);
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);
569 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
570 if (listener != null) {
571 listener.onSensorGone();
574 handleThingUpdateException("sensor", e);
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);
585 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
586 if (listener != null) {
587 listener.onGroupGone();
590 handleThingUpdateException("group", e);
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());
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);
615 lightPollingInterval = configPollingInterval;
617 // Delay the first execution to give a chance to have all light and group things registered
618 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
623 private void stopLightPolling() {
624 ScheduledFuture<?> job = lightPollingJob;
628 lightPollingJob = null;
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);
642 sensorPollingInterval = configSensorPollingInterval;
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);
651 private void stopSensorPolling() {
652 ScheduledFuture<?> job = sensorPollingJob;
656 sensorPollingJob = null;
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,
668 private void stopScenePolling() {
669 ScheduledFuture<?> job = scenePollingJob;
673 scenePollingJob = null;
677 public void dispose() {
678 logger.debug("Disposing Hue Bridge handler ...");
679 Future<?> job = initJob;
686 if (hueBridge != null) {
689 ServiceRegistration<?> localServiceRegistration = serviceRegistration;
690 if (localServiceRegistration != null) {
691 // remove trustmanager service
692 localServiceRegistration.unregister();
693 serviceRegistration = null;
695 propertiesInitializedSuccessfully = false;
699 public void initialize() {
700 logger.debug("Initializing Hue Bridge handler ...");
701 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
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");
708 if (hueBridge == null) {
709 hueBridge = new HueBridge(httpClient, ip, hueBridgeConfig.getPort(), hueBridgeConfig.protocol,
712 updateStatus(ThingStatus.UNKNOWN);
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);
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");
727 serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext().registerService(
728 TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
741 public @Nullable String getUserName() {
742 return hueBridgeConfig == null ? null : hueBridgeConfig.userName;
745 private synchronized void onUpdate() {
747 startSensorPolling();
752 * This method is called whenever the connection to the {@link HueBridge} is lost.
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");
760 * This method is called whenever the connection to the {@link HueBridge} is resumed.
762 * @throws ApiException if the physical device does not support this API call
763 * @throws IOException if the physical device could not be reached
765 private void onConnectionResumed() throws IOException, ApiException {
766 logger.debug("Bridge connection resumed.");
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;
786 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
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
792 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
793 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
794 if (hueBridgeConfig.userName == null) {
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");
801 onConnectionResumed();
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.
810 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
811 * be requested from the bridge.
813 * @param bridge the Hue Bridge the connection is not authorized
814 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
816 public boolean onNotAuthenticated() {
817 if (hueBridge == null) {
820 String userName = hueBridgeConfig.userName;
821 if (userName == null) {
825 hueBridge.authenticate(userName);
827 } catch (ConfigurationException e) {
828 handleConfigurationFailure(e);
829 } catch (Exception e) {
831 handleAuthenticationFailure(e, userName);
837 private void createUser() {
839 String newUser = createUserOnPhysicalBridge();
840 updateBridgeThingConfiguration(newUser);
841 } catch (LinkButtonException ex) {
842 handleLinkButtonNotPressed(ex);
843 } catch (Exception ex) {
844 handleExceptionWhileCreatingUser(ex);
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.");
856 private void updateBridgeThingConfiguration(String userName) {
857 Configuration config = editConfiguration();
858 config.put(USER_NAME, userName);
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.");
870 private void handleConfigurationFailure(ConfigurationException ex) {
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());
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");
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");
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");
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);
909 public boolean unregisterDiscoveryListener() {
910 if (discoveryService != null) {
911 discoveryService = null;
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);
934 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
935 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
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);
954 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
955 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
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);
974 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
975 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
979 * Recall scene to all lights that belong to the scene.
981 * @param id the ID of the scene to activate
984 public void recallScene(String id) {
985 if (hueBridge != null) {
986 hueBridge.recallScene(id).thenAccept(result -> {
988 hueBridge.handleErrors(result);
989 } catch (Exception e) {
990 logger.debug("Error while recalling scene: {}", e.getMessage());
992 }).exceptionally(e -> {
993 logger.debug("Error while recalling scene: {}", e.getMessage());
997 logger.debug("No bridge connected or selected. Cannot activate scene.");
1002 public @Nullable FullLight getLightById(String lightId) {
1003 return lastLightStates.get(lightId);
1007 public @Nullable FullSensor getSensorById(String sensorId) {
1008 return lastSensorStates.get(sensorId);
1012 public @Nullable FullGroup getGroupById(String groupId) {
1013 return lastGroupStates.get(groupId);
1016 public List<FullLight> getFullLights() {
1017 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
1018 return hueBridge.getFullLights();
1020 return ret != null ? ret : List.of();
1023 public List<FullSensor> getFullSensors() {
1024 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
1025 return hueBridge.getSensors();
1027 return ret != null ? ret : List.of();
1030 public List<FullGroup> getFullGroups() {
1031 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
1032 return hueBridge.getGroups();
1034 return ret != null ? ret : List.of();
1037 public void startSearch() {
1038 withReAuthentication("start search mode", () -> {
1039 hueBridge.startSearch();
1044 public void startSearch(List<String> serialNumbers) {
1045 withReAuthentication("start search mode", () -> {
1046 hueBridge.startSearch(serialNumbers);
1051 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1052 if (hueBridge != null) {
1055 return runnable.call();
1056 } catch (UnauthorizedException | IllegalStateException e) {
1057 lastBridgeConnectionState = false;
1058 if (onNotAuthenticated()) {
1059 return runnable.call();
1062 } catch (Exception e) {
1063 logger.debug("Bridge cannot {}.", taskDescription, e);
1069 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1070 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1073 public List<String> listScenesForConsole() {
1074 return consoleScenesList;
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);
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());
1092 public TranslationProvider getI18nProvider() {
1093 return i18nProvider;
1096 public LocaleProvider getLocaleProvider() {
1097 return localeProvider;