]> git.basschouten.com Git - openhab-addons.git/blob
0541ead4090c82753b9caff0f66ba05e5ff0f712
[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;
14
15 import java.io.IOException;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18 import java.time.Instant;
19 import java.time.ZoneId;
20 import java.time.ZonedDateTime;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.RejectedExecutionException;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35
36 import javax.measure.Quantity;
37 import javax.measure.Unit;
38
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.core.OpenHAB;
42 import org.openhab.core.common.NamedThreadFactory;
43 import org.openhab.core.items.GroupItem;
44 import org.openhab.core.items.Item;
45 import org.openhab.core.items.ItemNotFoundException;
46 import org.openhab.core.items.ItemRegistry;
47 import org.openhab.core.items.ItemUtil;
48 import org.openhab.core.library.CoreItemFactory;
49 import org.openhab.core.library.items.ColorItem;
50 import org.openhab.core.library.items.ContactItem;
51 import org.openhab.core.library.items.DimmerItem;
52 import org.openhab.core.library.items.NumberItem;
53 import org.openhab.core.library.items.RollershutterItem;
54 import org.openhab.core.library.items.SwitchItem;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.OnOffType;
57 import org.openhab.core.library.types.OpenClosedType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.persistence.FilterCriteria;
61 import org.openhab.core.persistence.FilterCriteria.Ordering;
62 import org.openhab.core.persistence.HistoricItem;
63 import org.openhab.core.persistence.PersistenceItemInfo;
64 import org.openhab.core.persistence.PersistenceService;
65 import org.openhab.core.persistence.QueryablePersistenceService;
66 import org.openhab.core.persistence.strategy.PersistenceCronStrategy;
67 import org.openhab.core.persistence.strategy.PersistenceStrategy;
68 import org.openhab.core.types.State;
69 import org.osgi.service.component.annotations.Activate;
70 import org.osgi.service.component.annotations.Component;
71 import org.osgi.service.component.annotations.ConfigurationPolicy;
72 import org.osgi.service.component.annotations.Modified;
73 import org.osgi.service.component.annotations.Reference;
74 import org.rrd4j.ConsolFun;
75 import org.rrd4j.DsType;
76 import org.rrd4j.core.FetchData;
77 import org.rrd4j.core.FetchRequest;
78 import org.rrd4j.core.RrdDb;
79 import org.rrd4j.core.RrdDb.Builder;
80 import org.rrd4j.core.RrdDbPool;
81 import org.rrd4j.core.RrdDef;
82 import org.rrd4j.core.Sample;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 /**
87  * This is the implementation of the RRD4j {@link PersistenceService}. To learn
88  * more about RRD4j please visit their
89  * <a href="https://github.com/rrd4j/rrd4j">website</a>.
90  *
91  * @author Kai Kreuzer - Initial contribution
92  * @author Jan N. Klug - some improvements
93  * @author Karel Goderis - remove TimerThread dependency
94  */
95 @NonNullByDefault
96 @Component(service = { PersistenceService.class,
97         QueryablePersistenceService.class }, configurationPid = "org.openhab.rrd4j", configurationPolicy = ConfigurationPolicy.OPTIONAL)
98 public class RRD4jPersistenceService implements QueryablePersistenceService {
99
100     private static final String DEFAULT_OTHER = "default_other";
101     private static final String DEFAULT_NUMERIC = "default_numeric";
102     private static final String DEFAULT_QUANTIFIABLE = "default_quantifiable";
103
104     private static final Set<String> SUPPORTED_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT,
105             CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.COLOR);
106
107     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3,
108             new NamedThreadFactory("RRD4j"));
109
110     private final Map<String, RrdDefConfig> rrdDefs = new ConcurrentHashMap<>();
111
112     private static final String DATASOURCE_STATE = "state";
113
114     private static final Path DB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "persistence", "rrd4j").toAbsolutePath();
115
116     private static final RrdDbPool DATABASE_POOL = new RrdDbPool();
117
118     private final Logger logger = LoggerFactory.getLogger(RRD4jPersistenceService.class);
119
120     private final Map<String, ScheduledFuture<?>> scheduledJobs = new HashMap<>();
121
122     private final ItemRegistry itemRegistry;
123
124     public static Path getDatabasePath(String name) {
125         return DB_FOLDER.resolve(name + ".rrd");
126     }
127
128     public static RrdDbPool getDatabasePool() {
129         return DATABASE_POOL;
130     }
131
132     @Activate
133     public RRD4jPersistenceService(final @Reference ItemRegistry itemRegistry) {
134         this.itemRegistry = itemRegistry;
135     }
136
137     @Override
138     public String getId() {
139         return "rrd4j";
140     }
141
142     @Override
143     public String getLabel(@Nullable Locale locale) {
144         return "RRD4j";
145     }
146
147     @Override
148     public void store(final Item item, @Nullable final String alias) {
149         if (!isSupportedItemType(item)) {
150             logger.trace("Ignoring item '{}' since its type {} is not supported", item.getName(), item.getType());
151             return;
152         }
153         final String name = alias == null ? item.getName() : alias;
154
155         Double value;
156
157         if (item instanceof NumberItem && item.getState() instanceof QuantityType) {
158             NumberItem nItem = (NumberItem) item;
159             QuantityType<?> qState = (QuantityType<?>) item.getState();
160             Unit<? extends Quantity<?>> unit = nItem.getUnit();
161             if (unit != null) {
162                 QuantityType<?> convertedState = qState.toUnit(unit);
163                 if (convertedState != null) {
164                     value = convertedState.doubleValue();
165                 } else {
166                     value = null;
167                     logger.warn(
168                             "Failed to convert state '{}' to unit '{}'. Please check your item definition for correctness.",
169                             qState, unit);
170                 }
171             } else {
172                 value = qState.doubleValue();
173             }
174         } else {
175             DecimalType state = item.getStateAs(DecimalType.class);
176             if (state != null) {
177                 value = state.toBigDecimal().doubleValue();
178             } else {
179                 value = null;
180             }
181         }
182
183         if (value == null) {
184             // we could not convert the value
185             return;
186         }
187
188         long now = System.currentTimeMillis() / 1000;
189
190         scheduler.schedule(() -> internalStore(name, value, now, true), 0, TimeUnit.SECONDS);
191     }
192
193     private synchronized void internalStore(String name, double value, long now, boolean retry) {
194         RrdDb db = null;
195         try {
196             db = getDB(name);
197         } catch (Exception e) {
198             logger.warn("Failed to open rrd4j database '{}' to store data ({})", name, e.toString());
199         }
200         if (db == null) {
201             return;
202         }
203
204         ConsolFun function = getConsolidationFunction(db);
205         if (function != ConsolFun.AVERAGE) {
206             try {
207                 // we store the last value again, so that the value change
208                 // in the database is not interpolated, but
209                 // happens right at this spot
210                 if (now - 1 > db.getLastUpdateTime()) {
211                     // only do it if there is not already a value
212                     double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
213                     if (!Double.isNaN(lastValue)) {
214                         Sample sample = db.createSample();
215                         sample.setTime(now - 1);
216                         sample.setValue(DATASOURCE_STATE, lastValue);
217                         sample.update();
218                         logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database (again)", name,
219                                 lastValue, now - 1);
220                     }
221                 }
222             } catch (IOException e) {
223                 logger.debug("Error storing last value (again): {}", e.getMessage());
224             }
225         }
226         try {
227             Sample sample = db.createSample();
228             sample.setTime(now);
229             double storeValue = value;
230             if (db.getDatasource(DATASOURCE_STATE).getType() == DsType.COUNTER) { // counter values must be
231                                                                                   // adjusted by stepsize
232                 storeValue = value * db.getRrdDef().getStep();
233             }
234             sample.setValue(DATASOURCE_STATE, storeValue);
235             sample.update();
236             logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database", name, storeValue, now);
237         } catch (IllegalArgumentException e) {
238             String message = e.getMessage();
239             if (message != null && message.contains("at least one second step is required") && retry) {
240                 // we try to store the value one second later
241                 ScheduledFuture<?> job = scheduledJobs.get(name);
242                 if (job != null) {
243                     job.cancel(true);
244                     scheduledJobs.remove(name);
245                 }
246                 job = scheduler.schedule(() -> internalStore(name, value, now + 1, false), 1, TimeUnit.SECONDS);
247                 scheduledJobs.put(name, job);
248             } else {
249                 logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
250             }
251         } catch (Exception e) {
252             logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
253         }
254         try {
255             db.close();
256         } catch (IOException e) {
257             logger.debug("Error closing rrd4j database: {}", e.getMessage());
258         }
259     }
260
261     @Override
262     public void store(Item item) {
263         store(item, null);
264     }
265
266     @Override
267     public Iterable<HistoricItem> query(FilterCriteria filter) {
268         ZonedDateTime filterBeginDate = filter.getBeginDate();
269         ZonedDateTime filterEndDate = filter.getEndDate();
270         if (filterBeginDate != null && filterEndDate != null && filterBeginDate.isAfter(filterEndDate)) {
271             throw new IllegalArgumentException("begin (" + filterBeginDate + ") before end (" + filterEndDate + ")");
272         }
273
274         String itemName = filter.getItemName();
275         if (itemName == null) {
276             logger.warn("Item name is missing in filter {}", filter);
277             return List.of();
278         }
279
280         RrdDb db = null;
281         try {
282             db = getDB(itemName);
283         } catch (Exception e) {
284             logger.warn("Failed to open rrd4j database '{}' for querying ({})", itemName, e.toString());
285             return List.of();
286         }
287         if (db == null) {
288             logger.debug("Could not find item '{}' in rrd4j database", itemName);
289             return List.of();
290         }
291
292         Item item = null;
293         Unit<?> unit = null;
294         try {
295             item = itemRegistry.getItem(itemName);
296             if (item instanceof NumberItem) {
297                 // we already retrieve the unit here once as it is a very costly operation,
298                 // see https://github.com/openhab/openhab-addons/issues/8928
299                 unit = ((NumberItem) item).getUnit();
300             }
301         } catch (ItemNotFoundException e) {
302             logger.debug("Could not find item '{}' in registry", itemName);
303         }
304
305         long start = 0L;
306         long end = filterEndDate == null ? System.currentTimeMillis() / 1000
307                 : filterEndDate.toInstant().getEpochSecond();
308
309         try {
310             if (filterBeginDate == null) {
311                 // as rrd goes back for years and gets more and more
312                 // inaccurate, we only support descending order
313                 // and a single return value
314                 // if there is no begin date is given - this case is
315                 // required specifically for the historicState()
316                 // query, which we want to support
317                 if (filter.getOrdering() == Ordering.DESCENDING && filter.getPageSize() == 1
318                         && filter.getPageNumber() == 0) {
319                     if (filterEndDate == null) {
320                         // we are asked only for the most recent value!
321                         double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
322                         if (!Double.isNaN(lastValue)) {
323                             HistoricItem rrd4jItem = new RRD4jItem(itemName, mapToState(lastValue, item, unit),
324                                     ZonedDateTime.ofInstant(Instant.ofEpochMilli(db.getLastArchiveUpdateTime() * 1000),
325                                             ZoneId.systemDefault()));
326                             return List.of(rrd4jItem);
327                         } else {
328                             return List.of();
329                         }
330                     } else {
331                         start = end;
332                     }
333                 } else {
334                     throw new UnsupportedOperationException(
335                             "rrd4j does not allow querys without a begin date, unless order is descending and a single value is requested");
336                 }
337             } else {
338                 start = filterBeginDate.toInstant().getEpochSecond();
339             }
340
341             // do not call method {@link RrdDb#createFetchRequest(ConsolFun, long, long, long)} if start > end to avoid
342             // an IAE to be thrown
343             if (start > end) {
344                 logger.debug("Could not query rrd4j database for item '{}': start ({}) > end ({})", itemName, start,
345                         end);
346                 return List.of();
347             }
348
349             FetchRequest request = db.createFetchRequest(getConsolidationFunction(db), start, end, 1);
350             FetchData result = request.fetchData();
351
352             List<HistoricItem> items = new ArrayList<>();
353             long ts = result.getFirstTimestamp();
354             long step = result.getRowCount() > 1 ? result.getStep() : 0;
355             for (double value : result.getValues(DATASOURCE_STATE)) {
356                 if (!Double.isNaN(value) && (((ts >= start) && (ts <= end)) || (start == end))) {
357                     RRD4jItem rrd4jItem = new RRD4jItem(itemName, mapToState(value, item, unit),
358                             ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault()));
359                     items.add(rrd4jItem);
360                 }
361                 ts += step;
362             }
363             return items;
364         } catch (IOException e) {
365             logger.warn("Could not query rrd4j database for item '{}': {}", itemName, e.getMessage());
366             return List.of();
367         } finally {
368             try {
369                 db.close();
370             } catch (IOException e) {
371                 logger.debug("Error closing rrd4j database: {}", e.getMessage());
372             }
373         }
374     }
375
376     @Override
377     public Set<PersistenceItemInfo> getItemInfo() {
378         return Set.of();
379     }
380
381     protected synchronized @Nullable RrdDb getDB(String alias) {
382         RrdDb db = null;
383         Path path = getDatabasePath(alias);
384         try {
385             Builder builder = RrdDb.getBuilder();
386             builder.setPool(DATABASE_POOL);
387
388             if (Files.exists(path)) {
389                 // recreate the RrdDb instance from the file
390                 builder.setPath(path.toString());
391                 db = builder.build();
392             } else {
393                 if (!Files.exists(DB_FOLDER)) {
394                     Files.createDirectories(DB_FOLDER);
395                 }
396                 RrdDef rrdDef = getRrdDef(alias, path);
397                 if (rrdDef != null) {
398                     // create a new database file
399                     builder.setRrdDef(rrdDef);
400                     db = builder.build();
401                 } else {
402                     logger.debug(
403                             "Did not create rrd4j database for item '{}' since no rrd definition could be determined. This is likely due to an unsupported item type.",
404                             alias);
405                 }
406             }
407         } catch (IOException e) {
408             logger.error("Could not create rrd4j database file '{}': {}", path, e.getMessage());
409         } catch (RejectedExecutionException e) {
410             // this happens if the system is shut down
411             logger.debug("Could not create rrd4j database file '{}': {}", path, e.getMessage());
412         }
413         return db;
414     }
415
416     private @Nullable RrdDefConfig getRrdDefConfig(String itemName) {
417         RrdDefConfig useRdc = null;
418         for (Map.Entry<String, RrdDefConfig> e : rrdDefs.entrySet()) {
419             // try to find special config
420             RrdDefConfig rdc = e.getValue();
421             if (rdc.appliesTo(itemName)) {
422                 useRdc = rdc;
423                 break;
424             }
425         }
426         if (useRdc == null) { // not defined, use defaults
427             try {
428                 Item item = itemRegistry.getItem(itemName);
429                 if (!isSupportedItemType(item)) {
430                     return null;
431                 }
432                 if (item instanceof NumberItem) {
433                     NumberItem numberItem = (NumberItem) item;
434                     useRdc = numberItem.getDimension() != null ? rrdDefs.get(DEFAULT_QUANTIFIABLE)
435                             : rrdDefs.get(DEFAULT_NUMERIC);
436                 } else {
437                     useRdc = rrdDefs.get(DEFAULT_OTHER);
438                 }
439             } catch (ItemNotFoundException e) {
440                 logger.debug("Could not find item '{}' in registry", itemName);
441                 return null;
442             }
443         }
444         logger.trace("Using rrd definition '{}' for item '{}'.", useRdc, itemName);
445         return useRdc;
446     }
447
448     private @Nullable RrdDef getRrdDef(String itemName, Path path) {
449         RrdDef rrdDef = new RrdDef(path.toString());
450         RrdDefConfig useRdc = getRrdDefConfig(itemName);
451         if (useRdc != null) {
452             rrdDef.setStep(useRdc.step);
453             rrdDef.setStartTime(System.currentTimeMillis() / 1000 - 1);
454             rrdDef.addDatasource(DATASOURCE_STATE, useRdc.dsType, useRdc.heartbeat, useRdc.min, useRdc.max);
455             for (RrdArchiveDef rad : useRdc.archives) {
456                 rrdDef.addArchive(rad.fcn, rad.xff, rad.steps, rad.rows);
457             }
458             return rrdDef;
459         } else {
460             return null;
461         }
462     }
463
464     public ConsolFun getConsolidationFunction(RrdDb db) {
465         try {
466             return db.getRrdDef().getArcDefs()[0].getConsolFun();
467         } catch (IOException e) {
468             return ConsolFun.MAX;
469         }
470     }
471
472     @SuppressWarnings({ "unchecked", "rawtypes" })
473     private State mapToState(double value, @Nullable Item item, @Nullable Unit unit) {
474         if (item instanceof GroupItem) {
475             item = ((GroupItem) item).getBaseItem();
476         }
477
478         if (item instanceof SwitchItem && !(item instanceof DimmerItem)) {
479             return value == 0.0d ? OnOffType.OFF : OnOffType.ON;
480         } else if (item instanceof ContactItem) {
481             return value == 0.0d ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
482         } else if (item instanceof DimmerItem || item instanceof RollershutterItem || item instanceof ColorItem) {
483             // make sure Items that need PercentTypes instead of DecimalTypes do receive the right information
484             return new PercentType((int) Math.round(value * 100));
485         } else if (item instanceof NumberItem) {
486             if (unit != null) {
487                 return new QuantityType(value, unit);
488             }
489         }
490         return new DecimalType(value);
491     }
492
493     private boolean isSupportedItemType(Item item) {
494         if (item instanceof GroupItem) {
495             final Item baseItem = ((GroupItem) item).getBaseItem();
496             if (baseItem != null) {
497                 item = baseItem;
498             }
499         }
500
501         return SUPPORTED_TYPES.contains(ItemUtil.getMainItemType(item.getType()));
502     }
503
504     @Activate
505     protected void activate(final Map<String, Object> config) {
506         modified(config);
507     }
508
509     @Modified
510     protected void modified(final Map<String, Object> config) {
511         // clean existing definitions
512         rrdDefs.clear();
513
514         // add default configurations
515
516         RrdDefConfig defaultNumeric = new RrdDefConfig(DEFAULT_NUMERIC);
517         // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
518         defaultNumeric.setDef("GAUGE,600,U,U,10");
519         // define 5 different boxes:
520         // 1. granularity of 10s for the last hour
521         // 2. granularity of 1m for the last week
522         // 3. granularity of 15m for the last year
523         // 4. granularity of 1h for the last 5 years
524         // 5. granularity of 1d for the last 10 years
525         defaultNumeric
526                 .addArchives("LAST,0.5,1,360:LAST,0.5,6,10080:LAST,0.5,90,36500:LAST,0.5,360,43800:LAST,0.5,8640,3650");
527         rrdDefs.put(DEFAULT_NUMERIC, defaultNumeric);
528
529         RrdDefConfig defaultQuantifiable = new RrdDefConfig(DEFAULT_QUANTIFIABLE);
530         // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
531         defaultQuantifiable.setDef("GAUGE,600,U,U,10");
532         // define 5 different boxes:
533         // 1. granularity of 10s for the last hour
534         // 2. granularity of 1m for the last week
535         // 3. granularity of 15m for the last year
536         // 4. granularity of 1h for the last 5 years
537         // 5. granularity of 1d for the last 10 years
538         defaultQuantifiable.addArchives(
539                 "AVERAGE,0.5,1,360:AVERAGE,0.5,6,10080:AVERAGE,0.5,90,36500:AVERAGE,0.5,360,43800:AVERAGE,0.5,8640,3650");
540         rrdDefs.put(DEFAULT_QUANTIFIABLE, defaultQuantifiable);
541
542         RrdDefConfig defaultOther = new RrdDefConfig(DEFAULT_OTHER);
543         // use 5 seconds as a step size for discrete values and allow a 1h silence between updates
544         defaultOther.setDef("GAUGE,3600,U,U,5");
545         // define 4 different boxes:
546         // 1. granularity of 5s for the last hour
547         // 2. granularity of 1m for the last week
548         // 3. granularity of 15m for the last year
549         // 4. granularity of 4h for the last 10 years
550         defaultOther.addArchives("LAST,0.5,1,720:LAST,0.5,12,10080:LAST,0.5,180,35040:LAST,0.5,2880,21900");
551         rrdDefs.put(DEFAULT_OTHER, defaultOther);
552
553         if (config.isEmpty()) {
554             logger.debug("using default configuration only");
555             return;
556         }
557
558         Iterator<String> keys = config.keySet().iterator();
559         while (keys.hasNext()) {
560             String key = keys.next();
561
562             if ("service.pid".equals(key) || "component.name".equals(key)) {
563                 // ignore service.pid and name
564                 continue;
565             }
566
567             String[] subkeys = key.split("\\.");
568             if (subkeys.length != 2) {
569                 logger.debug("config '{}' should have the format 'name.configkey'", key);
570                 continue;
571             }
572
573             Object v = config.get(key);
574             if (v instanceof String) {
575                 String value = (String) v;
576                 String name = subkeys[0].toLowerCase();
577                 String property = subkeys[1].toLowerCase();
578
579                 if (value.isBlank()) {
580                     logger.trace("Config is empty: {}", property);
581                     continue;
582                 } else {
583                     logger.trace("Processing config: {} = {}", property, value);
584                 }
585
586                 RrdDefConfig rrdDef = rrdDefs.get(name);
587                 if (rrdDef == null) {
588                     rrdDef = new RrdDefConfig(name);
589                     rrdDefs.put(name, rrdDef);
590                 }
591
592                 try {
593                     if ("def".equals(property)) {
594                         rrdDef.setDef(value);
595                     } else if ("archives".equals(property)) {
596                         rrdDef.addArchives(value);
597                     } else if ("items".equals(property)) {
598                         rrdDef.addItems(value);
599                     } else {
600                         logger.debug("Unknown property {} : {}", property, value);
601                     }
602                 } catch (IllegalArgumentException e) {
603                     logger.warn("Ignoring illegal configuration: {}", e.getMessage());
604                 }
605             }
606         }
607
608         for (RrdDefConfig rrdDef : rrdDefs.values()) {
609             if (rrdDef.isValid()) {
610                 logger.debug("Created {}", rrdDef);
611             } else {
612                 logger.info("Removing invalid definition {}", rrdDef);
613                 rrdDefs.remove(rrdDef.name);
614             }
615         }
616     }
617
618     private static class RrdArchiveDef {
619         public @Nullable ConsolFun fcn;
620         public double xff;
621         public int steps, rows;
622
623         @Override
624         public String toString() {
625             StringBuilder sb = new StringBuilder(" " + fcn);
626             sb.append(" xff = ").append(xff);
627             sb.append(" steps = ").append(steps);
628             sb.append(" rows = ").append(rows);
629             return sb.toString();
630         }
631     }
632
633     private class RrdDefConfig {
634         public String name;
635         public @Nullable DsType dsType;
636         public int heartbeat, step;
637         public double min, max;
638         public List<RrdArchiveDef> archives;
639         public List<String> itemNames;
640
641         private boolean isInitialized;
642
643         public RrdDefConfig(String name) {
644             this.name = name;
645             archives = new ArrayList<>();
646             itemNames = new ArrayList<>();
647             isInitialized = false;
648         }
649
650         public void setDef(String defString) {
651             String[] opts = defString.split(",");
652             if (opts.length != 5) { // check if correct number of parameters
653                 logger.warn("invalid number of parameters {}: {}", name, defString);
654                 return;
655             }
656
657             if ("ABSOLUTE".equals(opts[0])) { // dsType
658                 dsType = DsType.ABSOLUTE;
659             } else if ("COUNTER".equals(opts[0])) {
660                 dsType = DsType.COUNTER;
661             } else if ("DERIVE".equals(opts[0])) {
662                 dsType = DsType.DERIVE;
663             } else if ("GAUGE".equals(opts[0])) {
664                 dsType = DsType.GAUGE;
665             } else {
666                 logger.warn("{}: dsType {} not supported", name, opts[0]);
667             }
668
669             heartbeat = Integer.parseInt(opts[1]);
670
671             if ("U".equals(opts[2])) {
672                 min = Double.NaN;
673             } else {
674                 min = Double.parseDouble(opts[2]);
675             }
676
677             if ("U".equals(opts[3])) {
678                 max = Double.NaN;
679             } else {
680                 max = Double.parseDouble(opts[3]);
681             }
682
683             step = Integer.parseInt(opts[4]);
684
685             isInitialized = true; // successfully initialized
686
687             return;
688         }
689
690         public void addArchives(String archivesString) {
691             String splitArchives[] = archivesString.split(":");
692             for (String archiveString : splitArchives) {
693                 String[] opts = archiveString.split(",");
694                 if (opts.length != 4) { // check if correct number of parameters
695                     logger.warn("invalid number of parameters {}: {}", name, archiveString);
696                     return;
697                 }
698                 RrdArchiveDef arc = new RrdArchiveDef();
699
700                 if ("AVERAGE".equals(opts[0])) {
701                     arc.fcn = ConsolFun.AVERAGE;
702                 } else if ("MIN".equals(opts[0])) {
703                     arc.fcn = ConsolFun.MIN;
704                 } else if ("MAX".equals(opts[0])) {
705                     arc.fcn = ConsolFun.MAX;
706                 } else if ("LAST".equals(opts[0])) {
707                     arc.fcn = ConsolFun.LAST;
708                 } else if ("FIRST".equals(opts[0])) {
709                     arc.fcn = ConsolFun.FIRST;
710                 } else if ("TOTAL".equals(opts[0])) {
711                     arc.fcn = ConsolFun.TOTAL;
712                 } else {
713                     logger.warn("{}: consolidation function  {} not supported", name, opts[0]);
714                 }
715                 arc.xff = Double.parseDouble(opts[1]);
716                 arc.steps = Integer.parseInt(opts[2]);
717                 arc.rows = Integer.parseInt(opts[3]);
718                 archives.add(arc);
719             }
720         }
721
722         public void addItems(String itemsString) {
723             Collections.addAll(itemNames, itemsString.split(","));
724         }
725
726         public boolean appliesTo(String item) {
727             return itemNames.contains(item);
728         }
729
730         public boolean isValid() { // a valid configuration must be initialized
731             // and contain at least one function
732             return isInitialized && !archives.isEmpty();
733         }
734
735         @Override
736         public String toString() {
737             StringBuilder sb = new StringBuilder(name);
738             sb.append(" = ").append(dsType);
739             sb.append(" heartbeat = ").append(heartbeat);
740             sb.append(" min/max = ").append(min).append("/").append(max);
741             sb.append(" step = ").append(step);
742             sb.append(" ").append(archives.size()).append(" archives(s) = [");
743             for (RrdArchiveDef arc : archives) {
744                 sb.append(arc.toString());
745             }
746             sb.append("] ");
747             sb.append(itemNames.size()).append(" items(s) = [");
748             for (String item : itemNames) {
749                 sb.append(item).append(" ");
750             }
751             sb.append("]");
752             return sb.toString();
753         }
754     }
755
756     @Override
757     public List<PersistenceStrategy> getDefaultStrategies() {
758         return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE,
759                 new PersistenceCronStrategy("everyMinute", "0 * * * * ?"));
760     }
761 }