diff --git a/org.openhab.persistence.rrd4j/.classpath b/org.openhab.persistence.rrd4j/.classpath new file mode 100755 index 0000000..4581a4f --- /dev/null +++ b/org.openhab.persistence.rrd4j/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/org.openhab.persistence.rrd4j/.project b/org.openhab.persistence.rrd4j/.project new file mode 100755 index 0000000..004e54e --- /dev/null +++ b/org.openhab.persistence.rrd4j/.project @@ -0,0 +1,33 @@ + + + org.openhab.persistence.rrd4j + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.openhab.persistence.rrd4j/.settings/org.eclipse.pde.core.prefs b/org.openhab.persistence.rrd4j/.settings/org.eclipse.pde.core.prefs new file mode 100755 index 0000000..1845972 --- /dev/null +++ b/org.openhab.persistence.rrd4j/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +#Mon Oct 11 21:06:38 CEST 2010 +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/org.openhab.persistence.rrd4j/META-INF/MANIFEST.MF b/org.openhab.persistence.rrd4j/META-INF/MANIFEST.MF new file mode 100755 index 0000000..1a5fd88 --- /dev/null +++ b/org.openhab.persistence.rrd4j/META-INF/MANIFEST.MF @@ -0,0 +1,30 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: openHAB RRD4j Persistence Bundle +Bundle-SymbolicName: org.openhab.persistence.rrd4j;singleton:=true +Bundle-Version: 2.0.0.qualifier +Bundle-Vendor: openHAB +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Import-Package: javax.servlet;version="2.6.0", + org.apache.commons.io, + org.apache.commons.lang, + org.openhab.core.items, + org.openhab.core.library.items, + org.openhab.core.library.types, + org.openhab.core.persistence, + org.openhab.core.types, + org.openhab.io.net.http, + org.osgi.framework, + org.osgi.service.cm, + org.osgi.service.http;version="1.2.1", + org.slf4j +Bundle-ClassPath: ., + lib/rrd4j-2.1.1.jar +Service-Component: OSGI-INF/rrd4j.xml, OSGI-INF/chartservlet.xml +Bundle-Activator: org.openhab.persistence.rrd4j.internal.RRD4jActivator +Require-Bundle: org.eclipse.smarthome.core, + org.eclipse.smarthome.core.library, + org.eclipse.smarthome.core.persistence, + org.eclipse.smarthome.io.net, + org.eclipse.smarthome.ui, + org.eclipse.smarthome.model.sitemap;bundle-version="0.8.0" diff --git a/org.openhab.persistence.rrd4j/OSGI-INF/chartservlet.xml b/org.openhab.persistence.rrd4j/OSGI-INF/chartservlet.xml new file mode 100755 index 0000000..cd6a6c8 --- /dev/null +++ b/org.openhab.persistence.rrd4j/OSGI-INF/chartservlet.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/org.openhab.persistence.rrd4j/OSGI-INF/rrd4j.xml b/org.openhab.persistence.rrd4j/OSGI-INF/rrd4j.xml new file mode 100755 index 0000000..875f8f0 --- /dev/null +++ b/org.openhab.persistence.rrd4j/OSGI-INF/rrd4j.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/org.openhab.persistence.rrd4j/build.properties b/org.openhab.persistence.rrd4j/build.properties new file mode 100755 index 0000000..1a91dc1 --- /dev/null +++ b/org.openhab.persistence.rrd4j/build.properties @@ -0,0 +1,6 @@ +output.. = target/classes/ +bin.includes = META-INF/,\ + OSGI-INF/,\ + .,\ + lib/rrd4j-2.1.1.jar +source.. = src/main/java/ diff --git a/org.openhab.persistence.rrd4j/lib/rrd4j-2.1.1.jar b/org.openhab.persistence.rrd4j/lib/rrd4j-2.1.1.jar new file mode 100755 index 0000000..f3065aa Binary files /dev/null and b/org.openhab.persistence.rrd4j/lib/rrd4j-2.1.1.jar differ diff --git a/org.openhab.persistence.rrd4j/pom.xml b/org.openhab.persistence.rrd4j/pom.xml new file mode 100755 index 0000000..2b848b3 --- /dev/null +++ b/org.openhab.persistence.rrd4j/pom.xml @@ -0,0 +1,91 @@ + + + + + org.openhab.bundles + persistence + 2.0.0-SNAPSHOT + + + openHAB RRD4j Persistence + + + org.openhab.persistence.rrd4j + org.openhab.persistence.rrd4j + openhab-addon-persistence-rrd4j + ${project.name} + openhab-runtime + + + 4.0.0 + org.openhab.persistence + org.openhab.persistence.rrd4j + + eclipse-plugin + + + + + org.vafer + jdeb + + + package + + jdeb + + + ${basedir}/src/deb/control + + + ${basedir}/src/deb/etc/openhab/configurations/persistence/rrd4j.persist + file + + perm + /etc/openhab/configurations/persistence + root + root + 644 + + + + ${basedir}/target/${project.artifactId}-${project.version}.jar + file + + perm + /usr/share/openhab/addons + root + root + 644 + + + + directory + ${basedir}/src/deb/var/lib/openhab/persistence + **/.gitignore + + perm + /var/lib/openhab/persistence + root + openhab + 2775 + + + + + link + /etc/openhab/jetty/etc/rrd4j + /var/lib/openhab/persistence/rrd4j + true + + + + + + + + + + diff --git a/org.openhab.persistence.rrd4j/src/deb/control/conffiles b/org.openhab.persistence.rrd4j/src/deb/control/conffiles new file mode 100755 index 0000000..00af548 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/deb/control/conffiles @@ -0,0 +1 @@ +/etc/openhab/configurations/persistence/rrd4j.persist diff --git a/org.openhab.persistence.rrd4j/src/deb/control/control b/org.openhab.persistence.rrd4j/src/deb/control/control new file mode 100755 index 0000000..89d4486 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/deb/control/control @@ -0,0 +1,9 @@ +Package: [[deb.name]] +Version: [[version]] +Section: [[deb.section]] +Priority: optional +Architecture: all +Maintainer: [[deb.maintainer]] +Description: [[deb.description]] +Distribution: [[deb.distribution]] +Depends: [[deb.depends]] diff --git a/org.openhab.persistence.rrd4j/src/deb/etc/openhab/configurations/persistence/rrd4j.persist b/org.openhab.persistence.rrd4j/src/deb/etc/openhab/configurations/persistence/rrd4j.persist new file mode 100755 index 0000000..2447d12 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/deb/etc/openhab/configurations/persistence/rrd4j.persist @@ -0,0 +1 @@ +// Configuration file for "rrd4j" persistence module diff --git a/org.openhab.persistence.rrd4j/src/deb/var/lib/openhab/persistence/rrd4j/.gitignore b/org.openhab.persistence.rrd4j/src/deb/var/lib/openhab/persistence/rrd4j/.gitignore new file mode 100755 index 0000000..44c5ea8 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/deb/var/lib/openhab/persistence/rrd4j/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jActivator.java b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jActivator.java new file mode 100755 index 0000000..70738fe --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jActivator.java @@ -0,0 +1,41 @@ +/** + * 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.persistence.rrd4j.internal; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Extension of the default OSGi bundle activator + * + * @author Kai Kreuzer + * @since 1.0.0 + */ +public final class RRD4jActivator implements BundleActivator { + + private static Logger logger = LoggerFactory.getLogger(RRD4jActivator.class); + + /** + * Called whenever the OSGi framework starts our bundle + */ + public void start(BundleContext bc) throws Exception { + logger.debug("RRD4j persistence bundle has been started."); + } + + /** + * Called whenever the OSGi framework stops our bundle + */ + public void stop(BundleContext bc) throws Exception { + logger.debug("RRD4j persistence bundle has been stopped."); + } + +} diff --git a/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jItem.java b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jItem.java new file mode 100755 index 0000000..460fae6 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jItem.java @@ -0,0 +1,53 @@ +/** + * 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.persistence.rrd4j.internal; + +import java.text.DateFormat; +import java.util.Date; + +import org.eclipse.smarthome.core.persistence.HistoricItem; +import org.eclipse.smarthome.core.types.State; + +/** + * This is a Java bean used to return historic items from a rrd4j database. + * + * @author Kai Kreuzer + * @since 1.2.0 + * + */ +public class RRD4jItem implements HistoricItem { + + final private String name; + final private State state; + final private Date timestamp; + + public RRD4jItem(String name, State state, Date timestamp) { + this.name = name; + this.state = state; + this.timestamp = timestamp; + } + + public String getName() { + return name; + } + + public State getState() { + return state; + } + + public Date getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return DateFormat.getDateTimeInstance().format(timestamp) + ": " + name + " -> "+ state.toString(); + } + +} \ No newline at end of file diff --git a/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jService.java b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jService.java new file mode 100755 index 0000000..9be4fdd --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jService.java @@ -0,0 +1,316 @@ +/** + * 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.persistence.rrd4j.internal; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.RejectedExecutionException; + +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ItemNotFoundException; +import org.eclipse.smarthome.core.items.ItemRegistry; +import org.eclipse.smarthome.core.library.items.ContactItem; +import org.eclipse.smarthome.core.library.items.DimmerItem; +import org.eclipse.smarthome.core.library.items.NumberItem; +import org.eclipse.smarthome.core.library.items.SwitchItem; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.persistence.FilterCriteria; +import org.eclipse.smarthome.core.persistence.FilterCriteria.Ordering; +import org.eclipse.smarthome.core.persistence.HistoricItem; +import org.eclipse.smarthome.core.persistence.PersistenceService; +import org.eclipse.smarthome.core.persistence.QueryablePersistenceService; +import org.eclipse.smarthome.core.types.State; +import org.rrd4j.ConsolFun; +import org.rrd4j.DsType; +import org.rrd4j.core.FetchData; +import org.rrd4j.core.FetchRequest; +import org.rrd4j.core.RrdDb; +import org.rrd4j.core.RrdDef; +import org.rrd4j.core.Sample; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * This is the implementation of the RRD4j {@link PersistenceService}. To learn + * more about RRD4j please visit their website. + * + * @author Kai Kreuzer + * @since 1.0.0 + */ +public class RRD4jService implements QueryablePersistenceService { + + private static final String DATASOURCE_STATE = "state"; + + protected final static String DB_FOLDER = getUserDataFolder() + File.separator + "rrd4j"; + + private static final Logger logger = LoggerFactory.getLogger(RRD4jService.class); + + private Map timers = new HashMap(); + + protected ItemRegistry itemRegistry; + + public void setItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + } + + public void unsetItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = null; + } + + /** + * @{inheritDoc} + */ + public String getName() { + return "rrd4j"; + } + + /** + * @{inheritDoc} + */ + public synchronized void store(final Item item, final String alias) { + final String name = alias==null ? item.getName() : alias; + ConsolFun function = getConsolidationFunction(item); + RrdDb db = getDB(name, function); + if(db!=null) { + long now = System.currentTimeMillis()/1000; + if(function!=ConsolFun.AVERAGE) { + try { + // we store the last value again, so that the value change in the database is not interpolated, but + // happens right at this spot + if(now - 1 > db.getLastUpdateTime()) { + // only do it if there is not already a value + double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE); + if(!Double.isNaN(lastValue)) { + Sample sample = db.createSample(); + sample.setTime(now - 1); + sample.setValue(DATASOURCE_STATE, lastValue); + sample.update(); + logger.debug("Stored '{}' with state '{}' in rrd4j database", name, mapToState(lastValue, item.getName())); + } + } + } catch (IOException e) { + logger.debug("Error re-storing last value: {}", e.getMessage()); + } + } + try { + Sample sample = db.createSample(); + sample.setTime(now); + + DecimalType state = (DecimalType) item.getStateAs(DecimalType.class); + if (state!=null) { + double value = state.toBigDecimal().doubleValue(); + sample.setValue(DATASOURCE_STATE, value); + sample.update(); + logger.debug("Stored '{}' with state '{}' in rrd4j database", name, item.getState()); + } + } catch (IllegalArgumentException e) { + if(e.getMessage().contains("at least one second step is required")) { + + // we try to store the value one second later + TimerTask task = new TimerTask() { + public void run() { + store(item, name); + } + }; + Timer timer = timers.get(name); + if(timer!=null) { + timer.cancel(); + timers.remove(name); + } + timer = new Timer(); + timers.put(name, timer); + timer.schedule(task, 1000); + } else { + logger.warn("Could not persist '{}' to rrd4j database: {}", new String[] { name, e.getMessage() }); + } + } catch (Exception e) { + logger.warn("Could not persist '{}' to rrd4j database: {}", new String[] { name, e.getMessage() }); + } + try { + db.close(); + } catch (IOException e) { + logger.debug("Error closing rrd4j database: {}", e.getMessage()); + } + } + } + + /** + * @{inheritDoc} + */ + public void store(Item item) { + store(item, null); + } + + @Override + public Iterable query(FilterCriteria filter) { + String itemName = filter.getItemName(); + ConsolFun consolidationFunction = getConsolidationFunction(itemName); + RrdDb db = getDB(itemName, consolidationFunction); + if(db!=null) { + long start = 0L; + long end = filter.getEndDate()==null ? System.currentTimeMillis()/1000 : filter.getEndDate().getTime()/1000; + + try { + if(filter.getBeginDate()==null) { + // as rrd goes back for years and gets more and more inaccurate, we only support descending order and a single return value + // if there is no begin date is given - this case is required specifically for the historicState() query, which we + // want to support + if(filter.getOrdering()==Ordering.DESCENDING && filter.getPageSize()==1 && filter.getPageNumber()==0) { + if(filter.getEndDate()==null) { + // we are asked only for the most recent value! + double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE); + if(!Double.isNaN(lastValue)) { + HistoricItem rrd4jItem = new RRD4jItem(itemName, mapToState(lastValue, itemName), new Date(db.getLastArchiveUpdateTime() * 1000)); + return Collections.singletonList(rrd4jItem); + } else { + return Collections.emptyList(); + } + } else { + start = end; + } + } else { + throw new UnsupportedOperationException("rrd4j does not allow querys without a begin date, " + + "unless order is decending and a single value is requested"); + } + } else { + start = filter.getBeginDate().getTime()/1000; + } + FetchRequest request = db.createFetchRequest(consolidationFunction, start, end, 1); + + List items = new ArrayList(); + FetchData result = request.fetchData(); + long ts = result.getFirstTimestamp(); + long step = result.getRowCount() > 1 ? result.getStep() : 0; + for(double value : result.getValues(DATASOURCE_STATE)) { + if(!Double.isNaN(value)) { + RRD4jItem rrd4jItem = new RRD4jItem(itemName, mapToState(value, itemName), new Date(ts * 1000)); + items.add(rrd4jItem); + } + ts += step; + } + return items; + } catch (IOException e) { + logger.warn("Could not query rrd4j database for item '{}': {}", new String[] { itemName, e.getMessage() }); + } + } + return Collections.emptyList(); + } + + protected synchronized RrdDb getDB(String alias, ConsolFun function) { + RrdDb db = null; + File file = new File(DB_FOLDER + File.separator + alias + ".rrd"); + try { + if (file.exists()) { + // recreate the RrdDb instance from the file + db = new RrdDb(file.getAbsolutePath()); + } else { + File folder = new File(DB_FOLDER); + if(!folder.exists()) { + folder.mkdir(); + } + // create a new database file + db = new RrdDb(getRrdDef(function, file)); + } + } catch (IOException e) { + logger.error("Could not create rrd4j database file '{}': {}", new String[] { file.getAbsolutePath(), e.getMessage() }); + } catch(RejectedExecutionException e) { + // this happens if the system is shut down + logger.debug("Could not create rrd4j database file '{}': {}", new String[] { file.getAbsolutePath(), e.getMessage() }); + } + return db; + } + + private RrdDef getRrdDef(ConsolFun function, File file) { + RrdDef rrdDef = new RrdDef(file.getAbsolutePath()); + if(function==ConsolFun.AVERAGE) { + // for measurement values, we define archives that are suitable for charts + rrdDef.setStep(60); + rrdDef.setStartTime(System.currentTimeMillis()/1000-1); + rrdDef.addDatasource(DATASOURCE_STATE, DsType.GAUGE, 60, Double.NaN, Double.NaN); + rrdDef.addArchive(function, 0.5, 1, 480); // 8 hours (granularity 1 min) + rrdDef.addArchive(function, 0.5, 4, 360); // one day (granularity 4 min) + rrdDef.addArchive(function, 0.5, 15, 644); // one week (granularity 15 min) + rrdDef.addArchive(function, 0.5, 60, 720); // one month (granularity 1 hour) + rrdDef.addArchive(function, 0.5, 720, 730); // one year (granularity 12 hours) + rrdDef.addArchive(function, 0.5, 10080, 520); // ten years (granularity 7 days) + } else { + // for other things, we mainly provide a high level of detail for the last hour + rrdDef.setStep(1); + rrdDef.setStartTime(System.currentTimeMillis()/1000-1); + rrdDef.addDatasource(DATASOURCE_STATE, DsType.GAUGE, 3600, Double.NaN, Double.NaN); + rrdDef.addArchive(function, .999, 1, 3600); // 1 hour (granularity 1 sec) + rrdDef.addArchive(function, .999, 10, 1440); // 4 hours (granularity 10 sec) + rrdDef.addArchive(function, .999, 60, 1440); // one day (granularity 1 min) + rrdDef.addArchive(function, .999, 900, 2880); // one month (granularity 15 min) + rrdDef.addArchive(function, .999, 21600, 1460); // one year (granularity 6 hours) + rrdDef.addArchive(function, .999, 86400, 3650); // ten years (granularity 1 day) + } + return rrdDef; + } + + static public ConsolFun getConsolidationFunction(Item item) { + if(item instanceof NumberItem) { + return ConsolFun.AVERAGE; + } else { + // for all other values (like ON/OFF etc.) use the maximum value for consolidation + return ConsolFun.MAX; + } + } + + private ConsolFun getConsolidationFunction(String itemName) { + if(itemRegistry!=null) { + try { + Item item = itemRegistry.getItem(itemName); + return getConsolidationFunction(item); + } catch (ItemNotFoundException e) { + logger.debug("Could not find item '{}' in registry", itemName); + } + } + // use MAX as the default + return ConsolFun.MAX; + } + + private State mapToState(double value, String itemName) { + if(itemRegistry!=null) { + try { + Item item = itemRegistry.getItem(itemName); + if(item instanceof SwitchItem && !(item instanceof DimmerItem)) { + return value==0.0d ? OnOffType.OFF : OnOffType.ON; + } else if(item instanceof ContactItem) { + return value==0.0d ? OpenClosedType.CLOSED : OpenClosedType.OPEN; + } + } catch (ItemNotFoundException e) { + logger.debug("Could not find item '{}' in registry", itemName); + } + } + // just return a DecimalType as a fallback + return new DecimalType(value); + } + + static private String getUserDataFolder() { + String progArg = System.getProperty("smarthome.userdata"); + if (progArg != null) { + return progArg; + } else { + return "etc"; + } + } + +} diff --git a/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/charts/RRD4jChartServlet.java b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/charts/RRD4jChartServlet.java new file mode 100755 index 0000000..323bae4 --- /dev/null +++ b/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/charts/RRD4jChartServlet.java @@ -0,0 +1,297 @@ +/** + * 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.persistence.rrd4j.internal.charts; + +import java.awt.Color; +import java.awt.Font; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import javax.imageio.ImageIO; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.eclipse.smarthome.core.items.GroupItem; +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ItemNotFoundException; +import org.eclipse.smarthome.core.library.items.NumberItem; +import org.openhab.persistence.rrd4j.internal.RRD4jService; +import org.eclipse.smarthome.ui.chart.ChartProvider; +import org.eclipse.smarthome.ui.items.ItemUIRegistry; +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.rrd4j.graph.RrdGraph; +import org.rrd4j.graph.RrdGraphDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This servlet generates time-series charts for a given set of items. + * It accepts the following HTTP parameters: + *
    + *
  • w: width in pixels of image to generate
  • + *
  • h: height in pixels of image to generate
  • + *
  • period: the time span for the x-axis. Value can be h,4h,8h,12h,D,3D,W,2W,M,2M,4M,Y
  • + *
  • items: A comma separated list of item names to display + *
  • groups: A comma separated list of group names, whose members should be displayed + *
+ * + * @author Kai Kreuzer + * @author Chris Jackson + * @since 1.0.0 + * + */ +public class RRD4jChartServlet implements Servlet, ChartProvider { + + private static final Logger logger = LoggerFactory.getLogger(RRD4jChartServlet.class); + + /** the URI of this servlet */ + public static final String SERVLET_NAME = "/rrdchart.png"; + + protected static final Color[] LINECOLORS = new Color[] { + Color.RED, Color.GREEN, Color.BLUE, + Color.MAGENTA, Color.ORANGE, Color.CYAN, + Color.PINK, Color.DARK_GRAY, Color.YELLOW }; + protected static final Color[] AREACOLORS = new Color[] { + new Color(255, 0, 0, 30), new Color(0, 255, 0, 30), new Color(0, 0, 255, 30), + new Color(255, 0, 255, 30), new Color(255, 128, 0, 30), new Color(0, 255, 255, 30), + new Color(255, 0, 128, 30), new Color(255, 128, 128, 30), new Color(255, 255, 0, 30)}; + + protected static final Map PERIODS = new HashMap(); + + static { + PERIODS.put("h", -3600000L); + PERIODS.put("4h", -14400000L); + PERIODS.put("8h", -28800000L); + PERIODS.put("12h", -43200000L); + PERIODS.put("D", -86400000L); + PERIODS.put("3D", -259200000L); + PERIODS.put("W", -604800000L); + PERIODS.put("2W", -1209600000L); + PERIODS.put("M", -2592000000L); + PERIODS.put("2M", -5184000000L); + PERIODS.put("4M", -10368000000L); + PERIODS.put("Y", -31536000000L); + } + + protected HttpService httpService; + protected ItemUIRegistry itemUIRegistry; + + public void setHttpService(HttpService httpService) { + this.httpService = httpService; + } + + public void unsetHttpService(HttpService httpService) { + this.httpService = null; + } + + public void setItemUIRegistry(ItemUIRegistry itemUIRegistry) { + this.itemUIRegistry = itemUIRegistry; + } + + public void unsetItemUIRegistry(ItemUIRegistry itemUIRegistry) { + this.itemUIRegistry = null; + } + + protected void activate() { + try { + logger.debug("Starting up rrd chart servlet at " + SERVLET_NAME); + + Hashtable props = new Hashtable(); + httpService.registerServlet(SERVLET_NAME, this, props, createHttpContext()); + + } catch (NamespaceException e) { + logger.error("Error during servlet startup", e); + } catch (ServletException e) { + logger.error("Error during servlet startup", e); + } + } + + protected void deactivate() { + httpService.unregister(SERVLET_NAME); + } + + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + logger.debug("RRD4J Received incoming chart request: ", req); + + int width = 480; + try { + width = Integer.parseInt(req.getParameter("w")); + } catch (Exception e) { + } + int height = 240; + try { + height = Integer.parseInt(req.getParameter("h")); + } catch (Exception e) { + } + Long period = PERIODS.get(req.getParameter("period")); + if (period == null) { + // use a day as the default period + period = PERIODS.get("D"); + } + // Create the start and stop time + Date timeEnd = new Date(); + Date timeBegin = new Date(timeEnd.getTime() + period); + + // Set the content type to that provided by the chart provider + res.setContentType("image/"+getChartType()); + try { + BufferedImage chart = createChart(null, null, timeBegin, timeEnd, height, width, req.getParameter("items"), req.getParameter("groups")); + ImageIO.write(chart, getChartType().toString(), res.getOutputStream()); + } catch (ItemNotFoundException e) { + logger.debug("Item not found error while generating chart."); + } catch (IllegalArgumentException e) { + logger.debug("Illegal argument in chart: {}", e); + } + } + + /** + * Adds a line for the item to the graph definition. + * The color of the line is determined by the counter, it simply picks the according index from LINECOLORS (and rolls over if necessary). + * + * @param graphDef the graph definition to fill + * @param item the item to add a line for + * @param counter defines the number of the datasource and is used to determine the line color + */ + protected void addLine(RrdGraphDef graphDef, Item item, int counter) { + Color color = LINECOLORS[counter%LINECOLORS.length]; + String label = itemUIRegistry.getLabel(item.getName()); + if(label!=null && label.contains("[") && label.contains("]")) { + label = label.substring(0, label.indexOf('[')); + } + if(item instanceof NumberItem) { + // we only draw a line + graphDef.datasource(Integer.toString(counter), "./userdata/rrd4j/" + item.getName() + ".rrd", "state", RRD4jService.getConsolidationFunction(item)); + graphDef.line(Integer.toString(counter), color, label, 2); + } else { + // we draw a line and fill the area beneath it with a transparent color + graphDef.datasource(Integer.toString(counter), "./userdata/rrd4j/" + item.getName() + ".rrd", "state", RRD4jService.getConsolidationFunction(item)); + Color areaColor = AREACOLORS[counter%LINECOLORS.length]; + + graphDef.area(Integer.toString(counter), areaColor); + graphDef.line(Integer.toString(counter), color, label, 2); + } + } + + /** + * Creates a {@link SecureHttpContext} which handles the security for this + * servlet + * + * @return a {@link SecureHttpContext} + */ + protected HttpContext createHttpContext() { + HttpContext defaultHttpContext = httpService.createDefaultHttpContext(); + return defaultHttpContext; + } + + /** + * {@inheritDoc} + */ + public void init(ServletConfig config) throws ServletException { + } + + /** + * {@inheritDoc} + */ + public ServletConfig getServletConfig() { + return null; + } + + /** + * {@inheritDoc} + */ + public String getServletInfo() { + return null; + } + + /** + * {@inheritDoc} + */ + public void destroy() { + } + + // ---------------------------------------------------------- + // The following methods implement the ChartServlet interface + + public String getName() { + return "rrd4j"; + } + + @Override + public BufferedImage createChart(String service, String theme, Date startTime, Date endTime, int height, int width, + String items, String groups) throws ItemNotFoundException { + RrdGraphDef graphDef = new RrdGraphDef(); + + long period = (startTime.getTime() - endTime.getTime()) / 1000; + + graphDef.setWidth(width); + graphDef.setHeight(height); + graphDef.setAntiAliasing(true); + graphDef.setImageFormat("PNG"); + graphDef.setStartTime(period); + graphDef.setTextAntiAliasing(true); + graphDef.setLargeFont(new Font("SansSerif", Font.PLAIN, 15)); + graphDef.setSmallFont(new Font("SansSerif", Font.PLAIN, 11)); + + int seriesCounter = 0; + + // Loop through all the items + if (items != null) { + String[] itemNames = items.split(","); + for (String itemName : itemNames) { + Item item = itemUIRegistry.getItem(itemName); + addLine(graphDef, item, seriesCounter++); + } + } + + // Loop through all the groups and add each item from each group + if (groups != null) { + String[] groupNames = groups.split(","); + for (String groupName : groupNames) { + Item item = itemUIRegistry.getItem(groupName); + if (item instanceof GroupItem) { + GroupItem groupItem = (GroupItem) item; + for (Item member : groupItem.getMembers()) { + addLine(graphDef, member, seriesCounter++); + } + } else { + throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group."); + } + } + } + + // Write the chart as a PNG image + RrdGraph graph; + try { + graph = new RrdGraph(graphDef); + BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(), graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB); + graph.render(bi.getGraphics()); + + return bi; + } catch (IOException e) { + logger.error("Error generating graph: {}", e); + } + + return null; + } + + @Override + public ImageType getChartType() { + return ImageType.png; + } +}