/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.binding.hue.internal.discovery;
import static org.eclipse.smarthome.binding.hue.HueBindingConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
import org.eclipse.smarthome.config.discovery.DiscoveryResult;
import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
/**
* The {@link HueBridgeNupnpDiscovery} is responsible for discovering new hue bridges. It uses the 'NUPnP service
* provided by Philips'.
*
* @author Awelkiyar Wehabrebi - Initial contribution
* @author Christoph Knauf - Refactorings
* @author Andre Fuechsel - make {@link #startScan()} asynchronous
*/
public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService {
private static final String MODEL_NAME_PHILIPS_HUE = "<modelName>Philips hue";
protected static final String BRIDGE_INDICATOR = "fffe";
private static final String DISCOVERY_URL = "https://www.meethue.com/api/nupnp";
protected static final String LABEL_PATTERN = "Philips hue (IP)";
private static final String DESC_URL_PATTERN = "http://HOST/description.xml";
private static final int REQUEST_TIMEOUT = 5000;
private static final int DISCOVERY_TIMEOUT = 10;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class);
public HueBridgeNupnpDiscovery() {
super(SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT, false);
}
@Override
protected void startScan() {
scheduler.schedule(new Runnable() {
@Override
public void run() {
discoverHueBridges();
}
}, 0, TimeUnit.SECONDS);
}
/**
* Discover available Hue Bridges and then add them in the discovery inbox
*/
private void discoverHueBridges() {
for (BridgeJsonParameters bridge : getBridgeList()) {
if (isReachableAndValidHueBridge(bridge)) {
String host = bridge.getInternalIpAddress();
String serialNumber = bridge.getId().substring(0, 6) + bridge.getId().substring(10);
ThingUID uid = new ThingUID(THING_TYPE_BRIDGE, serialNumber);
DiscoveryResult result = DiscoveryResultBuilder.create(uid)
.withProperties(buildProperties(host, serialNumber))
.withLabel(LABEL_PATTERN.replace("IP", host)).withRepresentationProperty(SERIAL_NUMBER).build();
thingDiscovered(result);
}
}
}
/**
* Builds the bridge properties.
*
* @param host the ip of the bridge
* @param serialNumber the id of the bridge
* @return the bridge properties
*/
private Map<String, Object> buildProperties(String host, String serialNumber) {
Map<String, Object> properties = new HashMap<>(2);
properties.put(HOST, host);
properties.put(SERIAL_NUMBER, serialNumber);
return properties;
}
/**
* Checks if the Bridge is a reachable Hue Bridge with a valid id.
*
* @param bridge the {@link BridgeJsonParameters}s
* @return true if Bridge is a reachable Hue Bridge with a id containing
* BRIDGE_INDICATOR longer then 10
*/
private boolean isReachableAndValidHueBridge(BridgeJsonParameters bridge) {
String host = bridge.getInternalIpAddress();
String id = bridge.getId();
String description;
if (host == null) {
logger.debug("Bridge not discovered: ip is null");
return false;
}
if (id == null) {
logger.debug("Bridge not discovered: id is null");
return false;
}
if (id.length() < 10) {
logger.debug("Bridge not discovered: id {} is shorter then 10.", id);
return false;
}
if (!id.substring(6, 10).equals(BRIDGE_INDICATOR)) {
logger.debug(
"Bridge not discovered: id {} does not contain bridge indicator {} or its at the wrong position.",
id, BRIDGE_INDICATOR);
return false;
}
try {
description = doGetRequest(DESC_URL_PATTERN.replace("HOST", host));
} catch (IOException e) {
logger.debug("Bridge not discovered: Failure accessing description file for ip: {}", host);
return false;
}
if (!description.contains(MODEL_NAME_PHILIPS_HUE)) {
logger.debug("Bridge not discovered: Description does not containing the model name: {}", description);
return false;
}
return true;
}
/**
* Use the Philips Hue NUPnP service to find Hue Bridges in local Network.
*
* @return a list of available Hue Bridges
*/
private List<BridgeJsonParameters> getBridgeList() {
try {
Gson gson = new Gson();
String json = doGetRequest(DISCOVERY_URL);
return gson.fromJson(json, new TypeToken<List<BridgeJsonParameters>>() {
}.getType());
} catch (IOException e) {
logger.debug("Philips Hue NUPnP service not reachable. Can't discover bridges");
} catch (JsonParseException je) {
logger.debug("Invalid json respone from Hue NUPnP service. Can't discover bridges");
}
return new ArrayList<>();
}
/**
* Introduced in order to enable testing.
*
* @param url the url
* @return the http request result as String
* @throws IOException if request failed
*/
protected String doGetRequest(String url) throws IOException {
return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT);
}
}