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.time.Instant;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.List;
26 import java.util.concurrent.Callable;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.locks.ReentrantLock;
33 import java.util.stream.Collectors;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.eclipse.jetty.client.HttpClient;
38 import org.openhab.binding.hue.internal.config.HueBridgeConfig;
39 import org.openhab.binding.hue.internal.connection.HueBridge;
40 import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
41 import org.openhab.binding.hue.internal.discovery.HueDeviceDiscoveryService;
42 import org.openhab.binding.hue.internal.dto.ApiVersionUtils;
43 import org.openhab.binding.hue.internal.dto.Config;
44 import org.openhab.binding.hue.internal.dto.ConfigUpdate;
45 import org.openhab.binding.hue.internal.dto.FullConfig;
46 import org.openhab.binding.hue.internal.dto.FullGroup;
47 import org.openhab.binding.hue.internal.dto.FullLight;
48 import org.openhab.binding.hue.internal.dto.FullSensor;
49 import org.openhab.binding.hue.internal.dto.Scene;
50 import org.openhab.binding.hue.internal.dto.State;
51 import org.openhab.binding.hue.internal.dto.StateUpdate;
52 import org.openhab.binding.hue.internal.exceptions.ApiException;
53 import org.openhab.binding.hue.internal.exceptions.DeviceOffException;
54 import org.openhab.binding.hue.internal.exceptions.EmptyResponseException;
55 import org.openhab.binding.hue.internal.exceptions.EntityNotAvailableException;
56 import org.openhab.binding.hue.internal.exceptions.LinkButtonException;
57 import org.openhab.binding.hue.internal.exceptions.UnauthorizedException;
58 import org.openhab.core.config.core.Configuration;
59 import org.openhab.core.config.core.status.ConfigStatusMessage;
60 import org.openhab.core.i18n.CommunicationException;
61 import org.openhab.core.i18n.ConfigurationException;
62 import org.openhab.core.i18n.LocaleProvider;
63 import org.openhab.core.i18n.TranslationProvider;
64 import org.openhab.core.io.net.http.TlsTrustManagerProvider;
65 import org.openhab.core.library.types.HSBType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.StringType;
68 import org.openhab.core.thing.Bridge;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingTypeUID;
73 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
74 import org.openhab.core.thing.binding.ThingHandlerService;
75 import org.openhab.core.types.Command;
76 import org.openhab.core.types.StateOption;
77 import org.osgi.framework.FrameworkUtil;
78 import org.osgi.framework.ServiceRegistration;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
83 * {@link HueBridgeHandler} is the handler for a Hue Bridge and connects it to
84 * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
86 * @author Dennis Nobel - Initial contribution
87 * @author Oliver Libutzki - Adjustments
88 * @author Kai Kreuzer - improved state handling
89 * @author Andre Fuechsel - implemented getFullLights(), startSearch()
90 * @author Thomas Höfer - added thing properties
91 * @author Stefan Bußweiler - Added new thing status handling
92 * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
93 * @author Denis Dudnik - switched to internally integrated source of Jue library
94 * @author Samuel Leisering - Added support for sensor API
95 * @author Christoph Weitkamp - Added support for sensor API
96 * @author Laurent Garnier - Added support for groups
99 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
101 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
103 private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
104 private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
106 private static final String DEVICE_TYPE = "openHAB";
108 private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
109 private @Nullable ServiceRegistration<?> serviceRegistration;
110 private final HttpClient httpClient;
111 private final HueStateDescriptionProvider stateDescriptionOptionProvider;
112 private final TranslationProvider i18nProvider;
113 private final LocaleProvider localeProvider;
115 private final Map<String, FullLight> lastLightStates = new ConcurrentHashMap<>();
116 private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
117 private final Map<String, FullGroup> lastGroupStates = new ConcurrentHashMap<>();
119 private @Nullable HueDeviceDiscoveryService discoveryService;
120 private final Map<String, LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
121 private final Map<String, SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
122 private final Map<String, GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
124 private List<Scene> lastScenes = new CopyOnWriteArrayList<>();
125 private Instant lastScenesRetrieval = Instant.MIN;
127 final ReentrantLock pollingLock = new ReentrantLock();
129 abstract class PollingRunnable implements Runnable {
134 if (!lastBridgeConnectionState) {
135 // if user is not set in configuration try to create a new user on Hue Bridge
136 if (hueBridgeConfig.userName == null) {
137 hueBridge.getFullConfig();
139 lastBridgeConnectionState = tryResumeBridgeConnection();
141 if (lastBridgeConnectionState) {
143 if (thing.getStatus() != ThingStatus.ONLINE) {
144 updateStatus(ThingStatus.ONLINE);
147 } catch (ConfigurationException e) {
148 handleConfigurationFailure(e);
149 } catch (UnauthorizedException | IllegalStateException e) {
150 if (isReachable(hueBridge.getIPAddress())) {
151 lastBridgeConnectionState = false;
152 if (onNotAuthenticated()) {
153 updateStatus(ThingStatus.ONLINE);
155 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
156 lastBridgeConnectionState = false;
159 } catch (EmptyResponseException e) {
160 // Unexpected empty response is ignored
161 logger.debug("{}", e.getMessage());
162 } catch (ApiException | CommunicationException | IOException e) {
163 if (hueBridge != null && lastBridgeConnectionState) {
164 logger.debug("Connection to Hue Bridge {} lost: {}", hueBridge.getIPAddress(), e.getMessage(), e);
165 lastBridgeConnectionState = false;
168 } catch (RuntimeException e) {
169 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
170 lastBridgeConnectionState = false;
173 pollingLock.unlock();
177 private boolean isReachable(String ipAddress) {
179 // note that InetAddress.isReachable is unreliable, see
180 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
181 // That's why we do an HTTP access instead
183 // If there is no connection, this line will fail
184 hueBridge.authenticate("invalid");
185 } catch (ConfigurationException | IOException e) {
187 } catch (ApiException e) {
188 String message = e.getMessage();
189 return message != null && //
190 !message.contains("SocketTimeout") && //
191 !message.contains("ConnectException") && //
192 !message.contains("SocketException") && //
193 !message.contains("NoRouteToHostException");
198 protected abstract void doConnectedRun() throws IOException, ApiException;
201 private final Runnable sensorPollingRunnable = new PollingRunnable() {
203 protected void doConnectedRun() throws IOException, ApiException {
204 Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
206 final HueDeviceDiscoveryService discovery = discoveryService;
208 for (final FullSensor sensor : hueBridge.getSensors()) {
209 String sensorId = sensor.getId();
211 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
212 if (sensorStatusListener == null) {
213 logger.trace("Hue sensor '{}' added.", sensorId);
215 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
216 discovery.addSensorDiscovery(sensor);
219 lastSensorStates.put(sensorId, sensor);
221 if (sensorStatusListener.onSensorStateChanged(sensor)) {
222 lastSensorStates.put(sensorId, sensor);
225 lastSensorStateCopy.remove(sensorId);
228 // Check for removed sensors
229 lastSensorStateCopy.forEach((sensorId, sensor) -> {
230 logger.trace("Hue sensor '{}' removed.", sensorId);
231 lastSensorStates.remove(sensorId);
233 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
234 if (sensorStatusListener != null) {
235 sensorStatusListener.onSensorRemoved();
238 if (discovery != null && sensor != null) {
239 discovery.removeSensorDiscovery(sensor);
245 private final Runnable lightPollingRunnable = new PollingRunnable() {
247 protected void doConnectedRun() throws IOException, ApiException {
250 if (lastScenesRetrieval.isBefore(Instant.now().minusSeconds(SCENE_POLLING_INTERVAL))) {
252 lastScenesRetrieval = Instant.now();
256 private void updateLights() throws IOException, ApiException {
257 Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
259 List<FullLight> lights;
260 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
261 lights = hueBridge.getFullLights();
263 lights = hueBridge.getFullConfig().getLights();
266 final HueDeviceDiscoveryService discovery = discoveryService;
268 for (final FullLight fullLight : lights) {
269 final String lightId = fullLight.getId();
271 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
272 if (lightStatusListener == null) {
273 logger.trace("Hue light '{}' added.", lightId);
275 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
276 discovery.addLightDiscovery(fullLight);
279 lastLightStates.put(lightId, fullLight);
281 if (lightStatusListener.onLightStateChanged(fullLight)) {
282 lastLightStates.put(lightId, fullLight);
285 lastLightStateCopy.remove(lightId);
288 // Check for removed lights
289 lastLightStateCopy.forEach((lightId, light) -> {
290 logger.trace("Hue light '{}' removed.", lightId);
291 lastLightStates.remove(lightId);
293 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
294 if (lightStatusListener != null) {
295 lightStatusListener.onLightRemoved();
298 if (discovery != null && light != null) {
299 discovery.removeLightDiscovery(light);
304 private void updateGroups() throws IOException, ApiException {
305 Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
307 List<FullGroup> groups = hueBridge.getGroups();
309 final HueDeviceDiscoveryService discovery = discoveryService;
311 for (final FullGroup fullGroup : groups) {
312 State groupState = new State();
316 State colorRef = null;
317 HSBType firstColorHsb = null;
318 for (String lightId : fullGroup.getLightIds()) {
319 FullLight light = lastLightStates.get(lightId);
321 final State lightState = light.getState();
322 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
323 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
324 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
325 lightState.getColorMode(), lightState.getXY());
326 if (lightState.isOn()) {
328 sumBri += lightState.getBrightness();
330 if (lightState.getColorMode() != null) {
331 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
332 if (firstColorHsb == null) {
334 firstColorHsb = lightHsb;
335 colorRef = lightState;
336 } else if (!lightHsb.equals(firstColorHsb)) {
343 groupState.setOn(on);
344 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
345 if (colorRef != null) {
346 groupState.setColormode(colorRef.getColorMode());
347 groupState.setHue(colorRef.getHue());
348 groupState.setSaturation(colorRef.getSaturation());
349 groupState.setColorTemperature(colorRef.getColorTemperature());
350 groupState.setXY(colorRef.getXY());
352 fullGroup.setState(groupState);
353 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
354 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
355 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
358 String groupId = fullGroup.getId();
360 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
361 if (groupStatusListener == null) {
362 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
363 fullGroup.getLightIds().size());
365 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
366 discovery.addGroupDiscovery(fullGroup);
369 lastGroupStates.put(groupId, fullGroup);
371 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
372 lastGroupStates.put(groupId, fullGroup);
375 lastGroupStateCopy.remove(groupId);
378 // Check for removed groups
379 lastGroupStateCopy.forEach((groupId, group) -> {
380 logger.trace("Hue group '{}' removed.", groupId);
381 lastGroupStates.remove(groupId);
383 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
384 if (groupStatusListener != null) {
385 groupStatusListener.onGroupRemoved();
388 if (discovery != null && group != null) {
389 discovery.removeGroupDiscovery(group);
394 private void updateScenes() throws IOException, ApiException {
395 lastScenes = hueBridge.getScenes();
396 logger.trace("Scenes detected: {}", lastScenes);
398 setBridgeSceneChannelStateOptions(lastScenes, lastGroupStates);
399 notifyGroupSceneUpdate(lastScenes);
402 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
403 Map<String, String> groupNames = groups.entrySet().stream()
404 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
405 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
406 .collect(Collectors.toList());
407 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
409 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
410 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
414 private boolean lastBridgeConnectionState = false;
416 private boolean propertiesInitializedSuccessfully = false;
418 private @Nullable Future<?> initJob;
419 private @Nullable ScheduledFuture<?> lightPollingJob;
420 private @Nullable ScheduledFuture<?> sensorPollingJob;
422 private @NonNullByDefault({}) HueBridge hueBridge = null;
423 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
425 private List<String> consoleScenesList = new ArrayList<>();
427 public HueBridgeHandler(Bridge bridge, HttpClient httpClient,
428 HueStateDescriptionProvider stateDescriptionOptionProvider, TranslationProvider i18nProvider,
429 LocaleProvider localeProvider) {
431 this.httpClient = httpClient;
432 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
433 this.i18nProvider = i18nProvider;
434 this.localeProvider = localeProvider;
438 public Collection<Class<? extends ThingHandlerService>> getServices() {
439 return Set.of(HueDeviceDiscoveryService.class);
443 public void handleCommand(ChannelUID channelUID, Command command) {
444 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
445 recallScene(command.toString());
450 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
452 if (hueBridge != null) {
453 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
454 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
456 hueBridge.handleErrors(result);
457 listener.setPollBypass(fadeTime);
458 } catch (Exception e) {
459 listener.unsetPollBypass();
460 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
462 }).exceptionally(e -> {
463 listener.unsetPollBypass();
464 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
468 logger.debug("No bridge connected or selected. Cannot set light state.");
473 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
474 if (hueBridge != null) {
475 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
477 hueBridge.handleErrors(result);
478 } catch (Exception e) {
479 handleSensorUpdateException(sensor, e);
481 }).exceptionally(e -> {
482 handleSensorUpdateException(sensor, e);
486 logger.debug("No bridge connected or selected. Cannot set sensor state.");
491 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
492 if (hueBridge != null) {
493 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
495 hueBridge.handleErrors(result);
496 } catch (Exception e) {
497 handleSensorUpdateException(sensor, e);
499 }).exceptionally(e -> {
500 handleSensorUpdateException(sensor, e);
504 logger.debug("No bridge connected or selected. Cannot set sensor config.");
509 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
510 if (hueBridge != null) {
511 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
512 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
514 hueBridge.handleErrors(result);
515 setGroupPollBypass(group, fadeTime);
516 } catch (Exception e) {
517 unsetGroupPollBypass(group);
518 handleGroupUpdateException(group, e);
520 }).exceptionally(e -> {
521 unsetGroupPollBypass(group);
522 handleGroupUpdateException(group, e);
526 logger.debug("No bridge connected or selected. Cannot set group state.");
530 private void setGroupPollBypass(FullGroup group, long bypassTime) {
531 group.getLightIds().forEach((lightId) -> {
532 final LightStatusListener listener = lightStatusListeners.get(lightId);
533 if (listener != null) {
534 listener.setPollBypass(bypassTime);
539 private void unsetGroupPollBypass(FullGroup group) {
540 group.getLightIds().forEach((lightId) -> {
541 final LightStatusListener listener = lightStatusListeners.get(lightId);
542 if (listener != null) {
543 listener.unsetPollBypass();
548 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
549 long fadeTime, Throwable e) {
550 if (e instanceof DeviceOffException) {
551 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
552 // If there is only a change of the color temperature, we do not want the light
553 // to be turned on (i.e. change its brightness).
556 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
557 updateLightState(listener, light, stateUpdate, fadeTime);
559 } else if (e instanceof EntityNotAvailableException) {
560 logger.debug("Error while accessing light: {}", e.getMessage(), e);
561 final HueDeviceDiscoveryService discovery = discoveryService;
562 if (discovery != null) {
563 discovery.removeLightDiscovery(light);
565 listener.onLightGone();
567 handleThingUpdateException("light", e);
571 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
572 if (e instanceof EntityNotAvailableException) {
573 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
574 final HueDeviceDiscoveryService discovery = discoveryService;
575 if (discovery != null) {
576 discovery.removeSensorDiscovery(sensor);
578 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
579 if (listener != null) {
580 listener.onSensorGone();
583 handleThingUpdateException("sensor", e);
587 private void handleGroupUpdateException(FullGroup group, Throwable e) {
588 if (e instanceof EntityNotAvailableException) {
589 logger.debug("Error while accessing group: {}", e.getMessage(), e);
590 final HueDeviceDiscoveryService discovery = discoveryService;
591 if (discovery != null) {
592 discovery.removeGroupDiscovery(group);
594 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
595 if (listener != null) {
596 listener.onGroupGone();
599 handleThingUpdateException("group", e);
603 private void handleThingUpdateException(String thingType, Throwable e) {
604 if (e instanceof IOException) {
605 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
606 } else if (e instanceof ApiException) {
607 // This should not happen - if it does, it is most likely some bug that should be reported.
608 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
609 } else if (e instanceof IllegalStateException) {
610 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
614 private void startLightPolling() {
615 ScheduledFuture<?> job = lightPollingJob;
616 if (job == null || job.isCancelled()) {
617 long lightPollingInterval;
618 int configPollingInterval = hueBridgeConfig.pollingInterval;
619 if (configPollingInterval < 1) {
620 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
621 logger.warn("Wrong configuration value for polling interval. Using default value: {}s",
622 lightPollingInterval);
624 lightPollingInterval = configPollingInterval;
626 // Delay the first execution to give a chance to have all light and group things registered
627 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
632 private void stopLightPolling() {
633 ScheduledFuture<?> job = lightPollingJob;
637 lightPollingJob = null;
640 private void startSensorPolling() {
641 ScheduledFuture<?> job = sensorPollingJob;
642 if (job == null || job.isCancelled()) {
643 int configSensorPollingInterval = hueBridgeConfig.sensorPollingInterval;
644 if (configSensorPollingInterval > 0) {
645 long sensorPollingInterval;
646 if (configSensorPollingInterval < 50) {
647 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
648 logger.warn("Wrong configuration value for sensor polling interval. Using default value: {}ms",
649 sensorPollingInterval);
651 sensorPollingInterval = configSensorPollingInterval;
653 // Delay the first execution to give a chance to have all sensor things registered
654 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
655 TimeUnit.MILLISECONDS);
660 private void stopSensorPolling() {
661 ScheduledFuture<?> job = sensorPollingJob;
665 sensorPollingJob = null;
669 public void dispose() {
670 logger.debug("Disposing Hue Bridge handler ...");
671 Future<?> job = initJob;
677 if (hueBridge != null) {
680 ServiceRegistration<?> localServiceRegistration = serviceRegistration;
681 if (localServiceRegistration != null) {
682 // remove trustmanager service
683 localServiceRegistration.unregister();
684 serviceRegistration = null;
686 propertiesInitializedSuccessfully = false;
690 public void initialize() {
691 logger.debug("Initializing Hue Bridge handler ...");
692 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
694 String ip = hueBridgeConfig.ipAddress;
695 if (ip == null || ip.isEmpty()) {
696 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
697 "@text/offline.conf-error-no-ip-address");
699 if (hueBridge == null) {
700 hueBridge = new HueBridge(httpClient, ip, hueBridgeConfig.getPort(), hueBridgeConfig.protocol,
703 updateStatus(ThingStatus.UNKNOWN);
705 if (HueBridgeConfig.HTTPS.equals(hueBridgeConfig.protocol)) {
706 scheduler.submit(() -> {
707 // register trustmanager service
708 HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider(
709 ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate);
711 // Check before registering that the PEM certificate can be downloaded
712 if (tlsTrustManagerProvider.getPEMTrustManager() == null) {
713 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
714 "@text/offline.conf-error-https-connection");
718 serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext().registerService(
719 TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
732 public @Nullable String getUserName() {
733 return hueBridgeConfig == null ? null : hueBridgeConfig.userName;
736 private synchronized void onUpdate() {
738 startSensorPolling();
742 * This method is called whenever the connection to the {@link HueBridge} is lost.
744 public void onConnectionLost() {
745 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
746 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
750 * This method is called whenever the connection to the {@link HueBridge} is resumed.
752 * @throws ApiException if the physical device does not support this API call
753 * @throws IOException if the physical device could not be reached
755 private void onConnectionResumed() throws IOException, ApiException {
756 logger.debug("Bridge connection resumed.");
758 if (!propertiesInitializedSuccessfully) {
759 FullConfig fullConfig = hueBridge.getFullConfig();
760 Config config = fullConfig.getConfig();
761 if (config != null) {
762 Map<String, String> properties = editProperties();
763 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
764 serialNumber = serialNumber.toLowerCase();
765 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
766 properties.put(PROPERTY_MODEL_ID, config.getModelId());
767 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
768 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
769 updateProperties(properties);
770 propertiesInitializedSuccessfully = true;
776 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
778 * @return True if USER_NAME was not null.
779 * @throws ApiException if the physical device does not support this API call
780 * @throws IOException if the physical device could not be reached
782 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
783 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
784 if (hueBridgeConfig.userName == null) {
786 "User name for Hue Bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
787 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
788 "@text/offline.conf-error-no-username");
791 onConnectionResumed();
797 * This method is called whenever the connection to the {@link HueBridge} is available,
798 * but requests are not allowed due to a missing or invalid authentication.
800 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
801 * be requested from the bridge.
803 * @param bridge the Hue Bridge the connection is not authorized
804 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
806 public boolean onNotAuthenticated() {
807 if (hueBridge == null) {
810 String userName = hueBridgeConfig.userName;
811 if (userName == null) {
815 hueBridge.authenticate(userName);
817 } catch (ConfigurationException e) {
818 handleConfigurationFailure(e);
819 } catch (Exception e) {
821 handleAuthenticationFailure(e, userName);
827 private void createUser() {
829 String newUser = createUserOnPhysicalBridge();
830 updateBridgeThingConfiguration(newUser);
831 } catch (LinkButtonException ex) {
832 handleLinkButtonNotPressed(ex);
833 } catch (Exception ex) {
834 handleExceptionWhileCreatingUser(ex);
838 private String createUserOnPhysicalBridge() throws IOException, ApiException {
839 logger.info("Creating new user on Hue Bridge {} - please press the pairing button on the bridge.",
840 hueBridgeConfig.ipAddress);
841 String userName = hueBridge.link(DEVICE_TYPE);
842 logger.info("User has been successfully added to Hue Bridge.");
846 private void updateBridgeThingConfiguration(String userName) {
847 Configuration config = editConfiguration();
848 config.put(USER_NAME, userName);
850 updateConfiguration(config);
851 logger.debug("Updated configuration parameter '{}'", USER_NAME);
852 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
853 } catch (IllegalStateException e) {
854 logger.trace("Configuration update failed.", e);
855 logger.warn("Unable to update configuration of Hue Bridge.");
856 logger.warn("Please configure the user name manually.");
860 private void handleConfigurationFailure(ConfigurationException ex) {
862 "Invalid certificate for secured connection. You might want to enable the \"Use Self-Signed Certificate\" configuration.");
863 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getRawMessage());
866 private void handleAuthenticationFailure(Exception ex, String userName) {
867 logger.warn("User is not authenticated on Hue Bridge {}", hueBridgeConfig.ipAddress);
868 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
869 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
870 "@text/offline.conf-error-invalid-username");
873 private void handleLinkButtonNotPressed(LinkButtonException ex) {
874 logger.debug("Failed creating new user on Hue Bridge: {}", ex.getMessage());
875 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
876 "@text/offline.conf-error-press-pairing-button");
879 private void handleExceptionWhileCreatingUser(Exception ex) {
880 logger.warn("Failed creating new user on Hue Bridge", ex);
881 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
882 "@text/offline.conf-error-creation-username");
886 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
887 if (discoveryService == null) {
888 discoveryService = listener;
889 getFullLights().forEach(listener::addLightDiscovery);
890 getFullSensors().forEach(listener::addSensorDiscovery);
891 getFullGroups().forEach(listener::addGroupDiscovery);
899 public boolean unregisterDiscoveryListener() {
900 if (discoveryService != null) {
901 discoveryService = null;
909 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
910 final String lightId = lightStatusListener.getLightId();
911 if (!lightStatusListeners.containsKey(lightId)) {
912 lightStatusListeners.put(lightId, lightStatusListener);
913 final FullLight lastLightState = lastLightStates.get(lightId);
914 if (lastLightState != null) {
915 lightStatusListener.onLightAdded(lastLightState);
924 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
925 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
929 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
930 final String sensorId = sensorStatusListener.getSensorId();
931 if (!sensorStatusListeners.containsKey(sensorId)) {
932 sensorStatusListeners.put(sensorId, sensorStatusListener);
933 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
934 if (lastSensorState != null) {
935 sensorStatusListener.onSensorAdded(lastSensorState);
944 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
945 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
949 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
950 final String groupId = groupStatusListener.getGroupId();
951 if (!groupStatusListeners.containsKey(groupId)) {
952 groupStatusListeners.put(groupId, groupStatusListener);
953 final FullGroup lastGroupState = lastGroupStates.get(groupId);
954 if (lastGroupState != null) {
955 groupStatusListener.onGroupAdded(lastGroupState);
956 if (!lastScenes.isEmpty()) {
957 groupStatusListener.onScenesUpdated(lastScenes);
967 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
968 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
972 * Recall scene to all lights that belong to the scene.
974 * @param id the ID of the scene to activate
977 public void recallScene(String id) {
978 if (hueBridge != null) {
979 hueBridge.recallScene(id).thenAccept(result -> {
981 hueBridge.handleErrors(result);
982 } catch (Exception e) {
983 logger.debug("Error while recalling scene: {}", e.getMessage());
985 }).exceptionally(e -> {
986 logger.debug("Error while recalling scene: {}", e.getMessage());
990 logger.debug("No bridge connected or selected. Cannot activate scene.");
995 public @Nullable FullLight getLightById(String lightId) {
996 return lastLightStates.get(lightId);
1000 public @Nullable FullSensor getSensorById(String sensorId) {
1001 return lastSensorStates.get(sensorId);
1005 public @Nullable FullGroup getGroupById(String groupId) {
1006 return lastGroupStates.get(groupId);
1009 public List<FullLight> getFullLights() {
1010 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
1011 return hueBridge.getFullLights();
1013 return ret != null ? ret : List.of();
1016 public List<FullSensor> getFullSensors() {
1017 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
1018 return hueBridge.getSensors();
1020 return ret != null ? ret : List.of();
1023 public List<FullGroup> getFullGroups() {
1024 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
1025 return hueBridge.getGroups();
1027 return ret != null ? ret : List.of();
1030 public void startSearch() {
1031 withReAuthentication("start search mode", () -> {
1032 hueBridge.startSearch();
1037 public void startSearch(List<String> serialNumbers) {
1038 withReAuthentication("start search mode", () -> {
1039 hueBridge.startSearch(serialNumbers);
1044 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1045 if (hueBridge != null) {
1048 return runnable.call();
1049 } catch (UnauthorizedException | IllegalStateException e) {
1050 lastBridgeConnectionState = false;
1051 if (onNotAuthenticated()) {
1052 return runnable.call();
1055 } catch (Exception e) {
1056 logger.debug("Bridge cannot {}.", taskDescription, e);
1062 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1063 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1066 public List<String> listScenesForConsole() {
1067 return consoleScenesList;
1071 public Collection<ConfigStatusMessage> getConfigStatus() {
1072 // The bridge IP address to be used for checks
1073 // Check whether an IP address is provided
1074 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
1076 String ip = hueBridgeConfig.ipAddress;
1077 if (ip == null || ip.isEmpty()) {
1078 return List.of(ConfigStatusMessage.Builder.error(HOST).withMessageKeySuffix(IP_ADDRESS_MISSING)
1079 .withArguments(HOST).build());
1085 public TranslationProvider getI18nProvider() {
1086 return i18nProvider;
1089 public LocaleProvider getLocaleProvider() {
1090 return localeProvider;