org.openhab.binding.openpaths.internal.OpenPathsBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.openpaths.internal.OpenPathsBinding.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 *
 * 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.scribe.builder.*;
import org.scribe.model.*;
import org.scribe.oauth.*;
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.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;
    }

    /**
     * @{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 = (String) 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;
        }

        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();
        }
    }
}