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