2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.rrd4j.internal;
15 import java.io.IOException;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Locale;
28 import java.util.Objects;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ConcurrentSkipListMap;
32 import java.util.concurrent.Executors;
33 import java.util.concurrent.RejectedExecutionException;
34 import java.util.concurrent.ScheduledExecutorService;
35 import java.util.concurrent.ScheduledFuture;
36 import java.util.concurrent.TimeUnit;
37 import java.util.function.DoubleFunction;
38 import java.util.stream.Collectors;
39 import java.util.stream.Stream;
41 import javax.measure.Quantity;
42 import javax.measure.Unit;
44 import org.eclipse.jdt.annotation.NonNullByDefault;
45 import org.eclipse.jdt.annotation.Nullable;
46 import org.openhab.core.OpenHAB;
47 import org.openhab.core.common.NamedThreadFactory;
48 import org.openhab.core.items.GroupItem;
49 import org.openhab.core.items.Item;
50 import org.openhab.core.items.ItemNotFoundException;
51 import org.openhab.core.items.ItemRegistry;
52 import org.openhab.core.items.ItemUtil;
53 import org.openhab.core.library.CoreItemFactory;
54 import org.openhab.core.library.items.ColorItem;
55 import org.openhab.core.library.items.ContactItem;
56 import org.openhab.core.library.items.DimmerItem;
57 import org.openhab.core.library.items.NumberItem;
58 import org.openhab.core.library.items.RollershutterItem;
59 import org.openhab.core.library.items.SwitchItem;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.library.types.OnOffType;
62 import org.openhab.core.library.types.OpenClosedType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.persistence.FilterCriteria;
66 import org.openhab.core.persistence.FilterCriteria.Ordering;
67 import org.openhab.core.persistence.HistoricItem;
68 import org.openhab.core.persistence.PersistenceItemInfo;
69 import org.openhab.core.persistence.PersistenceService;
70 import org.openhab.core.persistence.QueryablePersistenceService;
71 import org.openhab.core.persistence.strategy.PersistenceCronStrategy;
72 import org.openhab.core.persistence.strategy.PersistenceStrategy;
73 import org.openhab.core.types.State;
74 import org.osgi.framework.Constants;
75 import org.osgi.service.component.annotations.Activate;
76 import org.osgi.service.component.annotations.Component;
77 import org.osgi.service.component.annotations.ConfigurationPolicy;
78 import org.osgi.service.component.annotations.Deactivate;
79 import org.osgi.service.component.annotations.Modified;
80 import org.osgi.service.component.annotations.Reference;
81 import org.rrd4j.ConsolFun;
82 import org.rrd4j.DsType;
83 import org.rrd4j.core.FetchData;
84 import org.rrd4j.core.FetchRequest;
85 import org.rrd4j.core.RrdDb;
86 import org.rrd4j.core.RrdDb.Builder;
87 import org.rrd4j.core.RrdDbPool;
88 import org.rrd4j.core.RrdDef;
89 import org.rrd4j.core.Sample;
90 import org.slf4j.Logger;
91 import org.slf4j.LoggerFactory;
94 * This is the implementation of the RRD4j {@link PersistenceService}. To learn
95 * more about RRD4j please visit their
96 * <a href="https://github.com/rrd4j/rrd4j">website</a>.
98 * @author Kai Kreuzer - Initial contribution
99 * @author Jan N. Klug - some improvements
100 * @author Karel Goderis - remove TimerThread dependency
103 @Component(service = { PersistenceService.class,
104 QueryablePersistenceService.class }, configurationPid = "org.openhab.rrd4j", configurationPolicy = ConfigurationPolicy.OPTIONAL, property = Constants.SERVICE_PID
105 + "=org.openhab.rrd4j")
106 public class RRD4jPersistenceService implements QueryablePersistenceService {
108 private record Key(long timestamp, String name) implements Comparable<Key> {
110 public int compareTo(Key other) {
111 int c = Long.compare(timestamp, other.timestamp);
113 return (c == 0) ? Objects.compare(name, other.name, String::compareTo) : c;
117 public static final String SERVICE_ID = "rrd4j";
119 private static final String DEFAULT_OTHER = "default_other";
120 private static final String DEFAULT_NUMERIC = "default_numeric";
121 private static final String DEFAULT_QUANTIFIABLE = "default_quantifiable";
123 private static final Set<String> SUPPORTED_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT,
124 CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.COLOR);
126 private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
127 new NamedThreadFactory("RRD4j"));
129 private final Map<String, RrdDefConfig> rrdDefs = new ConcurrentHashMap<>();
131 private final ConcurrentSkipListMap<Key, Double> storageMap = new ConcurrentSkipListMap<>(Key::compareTo);
133 private static final String DATASOURCE_STATE = "state";
135 private static final Path DB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "persistence", "rrd4j").toAbsolutePath();
137 private static final RrdDbPool DATABASE_POOL = new RrdDbPool();
139 private final Logger logger = LoggerFactory.getLogger(RRD4jPersistenceService.class);
140 private final ItemRegistry itemRegistry;
141 private boolean active = false;
143 public static Path getDatabasePath(String name) {
144 return DB_FOLDER.resolve(name + ".rrd");
147 public static RrdDbPool getDatabasePool() {
148 return DATABASE_POOL;
151 private final ScheduledFuture<?> storeJob;
154 public RRD4jPersistenceService(final @Reference ItemRegistry itemRegistry, Map<String, Object> config) {
155 this.itemRegistry = itemRegistry;
156 storeJob = scheduler.scheduleWithFixedDelay(() -> doStore(false), 1, 1, TimeUnit.SECONDS);
162 protected void modified(final Map<String, Object> config) {
163 // clean existing definitions
166 // add default configurations
168 RrdDefConfig defaultNumeric = new RrdDefConfig(DEFAULT_NUMERIC);
169 // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
170 defaultNumeric.setDef("GAUGE,600,U,U,10");
171 // define 5 different boxes:
172 // 1. granularity of 10s for the last hour
173 // 2. granularity of 1m for the last week
174 // 3. granularity of 15m for the last year
175 // 4. granularity of 1h for the last 5 years
176 // 5. granularity of 1d for the last 10 years
178 .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");
179 rrdDefs.put(DEFAULT_NUMERIC, defaultNumeric);
181 RrdDefConfig defaultQuantifiable = new RrdDefConfig(DEFAULT_QUANTIFIABLE);
182 // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
183 defaultQuantifiable.setDef("GAUGE,600,U,U,10");
184 // define 5 different boxes:
185 // 1. granularity of 10s for the last hour
186 // 2. granularity of 1m for the last week
187 // 3. granularity of 15m for the last year
188 // 4. granularity of 1h for the last 5 years
189 // 5. granularity of 1d for the last 10 years
190 defaultQuantifiable.addArchives(
191 "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");
192 rrdDefs.put(DEFAULT_QUANTIFIABLE, defaultQuantifiable);
194 RrdDefConfig defaultOther = new RrdDefConfig(DEFAULT_OTHER);
195 // use 5 seconds as a step size for discrete values and allow a 1h silence between updates
196 defaultOther.setDef("GAUGE,3600,U,U,5");
197 // define 4 different boxes:
198 // 1. granularity of 5s for the last hour
199 // 2. granularity of 1m for the last week
200 // 3. granularity of 15m for the last year
201 // 4. granularity of 4h for the last 10 years
202 defaultOther.addArchives("LAST,0.5,1,720:LAST,0.5,12,10080:LAST,0.5,180,35040:LAST,0.5,2880,21900");
203 rrdDefs.put(DEFAULT_OTHER, defaultOther);
205 if (config.isEmpty()) {
206 logger.debug("using default configuration only");
210 Iterator<String> keys = config.keySet().iterator();
211 while (keys.hasNext()) {
212 String key = keys.next();
214 if ("service.pid".equals(key) || "component.name".equals(key)) {
215 // ignore service.pid and name
219 String[] subkeys = key.split("\\.");
220 if (subkeys.length != 2) {
221 logger.debug("config '{}' should have the format 'name.configkey'", key);
225 Object v = config.get(key);
226 if (v instanceof String value) {
227 String name = subkeys[0].toLowerCase();
228 String property = subkeys[1].toLowerCase();
230 if (value.isBlank()) {
231 logger.trace("Config is empty: {}", property);
234 logger.trace("Processing config: {} = {}", property, value);
237 RrdDefConfig rrdDef = rrdDefs.get(name);
238 if (rrdDef == null) {
239 rrdDef = new RrdDefConfig(name);
240 rrdDefs.put(name, rrdDef);
244 if ("def".equals(property)) {
245 rrdDef.setDef(value);
246 } else if ("archives".equals(property)) {
247 rrdDef.addArchives(value);
248 } else if ("items".equals(property)) {
249 rrdDef.addItems(value);
251 logger.debug("Unknown property {} : {}", property, value);
253 } catch (IllegalArgumentException e) {
254 logger.warn("Ignoring illegal configuration: {}", e.getMessage());
259 for (RrdDefConfig rrdDef : rrdDefs.values()) {
260 if (rrdDef.isValid()) {
261 logger.debug("Created {}", rrdDef);
263 logger.info("Removing invalid definition {}", rrdDef);
264 rrdDefs.remove(rrdDef.name);
270 protected void deactivate() {
272 storeJob.cancel(false);
274 // make sure we really store everything
279 public String getId() {
284 public String getLabel(@Nullable Locale locale) {
289 public void store(final Item item, @Nullable final String alias) {
291 logger.warn("Tried to store {} but service is not yet ready (or shutting down).", item);
295 if (!isSupportedItemType(item)) {
296 logger.trace("Ignoring item '{}' since its type {} is not supported", item.getName(), item.getType());
299 final String name = alias == null ? item.getName() : alias;
303 if (item instanceof NumberItem nItem && item.getState() instanceof QuantityType<?> qState) {
304 Unit<? extends Quantity<?>> unit = nItem.getUnit();
306 QuantityType<?> convertedState = qState.toUnit(unit);
307 if (convertedState != null) {
308 value = convertedState.doubleValue();
312 "Failed to convert state '{}' to unit '{}'. Please check your item definition for correctness.",
316 value = qState.doubleValue();
319 DecimalType state = item.getStateAs(DecimalType.class);
321 value = state.toBigDecimal().doubleValue();
328 // we could not convert the value
332 long now = System.currentTimeMillis() / 1000;
333 Double oldValue = storageMap.put(new Key(now, name), value);
334 if (oldValue != null && !oldValue.equals(value)) {
336 "Discarding value {} for item {} with timestamp {} because a new value ({}) arrived with the same timestamp.",
337 oldValue, name, now, value);
341 private void doStore(boolean force) {
342 long now = System.currentTimeMillis() / 1000;
343 while (!storageMap.isEmpty()) {
344 Key key = storageMap.firstKey();
345 if (now > key.timestamp || force) {
346 // no new elements can be added for this timestamp because we are already past that time or the service
347 // requires forced storing
348 Double value = storageMap.pollFirstEntry().getValue();
349 writePointToDatabase(key.name, value, key.timestamp);
356 private synchronized void writePointToDatabase(String name, double value, long timestamp) {
359 db = getDB(name, true);
360 } catch (Exception e) {
361 logger.warn("Failed to open rrd4j database '{}' to store data ({})", name, e.toString());
367 ConsolFun function = getConsolidationFunction(db);
368 if (function != ConsolFun.AVERAGE) {
370 // we store the last value again, so that the value change
371 // in the database is not interpolated, but
372 // happens right at this spot
373 if (timestamp - 1 > db.getLastUpdateTime()) {
374 // only do it if there is not already a value
375 double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
376 if (!Double.isNaN(lastValue) && lastValue != value) {
377 Sample sample = db.createSample();
378 sample.setTime(timestamp - 1);
379 sample.setValue(DATASOURCE_STATE, lastValue);
381 logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database (again)", name,
382 lastValue, timestamp - 1);
385 } catch (IOException e) {
386 logger.debug("Error storing last value (again) for {}: {}", e.getMessage(), name);
390 Sample sample = db.createSample();
391 sample.setTime(timestamp);
392 double storeValue = value;
393 if (db.getDatasource(DATASOURCE_STATE).getType() == DsType.COUNTER) {
394 // counter values must be adjusted by stepsize
395 storeValue = value * db.getRrdDef().getStep();
397 sample.setValue(DATASOURCE_STATE, storeValue);
399 logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database", name, storeValue, timestamp);
400 } catch (Exception e) {
401 logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
405 } catch (IOException e) {
406 logger.debug("Error closing rrd4j database: {}", e.getMessage());
411 public void store(Item item) {
416 public Iterable<HistoricItem> query(FilterCriteria filter) {
417 ZonedDateTime filterBeginDate = filter.getBeginDate();
418 ZonedDateTime filterEndDate = filter.getEndDate();
419 if (filterBeginDate != null && filterEndDate != null && filterBeginDate.isAfter(filterEndDate)) {
420 throw new IllegalArgumentException("begin (" + filterBeginDate + ") before end (" + filterEndDate + ")");
423 String itemName = filter.getItemName();
424 if (itemName == null) {
425 logger.warn("Item name is missing in filter {}", filter);
428 logger.trace("Querying rrd4j database for item '{}'", itemName);
432 db = getDB(itemName, false);
433 } catch (Exception e) {
434 logger.warn("Failed to open rrd4j database '{}' for querying ({})", itemName, e.toString());
438 logger.debug("Could not find item '{}' in rrd4j database", itemName);
445 item = itemRegistry.getItem(itemName);
446 if (item instanceof GroupItem groupItem) {
447 item = groupItem.getBaseItem();
449 if (item instanceof NumberItem numberItem) {
450 // we already retrieve the unit here once as it is a very costly operation,
451 // see https://github.com/openhab/openhab-addons/issues/8928
452 unit = numberItem.getUnit();
454 } catch (ItemNotFoundException e) {
455 logger.debug("Could not find item '{}' in registry", itemName);
459 long end = filterEndDate == null ? System.currentTimeMillis() / 1000
460 : filterEndDate.toInstant().getEpochSecond();
462 DoubleFunction<State> toState = toStateMapper(item, unit);
465 if (filterBeginDate == null) {
466 // as rrd goes back for years and gets more and more inaccurate, we only support descending order
467 // and a single return value if no begin date is given - this case is required specifically for the
468 // historicState() query, which we want to support
469 if (filter.getOrdering() == Ordering.DESCENDING && filter.getPageSize() == 1
470 && filter.getPageNumber() == 0) {
471 if (filterEndDate == null || Duration.between(filterEndDate, ZonedDateTime.now()).getSeconds() < db
472 .getRrdDef().getStep()) {
473 // we are asked only for the most recent value!
474 double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
475 if (!Double.isNaN(lastValue)) {
476 HistoricItem rrd4jItem = new RRD4jItem(itemName, toState.apply(lastValue),
477 ZonedDateTime.ofInstant(Instant.ofEpochSecond(db.getLastArchiveUpdateTime()),
478 ZoneId.systemDefault()));
479 return List.of(rrd4jItem);
487 throw new UnsupportedOperationException(
488 "rrd4j does not allow querys without a begin date, unless order is descending and a single value is requested");
491 start = filterBeginDate.toInstant().getEpochSecond();
494 // do not call method {@link RrdDb#createFetchRequest(ConsolFun, long, long, long)} if start > end to avoid
495 // an IAE to be thrown
497 logger.debug("Could not query rrd4j database for item '{}': start ({}) > end ({})", itemName, start,
502 FetchRequest request = db.createFetchRequest(getConsolidationFunction(db), start, end, 1);
503 FetchData result = request.fetchData();
505 List<HistoricItem> items = new ArrayList<>();
506 long ts = result.getFirstTimestamp();
507 ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault());
508 long step = result.getRowCount() > 1 ? result.getStep() : 0;
510 double prevValue = Double.NaN;
511 State prevState = null;
512 for (double value : result.getValues(DATASOURCE_STATE)) {
513 if (!Double.isNaN(value) && (((ts >= start) && (ts <= end)) || (start == end))) {
516 if (prevValue == value) {
519 prevState = state = toState.apply(value);
523 RRD4jItem rrd4jItem = new RRD4jItem(itemName, state, zdt);
524 items.add(rrd4jItem);
526 zdt = zdt.plusSeconds(step);
530 } catch (IOException e) {
531 logger.warn("Could not query rrd4j database for item '{}': {}", itemName, e.getMessage());
536 } catch (IOException e) {
537 logger.debug("Error closing rrd4j database: {}", e.getMessage());
543 public Set<PersistenceItemInfo> getItemInfo() {
547 protected synchronized @Nullable RrdDb getDB(String alias, boolean createFileIfAbsent) {
549 Path path = getDatabasePath(alias);
551 Builder builder = RrdDb.getBuilder();
552 builder.setPool(DATABASE_POOL);
554 if (Files.exists(path)) {
555 // recreate the RrdDb instance from the file
556 builder.setPath(path.toString());
557 db = builder.build();
558 } else if (createFileIfAbsent) {
559 if (!Files.exists(DB_FOLDER)) {
560 Files.createDirectories(DB_FOLDER);
562 RrdDef rrdDef = getRrdDef(alias, path);
563 if (rrdDef != null) {
564 // create a new database file
565 builder.setRrdDef(rrdDef);
566 db = builder.build();
569 "Did not create rrd4j database for item '{}' since no rrd definition could be determined. This is likely due to an unsupported item type.",
573 } catch (IOException e) {
574 logger.error("Could not create rrd4j database file '{}': {}", path, e.getMessage());
575 } catch (RejectedExecutionException e) {
576 // this happens if the system is shut down
577 logger.debug("Could not create rrd4j database file '{}': {}", path, e.getMessage());
582 private @Nullable RrdDefConfig getRrdDefConfig(String itemName) {
583 RrdDefConfig useRdc = null;
584 for (Map.Entry<String, RrdDefConfig> e : rrdDefs.entrySet()) {
585 // try to find special config
586 RrdDefConfig rdc = e.getValue();
587 if (rdc.appliesTo(itemName)) {
592 if (useRdc == null) { // not defined, use defaults
594 Item item = itemRegistry.getItem(itemName);
595 if (!isSupportedItemType(item)) {
598 if (item instanceof NumberItem numberItem) {
599 useRdc = numberItem.getDimension() != null ? rrdDefs.get(DEFAULT_QUANTIFIABLE)
600 : rrdDefs.get(DEFAULT_NUMERIC);
602 useRdc = rrdDefs.get(DEFAULT_OTHER);
604 } catch (ItemNotFoundException e) {
605 logger.debug("Could not find item '{}' in registry", itemName);
609 logger.trace("Using rrd definition '{}' for item '{}'.", useRdc, itemName);
613 private @Nullable RrdDef getRrdDef(String itemName, Path path) {
614 RrdDef rrdDef = new RrdDef(path.toString());
615 RrdDefConfig useRdc = getRrdDefConfig(itemName);
616 if (useRdc != null) {
617 rrdDef.setStep(useRdc.step);
618 rrdDef.setStartTime(System.currentTimeMillis() / 1000 - useRdc.step);
619 rrdDef.addDatasource(DATASOURCE_STATE, useRdc.dsType, useRdc.heartbeat, useRdc.min, useRdc.max);
620 for (RrdArchiveDef rad : useRdc.archives) {
621 rrdDef.addArchive(rad.fcn, rad.xff, rad.steps, rad.rows);
629 public ConsolFun getConsolidationFunction(RrdDb db) {
631 return db.getRrdDef().getArcDefs()[0].getConsolFun();
632 } catch (IOException e) {
633 return ConsolFun.MAX;
638 * Get the state Mapper for a given item
640 * @param item the item (in case of a group item, the base item has to be supplied)
641 * @param unit the unit to use
642 * @return the state mapper
644 private <Q extends Quantity<Q>> DoubleFunction<State> toStateMapper(@Nullable Item item, @Nullable Unit<Q> unit) {
645 if (item instanceof SwitchItem && !(item instanceof DimmerItem)) {
646 return (value) -> OnOffType.from(value != 0.0d);
647 } else if (item instanceof ContactItem) {
648 return (value) -> value == 0.0d ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
649 } else if (item instanceof DimmerItem || item instanceof RollershutterItem || item instanceof ColorItem) {
650 // make sure Items that need PercentTypes instead of DecimalTypes do receive the right information
651 return (value) -> new PercentType((int) Math.round(value * 100));
652 } else if (item instanceof NumberItem) {
654 return (value) -> new QuantityType<>(value, unit);
657 return DecimalType::new;
660 private boolean isSupportedItemType(Item item) {
661 if (item instanceof GroupItem groupItem) {
662 final Item baseItem = groupItem.getBaseItem();
663 if (baseItem != null) {
668 return SUPPORTED_TYPES.contains(ItemUtil.getMainItemType(item.getType()));
671 public List<String> getRrdFiles() {
672 try (Stream<Path> stream = Files.list(DB_FOLDER)) {
673 return stream.filter(file -> !Files.isDirectory(file) && file.toFile().getName().endsWith(".rrd"))
674 .map(file -> file.toFile().getName()).collect(Collectors.toList());
675 } catch (IOException e) {
680 private static class RrdArchiveDef {
681 public @Nullable ConsolFun fcn;
683 public int steps, rows;
686 public String toString() {
687 StringBuilder sb = new StringBuilder(" " + fcn);
688 sb.append(" xff = ").append(xff);
689 sb.append(" steps = ").append(steps);
690 sb.append(" rows = ").append(rows);
691 return sb.toString();
695 private class RrdDefConfig {
697 public @Nullable DsType dsType;
698 public int heartbeat, step;
699 public double min, max;
700 public List<RrdArchiveDef> archives;
701 public List<String> itemNames;
703 private boolean isInitialized;
705 public RrdDefConfig(String name) {
707 archives = new ArrayList<>();
708 itemNames = new ArrayList<>();
709 isInitialized = false;
712 public void setDef(String defString) {
713 String[] opts = defString.split(",");
714 if (opts.length != 5) { // check if correct number of parameters
715 logger.warn("invalid number of parameters {}: {}", name, defString);
719 if ("ABSOLUTE".equals(opts[0])) { // dsType
720 dsType = DsType.ABSOLUTE;
721 } else if ("COUNTER".equals(opts[0])) {
722 dsType = DsType.COUNTER;
723 } else if ("DERIVE".equals(opts[0])) {
724 dsType = DsType.DERIVE;
725 } else if ("GAUGE".equals(opts[0])) {
726 dsType = DsType.GAUGE;
728 logger.warn("{}: dsType {} not supported", name, opts[0]);
731 heartbeat = Integer.parseInt(opts[1]);
733 if ("U".equals(opts[2])) {
736 min = Double.parseDouble(opts[2]);
739 if ("U".equals(opts[3])) {
742 max = Double.parseDouble(opts[3]);
745 step = Integer.parseInt(opts[4]);
747 isInitialized = true; // successfully initialized
752 public void addArchives(String archivesString) {
753 String[] splitArchives = archivesString.split(":");
754 for (String archiveString : splitArchives) {
755 String[] opts = archiveString.split(",");
756 if (opts.length != 4) { // check if correct number of parameters
757 logger.warn("invalid number of parameters {}: {}", name, archiveString);
760 RrdArchiveDef arc = new RrdArchiveDef();
762 if ("AVERAGE".equals(opts[0])) {
763 arc.fcn = ConsolFun.AVERAGE;
764 } else if ("MIN".equals(opts[0])) {
765 arc.fcn = ConsolFun.MIN;
766 } else if ("MAX".equals(opts[0])) {
767 arc.fcn = ConsolFun.MAX;
768 } else if ("LAST".equals(opts[0])) {
769 arc.fcn = ConsolFun.LAST;
770 } else if ("FIRST".equals(opts[0])) {
771 arc.fcn = ConsolFun.FIRST;
772 } else if ("TOTAL".equals(opts[0])) {
773 arc.fcn = ConsolFun.TOTAL;
775 logger.warn("{}: consolidation function {} not supported", name, opts[0]);
777 arc.xff = Double.parseDouble(opts[1]);
778 arc.steps = Integer.parseInt(opts[2]);
779 arc.rows = Integer.parseInt(opts[3]);
784 public void addItems(String itemsString) {
785 Collections.addAll(itemNames, itemsString.split(","));
788 public boolean appliesTo(String item) {
789 return itemNames.contains(item);
792 public boolean isValid() { // a valid configuration must be initialized
793 // and contain at least one function
794 return isInitialized && !archives.isEmpty();
798 public String toString() {
799 StringBuilder sb = new StringBuilder(name);
800 sb.append(" = ").append(dsType);
801 sb.append(" heartbeat = ").append(heartbeat);
802 sb.append(" min/max = ").append(min).append("/").append(max);
803 sb.append(" step = ").append(step);
804 sb.append(" ").append(archives.size()).append(" archives(s) = [");
805 for (RrdArchiveDef arc : archives) {
806 sb.append(arc.toString());
809 sb.append(itemNames.size()).append(" items(s) = [");
810 for (String item : itemNames) {
811 sb.append(item).append(" ");
814 return sb.toString();
819 public List<PersistenceStrategy> getDefaultStrategies() {
820 return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE,
821 new PersistenceCronStrategy("everyMinute", "0 * * * * ?"));