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.discovery;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
17 import java.io.IOException;
18 import java.util.Arrays;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.Optional;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hue.internal.connection.Clip2Bridge;
28 import org.openhab.core.config.discovery.AbstractDiscoveryService;
29 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
30 import org.openhab.core.config.discovery.DiscoveryService;
31 import org.openhab.core.io.net.http.HttpUtil;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingRegistry;
34 import org.openhab.core.thing.ThingUID;
35 import org.osgi.service.component.annotations.Activate;
36 import org.osgi.service.component.annotations.Component;
37 import org.osgi.service.component.annotations.Reference;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
41 import com.google.gson.Gson;
42 import com.google.gson.JsonParseException;
43 import com.google.gson.reflect.TypeToken;
46 * The {@link HueBridgeNupnpDiscovery} is responsible for discovering new Hue Bridges. It uses the 'NUPnP service
47 * provided by Philips'.
49 * @author Awelkiyar Wehabrebi - Initial contribution
50 * @author Christoph Knauf - Refactorings
51 * @author Andre Fuechsel - make {@link #startScan()} asynchronous
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.hue")
55 public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService {
57 protected static final String BRIDGE_INDICATOR = "fffe";
59 private static final String[] MODEL_NAME_PHILIPS_HUE = { "\"name\":\"Hue Bridge\"", "\"name\":\"Philips hue\"" };
60 private static final String DISCOVERY_URL = "https://discovery.meethue.com/";
61 private static final String CONFIG_URL_PATTERN = "http://%s/api/0/config";
62 private static final int REQUEST_TIMEOUT = 5000;
63 private static final int DISCOVERY_TIMEOUT = 10;
65 private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class);
66 private final ThingRegistry thingRegistry;
69 public HueBridgeNupnpDiscovery(final @Reference ThingRegistry thingRegistry) {
70 super(Set.of(THING_TYPE_BRIDGE, THING_TYPE_BRIDGE_API2), DISCOVERY_TIMEOUT, false);
71 this.thingRegistry = thingRegistry;
75 protected void startScan() {
76 scheduler.schedule(this::discoverHueBridges, 0, TimeUnit.SECONDS);
80 * Discover available Hue Bridges and then add them in the discovery inbox
82 private void discoverHueBridges() {
83 for (BridgeJsonParameters bridge : getBridgeList()) {
84 if (!isReachableAndValidHueBridge(bridge)) {
87 String host = bridge.getInternalIpAddress();
91 String id = bridge.getId();
95 String serialNumber = id.toLowerCase();
96 ThingUID uid = new ThingUID(THING_TYPE_BRIDGE, serialNumber);
97 ThingUID legacyUID = null;
98 String label = String.format(DISCOVERY_LABEL_PATTERN, host);
100 if (isClip2Supported(host)) {
102 uid = new ThingUID(THING_TYPE_BRIDGE_API2, serialNumber);
103 Optional<Thing> legacyThingOptional = getLegacyBridge(host);
104 if (legacyThingOptional.isPresent()) {
105 Thing legacyThing = legacyThingOptional.get();
106 String label2 = legacyThing.getLabel();
107 label = Objects.nonNull(label2) ? label2 : label;
111 DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
113 .withProperty(HOST, host) //
114 .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) //
115 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER);
117 if (Objects.nonNull(legacyUID)) {
118 builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString());
121 thingDiscovered(builder.build());
126 * Checks if the Bridge is a reachable Hue Bridge with a valid id.
128 * @param bridge the {@link BridgeJsonParameters}s
129 * @return true if Hue Bridge is a reachable Hue Bridge with an id containing
130 * BRIDGE_INDICATOR longer then 10
132 private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
133 String host = bridge.getInternalIpAddress();
134 String id = bridge.getId();
137 logger.debug("Bridge not discovered: ip is null");
141 logger.debug("Bridge not discovered: id is null");
144 if (id.length() < 10) {
145 logger.debug("Bridge not discovered: id {} is shorter than 10.", id);
148 if (!BRIDGE_INDICATOR.equals(id.substring(6, 10))) {
150 "Bridge not discovered: id {} does not contain bridge indicator {} or it's at the wrong position.",
151 id, BRIDGE_INDICATOR);
155 description = doGetRequest(String.format(CONFIG_URL_PATTERN, host));
156 } catch (IOException e) {
157 logger.debug("Bridge not discovered: Failure accessing description file for ip: {}", host);
160 if (description == null || !Arrays.stream(MODEL_NAME_PHILIPS_HUE).anyMatch(description::contains)) {
161 logger.debug("Bridge not discovered: Description does not contain the model name: {}", description);
168 * Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
170 * @return a list of available Hue Bridges
172 private List<BridgeJsonParameters> getBridgeList() {
174 Gson gson = new Gson();
175 String json = doGetRequest(DISCOVERY_URL);
177 logger.debug("Philips Hue NUPnP service call failed. Can't discover bridges");
180 List<BridgeJsonParameters> bridgeParameters = gson.fromJson(json,
181 new TypeToken<List<BridgeJsonParameters>>() {
183 if (bridgeParameters == null) {
184 logger.debug("Philips Hue NUPnP service returned empty JSON. Can't discover bridges");
187 return bridgeParameters;
188 } catch (IOException e) {
189 logger.debug("Philips Hue NUPnP service not reachable. Can't discover bridges");
190 } catch (JsonParseException e) {
191 logger.debug("Invalid json response from Hue NUPnP service. Can't discover bridges");
197 * Introduced in order to enable testing.
200 * @return the http request result as String
201 * @throws IOException if request failed
203 protected @Nullable String doGetRequest(String url) throws IOException {
204 return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
208 * Get the legacy Hue bridge (if any) on the given IP address.
210 * @param ipAddress the IP address.
211 * @return Optional of a legacy bridge thing.
213 private Optional<Thing> getLegacyBridge(String ipAddress) {
214 return thingRegistry.getAll().stream().filter(thing -> THING_TYPE_BRIDGE.equals(thing.getThingTypeUID())
215 && ipAddress.equals(thing.getConfiguration().get(HOST))).findFirst();
219 * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to
220 * avoid making live network calls.
222 protected boolean isClip2Supported(String ipAddress) {
224 return Clip2Bridge.isClip2Supported(ipAddress);
225 } catch (IOException e) {