]> git.basschouten.com Git - openhab-addons.git/blob
c4ce4769b588ce9e84d1fbe3009ee69e1259ee30
[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                 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);
89
90                 if (isClip2Supported(host)) {
91                     legacyUID = uid;
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;
98                     }
99                 }
100
101                 DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
102                         .withLabel(label) //
103                         .withProperty(HOST, host) //
104                         .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) //
105                         .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER);
106
107                 if (Objects.nonNull(legacyUID)) {
108                     builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString());
109                 }
110
111                 thingDiscovered(builder.build());
112             }
113         }
114     }
115
116     /**
117      * Checks if the Bridge is a reachable Hue Bridge with a valid id.
118      *
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
122      */
123     private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
124         String host = bridge.getInternalIpAddress();
125         String id = bridge.getId();
126         String description;
127         if (host == null) {
128             logger.debug("Bridge not discovered: ip is null");
129             return false;
130         }
131         if (id == null) {
132             logger.debug("Bridge not discovered: id is null");
133             return false;
134         }
135         if (id.length() < 10) {
136             logger.debug("Bridge not discovered: id {} is shorter than 10.", id);
137             return false;
138         }
139         if (!BRIDGE_INDICATOR.equals(id.substring(6, 10))) {
140             logger.debug(
141                     "Bridge not discovered: id {} does not contain bridge indicator {} or it's at the wrong position.",
142                     id, BRIDGE_INDICATOR);
143             return false;
144         }
145         try {
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);
149             return false;
150         }
151         if (description == null || !description.contains(MODEL_NAME_PHILIPS_HUE)) {
152             logger.debug("Bridge not discovered: Description does not contain the model name: {}", description);
153             return false;
154         }
155         return true;
156     }
157
158     /**
159      * Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
160      *
161      * @return a list of available Hue Bridges
162      */
163     private List<BridgeJsonParameters> getBridgeList() {
164         try {
165             Gson gson = new Gson();
166             String json = doGetRequest(DISCOVERY_URL);
167             if (json == null) {
168                 logger.debug("Philips Hue NUPnP service call failed. Can't discover bridges");
169                 return List.of();
170             }
171             List<BridgeJsonParameters> bridgeParameters = gson.fromJson(json,
172                     new TypeToken<List<BridgeJsonParameters>>() {
173                     }.getType());
174             if (bridgeParameters == null) {
175                 logger.debug("Philips Hue NUPnP service returned empty JSON. Can't discover bridges");
176                 return List.of();
177             }
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");
183         }
184         return List.of();
185     }
186
187     /**
188      * Introduced in order to enable testing.
189      *
190      * @param url the url
191      * @return the http request result as String
192      * @throws IOException if request failed
193      */
194     protected @Nullable String doGetRequest(String url) throws IOException {
195         return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
196     }
197
198     /**
199      * Get the legacy Hue bridge (if any) on the given IP address.
200      *
201      * @param ipAddress the IP address.
202      * @return Optional of a legacy bridge thing.
203      */
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();
207     }
208
209     /**
210      * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to
211      * avoid making live network calls.
212      */
213     protected boolean isClip2Supported(String ipAddress) {
214         try {
215             return Clip2Bridge.isClip2Supported(ipAddress);
216         } catch (IOException e) {
217             return false;
218         }
219     }
220 }