2 * Copyright (c) 2010-2020 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.Collections;
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.Future;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.stream.Collectors;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.hue.internal.ApiVersionUtils;
37 import org.openhab.binding.hue.internal.Config;
38 import org.openhab.binding.hue.internal.ConfigUpdate;
39 import org.openhab.binding.hue.internal.FullConfig;
40 import org.openhab.binding.hue.internal.FullGroup;
41 import org.openhab.binding.hue.internal.FullLight;
42 import org.openhab.binding.hue.internal.FullSensor;
43 import org.openhab.binding.hue.internal.HueBridge;
44 import org.openhab.binding.hue.internal.HueConfigStatusMessage;
45 import org.openhab.binding.hue.internal.Scene;
46 import org.openhab.binding.hue.internal.State;
47 import org.openhab.binding.hue.internal.StateUpdate;
48 import org.openhab.binding.hue.internal.config.HueBridgeConfig;
49 import org.openhab.binding.hue.internal.discovery.HueDeviceDiscoveryService;
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.library.types.HSBType;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.ThingTypeUID;
65 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.StateOption;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
73 * {@link HueBridgeHandler} is the handler for a hue bridge and connects it to
74 * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
76 * @author Dennis Nobel - Initial contribution
77 * @author Oliver Libutzki - Adjustments
78 * @author Kai Kreuzer - improved state handling
79 * @author Andre Fuechsel - implemented getFullLights(), startSearch()
80 * @author Thomas Höfer - added thing properties
81 * @author Stefan Bußweiler - Added new thing status handling
82 * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
83 * @author Denis Dudnik - switched to internally integrated source of Jue library
84 * @author Samuel Leisering - Added support for sensor API
85 * @author Christoph Weitkamp - Added support for sensor API
86 * @author Laurent Garnier - Added support for groups
89 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
91 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
93 private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
95 private static final String DEVICE_TYPE = "EclipseSmartHome";
97 private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
99 private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
100 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
102 private final Map<String, @Nullable FullLight> lastLightStates = new ConcurrentHashMap<>();
103 private final Map<String, @Nullable FullSensor> lastSensorStates = new ConcurrentHashMap<>();
104 private final Map<String, @Nullable FullGroup> lastGroupStates = new ConcurrentHashMap<>();
106 private @Nullable HueDeviceDiscoveryService discoveryService;
107 private final Map<String, @Nullable LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
108 private final Map<String, @Nullable SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
109 private final Map<String, @Nullable GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
111 final ReentrantLock pollingLock = new ReentrantLock();
113 abstract class PollingRunnable implements Runnable {
118 if (!lastBridgeConnectionState) {
119 // if user is not set in configuration try to create a new user on Hue bridge
120 if (hueBridgeConfig.getUserName() == null) {
121 hueBridge.getFullConfig();
123 lastBridgeConnectionState = tryResumeBridgeConnection();
125 if (lastBridgeConnectionState) {
127 if (thing.getStatus() != ThingStatus.ONLINE) {
128 updateStatus(ThingStatus.ONLINE);
131 } catch (UnauthorizedException | IllegalStateException e) {
132 if (isReachable(hueBridge.getIPAddress())) {
133 lastBridgeConnectionState = false;
134 if (onNotAuthenticated()) {
135 updateStatus(ThingStatus.ONLINE);
137 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
138 lastBridgeConnectionState = false;
141 } catch (ApiException | IOException e) {
142 if (hueBridge != null && lastBridgeConnectionState) {
143 logger.debug("Connection to Hue Bridge {} lost.", hueBridge.getIPAddress());
144 lastBridgeConnectionState = false;
147 } catch (RuntimeException e) {
148 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
149 lastBridgeConnectionState = false;
152 pollingLock.unlock();
156 private boolean isReachable(String ipAddress) {
158 // note that InetAddress.isReachable is unreliable, see
159 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
160 // That's why we do an HTTP access instead
162 // If there is no connection, this line will fail
163 hueBridge.authenticate("invalid");
164 } catch (IOException e) {
166 } catch (ApiException e) {
167 if (e.getMessage().contains("SocketTimeout") || e.getMessage().contains("ConnectException")
168 || e.getMessage().contains("SocketException")
169 || e.getMessage().contains("NoRouteToHostException")) {
172 // this seems to be only an authentication issue
179 protected abstract void doConnectedRun() throws IOException, ApiException;
182 private final Runnable sensorPollingRunnable = new PollingRunnable() {
184 protected void doConnectedRun() throws IOException, ApiException {
185 Map<String, @Nullable FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
187 final HueDeviceDiscoveryService discovery = discoveryService;
189 for (final FullSensor sensor : hueBridge.getSensors()) {
190 String sensorId = sensor.getId();
192 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
193 if (sensorStatusListener == null) {
194 logger.trace("Hue sensor '{}' added.", sensorId);
196 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
197 discovery.addSensorDiscovery(sensor);
200 lastSensorStates.put(sensorId, sensor);
202 if (sensorStatusListener.onSensorStateChanged(sensor)) {
203 lastSensorStates.put(sensorId, sensor);
206 lastSensorStateCopy.remove(sensorId);
209 // Check for removed sensors
210 lastSensorStateCopy.forEach((sensorId, sensor) -> {
211 logger.trace("Hue sensor '{}' removed.", sensorId);
212 lastSensorStates.remove(sensorId);
214 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
215 if (sensorStatusListener != null) {
216 sensorStatusListener.onSensorRemoved();
219 if (discovery != null && sensor != null) {
220 discovery.removeSensorDiscovery(sensor);
226 private final Runnable lightPollingRunnable = new PollingRunnable() {
228 protected void doConnectedRun() throws IOException, ApiException {
233 private void updateLights() throws IOException, ApiException {
234 Map<String, @Nullable FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
236 List<FullLight> lights;
237 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
238 lights = hueBridge.getFullLights();
240 lights = hueBridge.getFullConfig().getLights();
243 final HueDeviceDiscoveryService discovery = discoveryService;
245 for (final FullLight fullLight : lights) {
246 final String lightId = fullLight.getId();
248 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
249 if (lightStatusListener == null) {
250 logger.trace("Hue light '{}' added.", lightId);
252 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
253 discovery.addLightDiscovery(fullLight);
256 lastLightStates.put(lightId, fullLight);
258 if (lightStatusListener.onLightStateChanged(fullLight)) {
259 lastLightStates.put(lightId, fullLight);
262 lastLightStateCopy.remove(lightId);
265 // Check for removed lights
266 lastLightStateCopy.forEach((lightId, light) -> {
267 logger.trace("Hue light '{}' removed.", lightId);
268 lastLightStates.remove(lightId);
270 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
271 if (lightStatusListener != null) {
272 lightStatusListener.onLightRemoved();
275 if (discovery != null && light != null) {
276 discovery.removeLightDiscovery(light);
281 private void updateGroups() throws IOException, ApiException {
282 Map<String, @Nullable FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
284 List<FullGroup> groups = hueBridge.getGroups();
286 final HueDeviceDiscoveryService discovery = discoveryService;
288 for (final FullGroup fullGroup : groups) {
289 State groupState = new State();
293 State colorRef = null;
294 HSBType firstColorHsb = null;
295 for (String lightId : fullGroup.getLightIds()) {
296 FullLight light = lastLightStates.get(lightId);
298 final State lightState = light.getState();
299 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
300 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
301 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
302 lightState.getColorMode(), lightState.getXY());
303 if (lightState.isOn()) {
305 sumBri += lightState.getBrightness();
307 if (lightState.getColorMode() != null) {
308 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
309 if (firstColorHsb == null) {
311 firstColorHsb = lightHsb;
312 colorRef = lightState;
313 } else if (!lightHsb.equals(firstColorHsb)) {
320 groupState.setOn(on);
321 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
322 if (colorRef != null) {
323 groupState.setColormode(colorRef.getColorMode());
324 groupState.setHue(colorRef.getHue());
325 groupState.setSaturation(colorRef.getSaturation());
326 groupState.setColorTemperature(colorRef.getColorTemperature());
327 groupState.setXY(colorRef.getXY());
329 fullGroup.setState(groupState);
330 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
331 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
332 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
335 String groupId = fullGroup.getId();
337 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
338 if (groupStatusListener == null) {
339 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
340 fullGroup.getLightIds().size());
342 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
343 discovery.addGroupDiscovery(fullGroup);
346 lastGroupStates.put(groupId, fullGroup);
348 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
349 lastGroupStates.put(groupId, fullGroup);
352 lastGroupStateCopy.remove(groupId);
355 // Check for removed groups
356 lastGroupStateCopy.forEach((groupId, group) -> {
357 logger.trace("Hue group '{}' removed.", groupId);
358 lastGroupStates.remove(groupId);
360 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
361 if (groupStatusListener != null) {
362 groupStatusListener.onGroupRemoved();
365 if (discovery != null && group != null) {
366 discovery.removeGroupDiscovery(group);
372 private final Runnable scenePollingRunnable = new PollingRunnable() {
374 protected void doConnectedRun() throws IOException, ApiException {
375 List<Scene> scenes = hueBridge.getScenes();
376 logger.trace("Scenes detected: {}", scenes);
378 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
379 notifyGroupSceneUpdate(scenes);
382 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, @Nullable FullGroup> groups) {
383 Map<String, String> groupNames = groups.entrySet().stream()
384 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
385 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
386 .collect(Collectors.toList());
387 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
389 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
390 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
394 private boolean lastBridgeConnectionState = false;
396 private boolean propertiesInitializedSuccessfully = false;
398 private @Nullable Future<?> initJob;
399 private @Nullable ScheduledFuture<?> lightPollingJob;
400 private @Nullable ScheduledFuture<?> sensorPollingJob;
401 private @Nullable ScheduledFuture<?> scenePollingJob;
403 private @NonNullByDefault({}) HueBridge hueBridge = null;
404 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
406 private List<String> consoleScenesList = new ArrayList<>();
408 public HueBridgeHandler(Bridge bridge, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
410 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
414 public Collection<Class<? extends ThingHandlerService>> getServices() {
415 return Collections.singleton(HueDeviceDiscoveryService.class);
419 public void handleCommand(ChannelUID channelUID, Command command) {
420 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
421 recallScene(command.toString());
426 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
428 if (hueBridge != null) {
429 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
430 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
432 hueBridge.handleErrors(result);
433 listener.setPollBypass(fadeTime);
434 } catch (Exception e) {
435 listener.unsetPollBypass();
436 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
438 }).exceptionally(e -> {
439 listener.unsetPollBypass();
440 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
444 logger.debug("No bridge connected or selected. Cannot set light state.");
449 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
450 if (hueBridge != null) {
451 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
453 hueBridge.handleErrors(result);
454 } catch (Exception e) {
455 handleSensorUpdateException(sensor, e);
457 }).exceptionally(e -> {
458 handleSensorUpdateException(sensor, e);
462 logger.debug("No bridge connected or selected. Cannot set sensor state.");
467 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
468 if (hueBridge != null) {
469 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
471 hueBridge.handleErrors(result);
472 } catch (Exception e) {
473 handleSensorUpdateException(sensor, e);
475 }).exceptionally(e -> {
476 handleSensorUpdateException(sensor, e);
480 logger.debug("No bridge connected or selected. Cannot set sensor config.");
485 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
486 if (hueBridge != null) {
487 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
488 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
490 hueBridge.handleErrors(result);
491 setGroupPollBypass(group, fadeTime);
492 } catch (Exception e) {
493 unsetGroupPollBypass(group);
494 handleGroupUpdateException(group, e);
496 }).exceptionally(e -> {
497 unsetGroupPollBypass(group);
498 handleGroupUpdateException(group, e);
502 logger.debug("No bridge connected or selected. Cannot set group state.");
506 private void setGroupPollBypass(FullGroup group, long bypassTime) {
507 group.getLightIds().forEach((lightId) -> {
508 final LightStatusListener listener = lightStatusListeners.get(lightId);
509 if (listener != null) {
510 listener.setPollBypass(bypassTime);
515 private void unsetGroupPollBypass(FullGroup group) {
516 group.getLightIds().forEach((lightId) -> {
517 final LightStatusListener listener = lightStatusListeners.get(lightId);
518 if (listener != null) {
519 listener.unsetPollBypass();
524 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
525 long fadeTime, Throwable e) {
526 if (e instanceof DeviceOffException) {
527 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
528 // If there is only a change of the color temperature, we do not want the light
529 // to be turned on (i.e. change its brightness).
532 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
533 updateLightState(listener, light, stateUpdate, fadeTime);
535 } else if (e instanceof EntityNotAvailableException) {
536 logger.debug("Error while accessing light: {}", e.getMessage(), e);
537 final HueDeviceDiscoveryService discovery = discoveryService;
538 if (discovery != null) {
539 discovery.removeLightDiscovery(light);
541 listener.onLightGone();
543 handleThingUpdateException("light", e);
547 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
548 if (e instanceof EntityNotAvailableException) {
549 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
550 final HueDeviceDiscoveryService discovery = discoveryService;
551 if (discovery != null) {
552 discovery.removeSensorDiscovery(sensor);
554 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
555 if (listener != null) {
556 listener.onSensorGone();
559 handleThingUpdateException("sensor", e);
563 private void handleGroupUpdateException(FullGroup group, Throwable e) {
564 if (e instanceof EntityNotAvailableException) {
565 logger.debug("Error while accessing group: {}", e.getMessage(), e);
566 final HueDeviceDiscoveryService discovery = discoveryService;
567 if (discovery != null) {
568 discovery.removeGroupDiscovery(group);
570 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
571 if (listener != null) {
572 listener.onGroupGone();
575 handleThingUpdateException("group", e);
579 private void handleThingUpdateException(String thingType, Throwable e) {
580 if (e instanceof IOException) {
581 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
582 } else if (e instanceof ApiException) {
583 // This should not happen - if it does, it is most likely some bug that should be reported.
584 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
585 } else if (e instanceof IllegalStateException) {
586 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
590 private void startLightPolling() {
591 ScheduledFuture<?> job = lightPollingJob;
592 if (job == null || job.isCancelled()) {
593 long lightPollingInterval;
594 int configPollingInterval = hueBridgeConfig.getPollingInterval();
595 if (configPollingInterval < 1) {
596 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
597 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
598 lightPollingInterval);
600 lightPollingInterval = configPollingInterval;
602 // Delay the first execution to give a chance to have all light and group things registered
603 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
608 private void stopLightPolling() {
609 ScheduledFuture<?> job = lightPollingJob;
613 lightPollingJob = null;
616 private void startSensorPolling() {
617 ScheduledFuture<?> job = sensorPollingJob;
618 if (job == null || job.isCancelled()) {
619 int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
620 if (configSensorPollingInterval > 0) {
621 long sensorPollingInterval;
622 if (configSensorPollingInterval < 50) {
623 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
624 logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
625 sensorPollingInterval);
627 sensorPollingInterval = configSensorPollingInterval;
629 // Delay the first execution to give a chance to have all sensor things registered
630 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
631 TimeUnit.MILLISECONDS);
636 private void stopSensorPolling() {
637 ScheduledFuture<?> job = sensorPollingJob;
641 sensorPollingJob = null;
644 private void startScenePolling() {
645 ScheduledFuture<?> job = scenePollingJob;
646 if (job == null || job.isCancelled()) {
647 // Delay the first execution to give a chance to have all group things registered
648 scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
653 private void stopScenePolling() {
654 ScheduledFuture<?> job = scenePollingJob;
658 scenePollingJob = null;
662 public void dispose() {
663 logger.debug("Handler disposed.");
664 Future<?> job = initJob;
671 if (hueBridge != null) {
677 public void initialize() {
678 logger.debug("Initializing hue bridge handler.");
679 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
681 String ip = hueBridgeConfig.getIpAddress();
682 if (ip == null || ip.isEmpty()) {
683 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
684 "@text/offline.conf-error-no-ip-address");
686 if (hueBridge == null) {
687 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
688 hueBridge.setTimeout(5000);
690 // Try a first connection that will fail, then try to authenticate,
691 // and finally change the bridge status to ONLINE
692 initJob = scheduler.submit(new PollingRunnable() {
694 protected void doConnectedRun() throws IOException, ApiException {
702 public @Nullable String getUserName() {
703 return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
706 private synchronized void onUpdate() {
707 if (hueBridge != null) {
709 startSensorPolling();
715 * This method is called whenever the connection to the {@link HueBridge} is lost.
717 public void onConnectionLost() {
718 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
719 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
723 * This method is called whenever the connection to the {@link HueBridge} is resumed.
725 * @throws ApiException if the physical device does not support this API call
726 * @throws IOException if the physical device could not be reached
728 private void onConnectionResumed() throws IOException, ApiException {
729 logger.debug("Bridge connection resumed.");
731 if (!propertiesInitializedSuccessfully) {
732 FullConfig fullConfig = hueBridge.getFullConfig();
733 Config config = fullConfig.getConfig();
734 if (config != null) {
735 Map<String, String> properties = editProperties();
736 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
737 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
738 properties.put(PROPERTY_MODEL_ID, config.getModelId());
739 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
740 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
741 updateProperties(properties);
742 propertiesInitializedSuccessfully = true;
748 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
750 * @return True if USER_NAME was not null.
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 boolean tryResumeBridgeConnection() throws IOException, ApiException {
755 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
756 if (hueBridgeConfig.getUserName() == null) {
758 "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
759 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
760 "@text/offline.conf-error-no-username");
763 onConnectionResumed();
769 * This method is called whenever the connection to the {@link HueBridge} is available,
770 * but requests are not allowed due to a missing or invalid authentication.
772 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
773 * be requested from the bridge.
775 * @param bridge the hue bridge the connection is not authorized
776 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
778 public boolean onNotAuthenticated() {
779 if (hueBridge == null) {
782 String userName = hueBridgeConfig.getUserName();
783 if (userName == null) {
787 hueBridge.authenticate(userName);
789 } catch (Exception e) {
790 handleAuthenticationFailure(e, userName);
796 private void createUser() {
798 String newUser = createUserOnPhysicalBridge();
799 updateBridgeThingConfiguration(newUser);
800 } catch (LinkButtonException ex) {
801 handleLinkButtonNotPressed(ex);
802 } catch (Exception ex) {
803 handleExceptionWhileCreatingUser(ex);
807 private String createUserOnPhysicalBridge() throws IOException, ApiException {
808 logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
809 hueBridgeConfig.getIpAddress());
810 String userName = hueBridge.link(DEVICE_TYPE);
811 logger.info("User has been successfully added to Hue bridge.");
815 private void updateBridgeThingConfiguration(String userName) {
816 Configuration config = editConfiguration();
817 config.put(USER_NAME, userName);
819 updateConfiguration(config);
820 logger.debug("Updated configuration parameter '{}'", USER_NAME);
821 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
822 } catch (IllegalStateException e) {
823 logger.trace("Configuration update failed.", e);
824 logger.warn("Unable to update configuration of Hue bridge.");
825 logger.warn("Please configure the user name manually.");
829 private void handleAuthenticationFailure(Exception ex, String userName) {
830 logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
831 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
832 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
833 "@text/offline.conf-error-invalid-username");
836 private void handleLinkButtonNotPressed(LinkButtonException ex) {
837 logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
838 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
839 "@text/offline.conf-error-press-pairing-button");
842 private void handleExceptionWhileCreatingUser(Exception ex) {
843 logger.warn("Failed creating new user on Hue bridge", ex);
844 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
845 "@text/offline.conf-error-creation-username");
849 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
850 if (discoveryService == null) {
851 discoveryService = listener;
852 getFullLights().forEach(listener::addLightDiscovery);
853 getFullSensors().forEach(listener::addSensorDiscovery);
854 getFullGroups().forEach(listener::addGroupDiscovery);
862 public boolean unregisterDiscoveryListener() {
863 if (discoveryService != null) {
864 discoveryService = null;
872 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
873 final String lightId = lightStatusListener.getLightId();
874 if (!lightStatusListeners.containsKey(lightId)) {
875 lightStatusListeners.put(lightId, lightStatusListener);
876 final FullLight lastLightState = lastLightStates.get(lightId);
877 if (lastLightState != null) {
878 lightStatusListener.onLightAdded(lastLightState);
887 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
888 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
892 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
893 final String sensorId = sensorStatusListener.getSensorId();
894 if (!sensorStatusListeners.containsKey(sensorId)) {
895 sensorStatusListeners.put(sensorId, sensorStatusListener);
896 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
897 if (lastSensorState != null) {
898 sensorStatusListener.onSensorAdded(lastSensorState);
907 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
908 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
912 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
913 final String groupId = groupStatusListener.getGroupId();
914 if (!groupStatusListeners.containsKey(groupId)) {
915 groupStatusListeners.put(groupId, groupStatusListener);
916 final FullGroup lastGroupState = lastGroupStates.get(groupId);
917 if (lastGroupState != null) {
918 groupStatusListener.onGroupAdded(lastGroupState);
927 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
928 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
932 * Recall scene to all lights that belong to the scene.
934 * @param id the ID of the scene to activate
937 public void recallScene(String id) {
938 if (hueBridge != null) {
939 hueBridge.recallScene(id).thenAccept(result -> {
941 hueBridge.handleErrors(result);
942 } catch (Exception e) {
943 logger.debug("Error while recalling scene: {}", e.getMessage());
945 }).exceptionally(e -> {
946 logger.debug("Error while recalling scene: {}", e.getMessage());
950 logger.debug("No bridge connected or selected. Cannot activate scene.");
955 public @Nullable FullLight getLightById(String lightId) {
956 return lastLightStates.get(lightId);
960 public @Nullable FullSensor getSensorById(String sensorId) {
961 return lastSensorStates.get(sensorId);
965 public @Nullable FullGroup getGroupById(String groupId) {
966 return lastGroupStates.get(groupId);
969 public List<FullLight> getFullLights() {
970 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
971 return hueBridge.getFullLights();
973 return ret != null ? ret : List.of();
976 public List<FullSensor> getFullSensors() {
977 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
978 return hueBridge.getSensors();
980 return ret != null ? ret : List.of();
983 public List<FullGroup> getFullGroups() {
984 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
985 return hueBridge.getGroups();
987 return ret != null ? ret : List.of();
990 public void startSearch() {
991 withReAuthentication("start search mode", () -> {
992 hueBridge.startSearch();
997 public void startSearch(List<String> serialNumbers) {
998 withReAuthentication("start search mode", () -> {
999 hueBridge.startSearch(serialNumbers);
1004 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1005 if (hueBridge != null) {
1008 return runnable.call();
1009 } catch (UnauthorizedException | IllegalStateException e) {
1010 lastBridgeConnectionState = false;
1011 if (onNotAuthenticated()) {
1012 return runnable.call();
1015 } catch (Exception e) {
1016 logger.debug("Bridge cannot {}.", taskDescription, e);
1022 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1023 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1026 public List<String> listScenesForConsole() {
1027 return consoleScenesList;
1031 public Collection<ConfigStatusMessage> getConfigStatus() {
1032 // The bridge IP address to be used for checks
1033 // Check whether an IP address is provided
1034 String ip = hueBridgeConfig.getIpAddress();
1035 if (ip == null || ip.isEmpty()) {
1036 return List.of(ConfigStatusMessage.Builder.error(HOST)
1037 .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());