2 * Copyright (c) 2010-2024 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.api.dto.clip1.ApiVersionUtils;
39 import org.openhab.binding.hue.internal.api.dto.clip1.Config;
40 import org.openhab.binding.hue.internal.api.dto.clip1.ConfigUpdate;
41 import org.openhab.binding.hue.internal.api.dto.clip1.FullConfig;
42 import org.openhab.binding.hue.internal.api.dto.clip1.FullGroup;
43 import org.openhab.binding.hue.internal.api.dto.clip1.FullLight;
44 import org.openhab.binding.hue.internal.api.dto.clip1.FullSensor;
45 import org.openhab.binding.hue.internal.api.dto.clip1.Scene;
46 import org.openhab.binding.hue.internal.api.dto.clip1.State;
47 import org.openhab.binding.hue.internal.api.dto.clip1.StateUpdate;
48 import org.openhab.binding.hue.internal.config.HueBridgeConfig;
49 import org.openhab.binding.hue.internal.connection.HueBridge;
50 import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
51 import org.openhab.binding.hue.internal.discovery.HueDeviceDiscoveryService;
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) {
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) {
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) {
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)).toList();
406 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
408 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
409 + scene.toStateOption(groupNames).getLabel() + "\"").toList();
413 private boolean lastBridgeConnectionState = false;
415 private boolean propertiesInitializedSuccessfully = false;
417 private @Nullable Future<?> initJob;
418 private @Nullable ScheduledFuture<?> lightPollingJob;
419 private @Nullable ScheduledFuture<?> sensorPollingJob;
421 private @NonNullByDefault({}) HueBridge hueBridge = null;
422 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
424 private List<String> consoleScenesList = new ArrayList<>();
426 public HueBridgeHandler(Bridge bridge, HttpClient httpClient,
427 HueStateDescriptionProvider stateDescriptionOptionProvider, TranslationProvider i18nProvider,
428 LocaleProvider localeProvider) {
430 this.httpClient = httpClient;
431 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
432 this.i18nProvider = i18nProvider;
433 this.localeProvider = localeProvider;
437 public Collection<Class<? extends ThingHandlerService>> getServices() {
438 return Set.of(HueDeviceDiscoveryService.class);
442 public void handleCommand(ChannelUID channelUID, Command command) {
443 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
444 recallScene(command.toString());
449 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
451 if (hueBridge != null) {
452 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
453 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
455 hueBridge.handleErrors(result);
456 listener.setPollBypass(fadeTime);
457 } catch (Exception e) {
458 listener.unsetPollBypass();
459 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
461 }).exceptionally(e -> {
462 listener.unsetPollBypass();
463 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
467 logger.debug("No bridge connected or selected. Cannot set light state.");
472 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
473 if (hueBridge != null) {
474 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
476 hueBridge.handleErrors(result);
477 } catch (Exception e) {
478 handleSensorUpdateException(sensor, e);
480 }).exceptionally(e -> {
481 handleSensorUpdateException(sensor, e);
485 logger.debug("No bridge connected or selected. Cannot set sensor state.");
490 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
491 if (hueBridge != null) {
492 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
494 hueBridge.handleErrors(result);
495 } catch (Exception e) {
496 handleSensorUpdateException(sensor, e);
498 }).exceptionally(e -> {
499 handleSensorUpdateException(sensor, e);
503 logger.debug("No bridge connected or selected. Cannot set sensor config.");
508 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
509 if (hueBridge != null) {
510 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
511 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
513 hueBridge.handleErrors(result);
514 setGroupPollBypass(group, fadeTime);
515 } catch (Exception e) {
516 unsetGroupPollBypass(group);
517 handleGroupUpdateException(group, e);
519 }).exceptionally(e -> {
520 unsetGroupPollBypass(group);
521 handleGroupUpdateException(group, e);
525 logger.debug("No bridge connected or selected. Cannot set group state.");
529 private void setGroupPollBypass(FullGroup group, long bypassTime) {
530 group.getLightIds().forEach((lightId) -> {
531 final LightStatusListener listener = lightStatusListeners.get(lightId);
532 if (listener != null) {
533 listener.setPollBypass(bypassTime);
538 private void unsetGroupPollBypass(FullGroup group) {
539 group.getLightIds().forEach((lightId) -> {
540 final LightStatusListener listener = lightStatusListeners.get(lightId);
541 if (listener != null) {
542 listener.unsetPollBypass();
547 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
548 long fadeTime, Throwable e) {
549 if (e instanceof DeviceOffException) {
550 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
551 // If there is only a change of the color temperature, we do not want the light
552 // to be turned on (i.e. change its brightness).
555 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
556 updateLightState(listener, light, stateUpdate, fadeTime);
558 } else if (e instanceof EntityNotAvailableException) {
559 logger.debug("Error while accessing light: {}", e.getMessage(), e);
560 final HueDeviceDiscoveryService discovery = discoveryService;
561 if (discovery != null) {
562 discovery.removeLightDiscovery(light);
564 listener.onLightGone();
566 handleThingUpdateException("light", e);
570 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
571 if (e instanceof EntityNotAvailableException) {
572 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
573 final HueDeviceDiscoveryService discovery = discoveryService;
574 if (discovery != null) {
575 discovery.removeSensorDiscovery(sensor);
577 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
578 if (listener != null) {
579 listener.onSensorGone();
582 handleThingUpdateException("sensor", e);
586 private void handleGroupUpdateException(FullGroup group, Throwable e) {
587 if (e instanceof EntityNotAvailableException) {
588 logger.debug("Error while accessing group: {}", e.getMessage(), e);
589 final HueDeviceDiscoveryService discovery = discoveryService;
590 if (discovery != null) {
591 discovery.removeGroupDiscovery(group);
593 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
594 if (listener != null) {
595 listener.onGroupGone();
598 handleThingUpdateException("group", e);
602 private void handleThingUpdateException(String thingType, Throwable e) {
603 if (e instanceof IOException) {
604 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
605 } else if (e instanceof ApiException) {
606 // This should not happen - if it does, it is most likely some bug that should be reported.
607 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
608 } else if (e instanceof IllegalStateException) {
609 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
613 private void startLightPolling() {
614 ScheduledFuture<?> job = lightPollingJob;
615 if (job == null || job.isCancelled()) {
616 long lightPollingInterval;
617 int configPollingInterval = hueBridgeConfig.pollingInterval;
618 if (configPollingInterval < 1) {
619 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
620 logger.warn("Wrong configuration value for polling interval. Using default value: {}s",
621 lightPollingInterval);
623 lightPollingInterval = configPollingInterval;
625 // Delay the first execution to give a chance to have all light and group things registered
626 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
631 private void stopLightPolling() {
632 ScheduledFuture<?> job = lightPollingJob;
636 lightPollingJob = null;
639 private void startSensorPolling() {
640 ScheduledFuture<?> job = sensorPollingJob;
641 if (job == null || job.isCancelled()) {
642 int configSensorPollingInterval = hueBridgeConfig.sensorPollingInterval;
643 if (configSensorPollingInterval > 0) {
644 long sensorPollingInterval;
645 if (configSensorPollingInterval < 50) {
646 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
647 logger.warn("Wrong configuration value for sensor polling interval. Using default value: {}ms",
648 sensorPollingInterval);
650 sensorPollingInterval = configSensorPollingInterval;
652 // Delay the first execution to give a chance to have all sensor things registered
653 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
654 TimeUnit.MILLISECONDS);
659 private void stopSensorPolling() {
660 ScheduledFuture<?> job = sensorPollingJob;
664 sensorPollingJob = null;
668 public void dispose() {
669 logger.debug("Disposing Hue Bridge handler ...");
670 Future<?> job = initJob;
676 if (hueBridge != null) {
679 ServiceRegistration<?> localServiceRegistration = serviceRegistration;
680 if (localServiceRegistration != null) {
681 // remove trustmanager service
682 localServiceRegistration.unregister();
683 serviceRegistration = null;
685 propertiesInitializedSuccessfully = false;
689 public void initialize() {
690 logger.debug("Initializing Hue Bridge handler ...");
691 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
693 String ip = hueBridgeConfig.ipAddress;
694 if (ip == null || ip.isEmpty()) {
695 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
696 "@text/offline.conf-error-no-ip-address");
698 if (hueBridge == null) {
699 hueBridge = new HueBridge(httpClient, ip, hueBridgeConfig.getPort(), hueBridgeConfig.protocol,
702 updateStatus(ThingStatus.UNKNOWN);
704 if (HueBridgeConfig.HTTPS.equals(hueBridgeConfig.protocol)) {
705 scheduler.submit(() -> {
706 // register trustmanager service
707 HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider(
708 ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate);
710 // Check before registering that the PEM certificate can be downloaded
711 if (tlsTrustManagerProvider.getPEMTrustManager() == null) {
712 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
713 "@text/offline.conf-error-https-connection");
717 serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext().registerService(
718 TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
731 public @Nullable String getUserName() {
732 return hueBridgeConfig == null ? null : hueBridgeConfig.userName;
735 private synchronized void onUpdate() {
737 startSensorPolling();
741 * This method is called whenever the connection to the {@link HueBridge} is lost.
743 public void onConnectionLost() {
744 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
745 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
749 * This method is called whenever the connection to the {@link HueBridge} is resumed.
751 * @throws ApiException if the physical device does not support this API call
752 * @throws IOException if the physical device could not be reached
754 private void onConnectionResumed() throws IOException, ApiException {
755 logger.debug("Bridge connection resumed.");
757 if (!propertiesInitializedSuccessfully) {
758 FullConfig fullConfig = hueBridge.getFullConfig();
759 Config config = fullConfig.getConfig();
760 if (config != null) {
761 Map<String, String> properties = editProperties();
762 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
763 serialNumber = serialNumber.toLowerCase();
764 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
765 properties.put(PROPERTY_MODEL_ID, config.getModelId());
766 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
767 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
768 updateProperties(properties);
769 propertiesInitializedSuccessfully = true;
775 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
777 * @return True if USER_NAME was not null.
778 * @throws ApiException if the physical device does not support this API call
779 * @throws IOException if the physical device could not be reached
781 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
782 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
783 if (hueBridgeConfig.userName == null) {
785 "User name for Hue Bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
786 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
787 "@text/offline.conf-error-no-username");
790 onConnectionResumed();
796 * This method is called whenever the connection to the {@link HueBridge} is available,
797 * but requests are not allowed due to a missing or invalid authentication.
799 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
800 * be requested from the bridge.
802 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
804 public boolean onNotAuthenticated() {
805 if (hueBridge == null) {
808 String userName = hueBridgeConfig.userName;
809 if (userName == null) {
813 hueBridge.authenticate(userName);
815 } catch (ConfigurationException e) {
816 handleConfigurationFailure(e);
817 } catch (Exception e) {
819 handleAuthenticationFailure(e, userName);
825 private void createUser() {
827 String newUser = createUserOnPhysicalBridge();
828 updateBridgeThingConfiguration(newUser);
829 } catch (LinkButtonException ex) {
830 handleLinkButtonNotPressed(ex);
831 } catch (Exception ex) {
832 handleExceptionWhileCreatingUser(ex);
836 private String createUserOnPhysicalBridge() throws IOException, ApiException {
837 logger.info("Creating new user on Hue Bridge {} - please press the pairing button on the bridge.",
838 hueBridgeConfig.ipAddress);
839 String userName = hueBridge.link(DEVICE_TYPE);
840 logger.info("User has been successfully added to Hue Bridge.");
844 private void updateBridgeThingConfiguration(String userName) {
845 Configuration config = editConfiguration();
846 config.put(USER_NAME, userName);
848 updateConfiguration(config);
849 logger.debug("Updated configuration parameter '{}'", USER_NAME);
850 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
851 } catch (IllegalStateException e) {
852 logger.trace("Configuration update failed.", e);
853 logger.warn("Unable to update configuration of Hue Bridge.");
854 logger.warn("Please configure the user name manually.");
858 private void handleConfigurationFailure(ConfigurationException ex) {
860 "Invalid certificate for secured connection. You might want to enable the \"Use Self-Signed Certificate\" configuration.");
861 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getRawMessage());
864 private void handleAuthenticationFailure(Exception ex, String userName) {
865 logger.warn("User is not authenticated on Hue Bridge {}", hueBridgeConfig.ipAddress);
866 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
867 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
868 "@text/offline.conf-error-invalid-username");
871 private void handleLinkButtonNotPressed(LinkButtonException ex) {
872 logger.debug("Failed creating new user on Hue Bridge: {}", ex.getMessage());
873 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
874 "@text/offline.conf-error-press-pairing-button");
877 private void handleExceptionWhileCreatingUser(Exception ex) {
878 logger.warn("Failed creating new user on Hue Bridge", ex);
879 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
880 "@text/offline.conf-error-creation-username");
884 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
885 if (discoveryService == null) {
886 discoveryService = listener;
887 getFullLights().forEach(listener::addLightDiscovery);
888 getFullSensors().forEach(listener::addSensorDiscovery);
889 getFullGroups().forEach(listener::addGroupDiscovery);
897 public boolean unregisterDiscoveryListener() {
898 if (discoveryService != null) {
899 discoveryService = null;
907 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
908 final String lightId = lightStatusListener.getLightId();
909 if (!lightStatusListeners.containsKey(lightId)) {
910 lightStatusListeners.put(lightId, lightStatusListener);
911 final FullLight lastLightState = lastLightStates.get(lightId);
912 if (lastLightState != null) {
913 lightStatusListener.onLightAdded(lastLightState);
922 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
923 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
927 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
928 final String sensorId = sensorStatusListener.getSensorId();
929 if (!sensorStatusListeners.containsKey(sensorId)) {
930 sensorStatusListeners.put(sensorId, sensorStatusListener);
931 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
932 if (lastSensorState != null) {
933 sensorStatusListener.onSensorAdded(lastSensorState);
942 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
943 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
947 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
948 final String groupId = groupStatusListener.getGroupId();
949 if (!groupStatusListeners.containsKey(groupId)) {
950 groupStatusListeners.put(groupId, groupStatusListener);
951 final FullGroup lastGroupState = lastGroupStates.get(groupId);
952 if (lastGroupState != null) {
953 groupStatusListener.onGroupAdded(lastGroupState);
954 if (!lastScenes.isEmpty()) {
955 groupStatusListener.onScenesUpdated(lastScenes);
965 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
966 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
970 * Recall scene to all lights that belong to the scene.
972 * @param id the ID of the scene to activate
975 public void recallScene(String id) {
976 if (hueBridge != null) {
977 hueBridge.recallScene(id).thenAccept(result -> {
979 hueBridge.handleErrors(result);
980 } catch (Exception e) {
981 logger.debug("Error while recalling scene: {}", e.getMessage());
983 }).exceptionally(e -> {
984 logger.debug("Error while recalling scene: {}", e.getMessage());
988 logger.debug("No bridge connected or selected. Cannot activate scene.");
993 public @Nullable FullLight getLightById(String lightId) {
994 return lastLightStates.get(lightId);
998 public @Nullable FullSensor getSensorById(String sensorId) {
999 return lastSensorStates.get(sensorId);
1003 public @Nullable FullGroup getGroupById(String groupId) {
1004 return lastGroupStates.get(groupId);
1007 public List<FullLight> getFullLights() {
1008 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
1009 return hueBridge.getFullLights();
1011 return ret != null ? ret : List.of();
1014 public List<FullSensor> getFullSensors() {
1015 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
1016 return hueBridge.getSensors();
1018 return ret != null ? ret : List.of();
1021 public List<FullGroup> getFullGroups() {
1022 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
1023 return hueBridge.getGroups();
1025 return ret != null ? ret : List.of();
1028 public void startSearch() {
1029 withReAuthentication("start search mode", () -> {
1030 hueBridge.startSearch();
1035 public void startSearch(List<String> serialNumbers) {
1036 withReAuthentication("start search mode", () -> {
1037 hueBridge.startSearch(serialNumbers);
1042 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1043 if (hueBridge != null) {
1046 return runnable.call();
1047 } catch (UnauthorizedException | IllegalStateException e) {
1048 lastBridgeConnectionState = false;
1049 if (onNotAuthenticated()) {
1050 return runnable.call();
1053 } catch (Exception e) {
1054 logger.debug("Bridge cannot {}.", taskDescription, e);
1060 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1061 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1064 public List<String> listScenesForConsole() {
1065 return consoleScenesList;
1069 public Collection<ConfigStatusMessage> getConfigStatus() {
1070 // The bridge IP address to be used for checks
1071 // Check whether an IP address is provided
1072 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
1074 String ip = hueBridgeConfig.ipAddress;
1075 if (ip == null || ip.isEmpty()) {
1076 return List.of(ConfigStatusMessage.Builder.error(HOST).withMessageKeySuffix(IP_ADDRESS_MISSING)
1077 .withArguments(HOST).build());
1083 public TranslationProvider getI18nProvider() {
1084 return i18nProvider;
1087 public LocaleProvider getLocaleProvider() {
1088 return localeProvider;