2 * Copyright (c) 2010-2023 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.HashMap;
22 import java.util.List;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.Future;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.locks.ReentrantLock;
31 import java.util.stream.Collectors;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.openhab.binding.hue.internal.config.HueBridgeConfig;
37 import org.openhab.binding.hue.internal.connection.HueBridge;
38 import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
39 import org.openhab.binding.hue.internal.discovery.HueDeviceDiscoveryService;
40 import org.openhab.binding.hue.internal.dto.ApiVersionUtils;
41 import org.openhab.binding.hue.internal.dto.Config;
42 import org.openhab.binding.hue.internal.dto.ConfigUpdate;
43 import org.openhab.binding.hue.internal.dto.FullConfig;
44 import org.openhab.binding.hue.internal.dto.FullGroup;
45 import org.openhab.binding.hue.internal.dto.FullLight;
46 import org.openhab.binding.hue.internal.dto.FullSensor;
47 import org.openhab.binding.hue.internal.dto.Scene;
48 import org.openhab.binding.hue.internal.dto.State;
49 import org.openhab.binding.hue.internal.dto.StateUpdate;
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.EmptyResponseException;
53 import org.openhab.binding.hue.internal.exceptions.EntityNotAvailableException;
54 import org.openhab.binding.hue.internal.exceptions.LinkButtonException;
55 import org.openhab.binding.hue.internal.exceptions.UnauthorizedException;
56 import org.openhab.core.config.core.Configuration;
57 import org.openhab.core.config.core.status.ConfigStatusMessage;
58 import org.openhab.core.i18n.CommunicationException;
59 import org.openhab.core.i18n.ConfigurationException;
60 import org.openhab.core.i18n.LocaleProvider;
61 import org.openhab.core.i18n.TranslationProvider;
62 import org.openhab.core.io.net.http.TlsTrustManagerProvider;
63 import org.openhab.core.library.types.HSBType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.ThingStatus;
69 import org.openhab.core.thing.ThingStatusDetail;
70 import org.openhab.core.thing.ThingTypeUID;
71 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.StateOption;
75 import org.osgi.framework.FrameworkUtil;
76 import org.osgi.framework.ServiceRegistration;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
81 * {@link HueBridgeHandler} is the handler for a Hue Bridge and connects it to
82 * the framework. All {@link HueLightHandler}s use the {@link HueBridgeHandler} to execute the actual commands.
84 * @author Dennis Nobel - Initial contribution
85 * @author Oliver Libutzki - Adjustments
86 * @author Kai Kreuzer - improved state handling
87 * @author Andre Fuechsel - implemented getFullLights(), startSearch()
88 * @author Thomas Höfer - added thing properties
89 * @author Stefan Bußweiler - Added new thing status handling
90 * @author Jochen Hiller - fixed status updates, use reachable=true/false for state compare
91 * @author Denis Dudnik - switched to internally integrated source of Jue library
92 * @author Samuel Leisering - Added support for sensor API
93 * @author Christoph Weitkamp - Added support for sensor API
94 * @author Laurent Garnier - Added support for groups
97 public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient {
99 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
101 private static final long BYPASS_MIN_DURATION_BEFORE_CMD = 1500L;
102 private static final long SCENE_POLLING_INTERVAL = TimeUnit.SECONDS.convert(10, TimeUnit.MINUTES);
104 private static final String DEVICE_TYPE = "openHAB";
106 private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
107 private @Nullable ServiceRegistration<?> serviceRegistration;
108 private final HttpClient httpClient;
109 private final HueStateDescriptionProvider stateDescriptionOptionProvider;
110 private final TranslationProvider i18nProvider;
111 private final LocaleProvider localeProvider;
113 private final Map<String, FullLight> lastLightStates = new ConcurrentHashMap<>();
114 private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
115 private final Map<String, FullGroup> lastGroupStates = new ConcurrentHashMap<>();
117 private @Nullable HueDeviceDiscoveryService discoveryService;
118 private final Map<String, LightStatusListener> lightStatusListeners = new ConcurrentHashMap<>();
119 private final Map<String, SensorStatusListener> sensorStatusListeners = new ConcurrentHashMap<>();
120 private final Map<String, GroupStatusListener> groupStatusListeners = new ConcurrentHashMap<>();
122 final ReentrantLock pollingLock = new ReentrantLock();
124 abstract class PollingRunnable implements Runnable {
129 if (!lastBridgeConnectionState) {
130 // if user is not set in configuration try to create a new user on Hue Bridge
131 if (hueBridgeConfig.userName == null) {
132 hueBridge.getFullConfig();
134 lastBridgeConnectionState = tryResumeBridgeConnection();
136 if (lastBridgeConnectionState) {
138 if (thing.getStatus() != ThingStatus.ONLINE) {
139 updateStatus(ThingStatus.ONLINE);
142 } catch (ConfigurationException e) {
143 handleConfigurationFailure(e);
144 } catch (UnauthorizedException | IllegalStateException e) {
145 if (isReachable(hueBridge.getIPAddress())) {
146 lastBridgeConnectionState = false;
147 if (onNotAuthenticated()) {
148 updateStatus(ThingStatus.ONLINE);
150 } else if (lastBridgeConnectionState || thing.getStatus() == ThingStatus.INITIALIZING) {
151 lastBridgeConnectionState = false;
154 } catch (EmptyResponseException e) {
155 // Unexpected empty response is ignored
156 logger.debug("{}", e.getMessage());
157 } catch (ApiException | CommunicationException | IOException e) {
158 if (hueBridge != null && lastBridgeConnectionState) {
159 logger.debug("Connection to Hue Bridge {} lost: {}", hueBridge.getIPAddress(), e.getMessage(), e);
160 lastBridgeConnectionState = false;
163 } catch (RuntimeException e) {
164 logger.warn("An unexpected error occurred: {}", e.getMessage(), e);
165 lastBridgeConnectionState = false;
168 pollingLock.unlock();
172 private boolean isReachable(String ipAddress) {
174 // note that InetAddress.isReachable is unreliable, see
175 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
176 // That's why we do an HTTP access instead
178 // If there is no connection, this line will fail
179 hueBridge.authenticate("invalid");
180 } catch (ConfigurationException | IOException e) {
182 } catch (ApiException e) {
183 String message = e.getMessage();
184 return message != null && //
185 !message.contains("SocketTimeout") && //
186 !message.contains("ConnectException") && //
187 !message.contains("SocketException") && //
188 !message.contains("NoRouteToHostException");
193 protected abstract void doConnectedRun() throws IOException, ApiException;
196 private final Runnable sensorPollingRunnable = new PollingRunnable() {
198 protected void doConnectedRun() throws IOException, ApiException {
199 Map<String, FullSensor> lastSensorStateCopy = new HashMap<>(lastSensorStates);
201 final HueDeviceDiscoveryService discovery = discoveryService;
203 for (final FullSensor sensor : hueBridge.getSensors()) {
204 String sensorId = sensor.getId();
206 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
207 if (sensorStatusListener == null) {
208 logger.trace("Hue sensor '{}' added.", sensorId);
210 if (discovery != null && !lastSensorStateCopy.containsKey(sensorId)) {
211 discovery.addSensorDiscovery(sensor);
214 lastSensorStates.put(sensorId, sensor);
216 if (sensorStatusListener.onSensorStateChanged(sensor)) {
217 lastSensorStates.put(sensorId, sensor);
220 lastSensorStateCopy.remove(sensorId);
223 // Check for removed sensors
224 lastSensorStateCopy.forEach((sensorId, sensor) -> {
225 logger.trace("Hue sensor '{}' removed.", sensorId);
226 lastSensorStates.remove(sensorId);
228 final SensorStatusListener sensorStatusListener = sensorStatusListeners.get(sensorId);
229 if (sensorStatusListener != null) {
230 sensorStatusListener.onSensorRemoved();
233 if (discovery != null && sensor != null) {
234 discovery.removeSensorDiscovery(sensor);
240 private final Runnable lightPollingRunnable = new PollingRunnable() {
242 protected void doConnectedRun() throws IOException, ApiException {
247 private void updateLights() throws IOException, ApiException {
248 Map<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);
250 List<FullLight> lights;
251 if (ApiVersionUtils.supportsFullLights(hueBridge.getVersion())) {
252 lights = hueBridge.getFullLights();
254 lights = hueBridge.getFullConfig().getLights();
257 final HueDeviceDiscoveryService discovery = discoveryService;
259 for (final FullLight fullLight : lights) {
260 final String lightId = fullLight.getId();
262 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
263 if (lightStatusListener == null) {
264 logger.trace("Hue light '{}' added.", lightId);
266 if (discovery != null && !lastLightStateCopy.containsKey(lightId)) {
267 discovery.addLightDiscovery(fullLight);
270 lastLightStates.put(lightId, fullLight);
272 if (lightStatusListener.onLightStateChanged(fullLight)) {
273 lastLightStates.put(lightId, fullLight);
276 lastLightStateCopy.remove(lightId);
279 // Check for removed lights
280 lastLightStateCopy.forEach((lightId, light) -> {
281 logger.trace("Hue light '{}' removed.", lightId);
282 lastLightStates.remove(lightId);
284 final LightStatusListener lightStatusListener = lightStatusListeners.get(lightId);
285 if (lightStatusListener != null) {
286 lightStatusListener.onLightRemoved();
289 if (discovery != null && light != null) {
290 discovery.removeLightDiscovery(light);
295 private void updateGroups() throws IOException, ApiException {
296 Map<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);
298 List<FullGroup> groups = hueBridge.getGroups();
300 final HueDeviceDiscoveryService discovery = discoveryService;
302 for (final FullGroup fullGroup : groups) {
303 State groupState = new State();
307 State colorRef = null;
308 HSBType firstColorHsb = null;
309 for (String lightId : fullGroup.getLightIds()) {
310 FullLight light = lastLightStates.get(lightId);
312 final State lightState = light.getState();
313 logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
314 fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(),
315 lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(),
316 lightState.getColorMode(), lightState.getXY());
317 if (lightState.isOn()) {
319 sumBri += lightState.getBrightness();
321 if (lightState.getColorMode() != null) {
322 HSBType lightHsb = LightStateConverter.toHSBType(lightState);
323 if (firstColorHsb == null) {
325 firstColorHsb = lightHsb;
326 colorRef = lightState;
327 } else if (!lightHsb.equals(firstColorHsb)) {
334 groupState.setOn(on);
335 groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri);
336 if (colorRef != null) {
337 groupState.setColormode(colorRef.getColorMode());
338 groupState.setHue(colorRef.getHue());
339 groupState.setSaturation(colorRef.getSaturation());
340 groupState.setColorTemperature(colorRef.getColorTemperature());
341 groupState.setXY(colorRef.getXY());
343 fullGroup.setState(groupState);
344 logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(),
345 fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(),
346 groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(),
349 String groupId = fullGroup.getId();
351 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
352 if (groupStatusListener == null) {
353 logger.trace("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(),
354 fullGroup.getLightIds().size());
356 if (discovery != null && !lastGroupStateCopy.containsKey(groupId)) {
357 discovery.addGroupDiscovery(fullGroup);
360 lastGroupStates.put(groupId, fullGroup);
362 if (groupStatusListener.onGroupStateChanged(fullGroup)) {
363 lastGroupStates.put(groupId, fullGroup);
366 lastGroupStateCopy.remove(groupId);
369 // Check for removed groups
370 lastGroupStateCopy.forEach((groupId, group) -> {
371 logger.trace("Hue group '{}' removed.", groupId);
372 lastGroupStates.remove(groupId);
374 final GroupStatusListener groupStatusListener = groupStatusListeners.get(groupId);
375 if (groupStatusListener != null) {
376 groupStatusListener.onGroupRemoved();
379 if (discovery != null && group != null) {
380 discovery.removeGroupDiscovery(group);
386 private final Runnable scenePollingRunnable = new PollingRunnable() {
388 protected void doConnectedRun() throws IOException, ApiException {
389 List<Scene> scenes = hueBridge.getScenes();
390 logger.trace("Scenes detected: {}", scenes);
392 setBridgeSceneChannelStateOptions(scenes, lastGroupStates);
393 notifyGroupSceneUpdate(scenes);
396 private void setBridgeSceneChannelStateOptions(List<Scene> scenes, Map<String, FullGroup> groups) {
397 Map<String, String> groupNames = groups.entrySet().stream()
398 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
399 List<StateOption> stateOptions = scenes.stream().map(scene -> scene.toStateOption(groupNames))
400 .collect(Collectors.toList());
401 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
403 consoleScenesList = scenes.stream().map(scene -> "Id is \"" + scene.getId() + "\" for scene \""
404 + scene.toStateOption(groupNames).getLabel() + "\"").collect(Collectors.toList());
408 private boolean lastBridgeConnectionState = false;
410 private boolean propertiesInitializedSuccessfully = false;
412 private @Nullable Future<?> initJob;
413 private @Nullable ScheduledFuture<?> lightPollingJob;
414 private @Nullable ScheduledFuture<?> sensorPollingJob;
415 private @Nullable ScheduledFuture<?> scenePollingJob;
417 private @NonNullByDefault({}) HueBridge hueBridge = null;
418 private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null;
420 private List<String> consoleScenesList = new ArrayList<>();
422 public HueBridgeHandler(Bridge bridge, HttpClient httpClient,
423 HueStateDescriptionProvider stateDescriptionOptionProvider, TranslationProvider i18nProvider,
424 LocaleProvider localeProvider) {
426 this.httpClient = httpClient;
427 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
428 this.i18nProvider = i18nProvider;
429 this.localeProvider = localeProvider;
433 public Collection<Class<? extends ThingHandlerService>> getServices() {
434 return Set.of(HueDeviceDiscoveryService.class);
438 public void handleCommand(ChannelUID channelUID, Command command) {
439 if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
440 recallScene(command.toString());
445 public void updateLightState(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
447 if (hueBridge != null) {
448 listener.setPollBypass(BYPASS_MIN_DURATION_BEFORE_CMD);
449 hueBridge.setLightState(light, stateUpdate).thenAccept(result -> {
451 hueBridge.handleErrors(result);
452 listener.setPollBypass(fadeTime);
453 } catch (Exception e) {
454 listener.unsetPollBypass();
455 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
457 }).exceptionally(e -> {
458 listener.unsetPollBypass();
459 handleLightUpdateException(listener, light, stateUpdate, fadeTime, e);
463 logger.debug("No bridge connected or selected. Cannot set light state.");
468 public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) {
469 if (hueBridge != null) {
470 hueBridge.setSensorState(sensor, stateUpdate).thenAccept(result -> {
472 hueBridge.handleErrors(result);
473 } catch (Exception e) {
474 handleSensorUpdateException(sensor, e);
476 }).exceptionally(e -> {
477 handleSensorUpdateException(sensor, e);
481 logger.debug("No bridge connected or selected. Cannot set sensor state.");
486 public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
487 if (hueBridge != null) {
488 hueBridge.updateSensorConfig(sensor, configUpdate).thenAccept(result -> {
490 hueBridge.handleErrors(result);
491 } catch (Exception e) {
492 handleSensorUpdateException(sensor, e);
494 }).exceptionally(e -> {
495 handleSensorUpdateException(sensor, e);
499 logger.debug("No bridge connected or selected. Cannot set sensor config.");
504 public void updateGroupState(FullGroup group, StateUpdate stateUpdate, long fadeTime) {
505 if (hueBridge != null) {
506 setGroupPollBypass(group, BYPASS_MIN_DURATION_BEFORE_CMD);
507 hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
509 hueBridge.handleErrors(result);
510 setGroupPollBypass(group, fadeTime);
511 } catch (Exception e) {
512 unsetGroupPollBypass(group);
513 handleGroupUpdateException(group, e);
515 }).exceptionally(e -> {
516 unsetGroupPollBypass(group);
517 handleGroupUpdateException(group, e);
521 logger.debug("No bridge connected or selected. Cannot set group state.");
525 private void setGroupPollBypass(FullGroup group, long bypassTime) {
526 group.getLightIds().forEach((lightId) -> {
527 final LightStatusListener listener = lightStatusListeners.get(lightId);
528 if (listener != null) {
529 listener.setPollBypass(bypassTime);
534 private void unsetGroupPollBypass(FullGroup group) {
535 group.getLightIds().forEach((lightId) -> {
536 final LightStatusListener listener = lightStatusListeners.get(lightId);
537 if (listener != null) {
538 listener.unsetPollBypass();
543 private void handleLightUpdateException(LightStatusListener listener, FullLight light, StateUpdate stateUpdate,
544 long fadeTime, Throwable e) {
545 if (e instanceof DeviceOffException) {
546 if (stateUpdate.getColorTemperature() != null && stateUpdate.getBrightness() == null) {
547 // If there is only a change of the color temperature, we do not want the light
548 // to be turned on (i.e. change its brightness).
551 updateLightState(listener, light, LightStateConverter.toOnOffLightState(OnOffType.ON), fadeTime);
552 updateLightState(listener, light, stateUpdate, fadeTime);
554 } else if (e instanceof EntityNotAvailableException) {
555 logger.debug("Error while accessing light: {}", e.getMessage(), e);
556 final HueDeviceDiscoveryService discovery = discoveryService;
557 if (discovery != null) {
558 discovery.removeLightDiscovery(light);
560 listener.onLightGone();
562 handleThingUpdateException("light", e);
566 private void handleSensorUpdateException(FullSensor sensor, Throwable e) {
567 if (e instanceof EntityNotAvailableException) {
568 logger.debug("Error while accessing sensor: {}", e.getMessage(), e);
569 final HueDeviceDiscoveryService discovery = discoveryService;
570 if (discovery != null) {
571 discovery.removeSensorDiscovery(sensor);
573 final SensorStatusListener listener = sensorStatusListeners.get(sensor.getId());
574 if (listener != null) {
575 listener.onSensorGone();
578 handleThingUpdateException("sensor", e);
582 private void handleGroupUpdateException(FullGroup group, Throwable e) {
583 if (e instanceof EntityNotAvailableException) {
584 logger.debug("Error while accessing group: {}", e.getMessage(), e);
585 final HueDeviceDiscoveryService discovery = discoveryService;
586 if (discovery != null) {
587 discovery.removeGroupDiscovery(group);
589 final GroupStatusListener listener = groupStatusListeners.get(group.getId());
590 if (listener != null) {
591 listener.onGroupGone();
594 handleThingUpdateException("group", e);
598 private void handleThingUpdateException(String thingType, Throwable e) {
599 if (e instanceof IOException) {
600 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
601 } else if (e instanceof ApiException) {
602 // This should not happen - if it does, it is most likely some bug that should be reported.
603 logger.warn("Error while accessing {}: {}", thingType, e.getMessage());
604 } else if (e instanceof IllegalStateException) {
605 logger.trace("Error while accessing {}: {}", thingType, e.getMessage());
609 private void startLightPolling() {
610 ScheduledFuture<?> job = lightPollingJob;
611 if (job == null || job.isCancelled()) {
612 long lightPollingInterval;
613 int configPollingInterval = hueBridgeConfig.pollingInterval;
614 if (configPollingInterval < 1) {
615 lightPollingInterval = TimeUnit.SECONDS.toSeconds(10);
616 logger.warn("Wrong configuration value for polling interval. Using default value: {}s",
617 lightPollingInterval);
619 lightPollingInterval = configPollingInterval;
621 // Delay the first execution to give a chance to have all light and group things registered
622 lightPollingJob = scheduler.scheduleWithFixedDelay(lightPollingRunnable, 3, lightPollingInterval,
627 private void stopLightPolling() {
628 ScheduledFuture<?> job = lightPollingJob;
632 lightPollingJob = null;
635 private void startSensorPolling() {
636 ScheduledFuture<?> job = sensorPollingJob;
637 if (job == null || job.isCancelled()) {
638 int configSensorPollingInterval = hueBridgeConfig.sensorPollingInterval;
639 if (configSensorPollingInterval > 0) {
640 long sensorPollingInterval;
641 if (configSensorPollingInterval < 50) {
642 sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500);
643 logger.warn("Wrong configuration value for sensor polling interval. Using default value: {}ms",
644 sensorPollingInterval);
646 sensorPollingInterval = configSensorPollingInterval;
648 // Delay the first execution to give a chance to have all sensor things registered
649 sensorPollingJob = scheduler.scheduleWithFixedDelay(sensorPollingRunnable, 4000, sensorPollingInterval,
650 TimeUnit.MILLISECONDS);
655 private void stopSensorPolling() {
656 ScheduledFuture<?> job = sensorPollingJob;
660 sensorPollingJob = null;
663 private void startScenePolling() {
664 ScheduledFuture<?> job = scenePollingJob;
665 if (job == null || job.isCancelled()) {
666 // Delay the first execution to give a chance to have all group things registered
667 scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5, SCENE_POLLING_INTERVAL,
672 private void stopScenePolling() {
673 ScheduledFuture<?> job = scenePollingJob;
677 scenePollingJob = null;
681 public void dispose() {
682 logger.debug("Disposing Hue Bridge handler ...");
683 Future<?> job = initJob;
690 if (hueBridge != null) {
693 ServiceRegistration<?> localServiceRegistration = serviceRegistration;
694 if (localServiceRegistration != null) {
695 // remove trustmanager service
696 localServiceRegistration.unregister();
697 serviceRegistration = null;
699 propertiesInitializedSuccessfully = false;
703 public void initialize() {
704 logger.debug("Initializing Hue Bridge handler ...");
705 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
707 String ip = hueBridgeConfig.ipAddress;
708 if (ip == null || ip.isEmpty()) {
709 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
710 "@text/offline.conf-error-no-ip-address");
712 if (hueBridge == null) {
713 hueBridge = new HueBridge(httpClient, ip, hueBridgeConfig.getPort(), hueBridgeConfig.protocol,
716 updateStatus(ThingStatus.UNKNOWN);
718 if (HueBridgeConfig.HTTPS.equals(hueBridgeConfig.protocol)) {
719 scheduler.submit(() -> {
720 // register trustmanager service
721 HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider(
722 ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate);
724 // Check before registering that the PEM certificate can be downloaded
725 if (tlsTrustManagerProvider.getPEMTrustManager() == null) {
726 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
727 "@text/offline.conf-error-https-connection");
731 serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext().registerService(
732 TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
745 public @Nullable String getUserName() {
746 return hueBridgeConfig == null ? null : hueBridgeConfig.userName;
749 private synchronized void onUpdate() {
751 startSensorPolling();
756 * This method is called whenever the connection to the {@link HueBridge} is lost.
758 public void onConnectionLost() {
759 logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
760 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.bridge-connection-lost");
764 * This method is called whenever the connection to the {@link HueBridge} is resumed.
766 * @throws ApiException if the physical device does not support this API call
767 * @throws IOException if the physical device could not be reached
769 private void onConnectionResumed() throws IOException, ApiException {
770 logger.debug("Bridge connection resumed.");
772 if (!propertiesInitializedSuccessfully) {
773 FullConfig fullConfig = hueBridge.getFullConfig();
774 Config config = fullConfig.getConfig();
775 if (config != null) {
776 Map<String, String> properties = editProperties();
777 String serialNumber = config.getBridgeId().substring(0, 6) + config.getBridgeId().substring(10);
778 serialNumber = serialNumber.toLowerCase();
779 properties.put(PROPERTY_SERIAL_NUMBER, serialNumber);
780 properties.put(PROPERTY_MODEL_ID, config.getModelId());
781 properties.put(PROPERTY_MAC_ADDRESS, config.getMACAddress());
782 properties.put(PROPERTY_FIRMWARE_VERSION, config.getSoftwareVersion());
783 updateProperties(properties);
784 propertiesInitializedSuccessfully = true;
790 * Check USER_NAME config for null. Call onConnectionResumed() otherwise.
792 * @return True if USER_NAME was not null.
793 * @throws ApiException if the physical device does not support this API call
794 * @throws IOException if the physical device could not be reached
796 private boolean tryResumeBridgeConnection() throws IOException, ApiException {
797 logger.debug("Connection to Hue Bridge {} established.", hueBridge.getIPAddress());
798 if (hueBridgeConfig.userName == null) {
800 "User name for Hue Bridge authentication not available in configuration. Setting ThingStatus to OFFLINE.");
801 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
802 "@text/offline.conf-error-no-username");
805 onConnectionResumed();
811 * This method is called whenever the connection to the {@link HueBridge} is available,
812 * but requests are not allowed due to a missing or invalid authentication.
814 * If there is a user name available, it attempts to re-authenticate. Otherwise new authentication credentials will
815 * be requested from the bridge.
817 * @param bridge the Hue Bridge the connection is not authorized
818 * @return returns {@code true} if re-authentication was successful, {@code false} otherwise
820 public boolean onNotAuthenticated() {
821 if (hueBridge == null) {
824 String userName = hueBridgeConfig.userName;
825 if (userName == null) {
829 hueBridge.authenticate(userName);
831 } catch (ConfigurationException e) {
832 handleConfigurationFailure(e);
833 } catch (Exception e) {
835 handleAuthenticationFailure(e, userName);
841 private void createUser() {
843 String newUser = createUserOnPhysicalBridge();
844 updateBridgeThingConfiguration(newUser);
845 } catch (LinkButtonException ex) {
846 handleLinkButtonNotPressed(ex);
847 } catch (Exception ex) {
848 handleExceptionWhileCreatingUser(ex);
852 private String createUserOnPhysicalBridge() throws IOException, ApiException {
853 logger.info("Creating new user on Hue Bridge {} - please press the pairing button on the bridge.",
854 hueBridgeConfig.ipAddress);
855 String userName = hueBridge.link(DEVICE_TYPE);
856 logger.info("User has been successfully added to Hue Bridge.");
860 private void updateBridgeThingConfiguration(String userName) {
861 Configuration config = editConfiguration();
862 config.put(USER_NAME, userName);
864 updateConfiguration(config);
865 logger.debug("Updated configuration parameter '{}'", USER_NAME);
866 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
867 } catch (IllegalStateException e) {
868 logger.trace("Configuration update failed.", e);
869 logger.warn("Unable to update configuration of Hue Bridge.");
870 logger.warn("Please configure the user name manually.");
874 private void handleConfigurationFailure(ConfigurationException ex) {
876 "Invalid certificate for secured connection. You might want to enable the \"Use Self-Signed Certificate\" configuration.");
877 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getRawMessage());
880 private void handleAuthenticationFailure(Exception ex, String userName) {
881 logger.warn("User is not authenticated on Hue Bridge {}", hueBridgeConfig.ipAddress);
882 logger.warn("Please configure a valid user or remove user from configuration to generate a new one.");
883 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
884 "@text/offline.conf-error-invalid-username");
887 private void handleLinkButtonNotPressed(LinkButtonException ex) {
888 logger.debug("Failed creating new user on Hue Bridge: {}", ex.getMessage());
889 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
890 "@text/offline.conf-error-press-pairing-button");
893 private void handleExceptionWhileCreatingUser(Exception ex) {
894 logger.warn("Failed creating new user on Hue Bridge", ex);
895 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
896 "@text/offline.conf-error-creation-username");
900 public boolean registerDiscoveryListener(HueDeviceDiscoveryService listener) {
901 if (discoveryService == null) {
902 discoveryService = listener;
903 getFullLights().forEach(listener::addLightDiscovery);
904 getFullSensors().forEach(listener::addSensorDiscovery);
905 getFullGroups().forEach(listener::addGroupDiscovery);
913 public boolean unregisterDiscoveryListener() {
914 if (discoveryService != null) {
915 discoveryService = null;
923 public boolean registerLightStatusListener(LightStatusListener lightStatusListener) {
924 final String lightId = lightStatusListener.getLightId();
925 if (!lightStatusListeners.containsKey(lightId)) {
926 lightStatusListeners.put(lightId, lightStatusListener);
927 final FullLight lastLightState = lastLightStates.get(lightId);
928 if (lastLightState != null) {
929 lightStatusListener.onLightAdded(lastLightState);
938 public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) {
939 return lightStatusListeners.remove(lightStatusListener.getLightId()) != null;
943 public boolean registerSensorStatusListener(SensorStatusListener sensorStatusListener) {
944 final String sensorId = sensorStatusListener.getSensorId();
945 if (!sensorStatusListeners.containsKey(sensorId)) {
946 sensorStatusListeners.put(sensorId, sensorStatusListener);
947 final FullSensor lastSensorState = lastSensorStates.get(sensorId);
948 if (lastSensorState != null) {
949 sensorStatusListener.onSensorAdded(lastSensorState);
958 public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener) {
959 return sensorStatusListeners.remove(sensorStatusListener.getSensorId()) != null;
963 public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) {
964 final String groupId = groupStatusListener.getGroupId();
965 if (!groupStatusListeners.containsKey(groupId)) {
966 groupStatusListeners.put(groupId, groupStatusListener);
967 final FullGroup lastGroupState = lastGroupStates.get(groupId);
968 if (lastGroupState != null) {
969 groupStatusListener.onGroupAdded(lastGroupState);
978 public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) {
979 return groupStatusListeners.remove(groupStatusListener.getGroupId()) != null;
983 * Recall scene to all lights that belong to the scene.
985 * @param id the ID of the scene to activate
988 public void recallScene(String id) {
989 if (hueBridge != null) {
990 hueBridge.recallScene(id).thenAccept(result -> {
992 hueBridge.handleErrors(result);
993 } catch (Exception e) {
994 logger.debug("Error while recalling scene: {}", e.getMessage());
996 }).exceptionally(e -> {
997 logger.debug("Error while recalling scene: {}", e.getMessage());
1001 logger.debug("No bridge connected or selected. Cannot activate scene.");
1006 public @Nullable FullLight getLightById(String lightId) {
1007 return lastLightStates.get(lightId);
1011 public @Nullable FullSensor getSensorById(String sensorId) {
1012 return lastSensorStates.get(sensorId);
1016 public @Nullable FullGroup getGroupById(String groupId) {
1017 return lastGroupStates.get(groupId);
1020 public List<FullLight> getFullLights() {
1021 List<FullLight> ret = withReAuthentication("search for new lights", () -> {
1022 return hueBridge.getFullLights();
1024 return ret != null ? ret : List.of();
1027 public List<FullSensor> getFullSensors() {
1028 List<FullSensor> ret = withReAuthentication("search for new sensors", () -> {
1029 return hueBridge.getSensors();
1031 return ret != null ? ret : List.of();
1034 public List<FullGroup> getFullGroups() {
1035 List<FullGroup> ret = withReAuthentication("search for new groups", () -> {
1036 return hueBridge.getGroups();
1038 return ret != null ? ret : List.of();
1041 public void startSearch() {
1042 withReAuthentication("start search mode", () -> {
1043 hueBridge.startSearch();
1048 public void startSearch(List<String> serialNumbers) {
1049 withReAuthentication("start search mode", () -> {
1050 hueBridge.startSearch(serialNumbers);
1055 private @Nullable <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
1056 if (hueBridge != null) {
1059 return runnable.call();
1060 } catch (UnauthorizedException | IllegalStateException e) {
1061 lastBridgeConnectionState = false;
1062 if (onNotAuthenticated()) {
1063 return runnable.call();
1066 } catch (Exception e) {
1067 logger.debug("Bridge cannot {}.", taskDescription, e);
1073 private void notifyGroupSceneUpdate(List<Scene> scenes) {
1074 groupStatusListeners.forEach((groupId, listener) -> listener.onScenesUpdated(scenes));
1077 public List<String> listScenesForConsole() {
1078 return consoleScenesList;
1082 public Collection<ConfigStatusMessage> getConfigStatus() {
1083 // The bridge IP address to be used for checks
1084 // Check whether an IP address is provided
1085 hueBridgeConfig = getConfigAs(HueBridgeConfig.class);
1087 String ip = hueBridgeConfig.ipAddress;
1088 if (ip == null || ip.isEmpty()) {
1089 return List.of(ConfigStatusMessage.Builder.error(HOST).withMessageKeySuffix(IP_ADDRESS_MISSING)
1090 .withArguments(HOST).build());
1096 public TranslationProvider getI18nProvider() {
1097 return i18nProvider;
1100 public LocaleProvider getLocaleProvider() {
1101 return localeProvider;