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.HueLightDiscoveryService;
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.types.Command;
67 import org.openhab.core.types.StateOption;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * {@link HueBridgeHandler} is the handler for a hue bridge and connects it to
73 * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
75 * @author Dennis Nobel - Initial contribution
76 * @author Oliver Libutzki - Adjustments
77 * @author Kai Kreuzer - improved state handling
78 * @author Andre Fuechsel - implemented getFullLights(), startSearch()
79 * @author Thomas Höfer - added thing properties
80 * @author Stefan Bußweiler - Added new thing status handling
81 * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
82 * @author Denis Dudnik - switched to internally integrated source of Jue library
83 * @author Samuel Leisering - Added support for sensor API
84 * @author Christoph Weitkamp - Added support for sensor API
85 * @author Laurent Garnier - Added support for groups
88 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
90 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
92 private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
94 private static final String DEVICE_TYPE = "EclipseSmartHome";
96 private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
98 private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
99 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
101 private final Map<String, @Nullable FullLight> lastLightStates = new ConcurrentHashMap<>();
102 private final Map<String, @Nullable FullSensor> lastSensorStates = new ConcurrentHashMap<>();
103 private final Map<String, @Nullable FullGroup> lastGroupStates = new ConcurrentHashMap<>();
105 private @Nullable HueLightDiscoveryService discoveryService;
106 private final Map<String, @Nullable LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
107 private final Map<String, @Nullable SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
108 private final Map<String, @Nullable GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
110 final ReentrantLock pollingLock = new ReentrantLock();
112 abstract class PollingRunnable implements Runnable {
117 if (!lastBridgeConnectionState) {
118 // if user is not set in configuration try to create a new user on Hue bridge
119 if (hueBridgeConfig.getUserName() == null) {
120 hueBridge.getFullConfig();
122 lastBridgeConnectionState = tryResumeBridgeConnection();
124 if (lastBridgeConnectionState) {
126 if (thing.getStatus() != ThingStatus.ONLINE) {
127 updateStatus(ThingStatus.ONLINE);
130 } catch (UnauthorizedException | IllegalStateException e) {
131 if (isReachable(hueBridge.getIPAddress())) {
132 lastBridgeConnectionState = false;
133 if (onNotAuthenticated()) {
134 updateStatus(ThingStatus.ONLINE);
136 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
137 lastBridgeConnectionState = false;
140 } catch (ApiException | IOException e) {
141 if (hueBridge != null && lastBridgeConnectionState) {
142 logger.debug("Connection to Hue Bridge {} lost.", hueBridge.getIPAddress());
143 lastBridgeConnectionState = false;
146 } catch (RuntimeException e) {
147 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
148 lastBridgeConnectionState = false;
151 pollingLock.unlock();
155 private boolean isReachable(String ipAddress) {
157 // note that InetAddress.isReachable is unreliable, see
158 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
159 // That's why we do an HTTP access instead
161 // If there is no connection, this line will fail
162 hueBridge.authenticate("invalid");
163 } catch (IOException e) {
165 } catch (ApiException e) {
166 if (e.getMessage().contains("SocketTimeout") || e.getMessage().contains("ConnectException")
167 || e.getMessage().contains("SocketException")
168 || e.getMessage().contains("NoRouteToHostException")) {
171 // this seems to be only an authentication issue
178 protected abstract void doConnectedRun() throws IOException, ApiException;
181 private final Runnable sensorPollingRunnable = new PollingRunnable() {
183 protected void doConnectedRun() throws IOException, ApiException {
184 Map<String, @Nullable FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
186 final HueLightDiscoveryService discovery = discoveryService;
188 for (final FullSensor sensor : hueBridge.getSensors()) {
189 String sensorId = sensor.getId();
191 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
192 if (sensorStatusListener == null) {
193 logger.trace("Hue sensor '{}' added.", sensorId);
195 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
196 discovery.addSensorDiscovery(sensor);
199 lastSensorStates.put(sensorId, sensor);
201 if (sensorStatusListener.onSensorStateChanged(sensor)) {
202 lastSensorStates.put(sensorId, sensor);
205 lastSensorStateCopy.remove(sensorId);
208 // Check for removed sensors
209 lastSensorStateCopy.forEach((sensorId, sensor) -> {
210 logger.trace("Hue sensor '{}' removed.", sensorId);
211 lastSensorStates.remove(sensorId);
213 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
214 if (sensorStatusListener != null) {
215 sensorStatusListener.onSensorRemoved();
218 if (discovery != null && sensor != null) {
219 discovery.removeSensorDiscovery(sensor);
225 private final Runnable lightPollingRunnable = new PollingRunnable() {
227 protected void doConnectedRun() throws IOException, ApiException {
232 private void updateLights() throws IOException, ApiException {
233 Map<String, @Nullable FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
235 List<FullLight> lights;
236 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
237 lights = hueBridge.getFullLights();
239 lights = hueBridge.getFullConfig().getLights();
242 final HueLightDiscoveryService discovery = discoveryService;
244 for (final FullLight fullLight : lights) {
245 final String lightId = fullLight.getId();
247 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
248 if (lightStatusListener == null) {
249 logger.trace("Hue light '{}' added.", lightId);
251 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
252 discovery.addLightDiscovery(fullLight);
255 lastLightStates.put(lightId, fullLight);
257 if (lightStatusListener.onLightStateChanged(fullLight)) {
258 lastLightStates.put(lightId, fullLight);
261 lastLightStateCopy.remove(lightId);
264 // Check for removed lights
265 lastLightStateCopy.forEach((lightId, light) -> {
266 logger.trace("Hue light '{}' removed.", lightId);
267 lastLightStates.remove(lightId);
269 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
270 if (lightStatusListener != null) {
271 lightStatusListener.onLightRemoved();
274 if (discovery != null && light != null) {
275 discovery.removeLightDiscovery(light);
280 private void updateGroups() throws IOException, ApiException {
281 Map<String, @Nullable FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
283 List<FullGroup> groups = hueBridge.getGroups();
285 final HueLightDiscoveryService discovery = discoveryService;
287 for (final FullGroup fullGroup : groups) {
288 State groupState = new State();
292 State colorRef = null;
293 HSBType firstColorHsb = null;
294 for (String lightId : fullGroup.getLightIds()) {
295 FullLight light = lastLightStates.get(lightId);
297 final State lightState = light.getState();
298 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
299 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
300 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
301 lightState.getColorMode(), lightState.getXY());
302 if (lightState.isOn()) {
304 sumBri += lightState.getBrightness();
306 if (lightState.getColorMode() != null) {
307 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
308 if (firstColorHsb == null) {
310 firstColorHsb = lightHsb;
311 colorRef = lightState;
312 } else if (!lightHsb.equals(firstColorHsb)) {
319 groupState.setOn(on);
320 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
321 if (colorRef != null) {
322 groupState.setColormode(colorRef.getColorMode());
323 groupState.setHue(colorRef.getHue());
324 groupState.setSaturation(colorRef.getSaturation());
325 groupState.setColorTemperature(colorRef.getColorTemperature());
326 groupState.setXY(colorRef.getXY());
328 fullGroup.setState(groupState);
329 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
330 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
331 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
334 String groupId = fullGroup.getId();
336 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
337 if (groupStatusListener == null) {
338 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
339 fullGroup.getLightIds().size());
341 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
342 discovery.addGroupDiscovery(fullGroup);
345 lastGroupStates.put(groupId, fullGroup);
347 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
348 lastGroupStates.put(groupId, fullGroup);
351 lastGroupStateCopy.remove(groupId);
354 // Check for removed groups
355 lastGroupStateCopy.forEach((groupId, group) -> {
356 logger.trace("Hue group '{}' removed.", groupId);
357 lastGroupStates.remove(groupId);
359 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
360 if (groupStatusListener != null) {
361 groupStatusListener.onGroupRemoved();
364 if (discovery != null && group != null) {
365 discovery.removeGroupDiscovery(group);
371 private final Runnable scenePollingRunnable = new PollingRunnable() {
373 protected void doConnectedRun() throws IOException, ApiException {
374 List<Scene> scenes = hueBridge.getScenes();
375 logger.trace("Scenes detected: {}", scenes);
377 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
378 notifyGroupSceneUpdate(scenes);
381 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, @Nullable FullGroup> groups) {
382 Map<String, String> groupNames = groups.entrySet().stream()
383 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
384 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
385 .collect(Collectors.toList());
386 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
388 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
389 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
393 private boolean lastBridgeConnectionState = false;
395 private boolean propertiesInitializedSuccessfully = false;
397 private @Nullable Future<?> initJob;
398 private @Nullable ScheduledFuture<?> lightPollingJob;
399 private @Nullable ScheduledFuture<?> sensorPollingJob;
400 private @Nullable ScheduledFuture<?> scenePollingJob;
402 private @NonNullByDefault({}) HueBridge hueBridge = null;
403 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
405 private List<String> consoleScenesList = new ArrayList<>();
407 public HueBridgeHandler(Bridge bridge, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
409 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
413 public void handleCommand(ChannelUID channelUID, Command command) {
414 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
415 recallScene(command.toString());
420 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
422 if (hueBridge != null) {
423 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
424 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
426 hueBridge.handleErrors(result);
427 listener.setPollBypass(fadeTime);
428 } catch (Exception e) {
429 listener.unsetPollBypass();
430 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
432 }).exceptionally(e -> {
433 listener.unsetPollBypass();
434 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
438 logger.debug("No bridge connected or selected. Cannot set light state.");
443 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
444 if (hueBridge != null) {
445 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
447 hueBridge.handleErrors(result);
448 } catch (Exception e) {
449 handleSensorUpdateException(sensor, e);
451 }).exceptionally(e -> {
452 handleSensorUpdateException(sensor, e);
456 logger.debug("No bridge connected or selected. Cannot set sensor state.");
461 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
462 if (hueBridge != null) {
463 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
465 hueBridge.handleErrors(result);
466 } catch (Exception e) {
467 handleSensorUpdateException(sensor, e);
469 }).exceptionally(e -> {
470 handleSensorUpdateException(sensor, e);
474 logger.debug("No bridge connected or selected. Cannot set sensor config.");
479 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
480 if (hueBridge != null) {
481 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
482 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
484 hueBridge.handleErrors(result);
485 setGroupPollBypass(group, fadeTime);
486 } catch (Exception e) {
487 unsetGroupPollBypass(group);
488 handleGroupUpdateException(group, e);
490 }).exceptionally(e -> {
491 unsetGroupPollBypass(group);
492 handleGroupUpdateException(group, e);
496 logger.debug("No bridge connected or selected. Cannot set group state.");
500 private void setGroupPollBypass(FullGroup group, long bypassTime) {
501 group.getLightIds().forEach((lightId) -> {
502 final LightStatusListener listener = lightStatusListeners.get(lightId);
503 if (listener != null) {
504 listener.setPollBypass(bypassTime);
509 private void unsetGroupPollBypass(FullGroup group) {
510 group.getLightIds().forEach((lightId) -> {
511 final LightStatusListener listener = lightStatusListeners.get(lightId);
512 if (listener != null) {
513 listener.unsetPollBypass();
518 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
519 long fadeTime, Throwable e) {
520 if (e instanceof DeviceOffException) {
521 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
522 // If there is only a change of the color temperature, we do not want the light
523 // to be turned on (i.e. change its brightness).
526 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
527 updateLightState(listener, light, stateUpdate, fadeTime);
529 } else if (e instanceof EntityNotAvailableException) {
530 logger.debug("Error while accessing light: {}", e.getMessage(), e);
531 final HueLightDiscoveryService discovery = discoveryService;
532 if (discovery != null) {
533 discovery.removeLightDiscovery(light);
535 listener.onLightGone();
537 handleThingUpdateException("light", e);
541 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
542 if (e instanceof EntityNotAvailableException) {
543 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
544 final HueLightDiscoveryService discovery = discoveryService;
545 if (discovery != null) {
546 discovery.removeSensorDiscovery(sensor);
548 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
549 if (listener != null) {
550 listener.onSensorGone();
553 handleThingUpdateException("sensor", e);
557 private void handleGroupUpdateException(FullGroup group, Throwable e) {
558 if (e instanceof EntityNotAvailableException) {
559 logger.debug("Error while accessing group: {}", e.getMessage(), e);
560 final HueLightDiscoveryService discovery = discoveryService;
561 if (discovery != null) {
562 discovery.removeGroupDiscovery(group);
564 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
565 if (listener != null) {
566 listener.onGroupGone();
569 handleThingUpdateException("group", e);
573 private void handleThingUpdateException(String thingType, Throwable e) {
574 if (e instanceof IOException) {
575 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
576 } else if (e instanceof ApiException) {
577 // This should not happen - if it does, it is most likely some bug that should be reported.
578 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
579 } else if (e instanceof IllegalStateException) {
580 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
584 private void startLightPolling() {
585 ScheduledFuture<?> job = lightPollingJob;
586 if (job == null || job.isCancelled()) {
587 long lightPollingInterval;
588 int configPollingInterval = hueBridgeConfig.getPollingInterval();
589 if (configPollingInterval < 1) {
590 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
591 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
592 lightPollingInterval);
594 lightPollingInterval = configPollingInterval;
596 // Delay the first execution to give a chance to have all light and group things registered
597 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
602 private void stopLightPolling() {
603 ScheduledFuture<?> job = lightPollingJob;
607 lightPollingJob = null;
610 private void startSensorPolling() {
611 ScheduledFuture<?> job = sensorPollingJob;
612 if (job == null || job.isCancelled()) {
613 int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
614 if (configSensorPollingInterval > 0) {
615 long sensorPollingInterval;
616 if (configSensorPollingInterval < 50) {
617 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
618 logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
619 sensorPollingInterval);
621 sensorPollingInterval = configSensorPollingInterval;
623 // Delay the first execution to give a chance to have all sensor things registered
624 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
625 TimeUnit.MILLISECONDS);
630 private void stopSensorPolling() {
631 ScheduledFuture<?> job = sensorPollingJob;
635 sensorPollingJob = null;
638 private void startScenePolling() {
639 ScheduledFuture<?> job = scenePollingJob;
640 if (job == null || job.isCancelled()) {
641 // Delay the first execution to give a chance to have all group things registered
642 scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
647 private void stopScenePolling() {
648 ScheduledFuture<?> job = scenePollingJob;
652 scenePollingJob = null;
656 public void dispose() {
657 logger.debug("Handler disposed.");
658 Future<?> job = initJob;
665 if (hueBridge != null) {
671 public void initialize() {
672 logger.debug("Initializing hue bridge handler.");
673 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
675 String ip = hueBridgeConfig.getIpAddress();
676 if (ip == null || ip.isEmpty()) {
677 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
678 "@text/offline.conf-error-no-ip-address");
680 if (hueBridge == null) {
681 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
682 hueBridge.setTimeout(5000);
684 // Try a first connection that will fail, then try to authenticate,
685 // and finally change the bridge status to ONLINE
686 initJob = scheduler.submit(new PollingRunnable() {
688 protected void doConnectedRun() throws IOException, ApiException {
696 public @Nullable String getUserName() {
697 return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
700 private synchronized void onUpdate() {
701 if (hueBridge != null) {
703 startSensorPolling();
709 * This method is called whenever the connection to the {@link HueBridge} is lost.
711 public void onConnectionLost() {
712 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
713 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
717 * This method is called whenever the connection to the {@link HueBridge} is resumed.
719 * @throws ApiException if the physical device does not support this API call
720 * @throws IOException if the physical device could not be reached
722 private void onConnectionResumed() throws IOException, ApiException {
723 logger.debug("Bridge connection resumed.");
725 if (!propertiesInitializedSuccessfully) {
726 FullConfig fullConfig = hueBridge.getFullConfig();
727 Config config = fullConfig.getConfig();
728 if (config != null) {
729 Map<String, String> properties = editProperties();
730 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
731 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
732 properties.put(PROPERTY_MODEL_ID, config.getModelId());
733 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
734 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
735 updateProperties(properties);
736 propertiesInitializedSuccessfully = true;
742 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
744 * @return True if USER_NAME was not null.
745 * @throws ApiException if the physical device does not support this API call
746 * @throws IOException if the physical device could not be reached
748 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
749 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
750 if (hueBridgeConfig.getUserName() == null) {
752 "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
753 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
754 "@text/offline.conf-error-no-username");
757 onConnectionResumed();
763 * This method is called whenever the connection to the {@link HueBridge} is available,
764 * but requests are not allowed due to a missing or invalid authentication.
766 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
767 * be requested from the bridge.
769 * @param bridge the hue bridge the connection is not authorized
770 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
772 public boolean onNotAuthenticated() {
773 if (hueBridge == null) {
776 String userName = hueBridgeConfig.getUserName();
777 if (userName == null) {
781 hueBridge.authenticate(userName);
783 } catch (Exception e) {
784 handleAuthenticationFailure(e, userName);
790 private void createUser() {
792 String newUser = createUserOnPhysicalBridge();
793 updateBridgeThingConfiguration(newUser);
794 } catch (LinkButtonException ex) {
795 handleLinkButtonNotPressed(ex);
796 } catch (Exception ex) {
797 handleExceptionWhileCreatingUser(ex);
801 private String createUserOnPhysicalBridge() throws IOException, ApiException {
802 logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
803 hueBridgeConfig.getIpAddress());
804 String userName = hueBridge.link(DEVICE_TYPE);
805 logger.info("User has been successfully added to Hue bridge.");
809 private void updateBridgeThingConfiguration(String userName) {
810 Configuration config = editConfiguration();
811 config.put(USER_NAME, userName);
813 updateConfiguration(config);
814 logger.debug("Updated configuration parameter '{}'", USER_NAME);
815 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
816 } catch (IllegalStateException e) {
817 logger.trace("Configuration update failed.", e);
818 logger.warn("Unable to update configuration of Hue bridge.");
819 logger.warn("Please configure the user name manually.");
823 private void handleAuthenticationFailure(Exception ex, String userName) {
824 logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
825 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
826 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
827 "@text/offline.conf-error-invalid-username");
830 private void handleLinkButtonNotPressed(LinkButtonException ex) {
831 logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
832 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
833 "@text/offline.conf-error-press-pairing-button");
836 private void handleExceptionWhileCreatingUser(Exception ex) {
837 logger.warn("Failed creating new user on Hue bridge", ex);
838 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
839 "@text/offline.conf-error-creation-username");
843 public boolean registerDiscoveryListener(HueLightDiscoveryService listener) {
844 if (discoveryService == null) {
845 discoveryService = listener;
846 getFullLights().forEach(listener::addLightDiscovery);
847 getFullSensors().forEach(listener::addSensorDiscovery);
848 getFullGroups().forEach(listener::addGroupDiscovery);
856 public boolean unregisterDiscoveryListener() {
857 if (discoveryService != null) {
858 discoveryService = null;
866 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
867 final String lightId = lightStatusListener.getLightId();
868 if (!lightStatusListeners.containsKey(lightId)) {
869 lightStatusListeners.put(lightId, lightStatusListener);
870 final FullLight lastLightState = lastLightStates.get(lightId);
871 if (lastLightState != null) {
872 lightStatusListener.onLightAdded(lastLightState);
881 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
882 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
886 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
887 final String sensorId = sensorStatusListener.getSensorId();
888 if (!sensorStatusListeners.containsKey(sensorId)) {
889 sensorStatusListeners.put(sensorId, sensorStatusListener);
890 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
891 if (lastSensorState != null) {
892 sensorStatusListener.onSensorAdded(lastSensorState);
901 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
902 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
906 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
907 final String groupId = groupStatusListener.getGroupId();
908 if (!groupStatusListeners.containsKey(groupId)) {
909 groupStatusListeners.put(groupId, groupStatusListener);
910 final FullGroup lastGroupState = lastGroupStates.get(groupId);
911 if (lastGroupState != null) {
912 groupStatusListener.onGroupAdded(lastGroupState);
921 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
922 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
926 * Recall scene to all lights that belong to the scene.
928 * @param id the ID of the scene to activate
931 public void recallScene(String id) {
932 if (hueBridge != null) {
933 hueBridge.recallScene(id).thenAccept(result -> {
935 hueBridge.handleErrors(result);
936 } catch (Exception e) {
937 logger.debug("Error while recalling scene: {}", e.getMessage());
939 }).exceptionally(e -> {
940 logger.debug("Error while recalling scene: {}", e.getMessage());
944 logger.debug("No bridge connected or selected. Cannot activate scene.");
949 public @Nullable FullLight getLightById(String lightId) {
950 return lastLightStates.get(lightId);
954 public @Nullable FullSensor getSensorById(String sensorId) {
955 return lastSensorStates.get(sensorId);
959 public @Nullable FullGroup getGroupById(String groupId) {
960 return lastGroupStates.get(groupId);
963 public List<FullLight> getFullLights() {
964 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
965 return hueBridge.getFullLights();
967 return ret != null ? ret : Collections.emptyList();
970 public List<FullSensor> getFullSensors() {
971 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
972 return hueBridge.getSensors();
974 return ret != null ? ret : Collections.emptyList();
977 public List<FullGroup> getFullGroups() {
978 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
979 return hueBridge.getGroups();
981 return ret != null ? ret : Collections.emptyList();
984 public void startSearch() {
985 withReAuthentication("start search mode", () -> {
986 hueBridge.startSearch();
991 public void startSearch(List<String> serialNumbers) {
992 withReAuthentication("start search mode", () -> {
993 hueBridge.startSearch(serialNumbers);
998 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
999 if (hueBridge != null) {
1002 return runnable.call();
1003 } catch (UnauthorizedException | IllegalStateException e) {
1004 lastBridgeConnectionState = false;
1005 if (onNotAuthenticated()) {
1006 return runnable.call();
1009 } catch (Exception e) {
1010 logger.debug("Bridge cannot {}.", taskDescription, e);
1016 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1017 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1020 public List<String> listScenesForConsole() {
1021 return consoleScenesList;
1025 public Collection<ConfigStatusMessage> getConfigStatus() {
1026 // The bridge IP address to be used for checks
1027 Collection<ConfigStatusMessage> configStatusMessages;
1029 // Check whether an IP address is provided
1030 String ip = hueBridgeConfig.getIpAddress();
1031 if (ip == null || ip.isEmpty()) {
1032 configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(HOST)
1033 .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());
1035 configStatusMessages = Collections.emptyList();
1038 return configStatusMessages;