/**
* Copyright (c) 2010-2016 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.openhab.binding.openpaths.internal;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.openhab.binding.openpaths.OpenPathsBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.scribe.builder.ServiceBuilder;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.oauth.OAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binding for OpenPaths location detection.
*
* When signing up to the OpenPaths service (at https://openpaths.cc/) you will
* be issued with an ACCESS_KEY and SECRET_KEY. Using these keys to configure
* this binding you can periodically get openHAB to check the location of one or
* more users, and check against a predefined 'home' location to see if a user
* is inside the 'geofence'.
*
* @author Ben Jones, Robert Bausdorf
* @since 1.4.0
*/
public class OpenPathsBinding extends AbstractActiveBinding<OpenPathsBindingProvider>implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(OpenPathsBinding.class);
// default refresh interval (defaults to 5 minutes)
private long refreshInterval = 300000L;
// default fallback geo fence distance (defaults to 100m)
private float geoFence = 100;
// list of configured locations in openhab.conf
private Map<String, Location> locations;
// list of OpenPaths openpaths users configured in openhab.conf
private Map<String, OpenPathsUser> openPathsUsers;
@Override
protected String getName() {
return "OpenPaths Refresh Service";
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
public Map<String, OpenPathsBinding.Location> getLocations() {
return this.locations;
}
public Map<String, OpenPathsBinding.OpenPathsUser> getUsers() {
return this.openPathsUsers;
}
enum LocationBindingType {
on,
distance
}
/**
* @{inheritDoc
*/
@Override
public void execute() {
if (!bindingsExist()) {
logger.debug("There is no existing OpenPaths binding configuration => refresh cycle aborted!");
return;
}
for (OpenPathsBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
logger.trace("try binding provider item: " + itemName);
OpenPathsBindingConfig bindingConfig = provider.getItemConfig(itemName);
String bindingConfigName = bindingConfig.getName();
String[] bindingParts = bindingConfigName.split("\\:");
if (bindingParts.length < 1) {
logger.error("Empty OpenPaths binding config");
continue;
}
String name = bindingParts[0];
if (!openPathsUsers.containsKey(name)) {
logger.warn("There is no OpenPaths user configured for '" + name
+ "'. Please add this user to the binding configuration, including both the ACCESS_KEY and SECRET_KEY from the OpenPaths profile.");
continue;
}
Location location = null;
OpenPathsUser openPathsUser = openPathsUsers.get(name);
if (openPathsUser.lastUpdateTS + this.refreshInterval < System.currentTimeMillis()) {
String accessKey = openPathsUser.getAccessKey();
String secretKey = openPathsUser.getSecretKey();
if (StringUtils.isEmpty(accessKey)) {
logger.warn("There is no ACCESS_KEY configured for '" + name
+ "'. Please add this user to the binding configuration, including both the ACCESS_KEY and SECRET_KEY from the OpenPaths profile.");
continue;
}
if (StringUtils.isEmpty(secretKey)) {
logger.warn("There is no SECRET_KEY configured for '" + name
+ "'. Please add this user to the binding configuration, including both the ACCESS_KEY and SECRET_KEY from the OpenPaths profile.");
continue;
}
logger.debug("Requesting location for '{}'...", name);
location = getUserLocation(accessKey, secretKey);
if (location != null) {
openPathsUsers.get(name).setLastLocation(location);
logger.debug("New location received for '{}': {}", name, location.toString());
} else {
logger.warn("Unable to determine location for '{}'. Skipping.", name);
continue;
}
} else {
location = openPathsUsers.get(name).getLastLocation();
logger.trace("Using cached location for '{}'", openPathsUser.toString());
}
String bindingLocationName = bindingParts.length > 1 ? bindingParts[1] : "";
if (bindingLocationName.startsWith("current")) {
if (bindingLocationName.equals("currentLocation")) {
eventPublisher.postUpdate(itemName,
new StringType("" + location.getLatitude() + ", " + location.getLongitude()));
} else if (bindingLocationName.equals("currentLatitude")) {
eventPublisher.postUpdate(itemName, new DecimalType(new BigDecimal(location.getLatitude())));
} else if (bindingLocationName.equals("currentLongitude")) {
eventPublisher.postUpdate(itemName, new DecimalType(new BigDecimal(location.getLongitude())));
} else {
logger.warn("unsupported Binding: " + bindingLocationName);
}
continue;
}
if (!locations.containsKey(bindingLocationName)) {
logger.warn("location name " + bindingLocationName + " not configured, falling back to 'home'");
bindingLocationName = "home";
}
logger.debug("OpenPathsUser: " + name + "@" + bindingLocationName);
LocationBindingType bindingType = LocationBindingType.on;
if (bindingParts.length == 3) {
if (bindingParts[2].equals("distance")) {
bindingType = LocationBindingType.distance;
}
}
Location bindingLocation = locations.get(bindingLocationName);
logger.trace("Calculating distance between home ({}) and user location ({}) for '{}'...",
new Object[] { bindingLocation.toString(), location.toString(), name });
double distance = calculateDistance(bindingLocation, location);
bindingLocation.setDistance(distance);
logger.trace("Distance calculated as {} for '{}'@'{}'", distance, name, bindingLocationName);
if (bindingType.equals(LocationBindingType.on)) {
float fence = bindingLocation.getGeofence() == 0.0 ? geoFence : bindingLocation.getGeofence();
if (distance <= fence) {
logger.trace("Detected that '{}'@'{}' is inside the geofence ({}m)", name, bindingLocationName,
fence);
eventPublisher.postUpdate(itemName, OnOffType.ON);
} else {
logger.trace("Detected that '{}'@'{}' is outside the geofence ({}m)", name, bindingLocationName,
fence);
eventPublisher.postUpdate(itemName, OnOffType.OFF);
}
} else if (bindingType.equals(LocationBindingType.distance)) {
eventPublisher.postUpdate(itemName, new DecimalType(new BigDecimal(distance / 1000)));
}
}
}
}
@SuppressWarnings("unchecked")
private Location getUserLocation(String accessKey, String secretKey) {
// build the OAuth service using the access/secret keys
OAuthService service = new ServiceBuilder().provider(new OpenPathsApi()).apiKey(accessKey).apiSecret(secretKey)
.build();
// build the request
OAuthRequest request = new OAuthRequest(Verb.GET, "https://openpaths.cc/api/1");
service.signRequest(Token.empty(), request);
request.addQuerystringParameter("num_points", "1");
// send the request and check we got a successful response
Response response = request.send();
if (!response.isSuccessful()) {
logger.error("Failed to request the OpenPaths location, response code: " + response.getCode());
return null;
}
// parse the response to build our location object
Map<String, Object> locationData;
String toParse = "{}";
try {
ObjectMapper jsonReader = new ObjectMapper();
toParse = response.getBody();
toParse = toParse.substring(1, toParse.length() - 2);
locationData = jsonReader.readValue(toParse, Map.class);
} catch (JsonParseException e) {
logger.error("Error parsing JSON:\n" + toParse, e);
return null;
} catch (JsonMappingException e) {
logger.error("Error mapping JSON:\n" + toParse, e);
return null;
} catch (IOException e) {
logger.error("An I/O error occured while decoding JSON:\n" + response.getBody());
return null;
}
float latitude = Float.parseFloat(locationData.get("lat").toString());
float longitude = Float.parseFloat(locationData.get("lon").toString());
String device = locationData.get("device").toString();
return new Location(latitude, longitude, device);
}
private double calculateDistance(Location location1, Location location2) {
float lat1 = location1.getLatitude();
float lng1 = location1.getLongitude();
float lat2 = location2.getLatitude();
float lng2 = location2.getLongitude();
double dLat = Math.toRadians(lat2 - lat1);
double dLng = Math.toRadians(lng2 - lng1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1))
* Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double earthRadiusKm = 6369;
double distKm = earthRadiusKm * c;
// return the distance in meters
return distKm * 1000;
}
protected void addBindingProvider(OpenPathsBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(OpenPathsBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/**
* {@inheritDoc}
*/
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
openPathsUsers = new HashMap<String, OpenPathsUser>();
locations = new HashMap<String, Location>();
if (config != null) {
Enumeration<String> keys = config.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
String value = (String) config.get(key);
// the config-key enumeration contains additional keys that we
// don't want to process here ...
if ("service.pid".equals(key)) {
continue;
}
if ("refresh".equals(key)) {
if (StringUtils.isNotBlank(value)) {
refreshInterval = Long.parseLong(value);
logger.trace("Config: refresh=" + this.refreshInterval);
}
} else if ("geofence".equals(key)) {
// only for backward compatibility / as fallback
if (StringUtils.isNotBlank(value)) {
geoFence = Float.parseFloat(value);
logger.trace("Config: geofence=" + this.geoFence);
}
} else if (key.endsWith("lat")) {
String[] keyParts = key.split("\\.");
if (keyParts.length != 2) {
throw new ConfigurationException(key, "Invalid OpenPaths user location lattitude: " + key);
}
if (StringUtils.isNotBlank(value)) {
float lat = Float.parseFloat(value);
String name = keyParts[0];
if (locations.containsKey(name)) {
locations.get(name).setLatitude(lat);
logger.trace("Config: new Location " + name + "(" + locations.get(name) + ")");
} else {
Location loc = new Location();
loc.setLatitude(lat);
logger.trace("Config: update Location " + name + "(" + locations.get(name) + ")");
locations.put(name, loc);
}
}
} else if (key.endsWith("long")) {
String[] keyParts = key.split("\\.");
if (keyParts.length != 2) {
throw new ConfigurationException(key, "Invalid OpenPaths user location longitude: " + key);
}
if (StringUtils.isNotBlank(value)) {
float lon = Float.parseFloat(value);
String name = keyParts[0];
if (locations.containsKey(name)) {
locations.get(name).setLongitude(lon);
logger.trace("Config: new Location " + name + "(" + locations.get(name) + ")");
} else {
Location loc = new Location();
loc.setLongitude(lon);
logger.trace("Config: update Location " + name + "(" + locations.get(name) + ")");
locations.put(name, loc);
}
}
} else if (key.endsWith("geofence") && !key.equals("geofence")) {
String[] keyParts = key.split("\\.");
if (keyParts.length != 2) {
throw new ConfigurationException(key, "Invalid OpenPaths user location geofence: " + key);
}
if (StringUtils.isNotBlank(value)) {
float fence = Float.parseFloat(value);
String name = keyParts[0];
if (locations.containsKey(name)) {
locations.get(name).setGeofence(fence);
logger.trace("Config: new Location " + name + "(" + locations.get(name) + ")");
} else {
Location loc = new Location();
loc.setGeofence(fence);
logger.trace("Config: update Location " + name + "(" + locations.get(name) + ")");
locations.put(name, loc);
}
}
} else if (key.endsWith("key")) {
String[] keyParts = key.split("\\.");
if (keyParts.length != 2) {
throw new ConfigurationException(key, "Invalid OpenPaths user key: " + key);
}
String name = keyParts[0];
String configKey = keyParts[1];
if (!openPathsUsers.containsKey(name)) {
openPathsUsers.put(name, new OpenPathsUser(name));
}
OpenPathsUser openPathsUser = openPathsUsers.get(name);
if (configKey.equalsIgnoreCase("accesskey")) {
openPathsUser.setAccessKey(value);
} else if (configKey.equalsIgnoreCase("secretkey")) {
openPathsUser.setSecretKey(value);
} else {
throw new ConfigurationException(key, "Unrecognised configuration parameter: " + configKey);
}
}
}
if (!locations.containsKey("home")) {
throw new ConfigurationException("home.lat", "No location specified for 'home'");
}
setProperlyConfigured(true);
}
}
public class OpenPathsUser {
private final String name;
private String accessKey;
private String secretKey;
private Location lastLocation = null;
private long lastUpdateTS = 0;
public OpenPathsUser(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public Location getLastLocation() {
return lastLocation;
}
public void setLastLocation(Location lastLocation) {
this.lastLocation = lastLocation;
this.lastUpdateTS = System.currentTimeMillis();
}
public long getLastUpdateTS() {
return lastUpdateTS;
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append(name).append(", last updated ");
out.append(new Date(this.lastUpdateTS).toString());
out.append(": ").append(this.lastLocation.toString());
return out.toString();
}
}
public class Location {
private float latitude;
private float longitude;
private float geofence;
private String device;
private double distance;
public Location(float latitude, float longitude, String device) {
this.latitude = latitude;
this.longitude = longitude;
this.device = device != null ? device : "";
this.geofence = 0.0f;
this.distance = Float.MAX_VALUE;
}
public Location() {
this.latitude = 0.0f;
this.longitude = 0.0f;
this.device = "";
this.geofence = 0.0f;
this.distance = Float.MAX_VALUE;
}
public float getLatitude() {
return latitude;
}
public float getLongitude() {
return longitude;
}
public float getGeofence() {
return geofence;
}
public String getDevice() {
return device;
}
public void setLatitude(float latitude) {
this.latitude = latitude;
}
public void setLongitude(float longitude) {
this.longitude = longitude;
}
public void setGeofence(float geofence) {
this.geofence = geofence;
}
public void setDevice(String device) {
this.device = device != null ? device : "";
}
public double getDistance() {
return distance;
}
public void setDistance(double distance) {
this.distance = distance;
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append(latitude).append(", ").append(longitude);
if (geofence > 0) {
out.append(", Fence: ").append(geofence);
}
if (!device.isEmpty()) {
out.append("(from ").append(device).append(')');
}
return out.toString();
}
}
}