2 * Copyright (c) 2010-2021 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, FullLight> lastLightStates = new ConcurrentHashMap<>();
103 private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
104 private final Map<String, FullGroup> lastGroupStates = new ConcurrentHashMap<>();
106 private @Nullable HueDeviceDiscoveryService discoveryService;
107 private final Map<String, LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
108 private final Map<String, SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
109 private final Map<String, 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(), e.getMessage(), e);
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 String message = e.getMessage();
168 return message != null && //
169 !message.contains("SocketTimeout") && //
170 !message.contains("ConnectException") && //
171 !message.contains("SocketException") && //
172 !message.contains("NoRouteToHostException");
177 protected abstract void doConnectedRun() throws IOException, ApiException;
180 private final Runnable sensorPollingRunnable = new PollingRunnable() {
182 protected void doConnectedRun() throws IOException, ApiException {
183 Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
185 final HueDeviceDiscoveryService discovery = discoveryService;
187 for (final FullSensor sensor : hueBridge.getSensors()) {
188 String sensorId = sensor.getId();
190 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
191 if (sensorStatusListener == null) {
192 logger.trace("Hue sensor '{}' added.", sensorId);
194 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
195 discovery.addSensorDiscovery(sensor);
198 lastSensorStates.put(sensorId, sensor);
200 if (sensorStatusListener.onSensorStateChanged(sensor)) {
201 lastSensorStates.put(sensorId, sensor);
204 lastSensorStateCopy.remove(sensorId);
207 // Check for removed sensors
208 lastSensorStateCopy.forEach((sensorId, sensor) -> {
209 logger.trace("Hue sensor '{}' removed.", sensorId);
210 lastSensorStates.remove(sensorId);
212 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
213 if (sensorStatusListener != null) {
214 sensorStatusListener.onSensorRemoved();
217 if (discovery != null && sensor != null) {
218 discovery.removeSensorDiscovery(sensor);
224 private final Runnable lightPollingRunnable = new PollingRunnable() {
226 protected void doConnectedRun() throws IOException, ApiException {
231 private void updateLights() throws IOException, ApiException {
232 Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
234 List<FullLight> lights;
235 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
236 lights = hueBridge.getFullLights();
238 lights = hueBridge.getFullConfig().getLights();
241 final HueDeviceDiscoveryService discovery = discoveryService;
243 for (final FullLight fullLight : lights) {
244 final String lightId = fullLight.getId();
246 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
247 if (lightStatusListener == null) {
248 logger.trace("Hue light '{}' added.", lightId);
250 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
251 discovery.addLightDiscovery(fullLight);
254 lastLightStates.put(lightId, fullLight);
256 if (lightStatusListener.onLightStateChanged(fullLight)) {
257 lastLightStates.put(lightId, fullLight);
260 lastLightStateCopy.remove(lightId);
263 // Check for removed lights
264 lastLightStateCopy.forEach((lightId, light) -> {
265 logger.trace("Hue light '{}' removed.", lightId);
266 lastLightStates.remove(lightId);
268 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
269 if (lightStatusListener != null) {
270 lightStatusListener.onLightRemoved();
273 if (discovery != null && light != null) {
274 discovery.removeLightDiscovery(light);
279 private void updateGroups() throws IOException, ApiException {
280 Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
282 List<FullGroup> groups = hueBridge.getGroups();
284 final HueDeviceDiscoveryService discovery = discoveryService;
286 for (final FullGroup fullGroup : groups) {
287 State groupState = new State();
291 State colorRef = null;
292 HSBType firstColorHsb = null;
293 for (String lightId : fullGroup.getLightIds()) {
294 FullLight light = lastLightStates.get(lightId);
296 final State lightState = light.getState();
297 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
298 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
299 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
300 lightState.getColorMode(), lightState.getXY());
301 if (lightState.isOn()) {
303 sumBri += lightState.getBrightness();
305 if (lightState.getColorMode() != null) {
306 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
307 if (firstColorHsb == null) {
309 firstColorHsb = lightHsb;
310 colorRef = lightState;
311 } else if (!lightHsb.equals(firstColorHsb)) {
318 groupState.setOn(on);
319 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
320 if (colorRef != null) {
321 groupState.setColormode(colorRef.getColorMode());
322 groupState.setHue(colorRef.getHue());
323 groupState.setSaturation(colorRef.getSaturation());
324 groupState.setColorTemperature(colorRef.getColorTemperature());
325 groupState.setXY(colorRef.getXY());
327 fullGroup.setState(groupState);
328 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
329 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
330 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
333 String groupId = fullGroup.getId();
335 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
336 if (groupStatusListener == null) {
337 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
338 fullGroup.getLightIds().size());
340 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
341 discovery.addGroupDiscovery(fullGroup);
344 lastGroupStates.put(groupId, fullGroup);
346 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
347 lastGroupStates.put(groupId, fullGroup);
350 lastGroupStateCopy.remove(groupId);
353 // Check for removed groups
354 lastGroupStateCopy.forEach((groupId, group) -> {
355 logger.trace("Hue group '{}' removed.", groupId);
356 lastGroupStates.remove(groupId);
358 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
359 if (groupStatusListener != null) {
360 groupStatusListener.onGroupRemoved();
363 if (discovery != null && group != null) {
364 discovery.removeGroupDiscovery(group);
370 private final Runnable scenePollingRunnable = new PollingRunnable() {
372 protected void doConnectedRun() throws IOException, ApiException {
373 List<Scene> scenes = hueBridge.getScenes();
374 logger.trace("Scenes detected: {}", scenes);
376 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
377 notifyGroupSceneUpdate(scenes);
380 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
381 Map<String, String> groupNames = groups.entrySet().stream()
382 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
383 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
384 .collect(Collectors.toList());
385 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
387 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
388 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
392 private boolean lastBridgeConnectionState = false;
394 private boolean propertiesInitializedSuccessfully = false;
396 private @Nullable Future<?> initJob;
397 private @Nullable ScheduledFuture<?> lightPollingJob;
398 private @Nullable ScheduledFuture<?> sensorPollingJob;
399 private @Nullable ScheduledFuture<?> scenePollingJob;
401 private @NonNullByDefault({}) HueBridge hueBridge = null;
402 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
404 private List<String> consoleScenesList = new ArrayList<>();
406 public HueBridgeHandler(Bridge bridge, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
408 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
412 public Collection<Class<? extends ThingHandlerService>> getServices() {
413 return Collections.singleton(HueDeviceDiscoveryService.class);
417 public void handleCommand(ChannelUID channelUID, Command command) {
418 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
419 recallScene(command.toString());
424 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
426 if (hueBridge != null) {
427 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
428 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
430 hueBridge.handleErrors(result);
431 listener.setPollBypass(fadeTime);
432 } catch (Exception e) {
433 listener.unsetPollBypass();
434 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
436 }).exceptionally(e -> {
437 listener.unsetPollBypass();
438 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
442 logger.debug("No bridge connected or selected. Cannot set light state.");
447 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
448 if (hueBridge != null) {
449 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
451 hueBridge.handleErrors(result);
452 } catch (Exception e) {
453 handleSensorUpdateException(sensor, e);
455 }).exceptionally(e -> {
456 handleSensorUpdateException(sensor, e);
460 logger.debug("No bridge connected or selected. Cannot set sensor state.");
465 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
466 if (hueBridge != null) {
467 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
469 hueBridge.handleErrors(result);
470 } catch (Exception e) {
471 handleSensorUpdateException(sensor, e);
473 }).exceptionally(e -> {
474 handleSensorUpdateException(sensor, e);
478 logger.debug("No bridge connected or selected. Cannot set sensor config.");
483 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
484 if (hueBridge != null) {
485 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
486 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
488 hueBridge.handleErrors(result);
489 setGroupPollBypass(group, fadeTime);
490 } catch (Exception e) {
491 unsetGroupPollBypass(group);
492 handleGroupUpdateException(group, e);
494 }).exceptionally(e -> {
495 unsetGroupPollBypass(group);
496 handleGroupUpdateException(group, e);
500 logger.debug("No bridge connected or selected. Cannot set group state.");
504 private void setGroupPollBypass(FullGroup group, long bypassTime) {
505 group.getLightIds().forEach((lightId) -> {
506 final LightStatusListener listener = lightStatusListeners.get(lightId);
507 if (listener != null) {
508 listener.setPollBypass(bypassTime);
513 private void unsetGroupPollBypass(FullGroup group) {
514 group.getLightIds().forEach((lightId) -> {
515 final LightStatusListener listener = lightStatusListeners.get(lightId);
516 if (listener != null) {
517 listener.unsetPollBypass();
522 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
523 long fadeTime, Throwable e) {
524 if (e instanceof DeviceOffException) {
525 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
526 // If there is only a change of the color temperature, we do not want the light
527 // to be turned on (i.e. change its brightness).
530 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
531 updateLightState(listener, light, stateUpdate, fadeTime);
533 } else if (e instanceof EntityNotAvailableException) {
534 logger.debug("Error while accessing light: {}", e.getMessage(), e);
535 final HueDeviceDiscoveryService discovery = discoveryService;
536 if (discovery != null) {
537 discovery.removeLightDiscovery(light);
539 listener.onLightGone();
541 handleThingUpdateException("light", e);
545 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
546 if (e instanceof EntityNotAvailableException) {
547 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
548 final HueDeviceDiscoveryService discovery = discoveryService;
549 if (discovery != null) {
550 discovery.removeSensorDiscovery(sensor);
552 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
553 if (listener != null) {
554 listener.onSensorGone();
557 handleThingUpdateException("sensor", e);
561 private void handleGroupUpdateException(FullGroup group, Throwable e) {
562 if (e instanceof EntityNotAvailableException) {
563 logger.debug("Error while accessing group: {}", e.getMessage(), e);
564 final HueDeviceDiscoveryService discovery = discoveryService;
565 if (discovery != null) {
566 discovery.removeGroupDiscovery(group);
568 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
569 if (listener != null) {
570 listener.onGroupGone();
573 handleThingUpdateException("group", e);
577 private void handleThingUpdateException(String thingType, Throwable e) {
578 if (e instanceof IOException) {
579 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
580 } else if (e instanceof ApiException) {
581 // This should not happen - if it does, it is most likely some bug that should be reported.
582 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
583 } else if (e instanceof IllegalStateException) {
584 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
588 private void startLightPolling() {
589 ScheduledFuture<?> job = lightPollingJob;
590 if (job == null || job.isCancelled()) {
591 long lightPollingInterval;
592 int configPollingInterval = hueBridgeConfig.getPollingInterval();
593 if (configPollingInterval < 1) {
594 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
595 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
596 lightPollingInterval);
598 lightPollingInterval = configPollingInterval;
600 // Delay the first execution to give a chance to have all light and group things registered
601 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
606 private void stopLightPolling() {
607 ScheduledFuture<?> job = lightPollingJob;
611 lightPollingJob = null;
614 private void startSensorPolling() {
615 ScheduledFuture<?> job = sensorPollingJob;
616 if (job == null || job.isCancelled()) {
617 int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
618 if (configSensorPollingInterval > 0) {
619 long sensorPollingInterval;
620 if (configSensorPollingInterval < 50) {
621 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
622 logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
623 sensorPollingInterval);
625 sensorPollingInterval = configSensorPollingInterval;
627 // Delay the first execution to give a chance to have all sensor things registered
628 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
629 TimeUnit.MILLISECONDS);
634 private void stopSensorPolling() {
635 ScheduledFuture<?> job = sensorPollingJob;
639 sensorPollingJob = null;
642 private void startScenePolling() {
643 ScheduledFuture<?> job = scenePollingJob;
644 if (job == null || job.isCancelled()) {
645 // Delay the first execution to give a chance to have all group things registered
646 scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
651 private void stopScenePolling() {
652 ScheduledFuture<?> job = scenePollingJob;
656 scenePollingJob = null;
660 public void dispose() {
661 logger.debug("Handler disposed.");
662 Future<?> job = initJob;
669 if (hueBridge != null) {
675 public void initialize() {
676 logger.debug("Initializing hue bridge handler.");
677 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
679 String ip = hueBridgeConfig.getIpAddress();
680 if (ip == null || ip.isEmpty()) {
681 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
682 "@text/offline.conf-error-no-ip-address");
684 if (hueBridge == null) {
685 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
686 hueBridge.setTimeout(5000);
688 // Try a first connection that will fail, then try to authenticate,
689 // and finally change the bridge status to ONLINE
690 initJob = scheduler.submit(new PollingRunnable() {
692 protected void doConnectedRun() throws IOException, ApiException {
700 public @Nullable String getUserName() {
701 return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
704 private synchronized void onUpdate() {
705 if (hueBridge != null) {
707 startSensorPolling();
713 * This method is called whenever the connection to the {@link HueBridge} is lost.
715 public void onConnectionLost() {
716 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
717 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
721 * This method is called whenever the connection to the {@link HueBridge} is resumed.
723 * @throws ApiException if the physical device does not support this API call
724 * @throws IOException if the physical device could not be reached
726 private void onConnectionResumed() throws IOException, ApiException {
727 logger.debug("Bridge connection resumed.");
729 if (!propertiesInitializedSuccessfully) {
730 FullConfig fullConfig = hueBridge.getFullConfig();
731 Config config = fullConfig.getConfig();
732 if (config != null) {
733 Map<String, String> properties = editProperties();
734 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
735 serialNumber = serialNumber.toLowerCase();
736 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
737 properties.put(PROPERTY_MODEL_ID, config.getModelId());
738 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
739 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
740 updateProperties(properties);
741 propertiesInitializedSuccessfully = true;
747 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
749 * @return True if USER_NAME was not null.
750 * @throws ApiException if the physical device does not support this API call
751 * @throws IOException if the physical device could not be reached
753 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
754 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
755 if (hueBridgeConfig.getUserName() == null) {
757 "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
758 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
759 "@text/offline.conf-error-no-username");
762 onConnectionResumed();
768 * This method is called whenever the connection to the {@link HueBridge} is available,
769 * but requests are not allowed due to a missing or invalid authentication.
771 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
772 * be requested from the bridge.
774 * @param bridge the hue bridge the connection is not authorized
775 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
777 public boolean onNotAuthenticated() {
778 if (hueBridge == null) {
781 String userName = hueBridgeConfig.getUserName();
782 if (userName == null) {
786 hueBridge.authenticate(userName);
788 } catch (Exception e) {
789 handleAuthenticationFailure(e, userName);
795 private void createUser() {
797 String newUser = createUserOnPhysicalBridge();
798 updateBridgeThingConfiguration(newUser);
799 } catch (LinkButtonException ex) {
800 handleLinkButtonNotPressed(ex);
801 } catch (Exception ex) {
802 handleExceptionWhileCreatingUser(ex);
806 private String createUserOnPhysicalBridge() throws IOException, ApiException {
807 logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
808 hueBridgeConfig.getIpAddress());
809 String userName = hueBridge.link(DEVICE_TYPE);
810 logger.info("User has been successfully added to Hue bridge.");
814 private void updateBridgeThingConfiguration(String userName) {
815 Configuration config = editConfiguration();
816 config.put(USER_NAME, userName);
818 updateConfiguration(config);
819 logger.debug("Updated configuration parameter '{}'", USER_NAME);
820 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
821 } catch (IllegalStateException e) {
822 logger.trace("Configuration update failed.", e);
823 logger.warn("Unable to update configuration of Hue bridge.");
824 logger.warn("Please configure the user name manually.");
828 private void handleAuthenticationFailure(Exception ex, String userName) {
829 logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
830 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
831 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
832 "@text/offline.conf-error-invalid-username");
835 private void handleLinkButtonNotPressed(LinkButtonException ex) {
836 logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
837 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
838 "@text/offline.conf-error-press-pairing-button");
841 private void handleExceptionWhileCreatingUser(Exception ex) {
842 logger.warn("Failed creating new user on Hue bridge", ex);
843 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
844 "@text/offline.conf-error-creation-username");
848 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
849 if (discoveryService == null) {
850 discoveryService = listener;
851 getFullLights().forEach(listener::addLightDiscovery);
852 getFullSensors().forEach(listener::addSensorDiscovery);
853 getFullGroups().forEach(listener::addGroupDiscovery);
861 public boolean unregisterDiscoveryListener() {
862 if (discoveryService != null) {
863 discoveryService = null;
871 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
872 final String lightId = lightStatusListener.getLightId();
873 if (!lightStatusListeners.containsKey(lightId)) {
874 lightStatusListeners.put(lightId, lightStatusListener);
875 final FullLight lastLightState = lastLightStates.get(lightId);
876 if (lastLightState != null) {
877 lightStatusListener.onLightAdded(lastLightState);
886 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
887 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
891 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
892 final String sensorId = sensorStatusListener.getSensorId();
893 if (!sensorStatusListeners.containsKey(sensorId)) {
894 sensorStatusListeners.put(sensorId, sensorStatusListener);
895 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
896 if (lastSensorState != null) {
897 sensorStatusListener.onSensorAdded(lastSensorState);
906 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
907 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
911 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
912 final String groupId = groupStatusListener.getGroupId();
913 if (!groupStatusListeners.containsKey(groupId)) {
914 groupStatusListeners.put(groupId, groupStatusListener);
915 final FullGroup lastGroupState = lastGroupStates.get(groupId);
916 if (lastGroupState != null) {
917 groupStatusListener.onGroupAdded(lastGroupState);
926 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
927 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
931 * Recall scene to all lights that belong to the scene.
933 * @param id the ID of the scene to activate
936 public void recallScene(String id) {
937 if (hueBridge != null) {
938 hueBridge.recallScene(id).thenAccept(result -> {
940 hueBridge.handleErrors(result);
941 } catch (Exception e) {
942 logger.debug("Error while recalling scene: {}", e.getMessage());
944 }).exceptionally(e -> {
945 logger.debug("Error while recalling scene: {}", e.getMessage());
949 logger.debug("No bridge connected or selected. Cannot activate scene.");
954 public @Nullable FullLight getLightById(String lightId) {
955 return lastLightStates.get(lightId);
959 public @Nullable FullSensor getSensorById(String sensorId) {
960 return lastSensorStates.get(sensorId);
964 public @Nullable FullGroup getGroupById(String groupId) {
965 return lastGroupStates.get(groupId);
968 public List<FullLight> getFullLights() {
969 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
970 return hueBridge.getFullLights();
972 return ret != null ? ret : List.of();
975 public List<FullSensor> getFullSensors() {
976 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
977 return hueBridge.getSensors();
979 return ret != null ? ret : List.of();
982 public List<FullGroup> getFullGroups() {
983 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
984 return hueBridge.getGroups();
986 return ret != null ? ret : List.of();
989 public void startSearch() {
990 withReAuthentication("start search mode", () -> {
991 hueBridge.startSearch();
996 public void startSearch(List<String> serialNumbers) {
997 withReAuthentication("start search mode", () -> {
998 hueBridge.startSearch(serialNumbers);
1003 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1004 if (hueBridge != null) {
1007 return runnable.call();
1008 } catch (UnauthorizedException | IllegalStateException e) {
1009 lastBridgeConnectionState = false;
1010 if (onNotAuthenticated()) {
1011 return runnable.call();
1014 } catch (Exception e) {
1015 logger.debug("Bridge cannot {}.", taskDescription, e);
1021 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1022 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1025 public List<String> listScenesForConsole() {
1026 return consoleScenesList;
1030 public Collection<ConfigStatusMessage> getConfigStatus() {
1031 // The bridge IP address to be used for checks
1032 // Check whether an IP address is provided
1033 String ip = hueBridgeConfig.getIpAddress();
1034 if (ip == null || ip.isEmpty()) {
1035 return List.of(ConfigStatusMessage.Builder.error(HOST)
1036 .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());