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;
+ }
+}