2 * Copyright (c) 2010-2023 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;
20 import java.io.IOException;
21 import java.io.UncheckedIOException;
22 import java.time.Duration;
23 import java.time.ZonedDateTime;
24 import java.util.Hashtable;
27 import javax.imageio.ImageIO;
28 import javax.servlet.Servlet;
29 import javax.servlet.ServletConfig;
30 import javax.servlet.ServletException;
31 import javax.servlet.ServletRequest;
32 import javax.servlet.ServletResponse;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.core.i18n.TimeZoneProvider;
37 import org.openhab.core.items.GroupItem;
38 import org.openhab.core.items.Item;
39 import org.openhab.core.items.ItemNotFoundException;
40 import org.openhab.core.library.items.NumberItem;
41 import org.openhab.core.ui.chart.ChartProvider;
42 import org.openhab.core.ui.items.ItemUIRegistry;
43 import org.openhab.persistence.rrd4j.internal.RRD4jPersistenceService;
44 import org.osgi.service.component.annotations.Activate;
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Deactivate;
47 import org.osgi.service.component.annotations.Reference;
48 import org.osgi.service.http.HttpService;
49 import org.osgi.service.http.NamespaceException;
50 import org.rrd4j.ConsolFun;
51 import org.rrd4j.core.RrdDb;
52 import org.rrd4j.core.RrdDb.Builder;
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 | ServletException e) {
124 logger.error("Error during servlet startup", e);
129 protected void deactivate() {
130 httpService.unregister(SERVLET_NAME);
134 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
135 logger.debug("RRD4J received incoming chart request: {}", req);
137 int width = parseInt(req.getParameter("w"), DEFAULT_WIDTH);
138 int height = parseInt(req.getParameter("h"), DEFAULT_HEIGHT);
139 String periodParam = req.getParameter("period");
140 Duration period = periodParam == null ? DEFAULT_PERIOD : PERIODS.getOrDefault(periodParam, DEFAULT_PERIOD);
142 // Create the start and stop time
143 ZonedDateTime timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone());
144 ZonedDateTime timeBegin = timeEnd.minus(period);
147 BufferedImage chart = createChart(null, null, timeBegin, timeEnd, height, width, req.getParameter("items"),
148 req.getParameter("groups"), null, null);
149 // Set the content type to that provided by the chart provider
150 res.setContentType("image/" + getChartType());
151 ImageIO.write(chart, getChartType().toString(), res.getOutputStream());
152 } catch (ItemNotFoundException e) {
153 logger.debug("Item not found error while generating chart", e);
154 throw new ServletException("Item not found error while generating chart: " + e.getMessage());
155 } catch (IllegalArgumentException e) {
156 logger.debug("Illegal argument in chart", e);
157 throw new ServletException("Illegal argument in chart: " + e.getMessage());
161 private int parseInt(@Nullable String s, int defaultValue) {
166 return Integer.parseInt(s);
167 } catch (NumberFormatException e) {
168 logger.debug("'{}' is not an integer, using default: {}", s, defaultValue);
174 * Adds a line for the item to the graph definition.
175 * The color of the line is determined by the counter, it simply picks the according index from LINECOLORS (and
176 * rolls over if necessary).
178 * @param graphDef the graph definition to fill
179 * @param item the item to add a line for
180 * @param counter defines the number of the datasource and is used to determine the line color
182 protected void addLine(RrdGraphDef graphDef, Item item, int counter) {
183 Color color = LINECOLORS[counter % LINECOLORS.length];
184 String label = itemUIRegistry.getLabel(item.getName());
185 String rrdName = RRD4jPersistenceService.getDatabasePath(item.getName()).toString();
187 if (label != null && label.contains("[") && label.contains("]")) {
188 label = label.substring(0, label.indexOf('['));
191 Builder builder = RrdDb.getBuilder();
192 builder.setPool(RRD4jPersistenceService.getDatabasePool());
193 builder.setPath(rrdName);
195 RrdDb db = builder.build();
196 consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
198 } catch (IOException e) {
199 consolFun = ConsolFun.MAX;
201 if (item instanceof NumberItem) {
202 // we only draw a line
203 graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
204 graphDef.line(Integer.toString(counter), color, label, 2);
206 // we draw a line and fill the area beneath it with a transparent color
207 graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
208 Color areaColor = AREACOLORS[counter % LINECOLORS.length];
210 graphDef.area(Integer.toString(counter), areaColor);
211 graphDef.line(Integer.toString(counter), color, label, 2);
216 public void init(@Nullable ServletConfig config) throws ServletException {
220 public @Nullable ServletConfig getServletConfig() {
225 public @Nullable String getServletInfo() {
230 public void destroy() {
233 // ----------------------------------------------------------
234 // The following methods implement the ChartServlet interface
237 public String getName() {
242 public BufferedImage createChart(@Nullable String service, @Nullable String theme, ZonedDateTime startTime,
243 ZonedDateTime endTime, int height, int width, @Nullable String items, @Nullable String groups,
244 @Nullable Integer dpi, @Nullable Boolean legend) throws ItemNotFoundException {
245 RrdGraphDef graphDef = new RrdGraphDef(startTime.toEpochSecond(), endTime.toEpochSecond());
246 graphDef.setWidth(width);
247 graphDef.setHeight(height);
248 graphDef.setAntiAliasing(true);
249 graphDef.setImageFormat("PNG");
250 graphDef.setTextAntiAliasing(true);
251 graphDef.setFont(FontTag.TITLE, new Font("SansSerif", Font.PLAIN, 15));
252 graphDef.setFont(FontTag.DEFAULT, new Font("SansSerif", Font.PLAIN, 11));
254 int seriesCounter = 0;
256 // Loop through all the items
258 String[] itemNames = items.split(",");
259 for (String itemName : itemNames) {
260 Item item = itemUIRegistry.getItem(itemName);
261 addLine(graphDef, item, seriesCounter++);
265 // Loop through all the groups and add each item from each group
266 if (groups != null) {
267 String[] groupNames = groups.split(",");
268 for (String groupName : groupNames) {
269 Item item = itemUIRegistry.getItem(groupName);
270 if (item instanceof GroupItem) {
271 GroupItem groupItem = (GroupItem) item;
272 for (Item member : groupItem.getMembers()) {
273 addLine(graphDef, member, seriesCounter++);
276 throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
281 // Write the chart as a PNG image
283 RrdGraph graph = new RrdGraph(graphDef);
284 BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(),
285 graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB);
286 graph.render(bi.getGraphics());
288 } catch (IOException e) {
289 throw new UncheckedIOException("Error generating RrdGraph", e);
294 public ImageType getChartType() {
295 return ImageType.png;