]> git.basschouten.com Git - openhab-addons.git/blob
9c3f3e3e787933d243f9a8c7b2aa93bbce1d69f9
[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.List;
19 import java.util.Objects;
20 import java.util.Optional;
21 import java.util.Set;
22 import java.util.concurrent.TimeUnit;
23
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;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonParseException;
42 import com.google.gson.reflect.TypeToken;
43
44 /**
45  * The {@link HueBridgeNupnpDiscovery} is responsible for discovering new Hue Bridges. It uses the 'NUPnP service
46  * provided by Philips'.
47  *
48  * @author Awelkiyar Wehabrebi - Initial contribution
49  * @author Christoph Knauf - Refactorings
50  * @author Andre Fuechsel - make {@link #startScan()} asynchronous
51  */
52 @Component(service = DiscoveryService.class, configurationPid = "discovery.hue")
53 @NonNullByDefault
54 public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService {
55
56     protected static final String BRIDGE_INDICATOR = "fffe";
57
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;
63
64     private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class);
65     private final ThingRegistry thingRegistry;
66
67     @Activate
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;
71     }
72
73     @Override
74     protected void startScan() {
75         scheduler.schedule(this::discoverHueBridges, 0, TimeUnit.SECONDS);
76     }
77
78     /**
79      * Discover available Hue Bridges and then add them in the discovery inbox
80      */
81     private void discoverHueBridges() {
82         for (BridgeJsonParameters bridge : getBridgeList()) {
83             if (!isReachableAndValidHueBridge(bridge)) {
84                 continue;
85             }
86             String host = bridge.getInternalIpAddress();
87             if (host == null) {
88                 continue;
89             }
90             String id = bridge.getId();
91             if (id == null) {
92                 continue;
93             }
94             String serialNumber = id.toLowerCase();
95             ThingUID uid = new ThingUID(THING_TYPE_BRIDGE, serialNumber);
96             ThingUID legacyUID = null;
97             String label = String.format(DISCOVERY_LABEL_PATTERN, host);
98
99             if (isClip2Supported(host)) {
100                 legacyUID = uid;
101                 uid = new ThingUID(THING_TYPE_BRIDGE_API2, serialNumber);
102                 Optional<Thing> legacyThingOptional = getLegacyBridge(host);
103                 if (legacyThingOptional.isPresent()) {
104                     Thing legacyThing = legacyThingOptional.get();
105                     String label2 = legacyThing.getLabel();
106                     label = Objects.nonNull(label2) ? label2 : label;
107                 }
108             }
109
110             DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
111                     .withLabel(label) //
112                     .withProperty(HOST, host) //
113                     .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) //
114                     .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER);
115
116             if (Objects.nonNull(legacyUID)) {
117                 builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString());
118             }
119
120             thingDiscovered(builder.build());
121         }
122     }
123
124     /**
125      * Checks if the Bridge is a reachable Hue Bridge with a valid id.
126      *
127      * @param bridge the {@link BridgeJsonParameters}s
128      * @return true if Hue Bridge is a reachable Hue Bridge with an id containing
129      *         BRIDGE_INDICATOR longer then 10
130      */
131     private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
132         String host = bridge.getInternalIpAddress();
133         String id = bridge.getId();
134         String description;
135         if (host == null) {
136             logger.debug("Bridge not discovered: ip is null");
137             return false;
138         }
139         if (id == null) {
140             logger.debug("Bridge not discovered: id is null");
141             return false;
142         }
143         if (id.length() < 10) {
144             logger.debug("Bridge not discovered: id {} is shorter than 10.", id);
145             return false;
146         }
147         if (!BRIDGE_INDICATOR.equals(id.substring(6, 10))) {
148             logger.debug(
149                     "Bridge not discovered: id {} does not contain bridge indicator {} or it's at the wrong position.",
150                     id, BRIDGE_INDICATOR);
151             return false;
152         }
153         try {
154             description = doGetRequest(String.format(CONFIG_URL_PATTERN, host));
155         } catch (IOException e) {
156             logger.debug("Bridge not discovered: Failure accessing description file for ip: {}", host);
157             return false;
158         }
159         if (description == null || !description.contains(MODEL_NAME_PHILIPS_HUE)) {
160             logger.debug("Bridge not discovered: Description does not contain the model name: {}", description);
161             return false;
162         }
163         return true;
164     }
165
166     /**
167      * Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
168      *
169      * @return a list of available Hue Bridges
170      */
171     private List<BridgeJsonParameters> getBridgeList() {
172         try {
173             Gson gson = new Gson();
174             String json = doGetRequest(DISCOVERY_URL);
175             if (json == null) {
176                 logger.debug("Philips Hue NUPnP service call failed. Can't discover bridges");
177                 return List.of();
178             }
179             List<BridgeJsonParameters> bridgeParameters = gson.fromJson(json,
180                     new TypeToken<List<BridgeJsonParameters>>() {
181                     }.getType());
182             if (bridgeParameters == null) {
183                 logger.debug("Philips Hue NUPnP service returned empty JSON. Can't discover bridges");
184                 return List.of();
185             }
186             return bridgeParameters;
187         } catch (IOException e) {
188             logger.debug("Philips Hue NUPnP service not reachable. Can't discover bridges");
189         } catch (JsonParseException e) {
190             logger.debug("Invalid json response from Hue NUPnP service. Can't discover bridges");
191         }
192         return List.of();
193     }
194
195     /**
196      * Introduced in order to enable testing.
197      *
198      * @param url the url
199      * @return the http request result as String
200      * @throws IOException if request failed
201      */
202     protected @Nullable String doGetRequest(String url) throws IOException {
203         return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
204     }
205
206     /**
207      * Get the legacy Hue bridge (if any) on the given IP address.
208      *
209      * @param ipAddress the IP address.
210      * @return Optional of a legacy bridge thing.
211      */
212     private Optional<Thing> getLegacyBridge(String ipAddress) {
213         return thingRegistry.getAll().stream().filter(thing -> THING_TYPE_BRIDGE.equals(thing.getThingTypeUID())
214                 && ipAddress.equals(thing.getConfiguration().get(HOST))).findFirst();
215     }
216
217     /**
218      * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to
219      * avoid making live network calls.
220      */
221     protected boolean isClip2Supported(String ipAddress) {
222         try {
223             return Clip2Bridge.isClip2Supported(ipAddress);
224         } catch (IOException e) {
225             return false;
226         }
227     }
228 }