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.i18n.LocaleProvider;
58 import org.openhab.core.i18n.TranslationProvider;
59 import org.openhab.core.library.types.HSBType;
60 import org.openhab.core.library.types.OnOffType;
61 import org.openhab.core.library.types.StringType;
62 import org.openhab.core.thing.Bridge;
63 import org.openhab.core.thing.ChannelUID;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.ThingTypeUID;
67 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
68 import org.openhab.core.thing.binding.ThingHandlerService;
69 import org.openhab.core.types.Command;
70 import org.openhab.core.types.StateOption;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
75 * {@link HueBridgeHandler} is the handler for a hue bridge and connects it to
76 * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
78 * @author Dennis Nobel - Initial contribution
79 * @author Oliver Libutzki - Adjustments
80 * @author Kai Kreuzer - improved state handling
81 * @author Andre Fuechsel - implemented getFullLights(), startSearch()
82 * @author Thomas Höfer - added thing properties
83 * @author Stefan Bußweiler - Added new thing status handling
84 * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
85 * @author Denis Dudnik - switched to internally integrated source of Jue library
86 * @author Samuel Leisering - Added support for sensor API
87 * @author Christoph Weitkamp - Added support for sensor API
88 * @author Laurent Garnier - Added support for groups
91 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
93 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
95 private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
97 private static final String DEVICE_TYPE = "EclipseSmartHome";
99 private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
101 private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
102 private final HueStateDescriptionProvider stateDescriptionOptionProvider;
103 private final TranslationProvider i18nProvider;
104 private final LocaleProvider localeProvider;
106 private final Map<String, FullLight> lastLightStates = new ConcurrentHashMap<>();
107 private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
108 private final Map<String, FullGroup> lastGroupStates = new ConcurrentHashMap<>();
110 private @Nullable HueDeviceDiscoveryService discoveryService;
111 private final Map<String, LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
112 private final Map<String, SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
113 private final Map<String, GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
115 final ReentrantLock pollingLock = new ReentrantLock();
117 abstract class PollingRunnable implements Runnable {
122 if (!lastBridgeConnectionState) {
123 // if user is not set in configuration try to create a new user on Hue bridge
124 if (hueBridgeConfig.getUserName() == null) {
125 hueBridge.getFullConfig();
127 lastBridgeConnectionState = tryResumeBridgeConnection();
129 if (lastBridgeConnectionState) {
131 if (thing.getStatus() != ThingStatus.ONLINE) {
132 updateStatus(ThingStatus.ONLINE);
135 } catch (UnauthorizedException | IllegalStateException e) {
136 if (isReachable(hueBridge.getIPAddress())) {
137 lastBridgeConnectionState = false;
138 if (onNotAuthenticated()) {
139 updateStatus(ThingStatus.ONLINE);
141 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
142 lastBridgeConnectionState = false;
145 } catch (ApiException | IOException e) {
146 if (hueBridge != null && lastBridgeConnectionState) {
147 logger.debug("Connection to Hue Bridge {} lost: {}", hueBridge.getIPAddress(), e.getMessage(), e);
148 lastBridgeConnectionState = false;
151 } catch (RuntimeException e) {
152 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
153 lastBridgeConnectionState = false;
156 pollingLock.unlock();
160 private boolean isReachable(String ipAddress) {
162 // note that InetAddress.isReachable is unreliable, see
163 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
164 // That's why we do an HTTP access instead
166 // If there is no connection, this line will fail
167 hueBridge.authenticate("invalid");
168 } catch (IOException e) {
170 } catch (ApiException e) {
171 String message = e.getMessage();
172 return message != null && //
173 !message.contains("SocketTimeout") && //
174 !message.contains("ConnectException") && //
175 !message.contains("SocketException") && //
176 !message.contains("NoRouteToHostException");
181 protected abstract void doConnectedRun() throws IOException, ApiException;
184 private final Runnable sensorPollingRunnable = new PollingRunnable() {
186 protected void doConnectedRun() throws IOException, ApiException {
187 Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
189 final HueDeviceDiscoveryService discovery = discoveryService;
191 for (final FullSensor sensor : hueBridge.getSensors()) {
192 String sensorId = sensor.getId();
194 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
195 if (sensorStatusListener == null) {
196 logger.trace("Hue sensor '{}' added.", sensorId);
198 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
199 discovery.addSensorDiscovery(sensor);
202 lastSensorStates.put(sensorId, sensor);
204 if (sensorStatusListener.onSensorStateChanged(sensor)) {
205 lastSensorStates.put(sensorId, sensor);
208 lastSensorStateCopy.remove(sensorId);
211 // Check for removed sensors
212 lastSensorStateCopy.forEach((sensorId, sensor) -> {
213 logger.trace("Hue sensor '{}' removed.", sensorId);
214 lastSensorStates.remove(sensorId);
216 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
217 if (sensorStatusListener != null) {
218 sensorStatusListener.onSensorRemoved();
221 if (discovery != null && sensor != null) {
222 discovery.removeSensorDiscovery(sensor);
228 private final Runnable lightPollingRunnable = new PollingRunnable() {
230 protected void doConnectedRun() throws IOException, ApiException {
235 private void updateLights() throws IOException, ApiException {
236 Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
238 List<FullLight> lights;
239 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
240 lights = hueBridge.getFullLights();
242 lights = hueBridge.getFullConfig().getLights();
245 final HueDeviceDiscoveryService discovery = discoveryService;
247 for (final FullLight fullLight : lights) {
248 final String lightId = fullLight.getId();
250 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
251 if (lightStatusListener == null) {
252 logger.trace("Hue light '{}' added.", lightId);
254 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
255 discovery.addLightDiscovery(fullLight);
258 lastLightStates.put(lightId, fullLight);
260 if (lightStatusListener.onLightStateChanged(fullLight)) {
261 lastLightStates.put(lightId, fullLight);
264 lastLightStateCopy.remove(lightId);
267 // Check for removed lights
268 lastLightStateCopy.forEach((lightId, light) -> {
269 logger.trace("Hue light '{}' removed.", lightId);
270 lastLightStates.remove(lightId);
272 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
273 if (lightStatusListener != null) {
274 lightStatusListener.onLightRemoved();
277 if (discovery != null && light != null) {
278 discovery.removeLightDiscovery(light);
283 private void updateGroups() throws IOException, ApiException {
284 Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
286 List<FullGroup> groups = hueBridge.getGroups();
288 final HueDeviceDiscoveryService discovery = discoveryService;
290 for (final FullGroup fullGroup : groups) {
291 State groupState = new State();
295 State colorRef = null;
296 HSBType firstColorHsb = null;
297 for (String lightId : fullGroup.getLightIds()) {
298 FullLight light = lastLightStates.get(lightId);
300 final State lightState = light.getState();
301 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
302 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
303 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
304 lightState.getColorMode(), lightState.getXY());
305 if (lightState.isOn()) {
307 sumBri += lightState.getBrightness();
309 if (lightState.getColorMode() != null) {
310 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
311 if (firstColorHsb == null) {
313 firstColorHsb = lightHsb;
314 colorRef = lightState;
315 } else if (!lightHsb.equals(firstColorHsb)) {
322 groupState.setOn(on);
323 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
324 if (colorRef != null) {
325 groupState.setColormode(colorRef.getColorMode());
326 groupState.setHue(colorRef.getHue());
327 groupState.setSaturation(colorRef.getSaturation());
328 groupState.setColorTemperature(colorRef.getColorTemperature());
329 groupState.setXY(colorRef.getXY());
331 fullGroup.setState(groupState);
332 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
333 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
334 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
337 String groupId = fullGroup.getId();
339 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
340 if (groupStatusListener == null) {
341 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
342 fullGroup.getLightIds().size());
344 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
345 discovery.addGroupDiscovery(fullGroup);
348 lastGroupStates.put(groupId, fullGroup);
350 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
351 lastGroupStates.put(groupId, fullGroup);
354 lastGroupStateCopy.remove(groupId);
357 // Check for removed groups
358 lastGroupStateCopy.forEach((groupId, group) -> {
359 logger.trace("Hue group '{}' removed.", groupId);
360 lastGroupStates.remove(groupId);
362 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
363 if (groupStatusListener != null) {
364 groupStatusListener.onGroupRemoved();
367 if (discovery != null && group != null) {
368 discovery.removeGroupDiscovery(group);
374 private final Runnable scenePollingRunnable = new PollingRunnable() {
376 protected void doConnectedRun() throws IOException, ApiException {
377 List<Scene> scenes = hueBridge.getScenes();
378 logger.trace("Scenes detected: {}", scenes);
380 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
381 notifyGroupSceneUpdate(scenes);
384 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
385 Map<String, String> groupNames = groups.entrySet().stream()
386 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
387 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
388 .collect(Collectors.toList());
389 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
391 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
392 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
396 private boolean lastBridgeConnectionState = false;
398 private boolean propertiesInitializedSuccessfully = false;
400 private @Nullable Future<?> initJob;
401 private @Nullable ScheduledFuture<?> lightPollingJob;
402 private @Nullable ScheduledFuture<?> sensorPollingJob;
403 private @Nullable ScheduledFuture<?> scenePollingJob;
405 private @NonNullByDefault({}) HueBridge hueBridge = null;
406 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
408 private List<String> consoleScenesList = new ArrayList<>();
410 public HueBridgeHandler(Bridge bridge, HueStateDescriptionProvider stateDescriptionOptionProvider,
411 TranslationProvider i18nProvider, LocaleProvider localeProvider) {
413 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
414 this.i18nProvider = i18nProvider;
415 this.localeProvider = localeProvider;
419 public Collection<Class<? extends ThingHandlerService>> getServices() {
420 return Collections.singleton(HueDeviceDiscoveryService.class);
424 public void handleCommand(ChannelUID channelUID, Command command) {
425 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
426 recallScene(command.toString());
431 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
433 if (hueBridge != null) {
434 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
435 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
437 hueBridge.handleErrors(result);
438 listener.setPollBypass(fadeTime);
439 } catch (Exception e) {
440 listener.unsetPollBypass();
441 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
443 }).exceptionally(e -> {
444 listener.unsetPollBypass();
445 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
449 logger.debug("No bridge connected or selected. Cannot set light state.");
454 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
455 if (hueBridge != null) {
456 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
458 hueBridge.handleErrors(result);
459 } catch (Exception e) {
460 handleSensorUpdateException(sensor, e);
462 }).exceptionally(e -> {
463 handleSensorUpdateException(sensor, e);
467 logger.debug("No bridge connected or selected. Cannot set sensor state.");
472 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
473 if (hueBridge != null) {
474 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
476 hueBridge.handleErrors(result);
477 } catch (Exception e) {
478 handleSensorUpdateException(sensor, e);
480 }).exceptionally(e -> {
481 handleSensorUpdateException(sensor, e);
485 logger.debug("No bridge connected or selected. Cannot set sensor config.");
490 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
491 if (hueBridge != null) {
492 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
493 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
495 hueBridge.handleErrors(result);
496 setGroupPollBypass(group, fadeTime);
497 } catch (Exception e) {
498 unsetGroupPollBypass(group);
499 handleGroupUpdateException(group, e);
501 }).exceptionally(e -> {
502 unsetGroupPollBypass(group);
503 handleGroupUpdateException(group, e);
507 logger.debug("No bridge connected or selected. Cannot set group state.");
511 private void setGroupPollBypass(FullGroup group, long bypassTime) {
512 group.getLightIds().forEach((lightId) -> {
513 final LightStatusListener listener = lightStatusListeners.get(lightId);
514 if (listener != null) {
515 listener.setPollBypass(bypassTime);
520 private void unsetGroupPollBypass(FullGroup group) {
521 group.getLightIds().forEach((lightId) -> {
522 final LightStatusListener listener = lightStatusListeners.get(lightId);
523 if (listener != null) {
524 listener.unsetPollBypass();
529 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
530 long fadeTime, Throwable e) {
531 if (e instanceof DeviceOffException) {
532 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
533 // If there is only a change of the color temperature, we do not want the light
534 // to be turned on (i.e. change its brightness).
537 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
538 updateLightState(listener, light, stateUpdate, fadeTime);
540 } else if (e instanceof EntityNotAvailableException) {
541 logger.debug("Error while accessing light: {}", e.getMessage(), e);
542 final HueDeviceDiscoveryService discovery = discoveryService;
543 if (discovery != null) {
544 discovery.removeLightDiscovery(light);
546 listener.onLightGone();
548 handleThingUpdateException("light", e);
552 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
553 if (e instanceof EntityNotAvailableException) {
554 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
555 final HueDeviceDiscoveryService discovery = discoveryService;
556 if (discovery != null) {
557 discovery.removeSensorDiscovery(sensor);
559 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
560 if (listener != null) {
561 listener.onSensorGone();
564 handleThingUpdateException("sensor", e);
568 private void handleGroupUpdateException(FullGroup group, Throwable e) {
569 if (e instanceof EntityNotAvailableException) {
570 logger.debug("Error while accessing group: {}", e.getMessage(), e);
571 final HueDeviceDiscoveryService discovery = discoveryService;
572 if (discovery != null) {
573 discovery.removeGroupDiscovery(group);
575 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
576 if (listener != null) {
577 listener.onGroupGone();
580 handleThingUpdateException("group", e);
584 private void handleThingUpdateException(String thingType, Throwable e) {
585 if (e instanceof IOException) {
586 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
587 } else if (e instanceof ApiException) {
588 // This should not happen - if it does, it is most likely some bug that should be reported.
589 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
590 } else if (e instanceof IllegalStateException) {
591 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
595 private void startLightPolling() {
596 ScheduledFuture<?> job = lightPollingJob;
597 if (job == null || job.isCancelled()) {
598 long lightPollingInterval;
599 int configPollingInterval = hueBridgeConfig.getPollingInterval();
600 if (configPollingInterval < 1) {
601 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
602 logger.info("Wrong configuration value for polling interval. Using default value: {}s",
603 lightPollingInterval);
605 lightPollingInterval = configPollingInterval;
607 // Delay the first execution to give a chance to have all light and group things registered
608 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
613 private void stopLightPolling() {
614 ScheduledFuture<?> job = lightPollingJob;
618 lightPollingJob = null;
621 private void startSensorPolling() {
622 ScheduledFuture<?> job = sensorPollingJob;
623 if (job == null || job.isCancelled()) {
624 int configSensorPollingInterval = hueBridgeConfig.getSensorPollingInterval();
625 if (configSensorPollingInterval > 0) {
626 long sensorPollingInterval;
627 if (configSensorPollingInterval < 50) {
628 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
629 logger.info("Wrong configuration value for sensor polling interval. Using default value: {}ms",
630 sensorPollingInterval);
632 sensorPollingInterval = configSensorPollingInterval;
634 // Delay the first execution to give a chance to have all sensor things registered
635 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
636 TimeUnit.MILLISECONDS);
641 private void stopSensorPolling() {
642 ScheduledFuture<?> job = sensorPollingJob;
646 sensorPollingJob = null;
649 private void startScenePolling() {
650 ScheduledFuture<?> job = scenePollingJob;
651 if (job == null || job.isCancelled()) {
652 // Delay the first execution to give a chance to have all group things registered
653 scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
658 private void stopScenePolling() {
659 ScheduledFuture<?> job = scenePollingJob;
663 scenePollingJob = null;
667 public void dispose() {
668 logger.debug("Handler disposed.");
669 Future<?> job = initJob;
676 if (hueBridge != null) {
682 public void initialize() {
683 logger.debug("Initializing hue bridge handler.");
684 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
686 String ip = hueBridgeConfig.getIpAddress();
687 if (ip == null || ip.isEmpty()) {
688 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
689 "@text/offline.conf-error-no-ip-address");
691 if (hueBridge == null) {
692 hueBridge = new HueBridge(ip, hueBridgeConfig.getPort(), hueBridgeConfig.getProtocol(), scheduler);
693 hueBridge.setTimeout(5000);
695 updateStatus(ThingStatus.UNKNOWN);
697 // Try a first connection that will fail, then try to authenticate,
698 // and finally change the bridge status to ONLINE
699 initJob = scheduler.submit(new PollingRunnable() {
701 protected void doConnectedRun() throws IOException, ApiException {
709 public @Nullable String getUserName() {
710 return hueBridgeConfig == null ? null : hueBridgeConfig.getUserName();
713 private synchronized void onUpdate() {
714 if (hueBridge != null) {
716 startSensorPolling();
722 * This method is called whenever the connection to the {@link HueBridge} is lost.
724 public void onConnectionLost() {
725 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
726 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
730 * This method is called whenever the connection to the {@link HueBridge} is resumed.
732 * @throws ApiException if the physical device does not support this API call
733 * @throws IOException if the physical device could not be reached
735 private void onConnectionResumed() throws IOException, ApiException {
736 logger.debug("Bridge connection resumed.");
738 if (!propertiesInitializedSuccessfully) {
739 FullConfig fullConfig = hueBridge.getFullConfig();
740 Config config = fullConfig.getConfig();
741 if (config != null) {
742 Map<String, String> properties = editProperties();
743 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
744 serialNumber = serialNumber.toLowerCase();
745 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
746 properties.put(PROPERTY_MODEL_ID, config.getModelId());
747 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
748 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
749 updateProperties(properties);
750 propertiesInitializedSuccessfully = true;
756 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
758 * @return True if USER_NAME was not null.
759 * @throws ApiException if the physical device does not support this API call
760 * @throws IOException if the physical device could not be reached
762 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
763 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
764 if (hueBridgeConfig.getUserName() == null) {
766 "User name for Hue bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
767 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
768 "@text/offline.conf-error-no-username");
771 onConnectionResumed();
777 * This method is called whenever the connection to the {@link HueBridge} is available,
778 * but requests are not allowed due to a missing or invalid authentication.
780 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
781 * be requested from the bridge.
783 * @param bridge the hue bridge the connection is not authorized
784 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
786 public boolean onNotAuthenticated() {
787 if (hueBridge == null) {
790 String userName = hueBridgeConfig.getUserName();
791 if (userName == null) {
795 hueBridge.authenticate(userName);
797 } catch (Exception e) {
798 handleAuthenticationFailure(e, userName);
804 private void createUser() {
806 String newUser = createUserOnPhysicalBridge();
807 updateBridgeThingConfiguration(newUser);
808 } catch (LinkButtonException ex) {
809 handleLinkButtonNotPressed(ex);
810 } catch (Exception ex) {
811 handleExceptionWhileCreatingUser(ex);
815 private String createUserOnPhysicalBridge() throws IOException, ApiException {
816 logger.info("Creating new user on Hue bridge {} - please press the pairing button on the bridge.",
817 hueBridgeConfig.getIpAddress());
818 String userName = hueBridge.link(DEVICE_TYPE);
819 logger.info("User has been successfully added to Hue bridge.");
823 private void updateBridgeThingConfiguration(String userName) {
824 Configuration config = editConfiguration();
825 config.put(USER_NAME, userName);
827 updateConfiguration(config);
828 logger.debug("Updated configuration parameter '{}'", USER_NAME);
829 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
830 } catch (IllegalStateException e) {
831 logger.trace("Configuration update failed.", e);
832 logger.warn("Unable to update configuration of Hue bridge.");
833 logger.warn("Please configure the user name manually.");
837 private void handleAuthenticationFailure(Exception ex, String userName) {
838 logger.warn("User is not authenticated on Hue bridge {}", hueBridgeConfig.getIpAddress());
839 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
840 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
841 "@text/offline.conf-error-invalid-username");
844 private void handleLinkButtonNotPressed(LinkButtonException ex) {
845 logger.debug("Failed creating new user on Hue bridge: {}", ex.getMessage());
846 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
847 "@text/offline.conf-error-press-pairing-button");
850 private void handleExceptionWhileCreatingUser(Exception ex) {
851 logger.warn("Failed creating new user on Hue bridge", ex);
852 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
853 "@text/offline.conf-error-creation-username");
857 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
858 if (discoveryService == null) {
859 discoveryService = listener;
860 getFullLights().forEach(listener::addLightDiscovery);
861 getFullSensors().forEach(listener::addSensorDiscovery);
862 getFullGroups().forEach(listener::addGroupDiscovery);
870 public boolean unregisterDiscoveryListener() {
871 if (discoveryService != null) {
872 discoveryService = null;
880 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
881 final String lightId = lightStatusListener.getLightId();
882 if (!lightStatusListeners.containsKey(lightId)) {
883 lightStatusListeners.put(lightId, lightStatusListener);
884 final FullLight lastLightState = lastLightStates.get(lightId);
885 if (lastLightState != null) {
886 lightStatusListener.onLightAdded(lastLightState);
895 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
896 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
900 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
901 final String sensorId = sensorStatusListener.getSensorId();
902 if (!sensorStatusListeners.containsKey(sensorId)) {
903 sensorStatusListeners.put(sensorId, sensorStatusListener);
904 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
905 if (lastSensorState != null) {
906 sensorStatusListener.onSensorAdded(lastSensorState);
915 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
916 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
920 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
921 final String groupId = groupStatusListener.getGroupId();
922 if (!groupStatusListeners.containsKey(groupId)) {
923 groupStatusListeners.put(groupId, groupStatusListener);
924 final FullGroup lastGroupState = lastGroupStates.get(groupId);
925 if (lastGroupState != null) {
926 groupStatusListener.onGroupAdded(lastGroupState);
935 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
936 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
940 * Recall scene to all lights that belong to the scene.
942 * @param id the ID of the scene to activate
945 public void recallScene(String id) {
946 if (hueBridge != null) {
947 hueBridge.recallScene(id).thenAccept(result -> {
949 hueBridge.handleErrors(result);
950 } catch (Exception e) {
951 logger.debug("Error while recalling scene: {}", e.getMessage());
953 }).exceptionally(e -> {
954 logger.debug("Error while recalling scene: {}", e.getMessage());
958 logger.debug("No bridge connected or selected. Cannot activate scene.");
963 public @Nullable FullLight getLightById(String lightId) {
964 return lastLightStates.get(lightId);
968 public @Nullable FullSensor getSensorById(String sensorId) {
969 return lastSensorStates.get(sensorId);
973 public @Nullable FullGroup getGroupById(String groupId) {
974 return lastGroupStates.get(groupId);
977 public List<FullLight> getFullLights() {
978 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
979 return hueBridge.getFullLights();
981 return ret != null ? ret : List.of();
984 public List<FullSensor> getFullSensors() {
985 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
986 return hueBridge.getSensors();
988 return ret != null ? ret : List.of();
991 public List<FullGroup> getFullGroups() {
992 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
993 return hueBridge.getGroups();
995 return ret != null ? ret : List.of();
998 public void startSearch() {
999 withReAuthentication("start search mode", () -> {
1000 hueBridge.startSearch();
1005 public void startSearch(List<String> serialNumbers) {
1006 withReAuthentication("start search mode", () -> {
1007 hueBridge.startSearch(serialNumbers);
1012 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1013 if (hueBridge != null) {
1016 return runnable.call();
1017 } catch (UnauthorizedException | IllegalStateException e) {
1018 lastBridgeConnectionState = false;
1019 if (onNotAuthenticated()) {
1020 return runnable.call();
1023 } catch (Exception e) {
1024 logger.debug("Bridge cannot {}.", taskDescription, e);
1030 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1031 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1034 public List<String> listScenesForConsole() {
1035 return consoleScenesList;
1039 public Collection<ConfigStatusMessage> getConfigStatus() {
1040 // The bridge IP address to be used for checks
1041 // Check whether an IP address is provided
1042 String ip = hueBridgeConfig.getIpAddress();
1043 if (ip == null || ip.isEmpty()) {
1044 return List.of(ConfigStatusMessage.Builder.error(HOST)
1045 .withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());
1051 public TranslationProvider getI18nProvider() {
1052 return i18nProvider;
1055 public LocaleProvider getLocaleProvider() {
1056 return localeProvider;