]> git.basschouten.com Git - openhab-addons.git/blob
d150319d08ddf16cfeefa32779d1252155182c96
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.discovery;
14
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16
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;
22 import java.util.Set;
23 import java.util.concurrent.TimeUnit;
24
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;
40
41 import com.google.gson.Gson;
42 import com.google.gson.JsonParseException;
43 import com.google.gson.reflect.TypeToken;
44
45 /**
46  * The {@link HueBridgeNupnpDiscovery} is responsible for discovering new Hue Bridges. It uses the 'NUPnP service
47  * provided by Philips'.
48  *
49  * @author Awelkiyar Wehabrebi - Initial contribution
50  * @author Christoph Knauf - Refactorings
51  * @author Andre Fuechsel - make {@link #startScan()} asynchronous
52  */
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.hue")
54 @NonNullByDefault
55 public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService {
56
57     protected static final String BRIDGE_INDICATOR = "fffe";
58
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;
64
65     private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class);
66     private final ThingRegistry thingRegistry;
67
68     @Activate
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;
72     }
73
74     @Override
75     protected void startScan() {
76         scheduler.schedule(this::discoverHueBridges, 0, TimeUnit.SECONDS);
77     }
78
79     /**
80      * Discover available Hue Bridges and then add them in the discovery inbox
81      */
82     private void discoverHueBridges() {
83         for (BridgeJsonParameters bridge : getBridgeList()) {
84             if (!isReachableAndValidHueBridge(bridge)) {
85                 continue;
86             }
87             String host = bridge.getInternalIpAddress();
88             if (host == null) {
89                 continue;
90             }
91             String id = bridge.getId();
92             if (id == null) {
93                 continue;
94             }
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);
99
100             if (isClip2Supported(host)) {
101                 legacyUID = uid;
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;
108                 }
109             }
110
111             DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
112                     .withLabel(label) //
113                     .withProperty(HOST, host) //
114                     .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) //
115                     .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER);
116
117             if (Objects.nonNull(legacyUID)) {
118                 builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString());
119             }
120
121             thingDiscovered(builder.build());
122         }
123     }
124
125     /**
126      * Checks if the Bridge is a reachable Hue Bridge with a valid id.
127      *
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
131      */
132     private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
133         String host = bridge.getInternalIpAddress();
134         String id = bridge.getId();
135         String description;
136         if (host == null) {
137             logger.debug("Bridge not discovered: ip is null");
138             return false;
139         }
140         if (id == null) {
141             logger.debug("Bridge not discovered: id is null");
142             return false;
143         }
144         if (id.length() < 10) {
145             logger.debug("Bridge not discovered: id {} is shorter than 10.", id);
146             return false;
147         }
148         if (!BRIDGE_INDICATOR.equals(id.substring(6, 10))) {
149             logger.debug(
150                     "Bridge not discovered: id {} does not contain bridge indicator {} or it's at the wrong position.",
151                     id, BRIDGE_INDICATOR);
152             return false;
153         }
154         try {
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);
158             return false;
159         }
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);
162             return false;
163         }
164         return true;
165     }
166
167     /**
168      * Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
169      *
170      * @return a list of available Hue Bridges
171      */
172     private List<BridgeJsonParameters> getBridgeList() {
173         try {
174             Gson gson = new Gson();
175             String json = doGetRequest(DISCOVERY_URL);
176             if (json == null) {
177                 logger.debug("Philips Hue NUPnP service call failed. Can't discover bridges");
178                 return List.of();
179             }
180             List<BridgeJsonParameters> bridgeParameters = gson.fromJson(json,
181                     new TypeToken<List<BridgeJsonParameters>>() {
182                     }.getType());
183             if (bridgeParameters == null) {
184                 logger.debug("Philips Hue NUPnP service returned empty JSON. Can't discover bridges");
185                 return List.of();
186             }
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");
192         }
193         return List.of();
194     }
195
196     /**
197      * Introduced in order to enable testing.
198      *
199      * @param url the url
200      * @return the http request result as String
201      * @throws IOException if request failed
202      */
203     protected @Nullable String doGetRequest(String url) throws IOException {
204         return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
205     }
206
207     /**
208      * Get the legacy Hue bridge (if any) on the given IP address.
209      *
210      * @param ipAddress the IP address.
211      * @return Optional of a legacy bridge thing.
212      */
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();
216     }
217
218     /**
219      * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to
220      * avoid making live network calls.
221      */
222     protected boolean isClip2Supported(String ipAddress) {
223         try {
224             return Clip2Bridge.isClip2Supported(ipAddress);
225         } catch (IOException e) {
226             return false;
227         }
228     }
229 }