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