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.List;
19 import java.util.Objects;
20 import java.util.Optional;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hue.internal.connection.Clip2Bridge;
27 import org.openhab.core.config.discovery.AbstractDiscoveryService;
28 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
29 import org.openhab.core.config.discovery.DiscoveryService;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingRegistry;
33 import org.openhab.core.thing.ThingUID;
34 import org.osgi.service.component.annotations.Activate;
35 import org.osgi.service.component.annotations.Component;
36 import org.osgi.service.component.annotations.Reference;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonParseException;
42 import com.google.gson.reflect.TypeToken;
45 * The {@link HueBridgeNupnpDiscovery} is responsible for discovering new Hue Bridges. It uses the 'NUPnP service
46 * provided by Philips'.
48 * @author Awelkiyar Wehabrebi - Initial contribution
49 * @author Christoph Knauf - Refactorings
50 * @author Andre Fuechsel - make {@link #startScan()} asynchronous
52 @Component(service = DiscoveryService.class, configurationPid = "discovery.hue")
54 public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService {
56 protected static final String BRIDGE_INDICATOR = "fffe";
58 private static final String MODEL_NAME_PHILIPS_HUE = "\"name\":\"Philips hue\"";
59 private static final String DISCOVERY_URL = "https://discovery.meethue.com/";
60 private static final String CONFIG_URL_PATTERN = "http://%s/api/0/config";
61 private static final int REQUEST_TIMEOUT = 5000;
62 private static final int DISCOVERY_TIMEOUT = 10;
64 private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class);
65 private final ThingRegistry thingRegistry;
68 public HueBridgeNupnpDiscovery(final @Reference ThingRegistry thingRegistry) {
69 super(Set.of(THING_TYPE_BRIDGE, THING_TYPE_BRIDGE_API2), DISCOVERY_TIMEOUT, false);
70 this.thingRegistry = thingRegistry;
74 protected void startScan() {
75 scheduler.schedule(this::discoverHueBridges, 0, TimeUnit.SECONDS);
79 * Discover available Hue Bridges and then add them in the discovery inbox
81 private void discoverHueBridges() {
82 for (BridgeJsonParameters bridge : getBridgeList()) {
83 if (isReachableAndValidHueBridge(bridge)) {
84 String host = bridge.getInternalIpAddress();
85 String serialNumber = bridge.getId().toLowerCase();
86 ThingUID uid = new ThingUID(THING_TYPE_BRIDGE, serialNumber);
87 ThingUID legacyUID = null;
88 String label = String.format(DISCOVERY_LABEL_PATTERN, host);
90 if (isClip2Supported(host)) {
92 uid = new ThingUID(THING_TYPE_BRIDGE_API2, serialNumber);
93 Optional<Thing> legacyThingOptional = getLegacyBridge(host);
94 if (legacyThingOptional.isPresent()) {
95 Thing legacyThing = legacyThingOptional.get();
96 String label2 = legacyThing.getLabel();
97 label = Objects.nonNull(label2) ? label2 : label;
101 DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
103 .withProperty(HOST, host) //
104 .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) //
105 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER);
107 if (Objects.nonNull(legacyUID)) {
108 builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString());
111 thingDiscovered(builder.build());
117 * Checks if the Bridge is a reachable Hue Bridge with a valid id.
119 * @param bridge the {@link BridgeJsonParameters}s
120 * @return true if Hue Bridge is a reachable Hue Bridge with an id containing
121 * BRIDGE_INDICATOR longer then 10
123 private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
124 String host = bridge.getInternalIpAddress();
125 String id = bridge.getId();
128 logger.debug("Bridge not discovered: ip is null");
132 logger.debug("Bridge not discovered: id is null");
135 if (id.length() < 10) {
136 logger.debug("Bridge not discovered: id {} is shorter than 10.", id);
139 if (!BRIDGE_INDICATOR.equals(id.substring(6, 10))) {
141 "Bridge not discovered: id {} does not contain bridge indicator {} or it's at the wrong position.",
142 id, BRIDGE_INDICATOR);
146 description = doGetRequest(String.format(CONFIG_URL_PATTERN, host));
147 } catch (IOException e) {
148 logger.debug("Bridge not discovered: Failure accessing description file for ip: {}", host);
151 if (description == null || !description.contains(MODEL_NAME_PHILIPS_HUE)) {
152 logger.debug("Bridge not discovered: Description does not contain the model name: {}", description);
159 * Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
161 * @return a list of available Hue Bridges
163 private List<BridgeJsonParameters> getBridgeList() {
165 Gson gson = new Gson();
166 String json = doGetRequest(DISCOVERY_URL);
168 logger.debug("Philips Hue NUPnP service call failed. Can't discover bridges");
171 List<BridgeJsonParameters> bridgeParameters = gson.fromJson(json,
172 new TypeToken<List<BridgeJsonParameters>>() {
174 if (bridgeParameters == null) {
175 logger.debug("Philips Hue NUPnP service returned empty JSON. Can't discover bridges");
178 return bridgeParameters;
179 } catch (IOException e) {
180 logger.debug("Philips Hue NUPnP service not reachable. Can't discover bridges");
181 } catch (JsonParseException e) {
182 logger.debug("Invalid json response from Hue NUPnP service. Can't discover bridges");
188 * Introduced in order to enable testing.
191 * @return the http request result as String
192 * @throws IOException if request failed
194 protected @Nullable String doGetRequest(String url) throws IOException {
195 return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
199 * Get the legacy Hue bridge (if any) on the given IP address.
201 * @param ipAddress the IP address.
202 * @return Optional of a legacy bridge thing.
204 private Optional<Thing> getLegacyBridge(String ipAddress) {
205 return thingRegistry.getAll().stream().filter(thing -> THING_TYPE_BRIDGE.equals(thing.getThingTypeUID())
206 && ipAddress.equals(thing.getConfiguration().get(HOST))).findFirst();
210 * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to
211 * avoid making live network calls.
213 protected boolean isClip2Supported(String ipAddress) {
215 return Clip2Bridge.isClip2Supported(ipAddress);
216 } catch (IOException e) {