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