package org.fluxtream.connectors.fitbit;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.domain.AbstractFacet;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.ChannelMapping;
import org.fluxtream.core.services.impl.BodyTrackHelper;
import org.fluxtream.core.services.impl.FieldHandler;
import org.joda.time.LocalTime;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
*
* @author Candide Kemmler (candide@fluxtream.com)
*/
@Component("fitbitActivity")
public class FitbitActivityFieldHandler implements FieldHandler {
private static final String DATASET_KEY = "dataset";
@Autowired
BodyTrackHelper bodyTrackHelper;
@Autowired
Configuration env;
@Override
public List<BodyTrackHelper.BodyTrackUploadResult> handleField (final ApiKey apiKey, AbstractFacet facet) {
List<BodyTrackHelper.BodyTrackUploadResult> results = new ArrayList<BodyTrackHelper.BodyTrackUploadResult>();
FitbitTrackerActivityFacet fitbitActivityFacet = (FitbitTrackerActivityFacet) facet;
// The Fitbit activity data is daily data that covers an entire day. The start/end time may be the
// leading and trailing midnights for the date, or may both be at noon on the date, depending
// on the version that did the import. Either way they should now both be in UTC since
// it is a local time facet. Datastore points only have a single time associated with them.
// Set this time to be the middle value between start and end, which should be noon UTC in
// either case. Also convert to double seconds since that is what the datastore uses.
double dsTime = (double)(fitbitActivityFacet.start + fitbitActivityFacet.end)/2000.0;
List<String> channelNames = new ArrayList<String>();
List<List<Object>> data = new ArrayList<List<Object>>();
List<Object> record = new ArrayList<Object>();
// Add the timestamp to the start of the record
record.add(dsTime);
// Add each non-empty field to both the channelNames and data record so they correspond
if (fitbitActivityFacet.activeScore > 0) {
channelNames.add("activeScore");
record.add(fitbitActivityFacet.activeScore);
}
if (fitbitActivityFacet.floors > 0) {
channelNames.add("floors");
record.add(fitbitActivityFacet.floors);
}
if (fitbitActivityFacet.elevation > 0) {
channelNames.add("elevation");
record.add(fitbitActivityFacet.elevation);
}
if (fitbitActivityFacet.caloriesOut > 0) {
channelNames.add("caloriesOut");
record.add(fitbitActivityFacet.caloriesOut);
}
if (fitbitActivityFacet.fairlyActiveMinutes > 0) {
channelNames.add("fairlyActiveMinutes");
record.add(fitbitActivityFacet.fairlyActiveMinutes);
}
if (fitbitActivityFacet.lightlyActiveMinutes > 0) {
channelNames.add("lightlyActiveMinutes");
record.add(fitbitActivityFacet.lightlyActiveMinutes);
}
if (fitbitActivityFacet.sedentaryMinutes > 0) {
channelNames.add("sedentaryMinutes");
record.add(fitbitActivityFacet.sedentaryMinutes);
}
if (fitbitActivityFacet.veryActiveMinutes > 0) {
channelNames.add("veryActiveMinutes");
record.add(fitbitActivityFacet.veryActiveMinutes);
}
if (fitbitActivityFacet.steps > 0) {
channelNames.add("steps");
record.add(fitbitActivityFacet.steps);
}
if (fitbitActivityFacet.trackerDistance > 0) {
channelNames.add("trackerDistance");
record.add(fitbitActivityFacet.trackerDistance);
}
if (fitbitActivityFacet.loggedActivitiesDistance > 0) {
channelNames.add("loggedActivitiesDistance");
record.add(fitbitActivityFacet.loggedActivitiesDistance);
}
if (fitbitActivityFacet.veryActiveDistance > 0) {
channelNames.add("veryActiveDistance");
record.add(fitbitActivityFacet.veryActiveDistance);
}
if (fitbitActivityFacet.totalDistance > 0) {
channelNames.add("totalDistance");
record.add(fitbitActivityFacet.totalDistance);
}
if (fitbitActivityFacet.moderatelyActiveDistance > 0) {
channelNames.add("moderatelyActiveDistance");
record.add(fitbitActivityFacet.moderatelyActiveDistance);
}
if (fitbitActivityFacet.lightlyActiveDistance > 0) {
channelNames.add("lightlyActiveDistance");
record.add(fitbitActivityFacet.lightlyActiveDistance);
}
if (fitbitActivityFacet.sedentaryActiveDistance > 0) {
channelNames.add("sedentaryActiveDistance");
record.add(fitbitActivityFacet.sedentaryActiveDistance);
}
data.add(record);
results.add(bodyTrackHelper.uploadToBodyTrack(apiKey, "Fitbit", channelNames, data));
List<String> metrics = getWantedMetrics();
for (String metric : metrics) {
try {
Field jsonField = FitbitTrackerActivityFacet.class.getField(metric + "Json");
if (jsonField.get(fitbitActivityFacet)!=null) {
final BodyTrackHelper.BodyTrackUploadResult bodyTrackUploadResult = addIntradayData(apiKey, fitbitActivityFacet, jsonField, metric);
if (bodyTrackUploadResult!=null)
results.add(bodyTrackUploadResult);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
continue;
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
// TODO: check the status code in the BodyTrackUploadResult
return results;
}
private List<String> getWantedMetrics() {
List<String> wantedMetrics = Arrays.asList("steps", "calories", "distance", "floors", "elevation");
final Object wantedMetricsProperty = env.oauth.getProperty("fitbit.intraday.metrics.wanted");
if (wantedMetricsProperty!=null&&wantedMetricsProperty instanceof List) {
List<String> wantedMetricsConfig = (List<String>) wantedMetricsProperty;
if (wantedMetricsConfig.size()>0)
wantedMetrics = wantedMetricsConfig;
}
return wantedMetrics;
}
private BodyTrackHelper.BodyTrackUploadResult addIntradayData(final ApiKey apiKey,
FitbitTrackerActivityFacet fitbitActivityFacet,
final Field field,
final String metric) throws IllegalAccessException {
List<List<Object>> data = new ArrayList<List<Object>>();
long midnight = ISODateTimeFormat.date().withZoneUTC().parseDateTime(fitbitActivityFacet.date).toDateMidnight().getMillis();
JSONObject intradayJson = JSONObject.fromObject(field.get(fitbitActivityFacet));
final String intradayMetricKey = "activities-log-" + metric + "-intraday";
if (intradayJson.has(intradayMetricKey)) {
JSONObject intradayDataJson = intradayJson.getJSONObject(intradayMetricKey);
if (intradayDataJson.has(DATASET_KEY)) {
JSONArray intradayDataArray = intradayDataJson.getJSONArray(DATASET_KEY);
for (int i=0; i<intradayDataArray.size(); i++) {
JSONObject intradayDataRecord = intradayDataArray.getJSONObject(i);
String time = intradayDataRecord.getString("time") + "Z";
final LocalTime localTime = ISODateTimeFormat.timeNoMillis().parseLocalTime(time);
final long timeGmt = (midnight + localTime.getMillisOfDay())/1000;
int value = intradayDataRecord.getInt("value");
List<Object> record = new ArrayList<Object>();
record.add(timeGmt);
record.add(value);
if (metric.equals("calories")) {
int level = intradayDataRecord.getInt("level");
record.add(level);
int mets = intradayDataRecord.getInt("mets");
record.add(mets);
}
data.add(record);
}
}
final List<String> channelNames = !metric.equals("calories")
? Arrays.asList(metric + "Intraday")
: Arrays.asList(metric + "Intraday", "levelsIntraday", "metsIntraday");
return bodyTrackHelper.uploadToBodyTrack(apiKey, "Fitbit", channelNames, data);
}
return null;
}
@Override
public void addToDeclaredChannelMappings(final ApiKey apiKey, final List<ChannelMapping> channelMappings) {
final List<String> wantedMetrics = getWantedMetrics();
for (String wantedMetric : wantedMetrics) {
ChannelMapping.addToDeclaredMappings(apiKey, ChannelMapping.ChannelType.data, ChannelMapping.TimeType.local,
1, apiKey.getConnector().getDeviceNickname(), wantedMetric + "Intraday", channelMappings);
if (wantedMetric.equals("calories")) {
ChannelMapping.addToDeclaredMappings(apiKey, ChannelMapping.ChannelType.data, ChannelMapping.TimeType.local,
1, apiKey.getConnector().getDeviceNickname(), "levelsIntraday", channelMappings);
ChannelMapping.addToDeclaredMappings(apiKey, ChannelMapping.ChannelType.data, ChannelMapping.TimeType.local,
1, apiKey.getConnector().getDeviceNickname(), "metsIntraday", channelMappings);
}
}
}
}