]> git.basschouten.com Git - openhab-addons.git/blob
2118cff5ef6cd457cc06352a40262329b1b8e4a3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.IOException;
21 import java.io.UncheckedIOException;
22 import java.time.Duration;
23 import java.time.ZonedDateTime;
24 import java.util.Hashtable;
25 import java.util.Map;
26
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;
33
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;
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 | ServletException e) {
124             logger.error("Error during servlet startup", e);
125         }
126     }
127
128     @Deactivate
129     protected void deactivate() {
130         httpService.unregister(SERVLET_NAME);
131     }
132
133     @Override
134     public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
135         logger.debug("RRD4J received incoming chart request: {}", req);
136
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);
141
142         // Create the start and stop time
143         ZonedDateTime timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone());
144         ZonedDateTime timeBegin = timeEnd.minus(period);
145
146         try {
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());
158         }
159     }
160
161     private int parseInt(@Nullable String s, int defaultValue) {
162         if (s == null) {
163             return defaultValue;
164         }
165         try {
166             return Integer.parseInt(s);
167         } catch (NumberFormatException e) {
168             logger.debug("'{}' is not an integer, using default: {}", s, defaultValue);
169             return defaultValue;
170         }
171     }
172
173     /**
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).
177      *
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
181      */
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();
186         ConsolFun consolFun;
187         if (label != null && label.contains("[") && label.contains("]")) {
188             label = label.substring(0, label.indexOf('['));
189         }
190         try {
191             Builder builder = RrdDb.getBuilder();
192             builder.setPool(RRD4jPersistenceService.getDatabasePool());
193             builder.setPath(rrdName);
194
195             RrdDb db = builder.build();
196             consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
197             db.close();
198         } catch (IOException e) {
199             consolFun = ConsolFun.MAX;
200         }
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);
205         } else {
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];
209
210             graphDef.area(Integer.toString(counter), areaColor);
211             graphDef.line(Integer.toString(counter), color, label, 2);
212         }
213     }
214
215     @Override
216     public void init(@Nullable ServletConfig config) throws ServletException {
217     }
218
219     @Override
220     public @Nullable ServletConfig getServletConfig() {
221         return null;
222     }
223
224     @Override
225     public @Nullable String getServletInfo() {
226         return null;
227     }
228
229     @Override
230     public void destroy() {
231     }
232
233     // ----------------------------------------------------------
234     // The following methods implement the ChartServlet interface
235
236     @Override
237     public String getName() {
238         return "rrd4j";
239     }
240
241     @Override
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));
253
254         int seriesCounter = 0;
255
256         // Loop through all the items
257         if (items != null) {
258             String[] itemNames = items.split(",");
259             for (String itemName : itemNames) {
260                 Item item = itemUIRegistry.getItem(itemName);
261                 addLine(graphDef, item, seriesCounter++);
262             }
263         }
264
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 groupItem) {
271                     for (Item member : groupItem.getMembers()) {
272                         addLine(graphDef, member, seriesCounter++);
273                     }
274                 } else {
275                     throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
276                 }
277             }
278         }
279
280         // Write the chart as a PNG image
281         try {
282             RrdGraph graph = new RrdGraph(graphDef);
283             BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(),
284                     graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB);
285             graph.render(bi.getGraphics());
286             return bi;
287         } catch (IOException e) {
288             throw new UncheckedIOException("Error generating RrdGraph", e);
289         }
290     }
291
292     @Override
293     public ImageType getChartType() {
294         return ImageType.png;
295     }
296 }