2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.rrd4j.internal.charts;
15 import static java.util.Map.entry;
17 import java.awt.Color;
19 import java.awt.image.BufferedImage;
21 import java.io.IOException;
22 import java.io.UncheckedIOException;
23 import java.time.Duration;
24 import java.time.ZonedDateTime;
25 import java.util.Hashtable;
28 import javax.imageio.ImageIO;
29 import javax.servlet.Servlet;
30 import javax.servlet.ServletConfig;
31 import javax.servlet.ServletException;
32 import javax.servlet.ServletRequest;
33 import javax.servlet.ServletResponse;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.core.i18n.TimeZoneProvider;
38 import org.openhab.core.items.GroupItem;
39 import org.openhab.core.items.Item;
40 import org.openhab.core.items.ItemNotFoundException;
41 import org.openhab.core.library.items.NumberItem;
42 import org.openhab.core.ui.chart.ChartProvider;
43 import org.openhab.core.ui.items.ItemUIRegistry;
44 import org.openhab.persistence.rrd4j.internal.RRD4jPersistenceService;
45 import org.osgi.service.component.annotations.Activate;
46 import org.osgi.service.component.annotations.Component;
47 import org.osgi.service.component.annotations.Deactivate;
48 import org.osgi.service.component.annotations.Reference;
49 import org.osgi.service.http.HttpService;
50 import org.osgi.service.http.NamespaceException;
51 import org.rrd4j.ConsolFun;
52 import org.rrd4j.core.RrdDb;
53 import org.rrd4j.graph.RrdGraph;
54 import org.rrd4j.graph.RrdGraphConstants.FontTag;
55 import org.rrd4j.graph.RrdGraphDef;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * This servlet generates time-series charts for a given set of items.
61 * It accepts the following HTTP parameters:
63 * <li>w: width in pixels of image to generate</li>
64 * <li>h: height in pixels of image to generate</li>
65 * <li>period: the time span for the x-axis. Value can be h,4h,8h,12h,D,3D,W,2W,M,2M,4M,Y</li>
66 * <li>items: A comma separated list of item names to display
67 * <li>groups: A comma separated list of group names, whose members should be displayed
70 * @author Kai Kreuzer - Initial contribution
71 * @author Chris Jackson - a few improvements
72 * @author Jan N. Klug - a few improvements
76 @Component(service = ChartProvider.class)
77 public class RRD4jChartServlet implements Servlet, ChartProvider {
79 private final Logger logger = LoggerFactory.getLogger(RRD4jChartServlet.class);
81 private static final int DEFAULT_HEIGHT = 240;
82 private static final int DEFAULT_WIDTH = 480;
84 /** the URI of this servlet */
85 public static final String SERVLET_NAME = "/rrdchart.png";
87 protected static final Color[] LINECOLORS = new Color[] { Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA,
88 Color.ORANGE, Color.CYAN, Color.PINK, Color.DARK_GRAY, Color.YELLOW };
89 protected static final Color[] AREACOLORS = new Color[] { new Color(255, 0, 0, 30), new Color(0, 255, 0, 30),
90 new Color(0, 0, 255, 30), new Color(255, 0, 255, 30), new Color(255, 128, 0, 30),
91 new Color(0, 255, 255, 30), new Color(255, 0, 128, 30), new Color(255, 128, 128, 30),
92 new Color(255, 255, 0, 30) };
94 private static final Duration DEFAULT_PERIOD = Duration.ofDays(1);
96 private static final Map<String, Duration> PERIODS = Map.ofEntries( //
97 entry("h", Duration.ofHours(1)), entry("4h", Duration.ofHours(4)), //
98 entry("8h", Duration.ofHours(8)), entry("12h", Duration.ofHours(12)), //
99 entry("D", Duration.ofDays(1)), entry("2D", Duration.ofDays(2)), //
100 entry("3D", Duration.ofDays(3)), entry("W", Duration.ofDays(7)), //
101 entry("2W", Duration.ofDays(14)), entry("M", Duration.ofDays(30)), //
102 entry("2M", Duration.ofDays(60)), entry("4M", Duration.ofDays(120)), //
103 entry("Y", Duration.ofDays(365))//
106 private final HttpService httpService;
107 private final ItemUIRegistry itemUIRegistry;
108 private final TimeZoneProvider timeZoneProvider;
111 public RRD4jChartServlet(final @Reference HttpService httpService, final @Reference ItemUIRegistry itemUIRegistry,
112 final @Reference TimeZoneProvider timeZoneProvider) {
113 this.httpService = httpService;
114 this.itemUIRegistry = itemUIRegistry;
115 this.timeZoneProvider = timeZoneProvider;
119 protected void activate() {
121 logger.debug("Starting up rrd chart servlet at {}", SERVLET_NAME);
122 httpService.registerServlet(SERVLET_NAME, this, new Hashtable<>(), httpService.createDefaultHttpContext());
123 } catch (NamespaceException e) {
124 logger.error("Error during servlet startup", e);
125 } catch (ServletException e) {
126 logger.error("Error during servlet startup", e);
131 protected void deactivate() {
132 httpService.unregister(SERVLET_NAME);
136 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
137 logger.debug("RRD4J received incoming chart request: {}", req);
139 int width = parseInt(req.getParameter("w"), DEFAULT_WIDTH);
140 int height = parseInt(req.getParameter("h"), DEFAULT_HEIGHT);
141 String periodParam = req.getParameter("period");
142 Duration period = periodParam == null ? DEFAULT_PERIOD : PERIODS.getOrDefault(periodParam, DEFAULT_PERIOD);
144 // Create the start and stop time
145 ZonedDateTime timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone());
146 ZonedDateTime timeBegin = timeEnd.minus(period);
149 BufferedImage chart = createChart(null, null, timeBegin, timeEnd, height, width, req.getParameter("items"),
150 req.getParameter("groups"), null, null);
151 // Set the content type to that provided by the chart provider
152 res.setContentType("image/" + getChartType());
153 ImageIO.write(chart, getChartType().toString(), res.getOutputStream());
154 } catch (ItemNotFoundException e) {
155 logger.debug("Item not found error while generating chart", e);
156 throw new ServletException("Item not found error while generating chart: " + e.getMessage());
157 } catch (IllegalArgumentException e) {
158 logger.debug("Illegal argument in chart", e);
159 throw new ServletException("Illegal argument in chart: " + e.getMessage());
163 private int parseInt(@Nullable String s, int defaultValue) {
168 return Integer.parseInt(s);
169 } catch (NumberFormatException e) {
170 logger.debug("'{}' is not an integer, using default: {}", s, defaultValue);
176 * Adds a line for the item to the graph definition.
177 * The color of the line is determined by the counter, it simply picks the according index from LINECOLORS (and
178 * rolls over if necessary).
180 * @param graphDef the graph definition to fill
181 * @param item the item to add a line for
182 * @param counter defines the number of the datasource and is used to determine the line color
184 protected void addLine(RrdGraphDef graphDef, Item item, int counter) {
185 Color color = LINECOLORS[counter % LINECOLORS.length];
186 String label = itemUIRegistry.getLabel(item.getName());
187 String rrdName = RRD4jPersistenceService.DB_FOLDER + File.separator + item.getName() + ".rrd";
189 if (label != null && label.contains("[") && label.contains("]")) {
190 label = label.substring(0, label.indexOf('['));
193 RrdDb db = RrdDb.of(rrdName);
194 consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
196 } catch (IOException e) {
197 consolFun = ConsolFun.MAX;
199 if (item instanceof NumberItem) {
200 // we only draw a line
201 graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
202 graphDef.line(Integer.toString(counter), color, label, 2);
204 // we draw a line and fill the area beneath it with a transparent color
205 graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
206 Color areaColor = AREACOLORS[counter % LINECOLORS.length];
208 graphDef.area(Integer.toString(counter), areaColor);
209 graphDef.line(Integer.toString(counter), color, label, 2);
214 public void init(@Nullable ServletConfig config) throws ServletException {
218 public @Nullable ServletConfig getServletConfig() {
223 public @Nullable String getServletInfo() {
228 public void destroy() {
231 // ----------------------------------------------------------
232 // The following methods implement the ChartServlet interface
235 public String getName() {
240 public BufferedImage createChart(@Nullable String service, @Nullable String theme, ZonedDateTime startTime,
241 ZonedDateTime endTime, int height, int width, @Nullable String items, @Nullable String groups,
242 @Nullable Integer dpi, @Nullable Boolean legend) throws ItemNotFoundException {
243 RrdGraphDef graphDef = new RrdGraphDef(startTime.toEpochSecond(), endTime.toEpochSecond());
244 graphDef.setWidth(width);
245 graphDef.setHeight(height);
246 graphDef.setAntiAliasing(true);
247 graphDef.setImageFormat("PNG");
248 graphDef.setTextAntiAliasing(true);
249 graphDef.setFont(FontTag.TITLE, new Font("SansSerif", Font.PLAIN, 15));
250 graphDef.setFont(FontTag.DEFAULT, new Font("SansSerif", Font.PLAIN, 11));
252 int seriesCounter = 0;
254 // Loop through all the items
256 String[] itemNames = items.split(",");
257 for (String itemName : itemNames) {
258 Item item = itemUIRegistry.getItem(itemName);
259 addLine(graphDef, item, seriesCounter++);
263 // Loop through all the groups and add each item from each group
264 if (groups != null) {
265 String[] groupNames = groups.split(",");
266 for (String groupName : groupNames) {
267 Item item = itemUIRegistry.getItem(groupName);
268 if (item instanceof GroupItem) {
269 GroupItem groupItem = (GroupItem) item;
270 for (Item member : groupItem.getMembers()) {
271 addLine(graphDef, member, seriesCounter++);
274 throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
279 // Write the chart as a PNG image
281 RrdGraph graph = new RrdGraph(graphDef);
282 BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(),
283 graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB);
284 graph.render(bi.getGraphics());
286 } catch (IOException e) {
287 throw new UncheckedIOException("Error generating RrdGraph", e);
292 public ImageType getChartType() {
293 return ImageType.png;