<commit_before>/**
 * 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.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 <NAME>
 * @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' publishes
		String type = jsonPayload.get("_type");
		if (StringUtils.isEmpty(type) || !type.equals("location"))
			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
			    float latitude = Float.parseFloat(jsonPayload.get("lat"));
			    float longitude = Float.parseFloat(jsonPayload.get("lon"));
			    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;
		}
	}<commit_msg>Update to handle 'float' lat/lon payload<commit_after>/**
 * 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.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 <NAME>
 * @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' publishes
		String type = jsonPayload.get("_type");
		if (StringUtils.isEmpty(type) || !type.equals("location"))
			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;
		}
	}
{'fraction_non_alphanumeric': 0.11497227356746766, 'fraction_numerical': 0.0025138632162661737, 'mean_word_length': 3.7845772904138664, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2283518', 'n_tokens_mistral': 4488, 'n_tokens_neox': 3835, 'n_words': 1147}