]> git.basschouten.com Git - openhab-addons.git/blob
09f4efcef5ccae6037951ed43330a90dd8d7cbd3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.rrd4j.internal.charts;
14
15 import static java.util.Map.entry;
16
17 import java.awt.Color;
18 import java.awt.Font;
19 import java.awt.image.BufferedImage;
20 import java.io.File;
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;
26 import java.util.Map;
27
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;
34
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;
58
59 /**
60  * This servlet generates time-series charts for a given set of items.
61  * It accepts the following HTTP parameters:
62  * <ul>
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
68  * </ul>
69  *
70  * @author Kai Kreuzer - Initial contribution
71  * @author Chris Jackson - a few improvements
72  * @author Jan N. Klug - a few improvements
73  *
74  */
75 @NonNullByDefault
76 @Component(service = ChartProvider.class)
77 public class RRD4jChartServlet implements Servlet, ChartProvider {
78
79     private final Logger logger = LoggerFactory.getLogger(RRD4jChartServlet.class);
80
81     private static final int DEFAULT_HEIGHT = 240;
82     private static final int DEFAULT_WIDTH = 480;
83
84     /** the URI of this servlet */
85     public static final String SERVLET_NAME = "/rrdchart.png";
86
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) };
93
94     private static final Duration DEFAULT_PERIOD = Duration.ofDays(1);
95
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))//
104     );
105
106     private final HttpService httpService;
107     private final ItemUIRegistry itemUIRegistry;
108     private final TimeZoneProvider timeZoneProvider;
109
110     @Activate
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;
116     }
117
118     @Activate
119     protected void activate() {
120         try {
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);
127         }
128     }
129
130     @Deactivate
131     protected void deactivate() {
132         httpService.unregister(SERVLET_NAME);
133     }
134
135     @Override
136     public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
137         logger.debug("RRD4J received incoming chart request: {}", req);
138
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);
143
144         // Create the start and stop time
145         ZonedDateTime timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone());
146         ZonedDateTime timeBegin = timeEnd.minus(period);
147
148         try {
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());
160         }
161     }
162
163     private int parseInt(@Nullable String s, int defaultValue) {
164         if (s == null) {
165             return defaultValue;
166         }
167         try {
168             return Integer.parseInt(s);
169         } catch (NumberFormatException e) {
170             logger.debug("'{}' is not an integer, using default: {}", s, defaultValue);
171             return defaultValue;
172         }
173     }
174
175     /**
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).
179      *
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
183      */
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";
188         ConsolFun consolFun;
189         if (label != null && label.contains("[") && label.contains("]")) {
190             label = label.substring(0, label.indexOf('['));
191         }
192         try {
193             RrdDb db = RrdDb.of(rrdName);
194             consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
195             db.close();
196         } catch (IOException e) {
197             consolFun = ConsolFun.MAX;
198         }
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);
203         } else {
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];
207
208             graphDef.area(Integer.toString(counter), areaColor);
209             graphDef.line(Integer.toString(counter), color, label, 2);
210         }
211     }
212
213     @Override
214     public void init(@Nullable ServletConfig config) throws ServletException {
215     }
216
217     @Override
218     public @Nullable ServletConfig getServletConfig() {
219         return null;
220     }
221
222     @Override
223     public @Nullable String getServletInfo() {
224         return null;
225     }
226
227     @Override
228     public void destroy() {
229     }
230
231     // ----------------------------------------------------------
232     // The following methods implement the ChartServlet interface
233
234     @Override
235     public String getName() {
236         return "rrd4j";
237     }
238
239     @Override
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));
251
252         int seriesCounter = 0;
253
254         // Loop through all the items
255         if (items != null) {
256             String[] itemNames = items.split(",");
257             for (String itemName : itemNames) {
258                 Item item = itemUIRegistry.getItem(itemName);
259                 addLine(graphDef, item, seriesCounter++);
260             }
261         }
262
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++);
272                     }
273                 } else {
274                     throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
275                 }
276             }
277         }
278
279         // Write the chart as a PNG image
280         try {
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());
285             return bi;
286         } catch (IOException e) {
287             throw new UncheckedIOException("Error generating RrdGraph", e);
288         }
289     }
290
291     @Override
292     public ImageType getChartType() {
293         return ImageType.png;
294     }
295 }