/** * 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.mqttitude.internal; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; 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.core.events.EventPublisher; import org.openhab.core.library.types.OnOffType; import org.openhab.io.transport.mqtt.MqttMessageConsumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An MQTT consumer which subscribes to an MQTT topic and listens for Mqttitude * location publishes. * * Depending on the item binding configuration it will either manually calculate the * distance from 'home', or wait for enter/leave events sent from the mobile apps. * * @author Ben Jones * @since 1.4.0 */ public class MqttitudeConsumer implements MqttMessageConsumer { private static final Logger logger = LoggerFactory.getLogger(MqttitudeConsumer.class); // home location - optionally set for the binding if using non-region based item bindings private final Location homeLocation; private final float geoFence; // the topic this consumer is subscribed to private String topic; // the list of items we are monitoring on this topic private Map<String, MqttitudeItemConfig> itemConfigs = new HashMap<String, MqttitudeItemConfig>(); private EventPublisher eventPublisher; public MqttitudeConsumer(Location homeLocation, float geoFence) { this.homeLocation = homeLocation; this.geoFence = geoFence; } public void addItemConfig(MqttitudeItemConfig itemConfig) { // check this item config is for this topic if (!itemConfig.getTopic().equals(topic)) { logger.error("Attempting to add an item config with topic '{}', to a consumer with topic '{}'", itemConfig.getTopic(), topic); return; } synchronized (itemConfigs) { itemConfigs.put(itemConfig.getItemName(), itemConfig); } } public List<MqttitudeItemConfig> getItemConfigs() { synchronized (itemConfigs) { return new ArrayList<MqttitudeItemConfig>(itemConfigs.values()); } } /** * @{inheritDoc} */ @Override public String getTopic() { return topic; } /** * @{inheritDoc} */ @Override public void setTopic(String topic) { this.topic = topic; } /** * @{inheritDoc} */ @Override public void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } /** * @{inheritDoc} */ @Override public void processMessage(String topic, byte[] payload) { // convert the response to a string String decoded = new String(payload); logger.trace("Message received on topic {}: {}", topic, decoded); // read the payload into a JSON param/value map Map<String, String> jsonPayload = readJsonPayload(decoded); if (jsonPayload == null) { return; } // only interested in 'location' or 'transition' publishes String type = jsonPayload.get("_type"); if (StringUtils.isEmpty(type)) { return; } if (!type.equals("location") && !type.equals("transition")) { return; } // process all items being monitored on this topic for (MqttitudeItemConfig itemConfig : getItemConfigs()) { logger.trace("Checking item {}...", itemConfig.getItemName()); // if no region specified then we must be manually calculating distance from 'home' if (StringUtils.isEmpty(itemConfig.getRegion())) { // we must have a home location configured for the binding if (homeLocation == null) { logger.error( "Unable to calculate relative location for {} as there is no lat/lon configured for 'home'", itemConfig.getItemName()); continue; } // parse the published location Object lat = jsonPayload.get("lat"); Object lon = jsonPayload.get("lon"); float latitude; float longitude; if (lat instanceof Float) { latitude = (Float) lat; } else { latitude = Float.parseFloat(lat.toString()); } if (lon instanceof Float) { longitude = (Float) lon; } else { longitude = Float.parseFloat(lon.toString()); } Location location = new Location(latitude, longitude); logger.trace("Location received for {}: {}", itemConfig.getItemName(), location.toString()); // calculate the distance from 'home' double distance = calculateDistance(location, homeLocation); // update the item state based on the location relative to the geofence if (distance > geoFence) { logger.debug("{} is outside the 'home' geofence ({}m)", itemConfig.getItemName(), distance); eventPublisher.postUpdate(itemConfig.getItemName(), OnOffType.OFF); } else { logger.debug("{} is inside the 'home' geofence ({}m)", itemConfig.getItemName(), distance); eventPublisher.postUpdate(itemConfig.getItemName(), OnOffType.ON); } } else { // we are only interested in location updates with an 'event' (i.e. enter/leave) String event = jsonPayload.get("event"); if (StringUtils.isEmpty(event)) { logger.trace("Not a location enter/leave event, ignoring"); continue; } // check this event is for the region we are monitoring String desc = jsonPayload.get("desc"); if (StringUtils.isEmpty(desc)) { logger.trace("Location {} event has no region (missing or empty 'desc'), ignoring", event); continue; } if (!itemConfig.getRegion().equals(desc)) { logger.trace("Location {} event is for region '{}', ignoring", event, desc); continue; } if (event.equals("leave")) { logger.debug("{} has left region {}", itemConfig.getItemName(), itemConfig.getRegion()); eventPublisher.postUpdate(itemConfig.getItemName(), OnOffType.OFF); } else { logger.debug("{} has entered region {}", itemConfig.getItemName(), itemConfig.getRegion()); eventPublisher.postUpdate(itemConfig.getItemName(), OnOffType.ON); } } } } @SuppressWarnings("unchecked") private Map<String, String> readJsonPayload(String payload) { // parse the response to build our location object ObjectMapper jsonReader = new ObjectMapper(); try { return jsonReader.readValue(payload, Map.class); } catch (JsonParseException e) { logger.error("Error parsing JSON:\n" + payload); return null; } catch (JsonMappingException e) { logger.error("Error mapping JSON:\n" + payload); return null; } catch (IOException e) { logger.error("An I/O error occured while decoding JSON:\n" + payload); return null; } } 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; } }