/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE
* for more details.
*
* You should have received a copy of the GNU GENERAL PUBLIC LICENSE
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.service.graph;
import android.content.Context;
import android.util.Log;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import li.klass.fhem.domain.core.FhemDevice;
import li.klass.fhem.service.Command;
import li.klass.fhem.service.CommandExecutionService;
import li.klass.fhem.service.graph.gplot.GPlotSeries;
import li.klass.fhem.service.graph.gplot.SvgGraphDefinition;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
@Singleton
public class GraphService {
private static final String ENTRY_FORMAT = "yyyy-MM-dd_HH:mm:ss";
private static final DateTimeFormatter GRAPH_ENTRY_DATE_FORMATTER = DateTimeFormat.forPattern(ENTRY_FORMAT);
static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd_HH:mm");
static final String COMMAND_TEMPLATE = "get %s - - %s %s %s";
private static final Logger LOG = LoggerFactory.getLogger(GraphService.class);
private CommandExecutionService commandExecutionService;
private GraphIntervalProvider graphIntervalProvider;
@Inject
GraphService(CommandExecutionService commandExecutionService, GraphIntervalProvider graphIntervalProvider) {
this.commandExecutionService = commandExecutionService;
this.graphIntervalProvider = graphIntervalProvider;
}
/**
* Retrieves {@link GraphEntry} objects from FHEM. When the entries are available, the given listener object will
* be notified.
*
* @param device concerned device
* @param connectionId id of the server or absent (absent will use the currently selected server
* @param svgGraphDefinition svg graph definition
* @param startDate read FileLog entries from the given date
* @param endDate read FileLog entries up to the given date
* @param context context @return read graph data or null (if the device does not have a FileLog device)
*/
@SuppressWarnings("unchecked")
public GraphData getGraphData(FhemDevice device, Optional<String> connectionId, SvgGraphDefinition svgGraphDefinition,
final DateTime startDate, final DateTime endDate, Context context) {
Interval interval = getIntervalFor(startDate, endDate, context);
HashMap<GPlotSeries, List<GraphEntry>> data = newHashMap();
Set<GPlotSeries> series = Sets.newHashSet();
series.addAll(svgGraphDefinition.getPlotDefinition().getLeftAxis().getSeries());
series.addAll(svgGraphDefinition.getPlotDefinition().getRightAxis().getSeries());
LOG.info("getGraphData - getting graph data for device {} and {} series", device.getName(), series.size());
for (GPlotSeries plotSeries : series) {
data.put(plotSeries, getCurrentGraphEntriesFor(svgGraphDefinition.getLogDeviceName(), connectionId, plotSeries, interval, context, svgGraphDefinition.getPlotfunction()));
}
return new GraphData(data, interval);
}
private Interval getIntervalFor(DateTime startDate, DateTime endDate, Context context) {
return graphIntervalProvider.getIntervalFor(startDate, endDate, context);
}
/**
* Collects FileLog entries from FHEM matching a given column specification. The results will be turned into
* {@link GraphEntry} objects and be returned.
*
* @param logDevice logDevice to load graph entries from.
* @param connectionId id of the server or absent (absent will use the currently selected server)
* @param gPlotSeries chart description
* @param interval Interval containing start and end date
* @param context context
* @param plotfunction SPEC parameters to replace @return read logDevices entries converted to {@link GraphEntry} objects.
*/
private List<GraphEntry> getCurrentGraphEntriesFor(String logDevice,
Optional<String> connectionId, GPlotSeries gPlotSeries,
Interval interval, Context context, List<String> plotfunction) {
List<GraphEntry> graphEntries = findGraphEntries(loadLogData(logDevice, connectionId, interval, gPlotSeries, context, plotfunction));
LOG.info("getCurrentGraphEntriesFor - found {} graph entries for logDevice {}", graphEntries.size(), logDevice);
return graphEntries;
}
String loadLogData(String logDevice, Optional<String> connectionId, Interval interval,
GPlotSeries plotSeries, Context context, List<String> plotfunction) {
String fromDateFormatted = DATE_TIME_FORMATTER.print(interval.getStart());
String toDateFormatted = DATE_TIME_FORMATTER.print(interval.getEnd());
StringBuilder result = new StringBuilder();
String command = String.format(COMMAND_TEMPLATE, logDevice, fromDateFormatted, toDateFormatted, plotSeries.getLogDef());
for (int i = 0; i < plotfunction.size(); i++) {
command = command.replaceAll("<SPEC" + (i + 1) + ">", plotfunction.get(i));
}
String data = commandExecutionService.executeSync(new Command(command, connectionId), context);
if (data != null) {
result.append("\n\r").append(data.replaceAll("#[^\\\\]*\\\\[rn]", ""));
}
return result.toString();
}
/**
* Looks for any {@link GraphEntry} objects within the returned String. Unfortunately, FHEM does not return any
* line delimiters, to that a pretty complicated regular expression has to be applied.
*
* @param content content to parse
* @return found {@link GraphEntry} objects.
*/
List<GraphEntry> findGraphEntries(String content) {
List<GraphEntry> result = newArrayList();
if (content == null) return result;
content = content.replaceAll("\r", "");
String[] entries = content.split("\n");
for (String entry : entries) {
Optional<GraphEntry> parsed = parseEntry(entry);
if (parsed.isPresent()) {
result.add(parsed.get());
}
}
return result;
}
Optional<GraphEntry> parseEntry(String entry) {
String[] parts = entry.split(" ");
if (parts.length != 2) return Optional.absent();
String entryTime = parts[0];
String entryValue = parts[1];
try {
if (ENTRY_FORMAT.length() == entryTime.length()) {
DateTime entryDate = GRAPH_ENTRY_DATE_FORMATTER.parseDateTime(entryTime);
float entryFloatValue = Float.valueOf(entryValue);
return Optional.of(new GraphEntry(entryDate, entryFloatValue));
} else {
LOG.trace("silent ignore of {}, as having a wrong time format", entryTime);
}
} catch (NumberFormatException e) {
Log.e(GraphService.class.getName(), "cannot parse date " + entryTime, e);
} catch (Exception e) {
Log.e(GraphService.class.getName(), "cannot parse number " + entryValue, e);
}
return Optional.absent();
}
}