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